//! 网易云公开接口的元数据浏览层:榜单/歌单/歌手/专辑,带缓存与节流 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, last_call: Mutex>, } 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 { { 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::().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 { 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::>(), }); self.cache.put(key, full.clone()); full } }; let songs = full["songs"].as_array().cloned().unwrap_or_default(); let slice: Vec = 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 { 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 = 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 = 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 { 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::>(), }); self.cache.put(key, out.clone()); Ok(out) } pub async fn album_search(&self, q: &str, limit: usize) -> Result { 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 = 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 = items.into_iter().take(limit).collect(); Ok(json!({"total": full["total"], "items": slice})) } pub async fn album_detail(&self, aid: u64) -> Result { 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::>(), }); 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::>() .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), }) }