1use http::header::HOST;
7use log::{debug, info};
8use serde::Deserialize;
9use unm_engine::interface::Engine;
10use unm_request::build_client;
11use unm_types::{Context, RetrievedSongInfo, SerializedIdentifier, Song, SongSearchInformation};
12use url::Url;
13
14#[derive(Deserialize)]
15#[non_exhaustive]
16struct PyNCMResponse {
17 pub code: i32,
19 pub data: Vec<PyNCMResponseEntry>,
20}
21
22#[derive(Deserialize)]
23#[non_exhaustive]
24struct PyNCMResponseEntry {
25 pub id: i64,
27 pub url: Option<String>,
29}
30
31pub const ENGINE_ID: &str = "pyncm";
32
33pub struct PyNCMEngine;
36
37#[async_trait::async_trait]
38impl Engine for PyNCMEngine {
39 async fn search<'a>(
40 &self,
41 info: &'a Song,
42 ctx: &'a Context,
43 ) -> anyhow::Result<Option<SongSearchInformation>> {
44 info!("Searching with PyNCM engine…");
45
46 let response = fetch_song_info(&info.id, ctx).await?;
47
48 if response.code == 200 {
49 let match_result = find_match(&response.data, &info.id)?.map(|url| {
52 SongSearchInformation::builder()
53 .source(ENGINE_ID.into())
54 .identifier(url)
55 .build()
56 });
57
58 Ok(match_result)
59 } else {
60 Err(anyhow::anyhow!(
61 "failed to request. code: {}",
62 response.code
63 ))
64 }
65 }
66
67 async fn retrieve<'a>(
68 &self,
69 identifier: &'a SerializedIdentifier,
70 _: &'a Context,
71 ) -> anyhow::Result<RetrievedSongInfo> {
72 info!("Retrieving with PyNCM engine…");
73
74 Ok(RetrievedSongInfo::builder()
76 .source(ENGINE_ID.into())
77 .url(identifier.to_string())
78 .build())
79 }
80}
81
82async fn fetch_song_info(id: &str, ctx: &Context) -> anyhow::Result<PyNCMResponse> {
84 debug!("Fetching the song information…");
85
86 let bitrate = if ctx.enable_flac { 999000 } else { 320000 };
87 let url = Url::parse_with_params(
88 "http://76.76.21.21/api/pyncm?module=track&method=GetTrackAudio",
89 &[("song_ids", id), ("bitrate", &bitrate.to_string())],
90 )?;
91
92 let client = build_client(ctx.proxy_uri.as_deref())?;
93 let response = client
94 .get(url)
95 .header(HOST, "music.163-my-beloved.com")
96 .send()
97 .await?;
98 Ok(response.json::<PyNCMResponse>().await?)
99}
100
101fn find_match(data: &[PyNCMResponseEntry], song_id: &str) -> anyhow::Result<Option<String>> {
103 info!("Finding the matched song…");
104
105 data.iter()
106 .find(|entry| {
107 entry.id.to_string() == song_id && entry.url.is_some()
110 })
111 .map(|v| v.url.clone())
112 .ok_or_else(|| anyhow::anyhow!("no matched song"))
113}
114
115#[cfg(test)]
116mod tests {
117 use unm_types::ContextBuilder;
118
119 #[tokio::test]
120 async fn test_fetch_song_info() {
121 use super::fetch_song_info;
122
123 let song_id = "1939601619"; let result = fetch_song_info(song_id, &ContextBuilder::default().build().unwrap()).await;
125
126 if let Ok(response) = result {
127 assert_eq!(response.code, 200);
128 assert_eq!(response.data.len(), 1);
129 assert_eq!(response.data[0].id.to_string(), song_id);
130 assert!(response.data[0].url.is_some());
131 } else {
132 panic!("failed to fetch song info");
133 }
134 }
135}