ncm_api_rs/api/
voice_upload.rs1use super::Query;
2use crate::error::Result;
3use crate::request::{ApiClient, ApiResponse, CryptoType};
9use serde_json::json;
10
11impl ApiClient {
12 pub async fn voice_upload(
32 &self,
33 query: &Query,
34 file_name: &str,
35 file_data: Vec<u8>,
36 file_mimetype: Option<&str>,
37 ) -> Result<ApiResponse> {
38 let mimetype = file_mimetype.unwrap_or("audio/mpeg");
39
40 let ext = file_name.rsplit('.').next().unwrap_or("mp3");
42
43 let filename = if let Some(name) = query.get("songName") {
45 name.to_string()
46 } else {
47 file_name
48 .rsplit('.')
49 .next_back()
50 .unwrap_or(file_name)
51 .replace(' ', "")
52 .replace('.', "_")
53 };
54
55 let token_data = json!({
57 "bucket": "ymusic",
58 "ext": ext,
59 "filename": filename,
60 "local": false,
61 "nos_product": 0,
62 "type": "other"
63 });
64
65 let token_res = self
66 .request(
67 "/api/nos/token/alloc",
68 token_data,
69 query.to_option(CryptoType::Weapi),
70 )
71 .await?;
72
73 let result = &token_res.body["result"];
74 let object_key_raw = result["objectKey"].as_str().unwrap_or_default();
75 let object_key = object_key_raw.replace('/', "%2F");
76 let token = result["token"].as_str().unwrap_or_default().to_string();
77 let doc_id = result["docId"].clone();
78
79 let init_url = format!("https://ymusic.nos-hz.163yun.com/{}?uploads", object_key);
81
82 let init_res = self
83 .client
84 .post(&init_url)
85 .header("x-nos-token", &token)
86 .header("X-Nos-Meta-Content-Type", mimetype)
87 .send()
88 .await
89 .map_err(crate::error::NcmError::Http)?;
90
91 let init_xml = init_res
92 .text()
93 .await
94 .map_err(crate::error::NcmError::Http)?;
95
96 let upload_id = init_xml
98 .split("<UploadId>")
99 .nth(1)
100 .and_then(|s| s.split("</UploadId>").next())
101 .unwrap_or_default()
102 .to_string();
103
104 let block_size = 10 * 1024 * 1024;
106 let file_size = file_data.len();
107 let mut offset = 0usize;
108 let mut block_index = 1u32;
109 let mut etags = Vec::new();
110
111 while offset < file_size {
112 let end = (offset + block_size).min(file_size);
113 let chunk = file_data[offset..end].to_vec();
114
115 let part_url = format!(
116 "https://ymusic.nos-hz.163yun.com/{}?partNumber={}&uploadId={}",
117 object_key, block_index, upload_id
118 );
119
120 let part_res = self
121 .client
122 .put(&part_url)
123 .header("x-nos-token", &token)
124 .header("Content-Type", mimetype)
125 .body(chunk)
126 .send()
127 .await
128 .map_err(crate::error::NcmError::Http)?;
129
130 if let Some(etag) = part_res.headers().get("etag") {
131 etags.push(etag.to_str().unwrap_or_default().to_string());
132 }
133
134 offset = end;
135 block_index += 1;
136 }
137
138 let mut complete_xml = String::from("<CompleteMultipartUpload>");
140 for (i, etag) in etags.iter().enumerate() {
141 complete_xml.push_str(&format!(
142 "<Part><PartNumber>{}</PartNumber><ETag>{}</ETag></Part>",
143 i + 1,
144 etag
145 ));
146 }
147 complete_xml.push_str("</CompleteMultipartUpload>");
148
149 let complete_url = format!(
150 "https://ymusic.nos-hz.163yun.com/{}?uploadId={}",
151 object_key, upload_id
152 );
153
154 self.client
155 .post(&complete_url)
156 .header("Content-Type", "text/plain;charset=UTF-8")
157 .header("X-Nos-Meta-Content-Type", mimetype)
158 .header("x-nos-token", &token)
159 .body(complete_xml)
160 .send()
161 .await
162 .map_err(crate::error::NcmError::Http)?;
163
164 let dupkey = generate_dupkey();
166
167 let auto_publish = query.get("autoPublish").map(|v| v == "1").unwrap_or(false);
168 let privacy = query.get("privacy").map(|v| v == "1").unwrap_or(false);
169 let composed_songs: Vec<String> = query
170 .get("composedSongs")
171 .map(|s| s.split(',').map(|x| x.to_string()).collect())
172 .unwrap_or_default();
173
174 let voice_data = json!([{
175 "name": filename,
176 "autoPublish": auto_publish,
177 "autoPublishText": query.get_or("autoPublishText", ""),
178 "description": query.get_or("description", ""),
179 "voiceListId": query.get_or("voiceListId", ""),
180 "coverImgId": query.get_or("coverImgId", ""),
181 "dfsId": doc_id,
182 "categoryId": query.get_or("categoryId", ""),
183 "secondCategoryId": query.get_or("secondCategoryId", ""),
184 "composedSongs": composed_songs,
185 "privacy": privacy,
186 "publishTime": query.get_or("publishTime", "0").parse::<i64>().unwrap_or(0),
187 "orderNo": query.get_or("orderNo", "1").parse::<i64>().unwrap_or(1),
188 }]);
189
190 let voice_data_str = serde_json::to_string(&voice_data).unwrap_or_default();
191
192 let _pre_check = self
194 .request(
195 "/api/voice/workbench/voice/batch/upload/preCheck",
196 json!({
197 "dupkey": generate_dupkey(),
198 "voiceData": voice_data_str
199 }),
200 query.to_option(CryptoType::default()),
201 )
202 .await?;
203
204 let upload_result = self
206 .request(
207 "/api/voice/workbench/voice/batch/upload/v2",
208 json!({
209 "dupkey": dupkey,
210 "voiceData": voice_data_str
211 }),
212 query.to_option(CryptoType::default()),
213 )
214 .await?;
215
216 Ok(ApiResponse {
217 status: 200,
218 body: json!({
219 "code": 200,
220 "data": upload_result.body["data"]
221 }),
222 cookie: upload_result.cookie,
223 })
224 }
225}
226
227fn generate_dupkey() -> String {
229 use rand::Rng;
230 let hex_digits = b"0123456789abcdef";
231 let mut rng = rand::thread_rng();
232 let mut s = [0u8; 36];
233
234 for item in &mut s {
235 *item = hex_digits[rng.gen_range(0..16)];
236 }
237 s[14] = b'4';
238 s[19] = hex_digits[((s[19] & 0x3) | 0x8) as usize];
239 s[8] = b'-';
240 s[13] = b'-';
241 s[18] = b'-';
242 s[23] = b'-';
243
244 String::from_utf8_lossy(&s).to_string()
245}