use http::header::HOST;
use log::{debug, info};
use serde::Deserialize;
use unm_engine::interface::Engine;
use unm_request::build_client;
use unm_types::{Context, RetrievedSongInfo, SerializedIdentifier, Song, SongSearchInformation};
use url::Url;
#[derive(Deserialize)]
#[non_exhaustive]
struct PyNCMResponse {
pub code: i32,
pub data: Vec<PyNCMResponseEntry>,
}
#[derive(Deserialize)]
#[non_exhaustive]
struct PyNCMResponseEntry {
pub id: i64,
pub url: Option<String>,
}
pub const ENGINE_ID: &str = "pyncm";
pub struct PyNCMEngine;
#[async_trait::async_trait]
impl Engine for PyNCMEngine {
async fn search<'a>(
&self,
info: &'a Song,
ctx: &'a Context,
) -> anyhow::Result<Option<SongSearchInformation>> {
info!("Searching with PyNCM engine…");
let response = fetch_song_info(&info.id, ctx).await?;
if response.code == 200 {
let match_result = find_match(&response.data, &info.id)?.map(|url| {
SongSearchInformation::builder()
.source(ENGINE_ID.into())
.identifier(url)
.build()
});
Ok(match_result)
} else {
Err(anyhow::anyhow!(
"failed to request. code: {}",
response.code
))
}
}
async fn retrieve<'a>(
&self,
identifier: &'a SerializedIdentifier,
_: &'a Context,
) -> anyhow::Result<RetrievedSongInfo> {
info!("Retrieving with PyNCM engine…");
Ok(RetrievedSongInfo::builder()
.source(ENGINE_ID.into())
.url(identifier.to_string())
.build())
}
}
async fn fetch_song_info(id: &str, ctx: &Context) -> anyhow::Result<PyNCMResponse> {
debug!("Fetching the song information…");
let bitrate = if ctx.enable_flac { 999000 } else { 320000 };
let url = Url::parse_with_params(
"http://76.76.21.21/api/pyncm?module=track&method=GetTrackAudio",
&[("song_ids", id), ("bitrate", &bitrate.to_string())],
)?;
let client = build_client(ctx.proxy_uri.as_deref())?;
let response = client
.get(url)
.header(HOST, "music.163-my-beloved.com")
.send()
.await?;
Ok(response.json::<PyNCMResponse>().await?)
}
fn find_match(data: &[PyNCMResponseEntry], song_id: &str) -> anyhow::Result<Option<String>> {
info!("Finding the matched song…");
data.iter()
.find(|entry| {
entry.id.to_string() == song_id && entry.url.is_some()
})
.map(|v| v.url.clone())
.ok_or_else(|| anyhow::anyhow!("no matched song"))
}
#[cfg(test)]
mod tests {
use unm_types::ContextBuilder;
#[tokio::test]
async fn test_fetch_song_info() {
use super::fetch_song_info;
let song_id = "1939601619"; let result = fetch_song_info(song_id, &ContextBuilder::default().build().unwrap()).await;
if let Ok(response) = result {
assert_eq!(response.code, 200);
assert_eq!(response.data.len(), 1);
assert_eq!(response.data[0].id.to_string(), song_id);
assert!(response.data[0].url.is_some());
} else {
panic!("failed to fetch song info");
}
}
}