char-slop/tidal-dl
tidal ripper
git clone https://git.t4t.associates/char-slop/tidal-dl
3d04234
main
1use serde:: Deserialize ; 2 3# [ derive ( Debug , Clone , Copy , PartialEq , Eq , clap :: ValueEnum , Default )] 4pub enum AudioQuality { 5Normal , 6High , 7Hifi , 8Master , 9# [ default ] 10Max , 11} 12 13impl AudioQuality { 14pub fn tidal_value ( & self ) ->&' static str { 15match self { 16Self :: Normal =>"LOW" , 17Self :: High =>"HIGH" , 18Self :: Hifi =>"LOSSLESS" , 19Self :: Master =>"HI_RES" , 20Self :: Max =>"HI_RES_LOSSLESS" , 21} 22} 23} 24 25# [ derive ( Debug , Clone , PartialEq , Eq )] 26pub enum MediaKind { 27Track ( u64 ), 28Album ( u64 ), 29} 30 31pub fn parse_tidal_url ( s : & str ) -> anyhow:: Result < MediaKind > { 32let s = s. trim (); 33if let Ok ( id) = s. parse ::< u64 >() { 34return Ok ( MediaKind :: Track ( id)); 35} 36let lower = s. to_lowercase (); 37 anyhow:: ensure!( lower. contains ( "tidal.com" ), "not a tidal URL: {s}" ); 38for kindin [ "track" , "album" ] { 39let marker =format! ( "/{kind}/" ); 40if let Some ( pos) = lower. find ( & marker) { 41let rest =& s[ pos + marker. len () ..]; 42let end = rest. find ([ '/' , '?' , '#' ]). unwrap_or ( rest. len ()); 43let id: u64 = rest[ ..end] 44. parse () 45. map_err ( |_| anyhow:: anyhow!( "couldn't parse {kind} id from {s}" )) ?; 46return Ok ( match kind{ 47"track" =>MediaKind :: Track ( id), 48 _ =>MediaKind :: Album ( id), 49}); 50} 51} 52 anyhow:: bail!( "couldn't find a track or album id in {s}" ) 53} 54 55# [ derive ( Debug , Clone , Deserialize , Default )] 56# [ serde ( default )] 57pub struct Artist { 58# [ serde ( deserialize_with = "de_id" )] 59pub id : u64 , 60pub name : Option < String >, 61} 62 63# [ derive ( Debug , Clone , Deserialize , Default )] 64# [ serde ( default , rename_all = "camelCase" )] 65pub struct Album { 66# [ serde ( deserialize_with = "de_id" )] 67pub id : u64 , 68pub title : Option < String >, 69pub duration : i64 , 70pub number_of_tracks : i64 , 71pub number_of_volumes : i64 , 72pub release_date : Option < String >, 73pub cover : Option < String >, 74pub explicit : bool , 75pub audio_quality : Option < String >, 76# [ serde ( deserialize_with = "de_artist" )] 77pub artist : Artist , 78# [ serde ( default , deserialize_with = "de_artists" )] 79pub artists : Vec < Artist >, 80} 81 82# [ derive ( Debug , Clone , Deserialize , Default )] 83# [ serde ( default , rename_all = "camelCase" )] 84pub struct Track { 85# [ serde ( deserialize_with = "de_id" )] 86pub id : u64 , 87pub title : Option < String >, 88pub duration : i64 , 89pub track_number : i64 , 90pub volume_number : i64 , 91pub version : Option < String >, 92pub isrc : Option < String >, 93pub explicit : bool , 94pub audio_quality : Option < String >, 95pub copy_right : Option < String >, 96# [ serde ( deserialize_with = "de_artist" )] 97pub artist : Artist , 98# [ serde ( default , deserialize_with = "de_artists" )] 99pub artists : Vec < Artist >, 100pub album : Album , 101} 102 103# [ derive ( Debug , Clone , Deserialize , Default )] 104# [ serde ( default , rename_all = "camelCase" )] 105pub struct StreamManifest { 106pub track_id : Option < serde_json:: Value >, 107pub audio_quality : Option < String >, 108pub manifest_mime_type : Option < String >, 109pub manifest : Option < String >, 110} 111 112# [ derive ( Debug , Clone )] 113pub struct StreamUrl { 114pub url : String , 115pub urls : Vec < String >, 116pub codec : String , 117pub encryption_key : String , 118pub sound_quality : Option < String >, 119} 120 121fn de_id < ' de , D : serde:: Deserializer < ' de >>( d : D ) ->Result < u64 , D :: Error > { 122let v: Option < serde_json:: Value > = serde:: Deserialize :: deserialize ( d) ?; 123match v{ 124None |Some ( serde_json:: Value :: Null ) =>Ok ( 0 ), 125Some ( serde_json:: Value :: Number ( n)) =>Ok ( n. as_u64 (). unwrap_or ( 0 )), 126Some ( serde_json:: Value :: String ( s)) =>Ok ( s. parse (). unwrap_or ( 0 )), 127 _ =>Ok ( 0 ), 128} 129} 130 131fn de_artist < ' de , D : serde:: Deserializer < ' de >>( d : D ) ->Result < Artist , D :: Error > { 132let v: Option < Artist > = serde:: Deserialize :: deserialize ( d) ?; 133Ok ( v. unwrap_or_default ()) 134} 135 136fn de_artists < ' de , D : serde:: Deserializer < ' de >>( d : D ) ->Result < Vec < Artist >, D :: Error > { 137let v: Option < Vec < Artist >> = serde:: Deserialize :: deserialize ( d) ?; 138Ok ( v. unwrap_or_default ()) 139} 140 141pub fn artist_names ( artists : & [ Artist ], fallback : & Artist ) ->String { 142if artists. is_empty () { 143return fallback. name . clone (). unwrap_or_default (); 144} 145 artists 146. iter () 147. filter_map ( |a| a. name . clone ()) 148. collect ::< Vec < _ >>() 149. join ( ", " ) 150} 151 152pub fn album_year ( release_date : Option < & str >) ->String { 153 release_date 154. and_then ( |d| d. split ( '-' ). next ()) 155. unwrap_or ( "" ) 156. to_string () 157} 158 159pub fn cover_url ( cover : Option < & str >, w : u32 , h : u32 ) ->Option < String > { 160 cover. map ( |id|format! ( "https://resources.tidal.com/images/{}/{w}x{h}.jpg" , id. replace ( '-' , "/" ))) 161} 162 163# [ cfg ( test )] 164mod tests{ 165use super :: * ; 166 167# [ test ] 168fn parses_track_urls_and_bare_ids () { 169assert_eq! ( 170parse_tidal_url ( "https://tidal.com/browse/track/70973230" ). unwrap (), 171MediaKind :: Track ( 70973230 ) 172); 173assert_eq! ( 174parse_tidal_url ( "https://tidal.com/browse/album/58138532" ). unwrap (), 175MediaKind :: Album ( 58138532 ) 176); 177assert_eq! ( parse_tidal_url ( "70973230" ). unwrap (), MediaKind :: Track ( 70973230 )); 178assert! ( parse_tidal_url ( "https://example.com/foo" ). is_err ()); 179} 180}