Ctrl K
ComunidadGuías…

src/meta.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//! 网易云公开接口的元数据浏览层:榜单/歌单/歌手/专辑,带缓存与节流
use std::time::{Duration, Instant};

use serde_json::{json, Value};
use tokio::sync::Mutex;

use crate::state::TtlCache;

const UA: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36";
const BASE: &str = "https://music.163.com";

pub struct Meta {
    client: reqwest::Client,
    cache: TtlCache<Value>,
    last_call: Mutex<Option<Instant>>,
}

impl Meta {
    pub fn new(meta_ttl: u64) -> Self {
        let client = reqwest::Client::builder()
            .user_agent(UA)
            .no_proxy()
            .timeout(Duration::from_secs(10))
            .build()
            .expect("meta http client build failed");
        Self {
            client,
            cache: TtlCache::new(meta_ttl),
            last_call: Mutex::new(None),
        }
    }

    /// 带节流的 GET(全局最小 1s 间隔),避免触发上游风控
    async fn get(&self, path: &str, params: &[(&str, String)]) -> Result<Value, String> {
        {
            let mut last = self.last_call.lock().await;
            if let Some(t) = *last {
                let wait = Duration::from_secs(1).saturating_sub(t.elapsed());
                if !wait.is_zero() {
                    tokio::time::sleep(wait).await;
                }
            }
            *last = Some(Instant::now());
        }
        let resp = self
            .client
            .get(format!("{BASE}{path}"))
            .query(params)
            .send()
            .await
            .map_err(|e| e.to_string())?
            .error_for_status()
            .map_err(|e| e.to_string())?;
        resp.json::<Value>().await.map_err(|e| e.to_string())
    }

    pub fn charts(&self) -> Value {
        json!([
            {"id": 3778678, "name": "热歌榜"},
            {"id": 3779629, "name": "新歌榜"},
            {"id": 19723756, "name": "飙升榜"},
            {"id": 2884035, "name": "原创榜"},
        ])
    }

    /// 全量缓存 + 内存切片,分页零上游成本
    pub async fn playlist_detail(
        &self,
        pid: u64,
        limit: usize,
        offset: usize,
    ) -> Result<Value, String> {
        let key = format!("pl:{pid}");
        let full = match self.cache.get(&key) {
            Some(v) => v,
            None => {
                let d = self.get("/api/playlist/detail", &[("id", pid.to_string())]).await?;
                let r = &d["result"];
                let tracks = r["tracks"].as_array().cloned().unwrap_or_default();
                let total = r["trackCount"].as_u64().unwrap_or(tracks.len() as u64);
                let full = json!({
                    "name": r.get("name").cloned().unwrap_or(Value::Null),
                    "total": total,
                    "songs": tracks.iter().map(fmt_song).collect::<Vec<_>>(),
                });
                self.cache.put(key, full.clone());
                full
            }
        };
        let songs = full["songs"].as_array().cloned().unwrap_or_default();
        let slice: Vec<Value> = songs.into_iter().skip(offset).take(limit).collect();
        Ok(json!({"name": full["name"], "total": full["total"], "songs": slice}))
    }

    pub async fn playlist_search(
        &self,
        q: &str,
        limit: usize,
        offset: usize,
    ) -> Result<Value, String> {
        let key = format!("pls:{q}");
        let full = match self.cache.get(&key) {
            Some(v) => v,
            None => {
                let d = self
                    .get(
                        "/api/search/get",
                        &[("s", q.to_string()), ("type", "1000".into()), ("limit", "100".into())],
                    )
                    .await?;
                let r = &d["result"];
                let pls = r["playlists"].as_array().cloned().unwrap_or_default();
                let total = r["playlistCount"].as_u64().unwrap_or(pls.len() as u64);
                let items: Vec<Value> = pls
                    .iter()
                    .map(|p| {
                        json!({
                            "id": p["id"].clone(),
                            "name": p["name"].clone(),
                            "count": p["trackCount"].clone(),
                            "play_count": p["playCount"].clone(),
                            "cover": p["coverImgUrl"].clone(),
                        })
                    })
                    .collect();
                let full = json!({"total": total, "items": items});
                self.cache.put(key, full.clone());
                full
            }
        };
        let items = full["items"].as_array().cloned().unwrap_or_default();
        let slice: Vec<Value> = items.into_iter().skip(offset).take(limit).collect();
        Ok(json!({"total": full["total"], "items": slice}))
    }

