diff --git a/cli/src/main.rs b/cli/src/main.rs index 0a913be..618de62 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -156,8 +156,6 @@ impl From for SubtitleFormat { } } -const NA_STR: &str = "n/a"; - #[tokio::main] async fn main() { let cli = Cli::parse(); @@ -237,14 +235,8 @@ async fn run(cli: Cli) -> Result<()> { }; eprintln!("Lyrics ID: {}", lyrics.lyrics_id); - eprintln!( - "Language: {}", - lyrics.lyrics_language.as_deref().unwrap_or(NA_STR) - ); - eprintln!( - "Copyright: {}", - lyrics.lyrics_copyright.as_deref().unwrap_or(NA_STR) - ); + eprintln!("Language: {}", lyrics.lyrics_language); + eprintln!("Copyright: {}", lyrics.lyrics_copyright); eprintln!(); println!("{}", lyrics.lyrics_body); @@ -306,15 +298,9 @@ async fn run(cli: Cli) -> Result<()> { }; eprintln!("Subtitle ID: {}", subtitles.subtitle_id); - eprintln!( - "Language: {}", - subtitles.subtitle_language.as_deref().unwrap_or(NA_STR) - ); + eprintln!("Language: {}", subtitles.subtitle_language); eprintln!("Length: {}", subtitles.subtitle_length); - eprintln!( - "Copyright: {}", - subtitles.lyrics_copyright.as_deref().unwrap_or(NA_STR) - ); + eprintln!("Copyright: {}", subtitles.lyrics_copyright); eprintln!(); println!("{}", subtitles.subtitle_body); diff --git a/src/api_model.rs b/src/api_model.rs index 378daad..aa8038c 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -1,5 +1,3 @@ -use std::{marker::PhantomData, str::FromStr}; - use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use time::OffsetDateTime; @@ -229,269 +227,7 @@ where deserializer.deserialize_any(BoolFromIntVisitor) } -/** - * The Musixmatch API returns zero as a default value for numeric IDs. - * These values should be deserialized as [`None`]. - */ -pub fn null_if_zero<'de, D, N>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, - N: TryFrom, -{ - struct NullIfZeroVisitor { - n: PhantomData, - } - - impl<'de, N> Visitor<'de> for NullIfZeroVisitor - where - N: TryFrom, - { - type Value = Option; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("unsigned int or None") - } - - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_unit(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_u64(self, v: u64) -> Result - where - E: serde::de::Error, - { - if v < 1 { - Ok(None) - } else { - Ok(v.try_into().ok()) - } - } - - fn visit_u32(self, v: u32) -> Result - where - E: serde::de::Error, - { - self.visit_u64(v.into()) - } - - fn visit_u16(self, v: u16) -> Result - where - E: serde::de::Error, - { - self.visit_u64(v.into()) - } - - fn visit_u8(self, v: u8) -> Result - where - E: serde::de::Error, - { - self.visit_u64(v.into()) - } - } - - deserializer.deserialize_any(NullIfZeroVisitor { n: PhantomData }) -} - -/// The Musixmatch API returns an empty string as a default value for string fields. -/// These values should be deserialized as [`None`]. -pub fn null_if_empty<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct NullIfEmptyVisitor; - - impl<'de> Visitor<'de> for NullIfEmptyVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("string or None") - } - - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_unit(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - if v.is_empty() { - Ok(None) - } else { - Ok(Some(v.to_owned())) - } - } - } - - deserializer.deserialize_any(NullIfEmptyVisitor) -} - -/// Deserialize a numeric string into an integer. -/// Return None if the string is empty/null or if there was a parse error. -pub fn parse_int<'de, D, N>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, - N: FromStr + TryFrom, -{ - struct ParseIntVisitor { - n: PhantomData, - } - - impl<'de, N> Visitor<'de> for ParseIntVisitor - where - N: FromStr + TryFrom, - { - type Value = Option; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("numeric string or None") - } - - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_unit(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_u64(self, v: u64) -> Result - where - E: serde::de::Error, - { - Ok(v.try_into().ok()) - } - - fn visit_u32(self, v: u32) -> Result - where - E: serde::de::Error, - { - self.visit_u64(v.into()) - } - - fn visit_u16(self, v: u16) -> Result - where - E: serde::de::Error, - { - self.visit_u64(v.into()) - } - - fn visit_u8(self, v: u8) -> Result - where - E: serde::de::Error, - { - self.visit_u64(v.into()) - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - if v.is_empty() { - Ok(None) - } else { - Ok(v.parse().ok()) - } - } - } - - deserializer.deserialize_any(ParseIntVisitor { n: PhantomData }) -} - pub mod optional_date { - use super::*; - use serde::ser::Error as _; - use serde::Serializer; - use time::{macros::format_description, Date}; - - const DATE_FORMAT: &[time::format_description::FormatItem] = - format_description!("[year]-[month]-[day]"); - - pub fn serialize( - value: &Option, - serializer: S, - ) -> Result { - match value { - Some(date) => date - .format(&DATE_FORMAT) - .map_err(S::Error::custom)? - .serialize(serializer), - None => serializer.serialize_none(), - } - } - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - struct OptionalDateVisitor; - - impl<'de> Visitor<'de> for OptionalDateVisitor { - type Value = Option; - - fn expecting( - &self, - formatter: &mut serde::__private::fmt::Formatter, - ) -> serde::__private::fmt::Result { - formatter.write_str("date or empty string") - } - - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_unit(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - if v.is_empty() || v == "0000-00-00" { - Ok(None) - } else { - Ok(Date::parse(v, &DATE_FORMAT).ok()) - } - } - } - - deserializer.deserialize_any(OptionalDateVisitor) - } -} - -pub mod optional_datetime { use super::*; use serde::Serializer; use time::format_description::well_known::Rfc3339; @@ -521,20 +257,6 @@ pub mod optional_datetime { formatter.write_str("timestamp or empty string") } - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_unit(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - fn visit_str(self, v: &str) -> Result where E: serde::de::Error, @@ -549,14 +271,12 @@ pub mod optional_datetime { } } - deserializer.deserialize_any(OptionalDateVisitor) + deserializer.deserialize_str(OptionalDateVisitor) } } #[cfg(test)] mod tests { - use time::Date; - use super::*; use crate::models::subtitle::SubtitleBody; @@ -618,113 +338,17 @@ mod tests { ); } - #[test] - fn deserialize_null_if_zero() { - #[derive(Deserialize)] - struct S { - #[serde(default, deserialize_with = "null_if_zero")] - n: Option, - } - - let json = r#"{"n": 0}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, None); - - let json = r#"{"n": 1}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, Some(1)); - - let json = r#"{"n": null}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, None); - - let json = r#"{}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, None); - } - - #[test] - fn deserialize_null_if_empty() { - #[derive(Deserialize)] - struct S { - #[serde(default, deserialize_with = "null_if_empty")] - s: Option, - } - - let json = r#"{"s": ""}"#; - assert_eq!(serde_json::from_str::(json).unwrap().s, None); - - let json = r#"{"s": "a"}"#; - assert_eq!( - serde_json::from_str::(json).unwrap().s, - Some("a".to_owned()) - ); - - let json = r#"{"n": null}"#; - assert_eq!(serde_json::from_str::(json).unwrap().s, None); - - let json = r#"{}"#; - assert_eq!(serde_json::from_str::(json).unwrap().s, None); - } - - #[test] - fn deserialize_parse_int() { - #[derive(Deserialize)] - struct S { - #[serde(default, deserialize_with = "parse_int")] - n: Option, - } - - let json = r#"{"n": 0}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, Some(0)); - - let json = r#"{"n": "0"}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, Some(0)); - - let json = r#"{"n": null}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, None); - - let json = r#"{}"#; - assert_eq!(serde_json::from_str::(json).unwrap().n, None); - } - #[test] fn deserialize_optional_date() { #[derive(Deserialize)] struct S { #[serde(with = "optional_date")] - date: Option, - } - - let json_null_string = r#"{"date": null}"#; - let json_empty_string = r#"{"date": ""}"#; - let json_zero_date = r#"{"date": "0000-00-00"}"#; - let json_date = r#"{"date": "2022-08-27"}"#; - - let res = serde_json::from_str::(json_null_string).unwrap(); - assert!(res.date.is_none()); - - let res = serde_json::from_str::(json_empty_string).unwrap(); - assert!(res.date.is_none()); - - let res = serde_json::from_str::(json_zero_date).unwrap(); - assert!(res.date.is_none()); - - let res = serde_json::from_str::(json_date).unwrap(); - assert!(res.date.is_some()); - } - - #[test] - fn deserialize_optional_datetime() { - #[derive(Deserialize)] - struct S { - #[serde(with = "optional_datetime")] date: Option, } - let json_null_string = r#"{"date": null}"#; let json_empty_string = r#"{"date": ""}"#; let json_date = r#"{"date": "2022-08-27T23:47:20Z"}"#; - let res = serde_json::from_str::(json_null_string).unwrap(); - assert!(res.date.is_none()); - let res = serde_json::from_str::(json_empty_string).unwrap(); assert!(res.date.is_none()); diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs index 045779f..b556cc6 100644 --- a/src/apis/track_api.rs +++ b/src/apis/track_api.rs @@ -191,7 +191,7 @@ impl Musixmatch { /// Create a new query builder for searching tracks in the Musixmatch database. /// - /// **Note:** The search results are unordered the by default. You probably want + /// **Note:** The search results are unsorted the by default. You probably want /// to sort by popularity (`.s_track_rating(SortOrder::Desc)`) to get relevant results. /// /// # Example diff --git a/src/models/album.rs b/src/models/album.rs index 24abbf7..4996dcd 100644 --- a/src/models/album.rs +++ b/src/models/album.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use time::{Date, OffsetDateTime}; +use time::OffsetDateTime; use super::Genres; @@ -19,8 +19,10 @@ pub struct Album { /// Unique Musixmatch Album ID pub album_id: u64, /// Musicbrainz album ID - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_mbid: Option, + /// + /// **Note:** most albums dont have this entry set + #[serde(default)] + pub album_mbid: String, /// Album name pub album_name: String, /// Popularity of the album from 0 to 100 @@ -28,14 +30,14 @@ pub struct Album { pub album_rating: u8, /// Number of tracks on the album pub album_track_count: u16, - /// Album release date - #[serde(default, with = "crate::api_model::optional_date")] - pub album_release_date: Option, - /// Album type + /// Album release date (e.g. "2009-07-07") #[serde(default)] - pub album_release_type: AlbumType, + pub album_release_date: String, + /// Album type (Single / EP / Album) + pub album_release_type: String, /// Musixmatch artist ID + #[serde(default)] pub artist_id: u64, /// Artist name pub artist_name: String, @@ -46,15 +48,19 @@ pub struct Album { /// Secondary genres / Subgenres /// /// Example: primary_genres: Pop, secondary_genres: K-Pop / Mandopop + /// + /// Note that this schema is not applied to all tracks on Musixmatch. + /// There are for example many K-Pop tracks with both Pop and K-Pop + /// tagged as primary genre and this field empty. #[serde(default)] pub secondary_genres: Genres, /// Album copyright text - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_copyright: Option, + #[serde(default)] + pub album_copyright: String, /// Album label / recording company - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_label: Option, + #[serde(default)] + pub album_label: String, /// Human-readable URL-safe Album ID /// /// Example: `LMFAO/Party-Rock-5` @@ -65,31 +71,17 @@ pub struct Album { pub updated_time: OffsetDateTime, /// Album cover URL (100x100px) - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_100x100: Option, + #[serde(default)] + pub album_coverart_100x100: String, /// Album cover URL (350x350px) - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_350x350: Option, + #[serde(default)] + pub album_coverart_350x350: String, /// Album cover URL (500x500px) - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_500x500: Option, + #[serde(default)] + pub album_coverart_500x500: String, /// Album cover URL (800x800px) /// /// **Note:** not present on a lot of albums - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_800x800: Option, -} - -/// Album type -/// -/// Source: -#[allow(missing_docs)] -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum AlbumType { - #[default] - Album, - Single, - Compilation, - Remix, - Live, + #[serde(default)] + pub album_coverart_800x800: String, } diff --git a/src/models/artist.rs b/src/models/artist.rs index e6d1a76..87f337e 100644 --- a/src/models/artist.rs +++ b/src/models/artist.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use time::{Date, OffsetDateTime}; +use time::OffsetDateTime; use super::Genres; @@ -19,8 +19,10 @@ pub struct Artist { /// Musixmatch Artist ID pub artist_id: u64, /// Musicbrainz Artist ID - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_mbid: Option, + /// + /// **Note:** most tracks dont have this entry set + #[serde(default)] + pub artist_mbid: String, /// Artist name pub artist_name: String, @@ -28,8 +30,8 @@ pub struct Artist { #[serde(default)] pub artist_name_translation_list: Vec, /// Artist origin as a 2-letter country code (e.g. "US") - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_country: Option, + #[serde(default)] + pub artist_country: String, /// Alternative names for the artist (e.g. in different languages) #[serde(default)] pub artist_alias_list: Vec, @@ -51,20 +53,20 @@ pub struct Artist { pub secondary_genres: Genres, /// URL of the artist's Twitter profile - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_twitter_url: Option, + #[serde(default)] + pub artist_twitter_url: String, /// URL of the artist's website - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_website_url: Option, + #[serde(default)] + pub artist_website_url: String, /// URL of the artist's Instagram profile - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_instagram_url: Option, + #[serde(default)] + pub artist_instagram_url: String, /// URL of the artist's TikTok profile - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_tiktok_url: Option, + #[serde(default)] + pub artist_tiktok_url: String, /// URL of the artist's Facebook profile - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_facebook_url: Option, + #[serde(default)] + pub artist_facebook_url: String, /// URL-safe human-readable artist ID /// @@ -73,18 +75,22 @@ pub struct Artist { /// Date and time when the artist was last updated #[serde(with = "time::serde::rfc3339")] pub updated_time: OffsetDateTime, - /// Start year of the artist's presence - #[serde(default, deserialize_with = "crate::api_model::parse_int")] - pub begin_date_year: Option, - /// Start date of the artist's presence - #[serde(default, with = "crate::api_model::optional_date")] - pub begin_date: Option, - /// End year of the artist's presence - #[serde(default, deserialize_with = "crate::api_model::parse_int")] - pub end_date_year: Option, - /// End date of the artist's presence - #[serde(default, with = "crate::api_model::optional_date")] - pub end_date: Option, + /// Year of the start of the artist's presence + #[serde(default)] + pub begin_date_year: String, + /// Start date of the artist's presence in YYYY-MM-DD format + /// + /// **Info:** the default value is `"0000-00-00"` + #[serde(default)] + pub begin_date: String, + /// Year of the end of the artist's presence + #[serde(default)] + pub end_date_year: String, + /// End date of the artist's presence in YYYY-MM-DD format + /// + /// **Info:** the default value is `"0000-00-00"` + #[serde(default)] + pub end_date: String, } /// Alternative artist name (e.g. different languages) diff --git a/src/models/lyrics.rs b/src/models/lyrics.rs index dd9053f..b270576 100644 --- a/src/models/lyrics.rs +++ b/src/models/lyrics.rs @@ -21,11 +21,11 @@ pub struct Lyrics { /// Lyrics text pub lyrics_body: String, /// Language code (e.g. "en") - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub lyrics_language: Option, + #[serde(default)] + pub lyrics_language: String, /// Language name (e.g. "English") - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub lyrics_language_description: Option, + #[serde(default)] + pub lyrics_language_description: String, /// Copyright text of the lyrics /// /// Ends with a newline. @@ -35,8 +35,8 @@ pub struct Lyrics { /// Writer(s): David Hodges /// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p. /// ``` - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub lyrics_copyright: Option, + #[serde(default)] + pub lyrics_copyright: String, /// Date and time when the lyrics were last updated #[serde(with = "time::serde::rfc3339")] pub updated_time: OffsetDateTime, diff --git a/src/models/mod.rs b/src/models/mod.rs index 720d4e6..29a1c9d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -39,7 +39,6 @@ pub use artist::ArtistNameTranslationInner; pub(crate) mod album; pub use album::Album; -pub use album::AlbumType; pub(crate) mod snippet; pub use snippet::Snippet; diff --git a/src/models/snippet.rs b/src/models/snippet.rs index 9dc03a9..e8a5eee 100644 --- a/src/models/snippet.rs +++ b/src/models/snippet.rs @@ -18,8 +18,8 @@ pub struct Snippet { /// Unique Musixmatch Snippet ID pub snippet_id: u64, /// Snippet language code (e.g. "en") - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub snippet_language: Option, + #[serde(default)] + pub snippet_language: String, /// True if the track is instrumental #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] pub instrumental: bool, diff --git a/src/models/subtitle.rs b/src/models/subtitle.rs index 6c84a8b..17e8ac7 100644 --- a/src/models/subtitle.rs +++ b/src/models/subtitle.rs @@ -196,11 +196,11 @@ pub struct Subtitle { /// Subtitle / synchronized lyrics in the requested format pub subtitle_body: String, /// Language code (e.g. "en") - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub subtitle_language: Option, + #[serde(default)] + pub subtitle_language: String, /// Language name (e.g. "English") - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub subtitle_language_description: Option, + #[serde(default)] + pub subtitle_language_description: String, /// Copyright text of the lyrics /// /// Ends with a newline. @@ -210,8 +210,8 @@ pub struct Subtitle { /// Writer(s): David Hodges /// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p. /// ``` - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub lyrics_copyright: Option, + #[serde(default)] + pub lyrics_copyright: String, /// Duration of the subtitle in seconds pub subtitle_length: u32, /// Date and time when the subtitle was last updated @@ -239,7 +239,7 @@ pub struct SubtitleLines { /// List of subtitle lines pub lines: Vec, /// Language code (e.g. "en") - pub lang: Option, + pub lang: String, /// Duration of the subtitle in seconds pub length: u32, } @@ -268,10 +268,10 @@ impl SubtitleLines { pub fn to_ttml(&self) -> String { let mut ttml = format!( r#" - + -
"#, - self.lang.as_deref().unwrap_or_default(), +
"#, + self.lang, self.lang ); for i in 0..self.lines.len() { diff --git a/src/models/track.rs b/src/models/track.rs index 35f5f03..0a51323 100644 --- a/src/models/track.rs +++ b/src/models/track.rs @@ -20,26 +20,28 @@ pub struct Track { /// Unique Musixmatch Track ID pub track_id: u64, /// Track Musicbrainz ID - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub track_mbid: Option, + /// + /// **Note:** most tracks dont have this entry set + #[serde(default)] + pub track_mbid: String, /// [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub track_isrc: Option, + #[serde(default)] + pub track_isrc: String, /// [ISRCs](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) of equivalent tracks (e.g. on different albums) #[serde(default)] pub commontrack_isrcs: Vec>, /// Track ID on Spotify - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub track_spotify_id: Option, + #[serde(default)] + pub track_spotify_id: String, /// Spotify IDs of equivalent tracks (e.g. on different albums) #[serde(default)] pub commontrack_spotify_ids: Vec, /// Track ID on Soundcloud - #[serde(default, deserialize_with = "crate::api_model::null_if_zero")] - pub track_soundcloud_id: Option, + #[serde(default)] + pub track_soundcloud_id: u64, /// Track ID on XBox Music - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub track_xboxmusic_id: Option, + #[serde(default)] + pub track_xboxmusic_id: String, /// Title of the track pub track_name: String, @@ -76,39 +78,44 @@ pub struct Track { pub num_favourite: u32, /// Musixmatch lyrics ID - #[serde(default, deserialize_with = "crate::api_model::null_if_zero")] - pub lyrics_id: Option, + #[serde(default)] + pub lyrics_id: u64, /// Musixmatch subtitle ID - #[serde(default, deserialize_with = "crate::api_model::null_if_zero")] - pub subtitle_id: Option, + #[serde(default)] + pub subtitle_id: u64, /// Musixmatch album ID + #[serde(default)] pub album_id: u64, /// Album name + #[serde(default)] pub album_name: String, /// Musixmatch artist ID pub artist_id: u64, - /// Artist name - pub artist_name: String, /// Musicbrainz artist ID - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub artist_mbid: Option, + /// + /// **Note:** most tracks dont have this entry set + #[serde(default)] + pub artist_mbid: String, + /// Artist name + #[serde(default)] + pub artist_name: String, /// Album cover URL (100x100px) - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_100x100: Option, + #[serde(default)] + pub album_coverart_100x100: String, /// Album cover URL (350x350px) - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_350x350: Option, + #[serde(default)] + pub album_coverart_350x350: String, /// Album cover URL (500x500px) - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_500x500: Option, + #[serde(default)] + pub album_coverart_500x500: String, /// Album cover URL (800x800px) /// /// **Note:** not present on a lot of albums - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub album_coverart_800x800: Option, + #[serde(default)] + pub album_coverart_800x800: String, /// Human-readable Musixmatch ID /// @@ -118,7 +125,7 @@ pub struct Track { pub commontrack_vanity_id: String, /// Track release date - #[serde(default, with = "crate::api_model::optional_datetime")] + #[serde(default, with = "crate::api_model::optional_date")] pub first_release_date: Option, /// Date and time when the track was last updated #[serde(with = "time::serde::rfc3339")] diff --git a/src/models/translation.rs b/src/models/translation.rs index 46a4e7a..b203def 100644 --- a/src/models/translation.rs +++ b/src/models/translation.rs @@ -19,9 +19,9 @@ pub(crate) struct TranslationBody { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct Translation { - /// source language code (e.g. "en") - #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] - pub language_from: Option, + /// 2-character source language code (e.g. "en") + #[serde(default)] + pub language_from: String, /// Translated line #[serde(default)] pub description: String, @@ -102,7 +102,7 @@ impl TranslationMap { time: line.time, }) .collect::>(), - lang: Some(self.lang.to_owned()), + lang: self.lang.to_owned(), length: subtitles.length, } } diff --git a/tests/_fixtures.rs b/tests/_fixtures.rs new file mode 100644 index 0000000..a754885 --- /dev/null +++ b/tests/_fixtures.rs @@ -0,0 +1,21 @@ +use musixmatch_inofficial::{storage::FileStorage, Musixmatch}; + +#[ctor::ctor] +fn init() { + let _ = dotenvy::dotenv(); + env_logger::init(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(new_mxm().login()) + .unwrap(); +} + +pub fn new_mxm() -> Musixmatch { + Musixmatch::new( + &std::env::var("MUSIXMATCH_EMAIL").unwrap(), + &std::env::var("MUSIXMATCH_PASSWORD").unwrap(), + Some(Box::new(FileStorage::default())), + ) +} diff --git a/tests/album_test.rs b/tests/album_test.rs new file mode 100644 index 0000000..8f6d764 --- /dev/null +++ b/tests/album_test.rs @@ -0,0 +1,104 @@ +mod _fixtures; +use _fixtures::*; + +use musixmatch_inofficial::{ + models::{AlbumId, ArtistId}, + Error, +}; +use rstest::rstest; +use time::macros::datetime; + +#[rstest] +#[case::id(AlbumId::AlbumId(14248253))] +#[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))] +#[tokio::test] +async fn by_id(#[case] album_id: AlbumId<'_>) { + let album = new_mxm().album(album_id).await.unwrap(); + + assert_eq!(album.album_id, 14248253); + assert_eq!(album.album_mbid, "6c3cf9d8-88a8-43ed-850b-55813f01e451"); + assert_eq!(album.album_name, "Gangnam Style (강남스타일)"); + assert!(album.album_rating > 25); + assert_eq!(album.album_track_count, 1); + assert_eq!(album.album_release_date, "2012-01-01"); + assert_eq!(album.album_release_type, "Single"); + assert_eq!(album.artist_id, 410698); + assert_eq!(album.artist_name, "PSY"); + + let first_pri_genre = &album.primary_genres.music_genre_list[0].music_genre; + assert_eq!(first_pri_genre.music_genre_id, 14); + assert_eq!(first_pri_genre.music_genre_parent_id, 34); + assert_eq!(first_pri_genre.music_genre_name, "Pop"); + assert_eq!(first_pri_genre.music_genre_name_extended, "Pop"); + assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); + + let first_sec_genre = &album.secondary_genres.music_genre_list[0].music_genre; + assert_eq!(first_sec_genre.music_genre_id, 17); + assert_eq!(first_sec_genre.music_genre_parent_id, 34); + assert_eq!(first_sec_genre.music_genre_name, "Dance"); + assert_eq!(first_sec_genre.music_genre_name_extended, "Dance"); + assert_eq!( + first_sec_genre.music_genre_vanity.as_ref().unwrap(), + "Dance" + ); + + assert_eq!( + album.album_copyright, + "© 2012 Schoolboy/Universal Republic Records, a division of UMG Recordings, Inc." + ); + assert_eq!( + album.album_label, + "Silent Records/Universal Republic Records" + ); + assert_eq!(album.album_vanity_id, "410698/Gangnam-Style-Single"); + assert!(album.updated_time > datetime!(2022-6-3 0:00 UTC)); + assert_eq!( + album.album_coverart_100x100, + "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045.jpg" + ); + assert_eq!( + album.album_coverart_350x350, + "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_350_350.jpg" + ); + assert_eq!( + album.album_coverart_500x500, + "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_500_500.jpg" + ); +} + +#[tokio::test] +async fn by_id_missing() { + let err = new_mxm() + .album(AlbumId::AlbumId(999999999999)) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[tokio::test] +async fn artist_albums() { + let albums = new_mxm() + .artist_albums(ArtistId::ArtistId(1039), None, 10, 1) + .await + .unwrap(); + + assert_eq!(albums.len(), 10); +} + +#[tokio::test] +async fn artist_albums_missing() { + let err = new_mxm() + .artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[tokio::test] +async fn charts() { + let albums = new_mxm().chart_albums("US", 10, 1).await.unwrap(); + + assert_eq!(albums.len(), 10); +} diff --git a/tests/artist_test.rs b/tests/artist_test.rs new file mode 100644 index 0000000..9048190 --- /dev/null +++ b/tests/artist_test.rs @@ -0,0 +1,116 @@ +mod _fixtures; +use _fixtures::*; + +use musixmatch_inofficial::{models::ArtistId, Error}; +use rstest::rstest; +use time::macros::datetime; + +#[rstest] +#[case::id(ArtistId::ArtistId(410698))] +#[case::musicbrainz(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))] +#[tokio::test] +async fn by_id(#[case] artist_id: ArtistId<'_>) { + let artist = new_mxm().artist(artist_id).await.unwrap(); + + // dbg!(&artist); + + assert_eq!(artist.artist_id, 410698); + assert_eq!(artist.artist_mbid, "f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"); + assert_eq!(artist.artist_name, "PSY"); + assert!( + artist.artist_name_translation_list.iter().any(|tl| { + tl.artist_name_translation.language == "KO" + && tl.artist_name_translation.translation == "싸이" + }), + "missing Korean translation in: {:?}", + artist.artist_name_translation_list + ); + assert_eq!(artist.artist_country, "KR"); + assert!(artist.artist_rating > 50); + let first_genre = &artist.primary_genres.music_genre_list[0].music_genre; + assert_eq!(first_genre.music_genre_id, 14); + assert_eq!(first_genre.music_genre_parent_id, 34); + assert_eq!(first_genre.music_genre_name, "Pop"); + assert_eq!(first_genre.music_genre_name_extended, "Pop"); + assert_eq!(first_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); + assert_eq!(artist.artist_twitter_url, "https://twitter.com/psy_oppa"); + assert_eq!( + artist.artist_facebook_url, + "https://www.facebook.com/officialpsy" + ); + assert_eq!(artist.artist_vanity_id, "410698"); + assert!(artist.updated_time > datetime!(2016-6-30 0:00 UTC)); + assert_eq!(artist.begin_date_year, "1977"); + assert_eq!(artist.begin_date, "1977-12-31"); + assert_eq!(artist.end_date_year, ""); + assert_eq!(artist.end_date, "0000-00-00"); +} + +#[tokio::test] +async fn by_id_missing() { + let err = new_mxm() + .artist(ArtistId::ArtistId(999999999999)) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[tokio::test] +async fn related() { + let artists = new_mxm() + .artist_related(ArtistId::ArtistId(26485840), 10, 1) + .await + .unwrap(); + + assert_eq!(artists.len(), 10); +} + +#[tokio::test] +async fn related_missing() { + let err = new_mxm() + .artist_related(ArtistId::ArtistId(999999999999), 10, 1) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[tokio::test] +async fn search() { + let artists = new_mxm().artist_search("psy", 5, 1).await.unwrap(); + + assert_eq!(artists.len(), 5); + + let artist = &artists[0]; + assert_eq!(artist.artist_id, 410698); + assert_eq!(artist.artist_name, "PSY"); +} + +#[tokio::test] +async fn search_empty() { + let artists = new_mxm() + .artist_search( + "Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz", + 5, + 1, + ) + .await + .unwrap(); + + assert_eq!(artists.len(), 0); +} + +#[tokio::test] +async fn charts() { + let artists = new_mxm().chart_artists("US", 10, 1).await.unwrap(); + + assert_eq!(artists.len(), 10); +} + +#[tokio::test] +async fn charts_no_country() { + let artists = new_mxm().chart_artists("XY", 10, 1).await.unwrap(); + + assert_eq!(artists.len(), 10); +} diff --git a/tests/lyrics_test.rs b/tests/lyrics_test.rs new file mode 100644 index 0000000..e8d91af --- /dev/null +++ b/tests/lyrics_test.rs @@ -0,0 +1,145 @@ +mod _fixtures; +use _fixtures::*; + +use std::{fs::File, io::BufWriter, path::Path}; + +use futures::stream::{self, StreamExt}; +use musixmatch_inofficial::{models::TrackId, Error}; +use rstest::rstest; +use time::macros::datetime; + +#[tokio::test] +async fn from_match() { + let lyrics = new_mxm().matcher_lyrics("Shine", "Spektrem").await.unwrap(); + + // dbg!(&lyrics); + + assert_eq!(lyrics.lyrics_id, 25947036); + assert!(!lyrics.instrumental); + assert!(!lyrics.explicit); + assert!(lyrics + .lyrics_body + .starts_with("Eyes, in the sky, gazing far into the night\n")); + assert_eq!(lyrics.lyrics_language, "en"); + assert_eq!(lyrics.lyrics_language_description, "English"); + assert!( + lyrics.lyrics_copyright.contains("Kim Jeffeson"), + "copyright: {}", + lyrics.lyrics_copyright, + ); + assert!(lyrics.updated_time > datetime!(2021-6-3 0:00 UTC)); +} + +#[rstest] +#[case::trackid(TrackId::TrackId(205688271))] +#[case::commontrack(TrackId::Commontrack(118480583))] +#[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba"))] +#[case::isrc(TrackId::Isrc("KRA302000590"))] +#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51"))] +#[tokio::test] +async fn from_id(#[case] track_id: TrackId<'_>) { + let lyrics = new_mxm().track_lyrics(track_id).await.unwrap(); + + // dbg!(&lyrics); + + assert_eq!(lyrics.lyrics_id, 30126001); + assert_eq!(lyrics.lyrics_language, "ko"); + assert_eq!(lyrics.lyrics_language_description, "Korean"); + assert!( + lyrics.lyrics_copyright.contains("Kenneth Scott Chesak"), + "copyright: {}", + lyrics.lyrics_copyright, + ); + assert!(lyrics.updated_time > datetime!(2022-8-27 0:00 UTC)); +} + +/// This track has no lyrics +#[tokio::test] +async fn instrumental() { + let lyrics = new_mxm() + .matcher_lyrics("drivers license", "Bobby G") + .await + .unwrap(); + + assert_eq!(lyrics.lyrics_id, 25891609); + assert!(lyrics.instrumental); + assert!(!lyrics.explicit); + assert_eq!(lyrics.lyrics_body, ""); + assert_eq!(lyrics.lyrics_language, ""); + assert_eq!(lyrics.lyrics_language_description, ""); + assert_eq!(lyrics.lyrics_copyright, ""); + assert!(lyrics.updated_time > datetime!(2021-6-21 0:00 UTC)); +} + +/// This track does not exist +#[tokio::test] +async fn missing() { + let err = new_mxm() + .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn")) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[tokio::test] +async fn download_testdata() { + let json_path = Path::new("testfiles/lyrics.json"); + if json_path.exists() { + return; + } + + let lyrics = new_mxm() + .track_lyrics(TrackId::Commontrack(18576954)) + .await + .unwrap(); + + let json_file = File::create(json_path).unwrap(); + serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap(); +} + +#[tokio::test] +async fn download_testdata_translation() { + let json_path = Path::new("testfiles/translation.json"); + if json_path.exists() { + return; + } + + let translations = new_mxm() + .track_lyrics_translation(TrackId::Commontrack(18576954), "de") + .await + .unwrap(); + + let json_file = File::create(json_path).unwrap(); + serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap(); +} + +#[tokio::test] +async fn concurrency() { + let mxm = new_mxm(); + + let album = mxm + .album_tracks( + musixmatch_inofficial::models::AlbumId::AlbumId(17118624), + true, + 20, + 1, + ) + .await + .unwrap(); + + let x = stream::iter(album) + .map(|track| { + mxm.track_lyrics(musixmatch_inofficial::models::TrackId::TrackId( + track.track_id, + )) + }) + .buffered(8) + .collect::>() + .await + .into_iter() + .map(Result::unwrap) + .collect::>(); + + dbg!(x); +} diff --git a/tests/subtitle_test.rs b/tests/subtitle_test.rs new file mode 100644 index 0000000..6f9e86c --- /dev/null +++ b/tests/subtitle_test.rs @@ -0,0 +1,135 @@ +mod _fixtures; +use _fixtures::*; + +use std::{fs::File, io::BufWriter, path::Path}; + +use musixmatch_inofficial::{ + models::{SubtitleFormat, TrackId}, + Error, +}; +use rstest::rstest; +use time::macros::datetime; + +#[tokio::test] +async fn from_match() { + let subtitle = new_mxm() + .matcher_subtitle( + "Shine", + "Spektrem", + SubtitleFormat::Json, + Some(315.0), + Some(1.0), + ) + .await + .unwrap(); + + // dbg!(&subtitle); + + assert_eq!(subtitle.subtitle_id, 36913312); + assert_eq!(subtitle.subtitle_language, "en"); + assert_eq!(subtitle.subtitle_language_description, "English"); + assert!( + subtitle.lyrics_copyright.contains("Kim Jeffeson"), + "copyright: {}", + subtitle.lyrics_copyright, + ); + assert_eq!(subtitle.subtitle_length, 315); + assert!(subtitle.updated_time > datetime!(2021-6-30 0:00 UTC)); +} + +#[rstest] +#[case::trackid(TrackId::TrackId(205688271))] +#[case::commontrack(TrackId::Commontrack(118480583))] +#[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba"))] +#[case::isrc(TrackId::Isrc("KRA302000590"))] +#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51"))] +#[tokio::test] +async fn from_id(#[case] track_id: TrackId<'_>) { + let subtitle = new_mxm() + .track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0)) + .await + .unwrap(); + + // dbg!(&subtitle); + + assert_eq!(subtitle.subtitle_id, 36476905); + assert_eq!(subtitle.subtitle_language, "ko"); + assert_eq!(subtitle.subtitle_language_description, "Korean"); + assert!( + subtitle.lyrics_copyright.contains("Kenneth Scott Chesak"), + "copyright: {}", + subtitle.lyrics_copyright, + ); + assert_eq!(subtitle.subtitle_length, 175); + assert!(subtitle.updated_time > datetime!(2022-8-27 0:00 UTC)); +} + +/// This track has no lyrics +#[tokio::test] +async fn instrumental() { + let err = new_mxm() + .matcher_subtitle( + "drivers license", + "Bobby G", + SubtitleFormat::Json, + Some(246.0), + Some(1.0), + ) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +/// This track has not been synced +#[tokio::test] +async fn unsynced() { + let err = new_mxm() + .track_subtitle( + TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1"), + SubtitleFormat::Json, + Some(213.0), + Some(1.0), + ) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +/// Try to get subtitles with wrong length parameter +#[tokio::test] +async fn wrong_length() { + let err = new_mxm() + .track_subtitle( + TrackId::Commontrack(118480583), + SubtitleFormat::Json, + Some(200.0), + Some(1.0), + ) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[tokio::test] +async fn download_testdata() { + let json_path = Path::new("testfiles/subtitles.json"); + if json_path.exists() { + return; + } + + let subtitle = new_mxm() + .track_subtitle( + TrackId::Commontrack(18576954), + SubtitleFormat::Json, + Some(259.0), + Some(1.0), + ) + .await + .unwrap(); + + let json_file = File::create(json_path).unwrap(); + serde_json::to_writer_pretty(BufWriter::new(json_file), &subtitle).unwrap(); +} diff --git a/tests/tests.rs b/tests/tests.rs deleted file mode 100644 index 00a3663..0000000 --- a/tests/tests.rs +++ /dev/null @@ -1,980 +0,0 @@ -use rstest::rstest; -use time::macros::{date, datetime}; - -use musixmatch_inofficial::{ - models::{AlbumId, ArtistId, TrackId}, - storage::FileStorage, - Error, Musixmatch, -}; - -#[ctor::ctor] -fn init() { - let _ = dotenvy::dotenv(); - env_logger::init(); - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(new_mxm().login()) - .unwrap(); -} - -pub fn new_mxm() -> Musixmatch { - Musixmatch::new( - &std::env::var("MUSIXMATCH_EMAIL").unwrap(), - &std::env::var("MUSIXMATCH_PASSWORD").unwrap(), - Some(Box::::default()), - ) -} - -mod album { - use super::*; - use musixmatch_inofficial::models::AlbumType; - - #[rstest] - #[case::id(AlbumId::AlbumId(14248253))] - #[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))] - #[tokio::test] - async fn by_id(#[case] album_id: AlbumId<'_>) { - let album = new_mxm().album(album_id).await.unwrap(); - - assert_eq!(album.album_id, 14248253); - assert_eq!( - album.album_mbid.unwrap(), - "6c3cf9d8-88a8-43ed-850b-55813f01e451" - ); - assert_eq!(album.album_name, "Gangnam Style (강남스타일)"); - assert!(album.album_rating > 25); - assert_eq!(album.album_track_count, 1); - assert_eq!(album.album_release_date.unwrap(), date!(2012 - 01 - 01)); - assert_eq!(album.album_release_type, AlbumType::Single); - assert_eq!(album.artist_id, 410698); - assert_eq!(album.artist_name, "PSY"); - - let first_pri_genre = &album.primary_genres.music_genre_list[0].music_genre; - assert_eq!(first_pri_genre.music_genre_id, 14); - assert_eq!(first_pri_genre.music_genre_parent_id, 34); - assert_eq!(first_pri_genre.music_genre_name, "Pop"); - assert_eq!(first_pri_genre.music_genre_name_extended, "Pop"); - assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); - - let first_sec_genre = &album.secondary_genres.music_genre_list[0].music_genre; - assert_eq!(first_sec_genre.music_genre_id, 17); - assert_eq!(first_sec_genre.music_genre_parent_id, 34); - assert_eq!(first_sec_genre.music_genre_name, "Dance"); - assert_eq!(first_sec_genre.music_genre_name_extended, "Dance"); - assert_eq!( - first_sec_genre.music_genre_vanity.as_ref().unwrap(), - "Dance" - ); - - assert_eq!( - album.album_copyright.unwrap(), - "© 2012 Schoolboy/Universal Republic Records, a division of UMG Recordings, Inc." - ); - assert_eq!( - album.album_label.unwrap(), - "Silent Records/Universal Republic Records" - ); - assert_eq!(album.album_vanity_id, "410698/Gangnam-Style-Single"); - assert!(album.updated_time > datetime!(2022-6-3 0:00 UTC)); - assert_eq!( - album.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045.jpg" - ); - assert_eq!( - album.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_350_350.jpg" - ); - assert_eq!( - album.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_500_500.jpg" - ); - } - - #[tokio::test] - async fn album_tst() { - let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap(); - println!("type: {:?}", album.album_release_type); - println!("date: {:?}", album.album_release_date); - } - - #[tokio::test] - async fn by_id_missing() { - let err = new_mxm() - .album(AlbumId::AlbumId(999999999999)) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[tokio::test] - async fn artist_albums() { - let albums = new_mxm() - .artist_albums(ArtistId::ArtistId(1039), None, 10, 1) - .await - .unwrap(); - - assert_eq!(albums.len(), 10); - } - - #[tokio::test] - async fn artist_albums_missing() { - let err = new_mxm() - .artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[tokio::test] - async fn charts() { - let albums = new_mxm().chart_albums("US", 10, 1).await.unwrap(); - - assert_eq!(albums.len(), 10); - } -} - -mod artist { - use super::*; - - #[rstest] - #[case::id(ArtistId::ArtistId(410698))] - #[case::musicbrainz(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))] - #[tokio::test] - async fn by_id(#[case] artist_id: ArtistId<'_>) { - let artist = new_mxm().artist(artist_id).await.unwrap(); - - // dbg!(&artist); - - assert_eq!(artist.artist_id, 410698); - assert_eq!( - artist.artist_mbid.unwrap(), - "f99b7d67-4e63-4678-aa66-4c6ac0f7d24a" - ); - assert_eq!(artist.artist_name, "PSY"); - assert!( - artist.artist_name_translation_list.iter().any(|tl| { - tl.artist_name_translation.language == "KO" - && tl.artist_name_translation.translation == "싸이" - }), - "missing Korean translation in: {:?}", - artist.artist_name_translation_list - ); - assert_eq!(artist.artist_country.unwrap(), "KR"); - assert!(artist.artist_rating > 50); - let first_genre = &artist.primary_genres.music_genre_list[0].music_genre; - assert_eq!(first_genre.music_genre_id, 14); - assert_eq!(first_genre.music_genre_parent_id, 34); - assert_eq!(first_genre.music_genre_name, "Pop"); - assert_eq!(first_genre.music_genre_name_extended, "Pop"); - assert_eq!(first_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); - assert_eq!( - artist.artist_twitter_url.unwrap(), - "https://twitter.com/psy_oppa" - ); - assert_eq!( - artist.artist_facebook_url.unwrap(), - "https://www.facebook.com/officialpsy" - ); - assert_eq!(artist.artist_vanity_id, "410698"); - assert!(artist.updated_time > datetime!(2016-6-30 0:00 UTC)); - assert_eq!(artist.begin_date_year.unwrap(), 1977); - assert_eq!(artist.begin_date.unwrap(), date!(1977 - 12 - 31)); - assert_eq!(artist.end_date_year, None); - assert_eq!(artist.end_date, None); - } - - #[tokio::test] - async fn by_id_missing() { - let err = new_mxm() - .artist(ArtistId::ArtistId(999999999999)) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[tokio::test] - async fn related() { - let artists = new_mxm() - .artist_related(ArtistId::ArtistId(26485840), 10, 1) - .await - .unwrap(); - - assert_eq!(artists.len(), 10); - } - - #[tokio::test] - async fn related_missing() { - let err = new_mxm() - .artist_related(ArtistId::ArtistId(999999999999), 10, 1) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[tokio::test] - async fn search() { - let artists = new_mxm().artist_search("psy", 5, 1).await.unwrap(); - - assert_eq!(artists.len(), 5); - - let artist = &artists[0]; - assert_eq!(artist.artist_id, 410698); - assert_eq!(artist.artist_name, "PSY"); - } - - #[tokio::test] - async fn search_empty() { - let artists = new_mxm() - .artist_search( - "Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz", - 5, - 1, - ) - .await - .unwrap(); - - assert_eq!(artists.len(), 0); - } - - #[tokio::test] - async fn charts() { - let artists = new_mxm().chart_artists("US", 10, 1).await.unwrap(); - - assert_eq!(artists.len(), 10); - } - - #[tokio::test] - async fn charts_no_country() { - let artists = new_mxm().chart_artists("XY", 10, 1).await.unwrap(); - - assert_eq!(artists.len(), 10); - } -} - -mod track { - use super::*; - use musixmatch_inofficial::models::{ChartName, SortOrder}; - - #[rstest] - #[case::no_translation(false, false)] - #[case::translation_2c(true, false)] - #[case::translation_3c(true, true)] - #[tokio::test] - async fn from_match(#[case] translation_status: bool, #[case] lang_3c: bool) { - let track = new_mxm() - .matcher_track( - "Poker Face", - "Lady Gaga", - "The Fame", - translation_status, - lang_3c, - ) - .await - .unwrap(); - - // dbg!(&track); - - assert_eq!(track.track_id, 15476784); - assert_eq!( - track.track_mbid.unwrap(), - "080975b0-39b1-493c-ae64-5cb3292409bb" - ); - assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); - assert!( - track.commontrack_isrcs[0] - .iter() - .any(|isrc| isrc == "USUM70824409"), - "commontrack_isrcs: {:?}", - &track.commontrack_isrcs[0], - ); - assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO"); - assert!( - track - .commontrack_spotify_ids - .iter() - .any(|spid| spid == "1QV6tiMFM6fSOKOGLMHYYg"), - "commontrack_spotify_ids: {:?}", - track.commontrack_spotify_ids, - ); - assert_eq!(track.track_name, "Poker Face"); - assert!(track.track_rating > 50); - assert_eq!(track.commontrack_id, 47672612); - assert!(!track.instrumental); - assert!(track.explicit); - assert!(track.has_subtitles); - assert!(track.has_richsync); - assert!(track.has_track_structure); - assert!(track.num_favourite > 50); - assert_eq!(track.lyrics_id.unwrap(), 30678771); - assert_eq!(track.subtitle_id.unwrap(), 36450705); - assert_eq!(track.album_id, 13810402); - assert_eq!(track.album_name, "The Fame"); - assert_eq!(track.artist_id, 378462); - assert_eq!( - track.artist_mbid.unwrap(), - "650e7db6-b795-4eb5-a702-5ea2fc46c848" - ); - assert_eq!(track.artist_name, "Lady Gaga"); - assert_eq!( - track.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg" - ); - assert_eq!( - track.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg" - ); - assert_eq!( - track.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg" - ); - assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); - let first_release = track.first_release_date.unwrap(); - assert_eq!(first_release.date(), date!(2008 - 1 - 1)); - assert!(track.updated_time > datetime!(2023-1-17 0:00 UTC)); - - let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre; - assert_eq!(first_pri_genre.music_genre_id, 14); - assert_eq!(first_pri_genre.music_genre_parent_id, 34); - assert_eq!(first_pri_genre.music_genre_name, "Pop"); - assert_eq!(first_pri_genre.music_genre_name_extended, "Pop"); - assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); - - let first_sec_genre = &track.secondary_genres.music_genre_list[0].music_genre; - assert_eq!(first_sec_genre.music_genre_id, 17); - assert_eq!(first_sec_genre.music_genre_parent_id, 34); - assert_eq!(first_sec_genre.music_genre_name, "Dance"); - assert_eq!(first_sec_genre.music_genre_name_extended, "Dance"); - assert_eq!( - first_sec_genre.music_genre_vanity.as_ref().unwrap(), - "Dance" - ); - - if translation_status { - assert!( - track.track_lyrics_translation_status.iter().all(|tl| { - (if lang_3c { - tl.from == "eng" - } else { - tl.from == "en" - }) && tl.perc >= 0.0 - && tl.perc <= 1.0 - }), - "translation: {:?}", - track.track_lyrics_translation_status - ); - } else { - assert!(track.track_lyrics_translation_status.is_empty()) - } - } - - #[rstest] - #[case::trackid(TrackId::TrackId(205688271))] - #[case::commontrack(TrackId::Commontrack(118480583))] - #[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba"))] - #[case::isrc(TrackId::Isrc("KRA302000590"))] - #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51"))] - #[tokio::test] - async fn from_id(#[case] track_id: TrackId<'_>) { - let track = new_mxm().track(track_id, true, false).await.unwrap(); - - // dbg!(&track); - - assert_eq!(track.track_id, 205688271); - assert_eq!(track.track_isrc.unwrap(), "KRA302000590"); - assert_eq!(track.track_spotify_id.unwrap(), "1t2qYCAjUAoGfeFeoBlK51"); - assert_eq!(track.track_name, "Black Mamba"); - assert!(track.track_rating > 50); - assert_eq!(track.track_length, 175); - assert!(!track.explicit); - assert!(track.has_lyrics); - assert!(track.has_subtitles); - assert!(track.has_richsync); - assert!(track.num_favourite > 200); - assert_eq!(track.lyrics_id.unwrap(), 30126001); - assert_eq!(track.subtitle_id.unwrap(), 36476905); - assert_eq!(track.album_id, 41035954); - assert_eq!(track.album_name, "Black Mamba - Single"); - assert_eq!(track.artist_id, 46970441); - assert_eq!(track.artist_name, "aespa"); - assert_eq!( - track.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772.jpg" - ); - assert_eq!( - track.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_350_350.jpg" - ); - assert_eq!( - track.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_500_500.jpg" - ); - assert_eq!(track.commontrack_vanity_id, "aespa/Black-Mamba"); - - let release_date = track.first_release_date.unwrap(); - assert_eq!(release_date.date(), date!(2020 - 11 - 17)); - assert!(track.updated_time > datetime!(2022-8-27 0:00 UTC)); - - let first_tstatus = &track.track_lyrics_translation_status[0]; - assert_eq!(first_tstatus.from, "ko"); - assert!(first_tstatus.perc >= 0.0 && first_tstatus.perc <= 1.0); - } - - #[rstest] - #[case::no_translation(false, false)] - #[case::translation_2c(true, false)] - #[case::translation_3c(true, true)] - #[tokio::test] - async fn from_id_translations(#[case] translation_status: bool, #[case] lang_3c: bool) { - let track = new_mxm() - .track(TrackId::TrackId(15476784), translation_status, lang_3c) - .await - .unwrap(); - - // dbg!(&track); - - assert_eq!(track.track_id, 15476784); - assert_eq!( - track.track_mbid.unwrap(), - "080975b0-39b1-493c-ae64-5cb3292409bb" - ); - assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); - assert!( - track.commontrack_isrcs[0] - .iter() - .any(|isrc| isrc == "USUM70824409"), - "commontrack_isrcs: {:?}", - &track.commontrack_isrcs[0], - ); - assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO"); - assert!( - track - .commontrack_spotify_ids - .iter() - .any(|spid| spid == "1QV6tiMFM6fSOKOGLMHYYg"), - "commontrack_spotify_ids: {:?}", - track.commontrack_spotify_ids, - ); - assert_eq!(track.track_name, "Poker Face"); - assert!(track.track_rating > 50); - assert_eq!(track.commontrack_id, 47672612); - assert!(!track.instrumental); - assert!(track.explicit); - assert!(track.has_subtitles); - assert!(track.has_richsync); - assert!(track.has_track_structure); - assert!(track.num_favourite > 50); - assert_eq!(track.lyrics_id.unwrap(), 30678771); - assert_eq!(track.subtitle_id.unwrap(), 36450705); - assert_eq!(track.album_id, 13810402); - assert_eq!(track.album_name, "The Fame"); - assert_eq!(track.artist_id, 378462); - assert_eq!( - track.artist_mbid.unwrap(), - "650e7db6-b795-4eb5-a702-5ea2fc46c848" - ); - assert_eq!(track.artist_name, "Lady Gaga"); - assert_eq!( - track.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg" - ); - assert_eq!( - track.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg" - ); - assert_eq!( - track.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg" - ); - assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); - let first_release = track.first_release_date.unwrap(); - assert_eq!(first_release.date(), date!(2008 - 1 - 1)); - assert!(track.updated_time > datetime!(2023-1-17 0:00 UTC)); - - let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre; - assert_eq!(first_pri_genre.music_genre_id, 14); - assert_eq!(first_pri_genre.music_genre_parent_id, 34); - assert_eq!(first_pri_genre.music_genre_name, "Pop"); - assert_eq!(first_pri_genre.music_genre_name_extended, "Pop"); - assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); - - let first_sec_genre = &track.secondary_genres.music_genre_list[0].music_genre; - assert_eq!(first_sec_genre.music_genre_id, 17); - assert_eq!(first_sec_genre.music_genre_parent_id, 34); - assert_eq!(first_sec_genre.music_genre_name, "Dance"); - assert_eq!(first_sec_genre.music_genre_name_extended, "Dance"); - assert_eq!( - first_sec_genre.music_genre_vanity.as_ref().unwrap(), - "Dance" - ); - - if translation_status { - assert!( - track.track_lyrics_translation_status.iter().all(|tl| { - (if lang_3c { - tl.from == "eng" - } else { - tl.from == "en" - }) && tl.perc >= 0.0 - && tl.perc <= 1.0 - }), - "translation: {:?}", - track.track_lyrics_translation_status - ); - } else { - assert!(track.track_lyrics_translation_status.is_empty()) - } - } - - #[tokio::test] - async fn from_id_missing() { - let err = new_mxm() - .track(TrackId::TrackId(999999999999), false, false) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[tokio::test] - async fn album_tracks() { - let tracks = new_mxm() - .album_tracks(AlbumId::AlbumId(17118624), true, 20, 1) - .await - .unwrap(); - - // dbg!(&tracks); - - let track_names = tracks - .iter() - .map(|t| t.track_name.to_owned()) - .collect::>(); - - assert_eq!( - track_names, - vec![ - "Gäa", - "Vergiss mein nicht", - "Orome", - "Falke flieg", - "Minne", - "Das Lied der Ahnen", - "Hörst du den Wind", - "Nan Úye", - "Faolan", - "Hymne der Nacht", - "Avalon", - "Tolo Nan", - "Oonagh", - ] - ); - - tracks.iter().for_each(|t| { - assert!(t.has_lyrics); - assert!(t.has_subtitles); - }); - } - - #[tokio::test] - async fn album_missing() { - let err = new_mxm() - .album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[rstest] - #[case::top(ChartName::Top)] - #[case::hot(ChartName::Hot)] - #[tokio::test] - async fn charts(#[case] chart_name: ChartName) { - let tracks = new_mxm() - .chart_tracks("US", chart_name, true, 20, 1) - .await - .unwrap(); - - assert_eq!(tracks.len(), 20); - } - - #[tokio::test] - async fn search() { - let tracks = new_mxm() - .track_search() - .q_artist("Lena") - .q_track("Satellite") - .s_track_rating(SortOrder::Desc) - .send(1, 0) - .await - .unwrap(); - - // dbg!(&tracks); - - assert_eq!(tracks.len(), 1); - - let track = &tracks[0]; - assert_eq!(track.commontrack_id, 12426476); - assert_eq!(track.track_name, "Satellite"); - assert_eq!(track.artist_name, "Lena"); - } - - #[tokio::test] - async fn search_lyrics() { - let tracks = new_mxm() - .track_search() - .q_lyrics("the whole world stops and stares for a while") - .s_track_rating(SortOrder::Desc) - .send(10, 1) - .await - .unwrap(); - - assert_eq!(tracks.len(), 10); - - let track = &tracks[0]; - assert_eq!(track.track_name, "Just the Way You Are"); - assert_eq!(track.artist_name, "Bruno Mars"); - } - - #[tokio::test] - async fn search_empty() { - let artists = new_mxm() - .track_search() - .q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz") - .send(10, 1) - .await - .unwrap(); - - assert_eq!(artists.len(), 0); - } - - #[tokio::test] - async fn genres() { - let genres = new_mxm().genres().await.unwrap(); - assert!(genres.len() > 360); - } - - #[tokio::test] - async fn snippet() { - let snippet = new_mxm() - .track_snippet(TrackId::Commontrack(8874280)) - .await - .unwrap(); - - assert_eq!(snippet.snippet_id, 23036767); - assert_eq!(snippet.snippet_language.unwrap(), "en"); - assert!(!snippet.instrumental); - assert!(snippet.updated_time > datetime!(2022-8-29 0:00 UTC)); - assert_eq!( - snippet.snippet_body, - "There's not a thing that I would change" - ); - } -} - -mod lyrics { - use futures::stream::{self, StreamExt}; - use std::{fs::File, io::BufWriter, path::Path}; - - use super::*; - - #[tokio::test] - async fn from_match() { - let lyrics = new_mxm().matcher_lyrics("Shine", "Spektrem").await.unwrap(); - - // dbg!(&lyrics); - - assert_eq!(lyrics.lyrics_id, 25947036); - assert!(!lyrics.instrumental); - assert!(!lyrics.explicit); - assert!(lyrics - .lyrics_body - .starts_with("Eyes, in the sky, gazing far into the night\n")); - assert_eq!(lyrics.lyrics_language.unwrap(), "en"); - assert_eq!(lyrics.lyrics_language_description.unwrap(), "English"); - let copyright = lyrics.lyrics_copyright.unwrap(); - assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",); - assert!(lyrics.updated_time > datetime!(2021-6-3 0:00 UTC)); - } - - #[rstest] - #[case::trackid(TrackId::TrackId(205688271))] - #[case::commontrack(TrackId::Commontrack(118480583))] - #[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba"))] - #[case::isrc(TrackId::Isrc("KRA302000590"))] - #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51"))] - #[tokio::test] - async fn from_id(#[case] track_id: TrackId<'_>) { - let lyrics = new_mxm().track_lyrics(track_id).await.unwrap(); - - // dbg!(&lyrics); - - assert_eq!(lyrics.lyrics_id, 30126001); - assert_eq!(lyrics.lyrics_language.unwrap(), "ko"); - assert_eq!(lyrics.lyrics_language_description.unwrap(), "Korean"); - let copyright = lyrics.lyrics_copyright.unwrap(); - assert!( - copyright.contains("Kenneth Scott Chesak"), - "copyright: {copyright}", - ); - assert!(lyrics.updated_time > datetime!(2022-8-27 0:00 UTC)); - } - - /// This track has no lyrics - #[tokio::test] - async fn instrumental() { - let lyrics = new_mxm() - .matcher_lyrics("drivers license", "Bobby G") - .await - .unwrap(); - - assert_eq!(lyrics.lyrics_id, 25891609); - assert!(lyrics.instrumental); - assert!(!lyrics.explicit); - assert_eq!(lyrics.lyrics_body, ""); - assert_eq!(lyrics.lyrics_language, None); - assert_eq!(lyrics.lyrics_language_description, None); - assert_eq!(lyrics.lyrics_copyright, None); - assert!(lyrics.updated_time > datetime!(2021-6-21 0:00 UTC)); - } - - /// This track does not exist - #[tokio::test] - async fn missing() { - let err = new_mxm() - .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn")) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[tokio::test] - async fn download_testdata() { - let json_path = Path::new("testfiles/lyrics.json"); - if json_path.exists() { - return; - } - - let lyrics = new_mxm() - .track_lyrics(TrackId::Commontrack(18576954)) - .await - .unwrap(); - - let json_file = File::create(json_path).unwrap(); - serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap(); - } - - #[tokio::test] - async fn download_testdata_translation() { - let json_path = Path::new("testfiles/translation.json"); - if json_path.exists() { - return; - } - - let translations = new_mxm() - .track_lyrics_translation(TrackId::Commontrack(18576954), "de") - .await - .unwrap(); - - let json_file = File::create(json_path).unwrap(); - serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap(); - } - - #[tokio::test] - async fn concurrency() { - let mxm = new_mxm(); - - let album = mxm - .album_tracks( - musixmatch_inofficial::models::AlbumId::AlbumId(17118624), - true, - 20, - 1, - ) - .await - .unwrap(); - - let x = stream::iter(album) - .map(|track| { - mxm.track_lyrics(musixmatch_inofficial::models::TrackId::TrackId( - track.track_id, - )) - }) - .buffered(8) - .collect::>() - .await - .into_iter() - .map(Result::unwrap) - .collect::>(); - - dbg!(x); - } -} - -mod subtitles { - use std::{fs::File, io::BufWriter, path::Path}; - - use super::*; - use musixmatch_inofficial::models::SubtitleFormat; - - #[tokio::test] - async fn from_match() { - let subtitle = new_mxm() - .matcher_subtitle( - "Shine", - "Spektrem", - SubtitleFormat::Json, - Some(315.0), - Some(1.0), - ) - .await - .unwrap(); - - // dbg!(&subtitle); - - assert_eq!(subtitle.subtitle_id, 36913312); - assert_eq!(subtitle.subtitle_language.unwrap(), "en"); - assert_eq!(subtitle.subtitle_language_description.unwrap(), "English"); - let copyright = subtitle.lyrics_copyright.unwrap(); - assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",); - assert_eq!(subtitle.subtitle_length, 315); - assert!(subtitle.updated_time > datetime!(2021-6-30 0:00 UTC)); - } - - #[rstest] - #[case::trackid(TrackId::TrackId(205688271))] - #[case::commontrack(TrackId::Commontrack(118480583))] - #[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba"))] - #[case::isrc(TrackId::Isrc("KRA302000590"))] - #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51"))] - #[tokio::test] - async fn from_id(#[case] track_id: TrackId<'_>) { - let subtitle = new_mxm() - .track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0)) - .await - .unwrap(); - - // dbg!(&subtitle); - - assert_eq!(subtitle.subtitle_id, 36476905); - assert_eq!(subtitle.subtitle_language.unwrap(), "ko"); - assert_eq!(subtitle.subtitle_language_description.unwrap(), "Korean"); - let copyright = subtitle.lyrics_copyright.unwrap(); - assert!( - copyright.contains("Kenneth Scott Chesak"), - "copyright: {copyright}", - ); - assert_eq!(subtitle.subtitle_length, 175); - assert!(subtitle.updated_time > datetime!(2022-8-27 0:00 UTC)); - } - - /// This track has no lyrics - #[tokio::test] - async fn instrumental() { - let err = new_mxm() - .matcher_subtitle( - "drivers license", - "Bobby G", - SubtitleFormat::Json, - Some(246.0), - Some(1.0), - ) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - /// This track has not been synced - #[tokio::test] - async fn unsynced() { - let err = new_mxm() - .track_subtitle( - TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1"), - SubtitleFormat::Json, - Some(213.0), - Some(1.0), - ) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - /// Try to get subtitles with wrong length parameter - #[tokio::test] - async fn wrong_length() { - let err = new_mxm() - .track_subtitle( - TrackId::Commontrack(118480583), - SubtitleFormat::Json, - Some(200.0), - Some(1.0), - ) - .await - .unwrap_err(); - - assert!(matches!(err, Error::NotFound)); - } - - #[tokio::test] - async fn download_testdata() { - let json_path = Path::new("testfiles/subtitles.json"); - if json_path.exists() { - return; - } - - let subtitle = new_mxm() - .track_subtitle( - TrackId::Commontrack(18576954), - SubtitleFormat::Json, - Some(259.0), - Some(1.0), - ) - .await - .unwrap(); - - let json_file = File::create(json_path).unwrap(); - serde_json::to_writer_pretty(BufWriter::new(json_file), &subtitle).unwrap(); - } -} - -mod translation { - use std::{fs::File, io::BufReader, path::Path}; - - use musixmatch_inofficial::models::{Lyrics, Subtitle, TranslationList, TranslationMap}; - - #[test] - fn translation_test() { - let lyrics_path = Path::new("testfiles/lyrics.json"); - let subtitles_path = Path::new("testfiles/subtitles.json"); - let translation_path = Path::new("testfiles/translation.json"); - - let lyrics: Lyrics = - serde_json::from_reader(BufReader::new(File::open(lyrics_path).unwrap())).unwrap(); - let subtitle: Subtitle = - serde_json::from_reader(BufReader::new(File::open(subtitles_path).unwrap())).unwrap(); - let translations: TranslationList = - serde_json::from_reader(BufReader::new(File::open(translation_path).unwrap())).unwrap(); - - let t_map = TranslationMap::from(translations); - - let lyrics_trans = t_map.translate_lyrics(&lyrics.lyrics_body); - let expected_lyrics = std::fs::read_to_string("testfiles/translated_lyrics.txt").unwrap(); - assert_eq!(lyrics_trans.trim(), expected_lyrics.trim()); - - let subtitles_trans = t_map.translate_subtitles(&subtitle.to_lines().unwrap()); - - let expected_lrc = std::fs::read_to_string("testfiles/translated_subtitles.lrc").unwrap(); - let expected_ttml = std::fs::read_to_string("testfiles/translated_subtitles.xml").unwrap(); - - assert_eq!(subtitles_trans.to_lrc().trim(), expected_lrc.trim()); - assert_eq!(subtitles_trans.to_ttml().trim(), expected_ttml.trim()); - } -} diff --git a/tests/track_test.rs b/tests/track_test.rs new file mode 100644 index 0000000..5ef932e --- /dev/null +++ b/tests/track_test.rs @@ -0,0 +1,412 @@ +mod _fixtures; +use _fixtures::*; + +use musixmatch_inofficial::{ + models::{AlbumId, ChartName, SortOrder, TrackId}, + Error, +}; +use rstest::rstest; +use time::macros::{date, datetime}; + +#[rstest] +#[case::no_translation(false, false)] +#[case::translation_2c(true, false)] +#[case::translation_3c(true, true)] +#[tokio::test] +async fn from_match(#[case] translation_status: bool, #[case] lang_3c: bool) { + let track = new_mxm() + .matcher_track( + "Poker Face", + "Lady Gaga", + "The Fame", + translation_status, + lang_3c, + ) + .await + .unwrap(); + + // dbg!(&track); + + assert_eq!(track.track_id, 15476784); + assert_eq!(track.track_mbid, "080975b0-39b1-493c-ae64-5cb3292409bb"); + assert_eq!(track.track_isrc, "USUM70824409"); + assert!( + track.commontrack_isrcs[0] + .iter() + .any(|isrc| isrc == "USUM70824409"), + "commontrack_isrcs: {:?}", + &track.commontrack_isrcs[0], + ); + assert_eq!(track.track_spotify_id, "5R8dQOPq8haW94K7mgERlO"); + assert!( + track + .commontrack_spotify_ids + .iter() + .any(|spid| spid == "1QV6tiMFM6fSOKOGLMHYYg"), + "commontrack_spotify_ids: {:?}", + track.commontrack_spotify_ids, + ); + assert_eq!(track.track_name, "Poker Face"); + assert!(track.track_rating > 50); + assert_eq!(track.commontrack_id, 47672612); + assert!(!track.instrumental); + assert!(track.explicit); + assert!(track.has_subtitles); + assert!(track.has_richsync); + assert!(track.has_track_structure); + assert!(track.num_favourite > 50); + assert_eq!(track.lyrics_id, 30678771); + assert_eq!(track.subtitle_id, 36450705); + assert_eq!(track.album_id, 13810402); + assert_eq!(track.album_name, "The Fame"); + assert_eq!(track.artist_id, 378462); + assert_eq!(track.artist_mbid, "650e7db6-b795-4eb5-a702-5ea2fc46c848"); + assert_eq!(track.artist_name, "Lady Gaga"); + assert_eq!( + track.album_coverart_100x100, + "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg" + ); + assert_eq!( + track.album_coverart_350x350, + "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg" + ); + assert_eq!( + track.album_coverart_500x500, + "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg" + ); + assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); + let first_release = track.first_release_date.unwrap(); + assert_eq!(first_release.date(), date!(2008 - 1 - 1)); + assert!(track.updated_time > datetime!(2023-1-17 0:00 UTC)); + + let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre; + assert_eq!(first_pri_genre.music_genre_id, 14); + assert_eq!(first_pri_genre.music_genre_parent_id, 34); + assert_eq!(first_pri_genre.music_genre_name, "Pop"); + assert_eq!(first_pri_genre.music_genre_name_extended, "Pop"); + assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); + + let first_sec_genre = &track.secondary_genres.music_genre_list[0].music_genre; + assert_eq!(first_sec_genre.music_genre_id, 17); + assert_eq!(first_sec_genre.music_genre_parent_id, 34); + assert_eq!(first_sec_genre.music_genre_name, "Dance"); + assert_eq!(first_sec_genre.music_genre_name_extended, "Dance"); + assert_eq!( + first_sec_genre.music_genre_vanity.as_ref().unwrap(), + "Dance" + ); + + if translation_status { + assert!( + track.track_lyrics_translation_status.iter().all(|tl| { + (if lang_3c { + tl.from == "eng" + } else { + tl.from == "en" + }) && tl.perc >= 0.0 + && tl.perc <= 1.0 + }), + "translation: {:?}", + track.track_lyrics_translation_status + ); + } else { + assert!(track.track_lyrics_translation_status.is_empty()) + } +} + +#[rstest] +#[case::trackid(TrackId::TrackId(205688271))] +#[case::commontrack(TrackId::Commontrack(118480583))] +#[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba"))] +#[case::isrc(TrackId::Isrc("KRA302000590"))] +#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51"))] +#[tokio::test] +async fn from_id(#[case] track_id: TrackId<'_>) { + let track = new_mxm().track(track_id, true, false).await.unwrap(); + + // dbg!(&track); + + assert_eq!(track.track_id, 205688271); + assert_eq!(track.track_isrc, "KRA302000590"); + assert_eq!(track.track_spotify_id, "1t2qYCAjUAoGfeFeoBlK51"); + assert_eq!(track.track_name, "Black Mamba"); + assert!(track.track_rating > 50); + assert_eq!(track.track_length, 175); + assert!(!track.explicit); + assert!(track.has_lyrics); + assert!(track.has_subtitles); + assert!(track.has_richsync); + assert!(track.num_favourite > 200); + assert_eq!(track.lyrics_id, 30126001); + assert_eq!(track.subtitle_id, 36476905); + assert_eq!(track.album_id, 41035954); + assert_eq!(track.album_name, "Black Mamba - Single"); + assert_eq!(track.artist_id, 46970441); + assert_eq!(track.artist_name, "aespa"); + assert_eq!( + track.album_coverart_100x100, + "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772.jpg" + ); + assert_eq!( + track.album_coverart_350x350, + "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_350_350.jpg" + ); + assert_eq!( + track.album_coverart_500x500, + "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_500_500.jpg" + ); + assert_eq!(track.commontrack_vanity_id, "aespa/Black-Mamba"); + + let release_date = track.first_release_date.unwrap(); + assert_eq!(release_date.date(), date!(2020 - 11 - 17)); + assert!(track.updated_time > datetime!(2022-8-27 0:00 UTC)); + + let first_tstatus = &track.track_lyrics_translation_status[0]; + assert_eq!(first_tstatus.from, "ko"); + assert!(first_tstatus.perc >= 0.0 && first_tstatus.perc <= 1.0); +} + +#[rstest] +#[case::no_translation(false, false)] +#[case::translation_2c(true, false)] +#[case::translation_3c(true, true)] +#[tokio::test] +async fn from_id_translations(#[case] translation_status: bool, #[case] lang_3c: bool) { + let track = new_mxm() + .track(TrackId::TrackId(15476784), translation_status, lang_3c) + .await + .unwrap(); + + // dbg!(&track); + + assert_eq!(track.track_id, 15476784); + assert_eq!(track.track_mbid, "080975b0-39b1-493c-ae64-5cb3292409bb"); + assert_eq!(track.track_isrc, "USUM70824409"); + assert!( + track.commontrack_isrcs[0] + .iter() + .any(|isrc| isrc == "USUM70824409"), + "commontrack_isrcs: {:?}", + &track.commontrack_isrcs[0], + ); + assert_eq!(track.track_spotify_id, "5R8dQOPq8haW94K7mgERlO"); + assert!( + track + .commontrack_spotify_ids + .iter() + .any(|spid| spid == "1QV6tiMFM6fSOKOGLMHYYg"), + "commontrack_spotify_ids: {:?}", + track.commontrack_spotify_ids, + ); + assert_eq!(track.track_name, "Poker Face"); + assert!(track.track_rating > 50); + assert_eq!(track.commontrack_id, 47672612); + assert!(!track.instrumental); + assert!(track.explicit); + assert!(track.has_subtitles); + assert!(track.has_richsync); + assert!(track.has_track_structure); + assert!(track.num_favourite > 50); + assert_eq!(track.lyrics_id, 30678771); + assert_eq!(track.subtitle_id, 36450705); + assert_eq!(track.album_id, 13810402); + assert_eq!(track.album_name, "The Fame"); + assert_eq!(track.artist_id, 378462); + assert_eq!(track.artist_mbid, "650e7db6-b795-4eb5-a702-5ea2fc46c848"); + assert_eq!(track.artist_name, "Lady Gaga"); + assert_eq!( + track.album_coverart_100x100, + "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg" + ); + assert_eq!( + track.album_coverart_350x350, + "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg" + ); + assert_eq!( + track.album_coverart_500x500, + "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg" + ); + assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); + let first_release = track.first_release_date.unwrap(); + assert_eq!(first_release.date(), date!(2008 - 1 - 1)); + assert!(track.updated_time > datetime!(2023-1-17 0:00 UTC)); + + let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre; + assert_eq!(first_pri_genre.music_genre_id, 14); + assert_eq!(first_pri_genre.music_genre_parent_id, 34); + assert_eq!(first_pri_genre.music_genre_name, "Pop"); + assert_eq!(first_pri_genre.music_genre_name_extended, "Pop"); + assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); + + let first_sec_genre = &track.secondary_genres.music_genre_list[0].music_genre; + assert_eq!(first_sec_genre.music_genre_id, 17); + assert_eq!(first_sec_genre.music_genre_parent_id, 34); + assert_eq!(first_sec_genre.music_genre_name, "Dance"); + assert_eq!(first_sec_genre.music_genre_name_extended, "Dance"); + assert_eq!( + first_sec_genre.music_genre_vanity.as_ref().unwrap(), + "Dance" + ); + + if translation_status { + assert!( + track.track_lyrics_translation_status.iter().all(|tl| { + (if lang_3c { + tl.from == "eng" + } else { + tl.from == "en" + }) && tl.perc >= 0.0 + && tl.perc <= 1.0 + }), + "translation: {:?}", + track.track_lyrics_translation_status + ); + } else { + assert!(track.track_lyrics_translation_status.is_empty()) + } +} + +#[tokio::test] +async fn from_id_missing() { + let err = new_mxm() + .track(TrackId::TrackId(999999999999), false, false) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[tokio::test] +async fn album_tracks() { + let tracks = new_mxm() + .album_tracks(AlbumId::AlbumId(17118624), true, 20, 1) + .await + .unwrap(); + + // dbg!(&tracks); + + let track_names = tracks + .iter() + .map(|t| t.track_name.to_owned()) + .collect::>(); + + assert_eq!( + track_names, + vec![ + "Gäa", + "Vergiss mein nicht", + "Orome", + "Falke flieg", + "Minne", + "Das Lied der Ahnen", + "Hörst du den Wind", + "Nan Úye", + "Faolan", + "Hymne der Nacht", + "Avalon", + "Tolo Nan", + "Oonagh", + ] + ); + + tracks.iter().for_each(|t| { + assert!(t.has_lyrics); + assert!(t.has_subtitles); + }); +} + +#[tokio::test] +async fn album_missing() { + let err = new_mxm() + .album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} + +#[rstest] +#[case::top(ChartName::Top)] +#[case::hot(ChartName::Hot)] +#[tokio::test] +async fn charts(#[case] chart_name: ChartName) { + let tracks = new_mxm() + .chart_tracks("US", chart_name, true, 20, 1) + .await + .unwrap(); + + assert_eq!(tracks.len(), 20); +} + +#[tokio::test] +async fn search() { + let tracks = new_mxm() + .track_search() + .q_artist("Lena") + .q_track("Satellite") + .s_track_rating(SortOrder::Desc) + .send(1, 0) + .await + .unwrap(); + + // dbg!(&tracks); + + assert_eq!(tracks.len(), 1); + + let track = &tracks[0]; + assert_eq!(track.commontrack_id, 12426476); + assert_eq!(track.track_name, "Satellite"); + assert_eq!(track.artist_name, "Lena"); +} + +#[tokio::test] +async fn search_lyrics() { + let tracks = new_mxm() + .track_search() + .q_lyrics("the whole world stops and stares for a while") + .s_track_rating(SortOrder::Desc) + .send(10, 1) + .await + .unwrap(); + + assert_eq!(tracks.len(), 10); + + let track = &tracks[0]; + assert_eq!(track.track_name, "Just the Way You Are"); + assert_eq!(track.artist_name, "Bruno Mars"); +} + +#[tokio::test] +async fn search_empty() { + let artists = new_mxm() + .track_search() + .q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz") + .send(10, 1) + .await + .unwrap(); + + assert_eq!(artists.len(), 0); +} + +#[tokio::test] +async fn genres() { + let genres = new_mxm().genres().await.unwrap(); + assert!(genres.len() > 360); +} + +#[tokio::test] +async fn snippet() { + let snippet = new_mxm() + .track_snippet(TrackId::Commontrack(8874280)) + .await + .unwrap(); + + assert_eq!(snippet.snippet_id, 23036767); + assert_eq!(snippet.snippet_language, "en"); + assert!(!snippet.instrumental); + assert!(snippet.updated_time > datetime!(2022-8-29 0:00 UTC)); + assert_eq!( + snippet.snippet_body, + "There's not a thing that I would change" + ); +} diff --git a/tests/translation_test.rs b/tests/translation_test.rs new file mode 100644 index 0000000..2649dfd --- /dev/null +++ b/tests/translation_test.rs @@ -0,0 +1,31 @@ +use std::{fs::File, io::BufReader, path::Path}; + +use musixmatch_inofficial::models::{Lyrics, Subtitle, TranslationList, TranslationMap}; + +#[test] +fn translation_test() { + let lyrics_path = Path::new("testfiles/lyrics.json"); + let subtitles_path = Path::new("testfiles/subtitles.json"); + let translation_path = Path::new("testfiles/translation.json"); + + let lyrics: Lyrics = + serde_json::from_reader(BufReader::new(File::open(lyrics_path).unwrap())).unwrap(); + let subtitle: Subtitle = + serde_json::from_reader(BufReader::new(File::open(subtitles_path).unwrap())).unwrap(); + let translations: TranslationList = + serde_json::from_reader(BufReader::new(File::open(translation_path).unwrap())).unwrap(); + + let t_map = TranslationMap::from(translations); + + let lyrics_trans = t_map.translate_lyrics(&lyrics.lyrics_body); + let expected_lyrics = std::fs::read_to_string("testfiles/translated_lyrics.txt").unwrap(); + assert_eq!(lyrics_trans.trim(), expected_lyrics.trim()); + + let subtitles_trans = t_map.translate_subtitles(&subtitle.to_lines().unwrap()); + + let expected_lrc = std::fs::read_to_string("testfiles/translated_subtitles.lrc").unwrap(); + let expected_ttml = std::fs::read_to_string("testfiles/translated_subtitles.xml").unwrap(); + + assert_eq!(subtitles_trans.to_lrc().trim(), expected_lrc.trim()); + assert_eq!(subtitles_trans.to_ttml().trim(), expected_ttml.trim()); +}