use anyhow::{Context, Result};
use tracing::debug;
use crate::config::Config;
use crate::model::{
Album, AlbumHit, AlbumOrder, ArtistAlbum, ResolveOptions, Resolved, SingerHit, Song, SourceKind,
};
use crate::{netease, qq};
pub struct Sources {
pub client: reqwest::Client,
pub qq: qq::Session,
pub netease: netease::Session,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AlbumKey {
Qq(qq::album::AlbumRef),
Netease(String),
}
impl AlbumKey {
pub fn from_id(kind: SourceKind, id: &str) -> AlbumKey {
match kind {
SourceKind::Qq => AlbumKey::Qq(qq::album::AlbumRef::Mid(id.to_string())),
SourceKind::Netease => AlbumKey::Netease(id.to_string()),
}
}
}
fn fallback_queries(song: &Song) -> Vec<(String, usize)> {
let base = match song.singers.first() {
Some(s) => format!("{} {s}", song.title),
None => song.title.clone(),
};
let mut queries = vec![(base.clone(), 5)];
if !song.album.is_empty() && song.album != song.title {
queries.push((format!("{base} {}", song.album), 15));
}
queries
}
pub fn detect(input: &str) -> Option<SourceKind> {
let input = input.trim();
if !input.contains("://") {
None
} else if qq::is_qq_host(input) {
Some(SourceKind::Qq)
} else if netease::is_netease_host(input) {
Some(SourceKind::Netease)
} else {
None
}
}
pub fn parse_album(kind: SourceKind, input: &str) -> Option<AlbumKey> {
match kind {
SourceKind::Qq => qq::album::parse_link(input)
.or_else(|| qq::album::parse_mid_arg(input))
.map(AlbumKey::Qq),
SourceKind::Netease => netease::parse_link(input, "album")
.or_else(|| netease::parse_id_arg(input))
.map(AlbumKey::Netease),
}
}
pub fn parse_singer(kind: SourceKind, input: &str) -> Option<String> {
match kind {
SourceKind::Qq => {
qq::artist::parse_link(input).or_else(|| qq::artist::parse_mid_arg(input))
}
SourceKind::Netease => {
netease::parse_link(input, "artist").or_else(|| netease::parse_id_arg(input))
}
}
}
impl Sources {
pub fn new(client: reqwest::Client, config: Config) -> Self {
Self {
netease: netease::Session::new(client.clone(), config.netease.clone()),
qq: qq::Session::new(client.clone(), config),
client,
}
}
pub async fn search(&self, kind: SourceKind, keyword: &str, limit: usize) -> Result<Vec<Song>> {
match kind {
SourceKind::Qq => qq::search::search(&self.qq, keyword, limit).await,
SourceKind::Netease => netease::music::search(&self.netease, keyword, limit).await,
}
}
pub async fn resolve(&self, song: &Song, opts: ResolveOptions) -> Option<Resolved> {
match song.source {
SourceKind::Qq => qq::resolve::resolve(&self.qq, song, opts).await,
SourceKind::Netease => netease::resolve::resolve(&self.netease, song, opts).await,
}
}
pub async fn resolve_fallback(&self, song: &Song, opts: ResolveOptions) -> Option<Resolved> {
let queries = fallback_queries(song);
for kind in SourceKind::ALL.into_iter().filter(|&k| k != song.source) {
for (query, limit) in &queries {
let hits = match self.search(kind, query, *limit).await {
Ok(h) => h,
Err(e) => {
debug!(id = %song.id, %kind, query, "补链搜索失败: {e:#}");
break;
}
};
let matches: Vec<&Song> = hits
.iter()
.filter(|c| crate::merge::same_song(song, c))
.take(2)
.collect();
debug!(id = %song.id, %kind, query, hits = hits.len(), matches = matches.len(), "补链搜索");
if matches.is_empty() {
continue;
}
for cand in matches {
if let Some(mut r) = self.resolve(cand, opts).await {
debug!(id = %song.id, %kind, other = %cand.id, "已从其他音源补上链接");
r.fallback = Some(kind);
return Some(r);
}
}
break;
}
}
None
}
pub async fn lyric(&self, song: &Song) -> Result<Option<String>> {
match song.source {
SourceKind::Qq => qq::lyric::fetch(&self.client, &song.id).await,
SourceKind::Netease => netease::music::lyric(&self.netease, &song.id).await,
}
}
pub async fn song(&self, kind: SourceKind, input: &str) -> Result<Song> {
match kind {
SourceKind::Qq => {
let r = qq::song::resolve_ref(&self.qq, input).await?;
qq::search::song_detail(&self.qq, &r).await
}
SourceKind::Netease => {
let id = netease::resolve_id(&self.netease, input, "song").await?;
netease::music::song_detail(&self.netease, &id).await
}
}
}
pub async fn playlist(
&self,
kind: SourceKind,
input: &str,
) -> Result<(String, String, Vec<Song>)> {
match kind {
SourceKind::Qq => {
let id = qq::playlist::resolve_id(&self.qq, input).await?;
let p = qq::playlist::fetch(&self.qq, &id).await?;
Ok((id, p.name, p.songs))
}
SourceKind::Netease => {
let id = netease::resolve_id(&self.netease, input, "playlist").await?;
let p = netease::music::playlist(&self.netease, &id).await?;
Ok((id, p.name, p.songs))
}
}
}
pub async fn search_albums(
&self,
kind: SourceKind,
keyword: &str,
limit: usize,
) -> Result<Vec<AlbumHit>> {
match kind {
SourceKind::Qq => qq::album::search(&self.qq, keyword, limit).await,
SourceKind::Netease => {
netease::music::search_albums(&self.netease, keyword, limit).await
}
}
}
pub async fn album(&self, key: &AlbumKey) -> Result<Album> {
match key {
AlbumKey::Qq(r) => qq::album::fetch(&self.qq, r).await,
AlbumKey::Netease(id) => netease::music::album(&self.netease, id).await,
}
}
pub async fn search_singers(
&self,
kind: SourceKind,
keyword: &str,
limit: usize,
) -> Result<Vec<SingerHit>> {
match kind {
SourceKind::Qq => qq::artist::search(&self.qq, keyword, limit).await,
SourceKind::Netease => {
netease::music::search_singers(&self.netease, keyword, limit).await
}
}
}
pub async fn artist_albums(
&self,
kind: SourceKind,
id: &str,
order: AlbumOrder,
) -> Result<Vec<ArtistAlbum>> {
match kind {
SourceKind::Qq => qq::artist::albums(&self.qq, id, order).await,
SourceKind::Netease => netease::music::artist_albums(&self.netease, id, order)
.await
.context("获取网易云歌手专辑失败"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fallback_query_list() {
let song = |title: &str, singers: &[&str], album: &str| Song {
title: title.into(),
singers: singers.iter().map(|s| s.to_string()).collect(),
album: album.into(),
..Song::default()
};
assert_eq!(
fallback_queries(&song("田", &["田馥甄"], "无人知晓")),
[
("田 田馥甄".to_string(), 5),
("田 田馥甄 无人知晓".to_string(), 15)
]
);
assert_eq!(
fallback_queries(&song("一二三", &["田馥甄"], "一二三")).len(),
1
);
assert_eq!(fallback_queries(&song("x", &["y"], "")).len(), 1);
assert_eq!(fallback_queries(&song("x", &[], ""))[0].0, "x");
}
#[test]
fn detect_and_parse() {
assert_eq!(
detect("https://y.qq.com/n/ryqq/songDetail/x"),
Some(SourceKind::Qq)
);
assert_eq!(
detect("https://music.163.com/#/song?id=1"),
Some(SourceKind::Netease)
);
assert_eq!(detect("https://163cn.tv/x"), Some(SourceKind::Netease));
assert_eq!(detect("1481929839"), None);
assert_eq!(detect("https://example.com/"), None);
assert_eq!(
parse_album(
SourceKind::Netease,
"https://music.163.com/#/album?id=95902047"
),
Some(AlbumKey::Netease("95902047".into()))
);
assert_eq!(
parse_album(SourceKind::Netease, "95902047"),
Some(AlbumKey::Netease("95902047".into()))
);
assert_eq!(parse_album(SourceKind::Netease, "无人知晓"), None);
assert!(matches!(
parse_album(SourceKind::Qq, "0024bjiL2aocxT"),
Some(AlbumKey::Qq(_))
));
assert_eq!(
parse_singer(SourceKind::Netease, "9548").as_deref(),
Some("9548")
);
assert_eq!(
parse_singer(SourceKind::Qq, "0025NhlN2yWrP4").as_deref(),
Some("0025NhlN2yWrP4")
);
assert_eq!(parse_singer(SourceKind::Qq, "9548"), None);
assert_eq!(
AlbumKey::from_id(SourceKind::Netease, "1"),
AlbumKey::Netease("1".into())
);
}
}