use super::Query;
use crate::error::Result;
use crate::request::{ApiClient, ApiResponse, CryptoType};
use serde_json::json;
impl ApiClient {
pub async fn voice_upload(
&self,
query: &Query,
file_name: &str,
file_data: Vec<u8>,
file_mimetype: Option<&str>,
) -> Result<ApiResponse> {
let mimetype = file_mimetype.unwrap_or("audio/mpeg");
let ext = file_name.rsplit('.').next().unwrap_or("mp3");
let filename = if let Some(name) = query.get("songName") {
name.to_string()
} else {
file_name
.rsplit('.')
.next_back()
.unwrap_or(file_name)
.replace(' ', "")
.replace('.', "_")
};
let token_data = json!({
"bucket": "ymusic",
"ext": ext,
"filename": filename,
"local": false,
"nos_product": 0,
"type": "other"
});
let token_res = self
.request(
"/api/nos/token/alloc",
token_data,
query.to_option(CryptoType::Weapi),
)
.await?;
let result = &token_res.body["result"];
let object_key_raw = result["objectKey"].as_str().unwrap_or_default();
let object_key = object_key_raw.replace('/', "%2F");
let token = result["token"].as_str().unwrap_or_default().to_string();
let doc_id = result["docId"].clone();
let init_url = format!("https://ymusic.nos-hz.163yun.com/{}?uploads", object_key);
let init_res = self
.client
.post(&init_url)
.header("x-nos-token", &token)
.header("X-Nos-Meta-Content-Type", mimetype)
.send()
.await
.map_err(crate::error::NcmError::Http)?;
let init_xml = init_res
.text()
.await
.map_err(crate::error::NcmError::Http)?;
let upload_id = init_xml
.split("<UploadId>")
.nth(1)
.and_then(|s| s.split("</UploadId>").next())
.unwrap_or_default()
.to_string();
let block_size = 10 * 1024 * 1024;
let file_size = file_data.len();
let mut offset = 0usize;
let mut block_index = 1u32;
let mut etags = Vec::new();
while offset < file_size {
let end = (offset + block_size).min(file_size);
let chunk = file_data[offset..end].to_vec();
let part_url = format!(
"https://ymusic.nos-hz.163yun.com/{}?partNumber={}&uploadId={}",
object_key, block_index, upload_id
);
let part_res = self
.client
.put(&part_url)
.header("x-nos-token", &token)
.header("Content-Type", mimetype)
.body(chunk)
.send()
.await
.map_err(crate::error::NcmError::Http)?;
if let Some(etag) = part_res.headers().get("etag") {
etags.push(etag.to_str().unwrap_or_default().to_string());
}
offset = end;
block_index += 1;
}
let mut complete_xml = String::from("<CompleteMultipartUpload>");
for (i, etag) in etags.iter().enumerate() {
complete_xml.push_str(&format!(
"<Part><PartNumber>{}</PartNumber><ETag>{}</ETag></Part>",
i + 1,
etag
));
}
complete_xml.push_str("</CompleteMultipartUpload>");
let complete_url = format!(
"https://ymusic.nos-hz.163yun.com/{}?uploadId={}",
object_key, upload_id
);
self.client
.post(&complete_url)
.header("Content-Type", "text/plain;charset=UTF-8")
.header("X-Nos-Meta-Content-Type", mimetype)
.header("x-nos-token", &token)
.body(complete_xml)
.send()
.await
.map_err(crate::error::NcmError::Http)?;
let dupkey = generate_dupkey();
let auto_publish = query.get("autoPublish").map(|v| v == "1").unwrap_or(false);
let privacy = query.get("privacy").map(|v| v == "1").unwrap_or(false);
let composed_songs: Vec<String> = query
.get("composedSongs")
.map(|s| s.split(',').map(|x| x.to_string()).collect())
.unwrap_or_default();
let voice_data = json!([{
"name": filename,
"autoPublish": auto_publish,
"autoPublishText": query.get_or("autoPublishText", ""),
"description": query.get_or("description", ""),
"voiceListId": query.get_or("voiceListId", ""),
"coverImgId": query.get_or("coverImgId", ""),
"dfsId": doc_id,
"categoryId": query.get_or("categoryId", ""),
"secondCategoryId": query.get_or("secondCategoryId", ""),
"composedSongs": composed_songs,
"privacy": privacy,
"publishTime": query.get_or("publishTime", "0").parse::<i64>().unwrap_or(0),
"orderNo": query.get_or("orderNo", "1").parse::<i64>().unwrap_or(1),
}]);
let voice_data_str = serde_json::to_string(&voice_data).unwrap_or_default();
let _pre_check = self
.request(
"/api/voice/workbench/voice/batch/upload/preCheck",
json!({
"dupkey": generate_dupkey(),
"voiceData": voice_data_str
}),
query.to_option(CryptoType::default()),
)
.await?;
let upload_result = self
.request(
"/api/voice/workbench/voice/batch/upload/v2",
json!({
"dupkey": dupkey,
"voiceData": voice_data_str
}),
query.to_option(CryptoType::default()),
)
.await?;
Ok(ApiResponse {
status: 200,
body: json!({
"code": 200,
"data": upload_result.body["data"]
}),
cookie: upload_result.cookie,
})
}
}
fn generate_dupkey() -> String {
use rand::Rng;
let hex_digits = b"0123456789abcdef";
let mut rng = rand::thread_rng();
let mut s = [0u8; 36];
for item in &mut s {
*item = hex_digits[rng.gen_range(0..16)];
}
s[14] = b'4';
s[19] = hex_digits[((s[19] & 0x3) | 0x8) as usize];
s[8] = b'-';
s[13] = b'-';
s[18] = b'-';
s[23] = b'-';
String::from_utf8_lossy(&s).to_string()
}