use anyhow::{Context, Result, bail};
use base64::{Engine, engine::general_purpose::STANDARD as B64};
use serde_json::Value;
use crate::lrc::clean;
const ENDPOINT: &str = "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg";
pub async fn fetch(client: &reqwest::Client, mid: &str) -> Result<Option<String>> {
let v: Value = crate::net::send(
client
.get(ENDPOINT)
.query(&[
("songmid", mid),
("g_tk", "5381"),
("loginUin", "0"),
("hostUin", "0"),
("format", "json"),
("inCharset", "utf8"),
("outCharset", "utf-8"),
("platform", "yqq"),
])
.header("Referer", "https://y.qq.com/portal/player.html"),
)
.await
.context("请求歌词失败")?
.error_for_status()?
.json()
.await
.context("歌词响应不是 JSON")?;
match v.get("code").and_then(Value::as_i64) {
Some(0) => {}
Some(1101) => return Ok(None),
other => bail!("歌词接口返回 code={other:?}"),
}
let Some(b64) = v.get("lyric").and_then(Value::as_str) else {
return Ok(None);
};
decode(b64)
}
pub fn decode(b64: &str) -> Result<Option<String>> {
let bytes = B64.decode(b64.trim()).context("歌词 base64 解码失败")?;
let text = String::from_utf8_lossy(&bytes);
let cleaned = clean(&text);
Ok((!cleaned.is_empty()).then_some(cleaned))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_real_prefix() {
let b64 = B64.encode("[ti:夜曲]\n[ar:周杰伦]\n[by:]\n[00:00.00]夜曲 - 周杰伦\n");
assert_eq!(
decode(&b64).unwrap().unwrap(),
"[ti:夜曲]\n[ar:周杰伦]\n[by:]\n[00:00.00]夜曲 - 周杰伦"
);
assert_eq!(decode("").unwrap(), None);
assert!(decode("!!!").is_err());
}
}