use serde::Serialize;
use crate::probe::ProbeResult;
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, serde::Deserialize, clap::ValueEnum,
)]
#[serde(rename_all = "lowercase")]
pub enum SourceKind {
#[default]
Qq,
#[value(alias = "163")]
#[serde(alias = "163")]
Netease,
}
impl SourceKind {
pub const ALL: [SourceKind; 2] = [SourceKind::Qq, SourceKind::Netease];
pub fn label(self) -> &'static str {
match self {
SourceKind::Qq => "QQ",
SourceKind::Netease => "网易",
}
}
}
impl std::fmt::Display for SourceKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SourceKind::Qq => "qq",
SourceKind::Netease => "netease",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Track {
pub number: u32,
pub total: u32,
pub disc: Option<(u32, u32)>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Song {
pub source: SourceKind,
pub id: String,
pub title: String,
pub singers: Vec<String>,
pub album: String,
pub album_id: String,
pub interval: u64,
pub size_flac: u64,
pub size_320mp3: u64,
pub size_hires: u64,
pub cover_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub track: Option<Track>,
}
impl Song {
pub fn singer_text(&self) -> String {
self.singers.join(", ")
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
serde::Deserialize,
clap::ValueEnum,
)]
#[serde(rename_all = "lowercase")]
pub enum Level {
Std,
Hq,
Sq,
Hires,
Atmos,
Master,
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Level::Std => "std",
Level::Hq => "hq",
Level::Sq => "sq",
Level::Hires => "hires",
Level::Atmos => "atmos",
Level::Master => "master",
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResolveOptions {
pub lossless_only: bool,
pub max_level: Level,
}
#[derive(Debug, Clone, Serialize)]
pub struct Resolved {
pub via: &'static str,
pub quality: String,
pub level: Level,
#[serde(flatten)]
pub probe: ProbeResult,
#[serde(skip)]
pub referer: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback: Option<SourceKind>,
}
#[derive(Debug)]
pub struct ProviderCheck {
pub via: &'static str,
pub configured: bool,
pub result: Option<Resolved>,
pub note: Option<String>,
pub elapsed: std::time::Duration,
}
pub async fn check_provider<F>(via: &'static str, configured: bool, fut: F) -> ProviderCheck
where
F: std::future::Future<Output = Option<Resolved>>,
{
let start = std::time::Instant::now();
let result = if configured { fut.await } else { None };
ProviderCheck {
via,
configured,
result,
note: None,
elapsed: start.elapsed(),
}
}
pub fn below_320k(size: u64, duration_s: u64) -> bool {
(size as u128) * 8 < 320_000u128 * duration_s as u128
}
#[derive(Debug, Clone, Serialize)]
pub struct AlbumHit {
pub id: String,
pub name: String,
pub singer: String,
pub song_num: u64,
pub publish_date: String,
}
#[derive(Debug)]
pub struct Album {
pub id: String,
pub name: String,
pub singers: Vec<String>,
pub publish_date: String,
pub songs: Vec<Song>,
}
impl Album {
pub fn dir_name(&self) -> String {
if self.singers.is_empty() {
self.name.clone()
} else {
format!("{} - {}", self.singers.join(", "), self.name)
}
}
}
pub fn tracks_for(index_cds: &[u32]) -> Vec<Track> {
let total = index_cds.len() as u32;
let multi = index_cds.iter().any(|&c| c != index_cds[0]);
let discs = index_cds.iter().max().map_or(1, |m| m + 1);
index_cds
.iter()
.enumerate()
.map(|(i, &cd)| Track {
number: i as u32 + 1,
total,
disc: multi.then_some((cd + 1, discs)),
})
.collect()
}
#[derive(Debug, Clone, Serialize)]
pub struct SingerHit {
pub id: String,
pub name: String,
pub album_num: u64,
pub song_num: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum AlbumKind {
Studio,
Ep,
Single,
Live,
Other,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArtistAlbum {
pub id: String,
pub name: String,
pub singer: String,
pub album_type: String,
pub kind: AlbumKind,
pub publish_date: String,
pub song_num: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum AlbumOrder {
New,
Hot,
}
impl ArtistAlbum {
pub fn year(&self) -> &str {
self.publish_date.get(..4).unwrap_or(&self.publish_date)
}
}
pub fn exact_match<'a>(hits: &'a [SingerHit], keyword: &str) -> Option<&'a SingerHit> {
let k = keyword.trim().to_lowercase();
hits.iter().find(|h| h.name.trim().to_lowercase() == k)
}
pub fn main_singer(albums: &[ArtistAlbum]) -> Option<String> {
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for a in albums.iter().filter(|a| !a.singer.is_empty()) {
*counts.entry(&a.singer).or_default() += 1;
}
counts
.into_iter()
.max_by(|a, b| a.1.cmp(&b.1).then(b.0.cmp(a.0)))
.map(|(s, _)| s.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_match_picks_same_name() {
let hit = |id: &str, name: &str| SingerHit {
id: id.into(),
name: name.into(),
album_num: 0,
song_num: 0,
};
let hits = [hit("a", "Jay Chou Band"), hit("b", "jay chou")];
assert_eq!(exact_match(&hits, " Jay Chou ").unwrap().id, "b");
assert!(exact_match(&hits, "Jay").is_none());
}
#[test]
fn main_singer_most_common() {
let a = |singer: &str| ArtistAlbum {
id: String::new(),
name: String::new(),
singer: singer.into(),
album_type: String::new(),
kind: AlbumKind::Studio,
publish_date: String::new(),
song_num: 0,
};
let collab = "周杰伦, 言承旭, 吴建豪, 周渝民, 五月天 阿信";
assert_eq!(
main_singer(&[a("周杰伦"), a(collab)]).as_deref(),
Some("周杰伦")
);
assert_eq!(
main_singer(&[a("周杰伦"), a(collab), a(collab)]).as_deref(),
Some(collab)
);
assert_eq!(main_singer(&[a("")]), None);
assert_eq!(main_singer(&[]), None);
}
#[test]
fn filter_320k() {
assert!(!below_320k(26_691_277, 226));
assert!(below_320k(3_630_591, 226));
assert!(!below_320k(9_075_745, 226));
assert!(!below_320k(1, 0));
}
#[test]
fn level_order() {
assert!(Level::Master > Level::Atmos && Level::Atmos > Level::Hires);
assert!(Level::Hires > Level::Sq && Level::Sq > Level::Hq && Level::Hq > Level::Std);
}
#[test]
fn source_kind_alias_163() {
use clap::ValueEnum;
assert_eq!(SourceKind::from_str("163", false), Ok(SourceKind::Netease));
assert_eq!(
SourceKind::from_str("netease", false),
Ok(SourceKind::Netease)
);
assert_eq!(SourceKind::from_str("qq", false), Ok(SourceKind::Qq));
let v: SourceKind = serde_json::from_value(serde_json::json!("163")).unwrap();
assert_eq!(v, SourceKind::Netease);
}
#[test]
fn source_kind_names() {
assert_eq!(SourceKind::default(), SourceKind::Qq);
assert_eq!(SourceKind::Netease.to_string(), "netease");
assert_eq!(SourceKind::Qq.label(), "QQ");
assert_eq!(
serde_json::to_value(SourceKind::Netease).unwrap(),
"netease"
);
}
}