uta 0.1.1

Command-line music search and downloader for QQ Music and NetEase Cloud Music, lossless first, shipped as a single static binary. For learning and research only; non-commercial use.
//! 按音源分发:搜索、取直链、歌词、单曲、歌单、专辑、歌手。各音源的具体协议在各自模块(`qq/`、`netease/`)中。
//!
//! 用枚举分发而不是 trait object:音源数量少且固定,async fn 不需要 `async-trait`。

use anyhow::{Context, Result};

use crate::config::Config;
use crate::model::{
    Album, AlbumHit, AlbumOrder, ArtistAlbum, ResolveOptions, Resolved, SingerHit, Song, SourceKind,
};
use crate::{netease, qq};

/// 一次运行共享的全部音源上下文。
pub struct Sources {
    /// 接口请求用的客户端(连接 5s、总计 15s),封面下载也用它
    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 {
    /// 从搜索结果 / 歌手专辑列表中的 id 构造。
    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()),
        }
    }
}

/// 链接属于哪个音源;不是已知音源的链接返回 None。
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
    }
}

/// 专辑链接或 id(QQ:albummid / 数字 albumID;网易:数字 id);不是则返回 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),
    }
}

/// 歌手链接或 id(QQ:14 位 singermid;网易:数字 id);不是则返回 None。
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,
        }
    }

    /// 搜索歌曲,最多 `limit` 条。
    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,
        }
    }

    /// 解析下载直链;拿不到返回 None。
    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,
        }
    }

    /// 取歌词(已清理的 LRC);没有歌词返回 `Ok(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,
        }
    }

    /// 单曲:id 或链接(含短链)。
    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
            }
        }
    }

    /// 歌单:返回 (歌单 id, 歌单名, 曲目)。
    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 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())
        );
    }
}