use serde::Deserialize; #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default)] pub enum AudioQuality { Normal, High, Hifi, Master, #[default] Max, } impl AudioQuality { pub fn tidal_value(&self) -> &'static str { match self { Self::Normal => "LOW", Self::High => "HIGH", Self::Hifi => "LOSSLESS", Self::Master => "HI_RES", Self::Max => "HI_RES_LOSSLESS", } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum MediaKind { Track(u64), Album(u64), } pub fn parse_tidal_url(s: &str) -> anyhow::Result { let s = s.trim(); if let Ok(id) = s.parse::() { return Ok(MediaKind::Track(id)); } let lower = s.to_lowercase(); anyhow::ensure!(lower.contains("tidal.com"), "not a tidal URL: {s}"); for kind in ["track", "album"] { let marker = format!("/{kind}/"); if let Some(pos) = lower.find(&marker) { let rest = &s[pos + marker.len()..]; let end = rest.find(['/', '?', '#']).unwrap_or(rest.len()); let id: u64 = rest[..end] .parse() .map_err(|_| anyhow::anyhow!("couldn't parse {kind} id from {s}"))?; return Ok(match kind { "track" => MediaKind::Track(id), _ => MediaKind::Album(id), }); } } anyhow::bail!("couldn't find a track or album id in {s}") } #[derive(Debug, Clone, Deserialize, Default)] #[serde(default)] pub struct Artist { #[serde(deserialize_with = "de_id")] pub id: u64, pub name: Option, } #[derive(Debug, Clone, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct Album { #[serde(deserialize_with = "de_id")] pub id: u64, pub title: Option, pub duration: i64, pub number_of_tracks: i64, pub number_of_volumes: i64, pub release_date: Option, pub cover: Option, pub explicit: bool, pub audio_quality: Option, #[serde(deserialize_with = "de_artist")] pub artist: Artist, #[serde(default, deserialize_with = "de_artists")] pub artists: Vec, } #[derive(Debug, Clone, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct Track { #[serde(deserialize_with = "de_id")] pub id: u64, pub title: Option, pub duration: i64, pub track_number: i64, pub volume_number: i64, pub version: Option, pub isrc: Option, pub explicit: bool, pub audio_quality: Option, pub copy_right: Option, #[serde(deserialize_with = "de_artist")] pub artist: Artist, #[serde(default, deserialize_with = "de_artists")] pub artists: Vec, pub album: Album, } #[derive(Debug, Clone, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct StreamManifest { pub track_id: Option, pub audio_quality: Option, pub manifest_mime_type: Option, pub manifest: Option, } #[derive(Debug, Clone)] pub struct StreamUrl { pub url: String, pub urls: Vec, pub codec: String, pub encryption_key: String, pub sound_quality: Option, } fn de_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result { let v: Option = serde::Deserialize::deserialize(d)?; match v { None | Some(serde_json::Value::Null) => Ok(0), Some(serde_json::Value::Number(n)) => Ok(n.as_u64().unwrap_or(0)), Some(serde_json::Value::String(s)) => Ok(s.parse().unwrap_or(0)), _ => Ok(0), } } fn de_artist<'de, D: serde::Deserializer<'de>>(d: D) -> Result { let v: Option = serde::Deserialize::deserialize(d)?; Ok(v.unwrap_or_default()) } fn de_artists<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { let v: Option> = serde::Deserialize::deserialize(d)?; Ok(v.unwrap_or_default()) } pub fn artist_names(artists: &[Artist], fallback: &Artist) -> String { if artists.is_empty() { return fallback.name.clone().unwrap_or_default(); } artists .iter() .filter_map(|a| a.name.clone()) .collect::>() .join(", ") } pub fn album_year(release_date: Option<&str>) -> String { release_date .and_then(|d| d.split('-').next()) .unwrap_or("") .to_string() } pub fn cover_url(cover: Option<&str>, w: u32, h: u32) -> Option { cover.map(|id| format!("https://resources.tidal.com/images/{}/{w}x{h}.jpg", id.replace('-', "/"))) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_track_urls_and_bare_ids() { assert_eq!( parse_tidal_url("https://tidal.com/browse/track/70973230").unwrap(), MediaKind::Track(70973230) ); assert_eq!( parse_tidal_url("https://tidal.com/browse/album/58138532").unwrap(), MediaKind::Album(58138532) ); assert_eq!(parse_tidal_url("70973230").unwrap(), MediaKind::Track(70973230)); assert!(parse_tidal_url("https://example.com/foo").is_err()); } }