    /// 搜索歌手并返回热门歌曲
    pub async fn artist_hot(&self, q: &str, limit: usize) -> Result<Value, String> {
        let key = format!("ar:{q}:{limit}");
        if let Some(v) = self.cache.get(&key) {
            return Ok(v);
        }
        let d = self
            .get(
                "/api/search/get",
                &[("s", q.to_string()), ("type", "100".into()), ("limit", "1".into())],
            )
            .await?;
        let artists = d["result"]["artists"].as_array().cloned().unwrap_or_default();
        let first = artists
            .first()
            .ok_or_else(|| format!("未找到歌手: {q}"))?;
        let d2 = self.get(&format!("/api/artist/{}", first["id"]), &[]).await?;
        let songs = d2["hotSongs"].as_array().cloned().unwrap_or_default();
        let out = json!({
            "artist": first["name"].clone(),
            "songs": songs.iter().take(limit).map(fmt_song).collect::<Vec<_>>(),
        });
        self.cache.put(key, out.clone());
        Ok(out)
    }

    pub async fn album_search(&self, q: &str, limit: usize) -> Result<Value, String> {
        let key = format!("als:{q}");
        let full = match self.cache.get(&key) {
            Some(v) => v,
            None => {
                let d = self
                    .get(
                        "/api/search/get",
                        &[
                            ("s", q.to_string()),
                            ("type", "10".into()),
                            ("limit", limit.to_string()),
                        ],
                    )
                    .await?;
                let r = &d["result"];
                let albums = r["albums"].as_array().cloned().unwrap_or_default();
                let total = r["albumCount"].as_u64().unwrap_or(albums.len() as u64);
                let items: Vec<Value> = albums
                    .iter()
                    .map(|a| {
                        json!({
                            "id": a["id"].clone(),
                            "name": a["name"].clone(),
                            "artist": a["artist"]["name"].clone(),
                            "cover": a["picUrl"].as_str().map(|s| s.replace("100x100", "300x300")),
                        })
                    })
                    .collect();
                let full = json!({"total": total, "items": items});
                self.cache.put(key, full.clone());
                full
            }
        };
        let items = full["items"].as_array().cloned().unwrap_or_default();
        let slice: Vec<Value> = items.into_iter().take(limit).collect();
        Ok(json!({"total": full["total"], "items": slice}))
    }

    pub async fn album_detail(&self, aid: u64) -> Result<Value, String> {
        let key = format!("al:{aid}");
        if let Some(v) = self.cache.get(&key) {
            return Ok(v);
        }
        let d = self.get(&format!("/api/v1/album/{aid}"), &[]).await?;
        let al = &d["album"];
        let songs = d["songs"].as_array().cloned().unwrap_or_default();
        let out = json!({
            "name": al["name"].clone(),
            "artist": al["artist"]["name"].clone(),
            "songs": songs.iter().map(fmt_song).collect::<Vec<_>>(),
        });
        self.cache.put(key, out.clone());
        Ok(out)
    }
}

/// 兼容新旧两种字段(artists/album 与 ar/al)
fn fmt_song(t: &Value) -> Value {
    let artists = t
        .get("artists")
        .or_else(|| t.get("ar"))
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default();
    let album = t.get("album").or_else(|| t.get("al")).cloned().unwrap_or(Value::Null);
    let artist = artists
        .iter()
        .filter_map(|a| a.get("name").and_then(|n| n.as_str()))
        .collect::<Vec<_>>()
        .join("/");
    json!({
        "title": t.get("name").cloned().unwrap_or(Value::Null),
        "artist": artist,
        "album": album.get("name").cloned().unwrap_or(Value::Null),
        "cover": album.get("picUrl").cloned().unwrap_or(Value::Null),
    })
}