uta 0.1.2

Command-line music search and downloader for QQ Music and NetEase Cloud Music, lossless first, shipped as a single static binary. For learning and research only; non-commercial use.
//! 歌词:官方 `fcg_query_lyric_new.fcg`,响应 `lyric` 为 base64 编码的 LRC。

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";

/// 取歌词并清理;没有歌词时返回 `Ok(None)`。
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) => {}
        // 实测:无歌词(含不存在的 mid)返回 1101
        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)
}

/// base64 → UTF-8 → 清理;结果为空返回 None。
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());
    }
}