Compare commits

...

3 commits

19 changed files with 1486 additions and 1084 deletions

View file

@ -156,6 +156,8 @@ impl From<SubtitleFormatClap> for SubtitleFormat {
} }
} }
const NA_STR: &str = "n/a";
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
@ -235,8 +237,14 @@ async fn run(cli: Cli) -> Result<()> {
}; };
eprintln!("Lyrics ID: {}", lyrics.lyrics_id); eprintln!("Lyrics ID: {}", lyrics.lyrics_id);
eprintln!("Language: {}", lyrics.lyrics_language); eprintln!(
eprintln!("Copyright: {}", lyrics.lyrics_copyright); "Language: {}",
lyrics.lyrics_language.as_deref().unwrap_or(NA_STR)
);
eprintln!(
"Copyright: {}",
lyrics.lyrics_copyright.as_deref().unwrap_or(NA_STR)
);
eprintln!(); eprintln!();
println!("{}", lyrics.lyrics_body); println!("{}", lyrics.lyrics_body);
@ -298,9 +306,15 @@ async fn run(cli: Cli) -> Result<()> {
}; };
eprintln!("Subtitle ID: {}", subtitles.subtitle_id); eprintln!("Subtitle ID: {}", subtitles.subtitle_id);
eprintln!("Language: {}", subtitles.subtitle_language); eprintln!(
"Language: {}",
subtitles.subtitle_language.as_deref().unwrap_or(NA_STR)
);
eprintln!("Length: {}", subtitles.subtitle_length); eprintln!("Length: {}", subtitles.subtitle_length);
eprintln!("Copyright: {}", subtitles.lyrics_copyright); eprintln!(
"Copyright: {}",
subtitles.lyrics_copyright.as_deref().unwrap_or(NA_STR)
);
eprintln!(); eprintln!();
println!("{}", subtitles.subtitle_body); println!("{}", subtitles.subtitle_body);

View file

@ -1,3 +1,5 @@
use std::{marker::PhantomData, str::FromStr};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -227,7 +229,269 @@ where
deserializer.deserialize_any(BoolFromIntVisitor) 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<Option<N>, D::Error>
where
D: Deserializer<'de>,
N: TryFrom<u64>,
{
struct NullIfZeroVisitor<N> {
n: PhantomData<N>,
}
impl<'de, N> Visitor<'de> for NullIfZeroVisitor<N>
where
N: TryFrom<u64>,
{
type Value = Option<N>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("unsigned int or None")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v < 1 {
Ok(None)
} else {
Ok(v.try_into().ok())
}
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_u64(v.into())
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_u64(v.into())
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
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<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
struct NullIfEmptyVisitor;
impl<'de> Visitor<'de> for NullIfEmptyVisitor {
type Value = Option<String>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or None")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
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<Option<N>, D::Error>
where
D: Deserializer<'de>,
N: FromStr + TryFrom<u64>,
{
struct ParseIntVisitor<N> {
n: PhantomData<N>,
}
impl<'de, N> Visitor<'de> for ParseIntVisitor<N>
where
N: FromStr + TryFrom<u64>,
{
type Value = Option<N>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("numeric string or None")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.try_into().ok())
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_u64(v.into())
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_u64(v.into())
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_u64(v.into())
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
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 { 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<S: Serializer>(
value: &Option<Date>,
serializer: S,
) -> Result<S::Ok, S::Error> {
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<Option<Date>, D::Error> {
struct OptionalDateVisitor;
impl<'de> Visitor<'de> for OptionalDateVisitor {
type Value = Option<Date>;
fn expecting(
&self,
formatter: &mut serde::__private::fmt::Formatter,
) -> serde::__private::fmt::Result {
formatter.write_str("date or empty string")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
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 super::*;
use serde::Serializer; use serde::Serializer;
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
@ -257,6 +521,20 @@ pub mod optional_date {
formatter.write_str("timestamp or empty string") formatter.write_str("timestamp or empty string")
} }
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where where
E: serde::de::Error, E: serde::de::Error,
@ -271,12 +549,14 @@ pub mod optional_date {
} }
} }
deserializer.deserialize_str(OptionalDateVisitor) deserializer.deserialize_any(OptionalDateVisitor)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use time::Date;
use super::*; use super::*;
use crate::models::subtitle::SubtitleBody; use crate::models::subtitle::SubtitleBody;
@ -338,17 +618,113 @@ mod tests {
); );
} }
#[test]
fn deserialize_null_if_zero() {
#[derive(Deserialize)]
struct S {
#[serde(default, deserialize_with = "null_if_zero")]
n: Option<u64>,
}
let json = r#"{"n": 0}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, None);
let json = r#"{"n": 1}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, Some(1));
let json = r#"{"n": null}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, None);
let json = r#"{}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, None);
}
#[test]
fn deserialize_null_if_empty() {
#[derive(Deserialize)]
struct S {
#[serde(default, deserialize_with = "null_if_empty")]
s: Option<String>,
}
let json = r#"{"s": ""}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().s, None);
let json = r#"{"s": "a"}"#;
assert_eq!(
serde_json::from_str::<S>(json).unwrap().s,
Some("a".to_owned())
);
let json = r#"{"n": null}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().s, None);
let json = r#"{}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().s, None);
}
#[test]
fn deserialize_parse_int() {
#[derive(Deserialize)]
struct S {
#[serde(default, deserialize_with = "parse_int")]
n: Option<u64>,
}
let json = r#"{"n": 0}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, Some(0));
let json = r#"{"n": "0"}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, Some(0));
let json = r#"{"n": null}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, None);
let json = r#"{}"#;
assert_eq!(serde_json::from_str::<S>(json).unwrap().n, None);
}
#[test] #[test]
fn deserialize_optional_date() { fn deserialize_optional_date() {
#[derive(Deserialize)] #[derive(Deserialize)]
struct S { struct S {
#[serde(with = "optional_date")] #[serde(with = "optional_date")]
date: Option<Date>,
}
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::<S>(json_null_string).unwrap();
assert!(res.date.is_none());
let res = serde_json::from_str::<S>(json_empty_string).unwrap();
assert!(res.date.is_none());
let res = serde_json::from_str::<S>(json_zero_date).unwrap();
assert!(res.date.is_none());
let res = serde_json::from_str::<S>(json_date).unwrap();
assert!(res.date.is_some());
}
#[test]
fn deserialize_optional_datetime() {
#[derive(Deserialize)]
struct S {
#[serde(with = "optional_datetime")]
date: Option<OffsetDateTime>, date: Option<OffsetDateTime>,
} }
let json_null_string = r#"{"date": null}"#;
let json_empty_string = r#"{"date": ""}"#; let json_empty_string = r#"{"date": ""}"#;
let json_date = r#"{"date": "2022-08-27T23:47:20Z"}"#; let json_date = r#"{"date": "2022-08-27T23:47:20Z"}"#;
let res = serde_json::from_str::<S>(json_null_string).unwrap();
assert!(res.date.is_none());
let res = serde_json::from_str::<S>(json_empty_string).unwrap(); let res = serde_json::from_str::<S>(json_empty_string).unwrap();
assert!(res.date.is_none()); assert!(res.date.is_none());

View file

@ -191,7 +191,7 @@ impl Musixmatch {
/// Create a new query builder for searching tracks in the Musixmatch database. /// Create a new query builder for searching tracks in the Musixmatch database.
/// ///
/// **Note:** The search results are unsorted the by default. You probably want /// **Note:** The search results are unordered the by default. You probably want
/// to sort by popularity (`.s_track_rating(SortOrder::Desc)`) to get relevant results. /// to sort by popularity (`.s_track_rating(SortOrder::Desc)`) to get relevant results.
/// ///
/// # Example /// # Example

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::{Date, OffsetDateTime};
use super::Genres; use super::Genres;
@ -19,10 +19,8 @@ pub struct Album {
/// Unique Musixmatch Album ID /// Unique Musixmatch Album ID
pub album_id: u64, pub album_id: u64,
/// Musicbrainz album ID /// Musicbrainz album ID
/// #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
/// **Note:** most albums dont have this entry set pub album_mbid: Option<String>,
#[serde(default)]
pub album_mbid: String,
/// Album name /// Album name
pub album_name: String, pub album_name: String,
/// Popularity of the album from 0 to 100 /// Popularity of the album from 0 to 100
@ -30,14 +28,14 @@ pub struct Album {
pub album_rating: u8, pub album_rating: u8,
/// Number of tracks on the album /// Number of tracks on the album
pub album_track_count: u16, pub album_track_count: u16,
/// Album release date (e.g. "2009-07-07") /// Album release date
#[serde(default, with = "crate::api_model::optional_date")]
pub album_release_date: Option<Date>,
/// Album type
#[serde(default)] #[serde(default)]
pub album_release_date: String, pub album_release_type: AlbumType,
/// Album type (Single / EP / Album)
pub album_release_type: String,
/// Musixmatch artist ID /// Musixmatch artist ID
#[serde(default)]
pub artist_id: u64, pub artist_id: u64,
/// Artist name /// Artist name
pub artist_name: String, pub artist_name: String,
@ -48,19 +46,15 @@ pub struct Album {
/// Secondary genres / Subgenres /// Secondary genres / Subgenres
/// ///
/// Example: primary_genres: Pop, secondary_genres: K-Pop / Mandopop /// 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)] #[serde(default)]
pub secondary_genres: Genres, pub secondary_genres: Genres,
/// Album copyright text /// Album copyright text
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_copyright: String, pub album_copyright: Option<String>,
/// Album label / recording company /// Album label / recording company
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_label: String, pub album_label: Option<String>,
/// Human-readable URL-safe Album ID /// Human-readable URL-safe Album ID
/// ///
/// Example: `LMFAO/Party-Rock-5` /// Example: `LMFAO/Party-Rock-5`
@ -71,17 +65,31 @@ pub struct Album {
pub updated_time: OffsetDateTime, pub updated_time: OffsetDateTime,
/// Album cover URL (100x100px) /// Album cover URL (100x100px)
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_100x100: String, pub album_coverart_100x100: Option<String>,
/// Album cover URL (350x350px) /// Album cover URL (350x350px)
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_350x350: String, pub album_coverart_350x350: Option<String>,
/// Album cover URL (500x500px) /// Album cover URL (500x500px)
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_500x500: String, pub album_coverart_500x500: Option<String>,
/// Album cover URL (800x800px) /// Album cover URL (800x800px)
/// ///
/// **Note:** not present on a lot of albums /// **Note:** not present on a lot of albums
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_800x800: String, pub album_coverart_800x800: Option<String>,
}
/// Album type
///
/// Source: <https://developer.musixmatch.com/documentation/music-meta-data>
#[allow(missing_docs)]
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlbumType {
#[default]
Album,
Single,
Compilation,
Remix,
Live,
} }

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::{Date, OffsetDateTime};
use super::Genres; use super::Genres;
@ -19,10 +19,8 @@ pub struct Artist {
/// Musixmatch Artist ID /// Musixmatch Artist ID
pub artist_id: u64, pub artist_id: u64,
/// Musicbrainz Artist ID /// Musicbrainz Artist ID
/// #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
/// **Note:** most tracks dont have this entry set pub artist_mbid: Option<String>,
#[serde(default)]
pub artist_mbid: String,
/// Artist name /// Artist name
pub artist_name: String, pub artist_name: String,
@ -30,8 +28,8 @@ pub struct Artist {
#[serde(default)] #[serde(default)]
pub artist_name_translation_list: Vec<ArtistNameTranslation>, pub artist_name_translation_list: Vec<ArtistNameTranslation>,
/// Artist origin as a 2-letter country code (e.g. "US") /// Artist origin as a 2-letter country code (e.g. "US")
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub artist_country: String, pub artist_country: Option<String>,
/// Alternative names for the artist (e.g. in different languages) /// Alternative names for the artist (e.g. in different languages)
#[serde(default)] #[serde(default)]
pub artist_alias_list: Vec<ArtistAlias>, pub artist_alias_list: Vec<ArtistAlias>,
@ -53,20 +51,20 @@ pub struct Artist {
pub secondary_genres: Genres, pub secondary_genres: Genres,
/// URL of the artist's Twitter profile /// URL of the artist's Twitter profile
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub artist_twitter_url: String, pub artist_twitter_url: Option<String>,
/// URL of the artist's website /// URL of the artist's website
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub artist_website_url: String, pub artist_website_url: Option<String>,
/// URL of the artist's Instagram profile /// URL of the artist's Instagram profile
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub artist_instagram_url: String, pub artist_instagram_url: Option<String>,
/// URL of the artist's TikTok profile /// URL of the artist's TikTok profile
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub artist_tiktok_url: String, pub artist_tiktok_url: Option<String>,
/// URL of the artist's Facebook profile /// URL of the artist's Facebook profile
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub artist_facebook_url: String, pub artist_facebook_url: Option<String>,
/// URL-safe human-readable artist ID /// URL-safe human-readable artist ID
/// ///
@ -75,22 +73,18 @@ pub struct Artist {
/// Date and time when the artist was last updated /// Date and time when the artist was last updated
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]
pub updated_time: OffsetDateTime, pub updated_time: OffsetDateTime,
/// Year of the start of the artist's presence /// Start year of the artist's presence
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::parse_int")]
pub begin_date_year: String, pub begin_date_year: Option<u16>,
/// Start date of the artist's presence in YYYY-MM-DD format /// Start date of the artist's presence
/// #[serde(default, with = "crate::api_model::optional_date")]
/// **Info:** the default value is `"0000-00-00"` pub begin_date: Option<Date>,
#[serde(default)] /// End year of the artist's presence
pub begin_date: String, #[serde(default, deserialize_with = "crate::api_model::parse_int")]
/// Year of the end of the artist's presence pub end_date_year: Option<u16>,
#[serde(default)] /// End date of the artist's presence
pub end_date_year: String, #[serde(default, with = "crate::api_model::optional_date")]
/// End date of the artist's presence in YYYY-MM-DD format pub end_date: Option<Date>,
///
/// **Info:** the default value is `"0000-00-00"`
#[serde(default)]
pub end_date: String,
} }
/// Alternative artist name (e.g. different languages) /// Alternative artist name (e.g. different languages)

View file

@ -21,11 +21,11 @@ pub struct Lyrics {
/// Lyrics text /// Lyrics text
pub lyrics_body: String, pub lyrics_body: String,
/// Language code (e.g. "en") /// Language code (e.g. "en")
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub lyrics_language: String, pub lyrics_language: Option<String>,
/// Language name (e.g. "English") /// Language name (e.g. "English")
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub lyrics_language_description: String, pub lyrics_language_description: Option<String>,
/// Copyright text of the lyrics /// Copyright text of the lyrics
/// ///
/// Ends with a newline. /// Ends with a newline.
@ -35,8 +35,8 @@ pub struct Lyrics {
/// Writer(s): David Hodges /// Writer(s): David Hodges
/// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p. /// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p.
/// ``` /// ```
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub lyrics_copyright: String, pub lyrics_copyright: Option<String>,
/// Date and time when the lyrics were last updated /// Date and time when the lyrics were last updated
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]
pub updated_time: OffsetDateTime, pub updated_time: OffsetDateTime,

View file

@ -39,6 +39,7 @@ pub use artist::ArtistNameTranslationInner;
pub(crate) mod album; pub(crate) mod album;
pub use album::Album; pub use album::Album;
pub use album::AlbumType;
pub(crate) mod snippet; pub(crate) mod snippet;
pub use snippet::Snippet; pub use snippet::Snippet;

View file

@ -18,8 +18,8 @@ pub struct Snippet {
/// Unique Musixmatch Snippet ID /// Unique Musixmatch Snippet ID
pub snippet_id: u64, pub snippet_id: u64,
/// Snippet language code (e.g. "en") /// Snippet language code (e.g. "en")
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub snippet_language: String, pub snippet_language: Option<String>,
/// True if the track is instrumental /// True if the track is instrumental
#[serde(default, deserialize_with = "crate::api_model::bool_from_int")] #[serde(default, deserialize_with = "crate::api_model::bool_from_int")]
pub instrumental: bool, pub instrumental: bool,

View file

@ -196,11 +196,11 @@ pub struct Subtitle {
/// Subtitle / synchronized lyrics in the requested format /// Subtitle / synchronized lyrics in the requested format
pub subtitle_body: String, pub subtitle_body: String,
/// Language code (e.g. "en") /// Language code (e.g. "en")
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub subtitle_language: String, pub subtitle_language: Option<String>,
/// Language name (e.g. "English") /// Language name (e.g. "English")
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub subtitle_language_description: String, pub subtitle_language_description: Option<String>,
/// Copyright text of the lyrics /// Copyright text of the lyrics
/// ///
/// Ends with a newline. /// Ends with a newline.
@ -210,8 +210,8 @@ pub struct Subtitle {
/// Writer(s): David Hodges /// Writer(s): David Hodges
/// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p. /// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p.
/// ``` /// ```
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub lyrics_copyright: String, pub lyrics_copyright: Option<String>,
/// Duration of the subtitle in seconds /// Duration of the subtitle in seconds
pub subtitle_length: u32, pub subtitle_length: u32,
/// Date and time when the subtitle was last updated /// Date and time when the subtitle was last updated
@ -239,7 +239,7 @@ pub struct SubtitleLines {
/// List of subtitle lines /// List of subtitle lines
pub lines: Vec<SubtitleLine>, pub lines: Vec<SubtitleLine>,
/// Language code (e.g. "en") /// Language code (e.g. "en")
pub lang: String, pub lang: Option<String>,
/// Duration of the subtitle in seconds /// Duration of the subtitle in seconds
pub length: u32, pub length: u32,
} }
@ -268,10 +268,10 @@ impl SubtitleLines {
pub fn to_ttml(&self) -> String { pub fn to_ttml(&self) -> String {
let mut ttml = format!( let mut ttml = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml" xml:lang="{}"> <tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml" xml:lang="{0}">
<body> <body>
<div xml:lang="{}">"#, <div xml:lang="{0}">"#,
self.lang, self.lang self.lang.as_deref().unwrap_or_default(),
); );
for i in 0..self.lines.len() { for i in 0..self.lines.len() {

View file

@ -20,28 +20,26 @@ pub struct Track {
/// Unique Musixmatch Track ID /// Unique Musixmatch Track ID
pub track_id: u64, pub track_id: u64,
/// Track Musicbrainz ID /// Track Musicbrainz ID
/// #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
/// **Note:** most tracks dont have this entry set pub track_mbid: Option<String>,
#[serde(default)]
pub track_mbid: String,
/// [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) /// [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code)
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub track_isrc: String, pub track_isrc: Option<String>,
/// [ISRCs](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) of equivalent tracks (e.g. on different albums) /// [ISRCs](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) of equivalent tracks (e.g. on different albums)
#[serde(default)] #[serde(default)]
pub commontrack_isrcs: Vec<Vec<String>>, pub commontrack_isrcs: Vec<Vec<String>>,
/// Track ID on Spotify /// Track ID on Spotify
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub track_spotify_id: String, pub track_spotify_id: Option<String>,
/// Spotify IDs of equivalent tracks (e.g. on different albums) /// Spotify IDs of equivalent tracks (e.g. on different albums)
#[serde(default)] #[serde(default)]
pub commontrack_spotify_ids: Vec<String>, pub commontrack_spotify_ids: Vec<String>,
/// Track ID on Soundcloud /// Track ID on Soundcloud
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_zero")]
pub track_soundcloud_id: u64, pub track_soundcloud_id: Option<u64>,
/// Track ID on XBox Music /// Track ID on XBox Music
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub track_xboxmusic_id: String, pub track_xboxmusic_id: Option<String>,
/// Title of the track /// Title of the track
pub track_name: String, pub track_name: String,
@ -78,44 +76,39 @@ pub struct Track {
pub num_favourite: u32, pub num_favourite: u32,
/// Musixmatch lyrics ID /// Musixmatch lyrics ID
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_zero")]
pub lyrics_id: u64, pub lyrics_id: Option<u64>,
/// Musixmatch subtitle ID /// Musixmatch subtitle ID
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_zero")]
pub subtitle_id: u64, pub subtitle_id: Option<u64>,
/// Musixmatch album ID /// Musixmatch album ID
#[serde(default)]
pub album_id: u64, pub album_id: u64,
/// Album name /// Album name
#[serde(default)]
pub album_name: String, pub album_name: String,
/// Musixmatch artist ID /// Musixmatch artist ID
pub artist_id: u64, pub artist_id: u64,
/// Musicbrainz artist ID
///
/// **Note:** most tracks dont have this entry set
#[serde(default)]
pub artist_mbid: String,
/// Artist name /// Artist name
#[serde(default)]
pub artist_name: String, pub artist_name: String,
/// Musicbrainz artist ID
#[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub artist_mbid: Option<String>,
/// Album cover URL (100x100px) /// Album cover URL (100x100px)
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_100x100: String, pub album_coverart_100x100: Option<String>,
/// Album cover URL (350x350px) /// Album cover URL (350x350px)
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_350x350: String, pub album_coverart_350x350: Option<String>,
/// Album cover URL (500x500px) /// Album cover URL (500x500px)
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_500x500: String, pub album_coverart_500x500: Option<String>,
/// Album cover URL (800x800px) /// Album cover URL (800x800px)
/// ///
/// **Note:** not present on a lot of albums /// **Note:** not present on a lot of albums
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub album_coverart_800x800: String, pub album_coverart_800x800: Option<String>,
/// Human-readable Musixmatch ID /// Human-readable Musixmatch ID
/// ///
@ -125,7 +118,7 @@ pub struct Track {
pub commontrack_vanity_id: String, pub commontrack_vanity_id: String,
/// Track release date /// Track release date
#[serde(default, with = "crate::api_model::optional_date")] #[serde(default, with = "crate::api_model::optional_datetime")]
pub first_release_date: Option<OffsetDateTime>, pub first_release_date: Option<OffsetDateTime>,
/// Date and time when the track was last updated /// Date and time when the track was last updated
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]

View file

@ -19,9 +19,9 @@ pub(crate) struct TranslationBody {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive] #[non_exhaustive]
pub struct Translation { pub struct Translation {
/// 2-character source language code (e.g. "en") /// source language code (e.g. "en")
#[serde(default)] #[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub language_from: String, pub language_from: Option<String>,
/// Translated line /// Translated line
#[serde(default)] #[serde(default)]
pub description: String, pub description: String,
@ -102,7 +102,7 @@ impl TranslationMap {
time: line.time, time: line.time,
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
lang: self.lang.to_owned(), lang: Some(self.lang.to_owned()),
length: subtitles.length, length: subtitles.length,
} }
} }

View file

@ -1,21 +0,0 @@
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())),
)
}

View file

@ -1,104 +0,0 @@
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);
}

View file

@ -1,116 +0,0 @@
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);
}

View file

@ -1,145 +0,0 @@
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::<Vec<_>>()
.await
.into_iter()
.map(Result::unwrap)
.collect::<Vec<_>>();
dbg!(x);
}

View file

@ -1,135 +0,0 @@
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();
}

980
tests/tests.rs Normal file
View file

@ -0,0 +1,980 @@
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::<FileStorage>::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::<Vec<_>>();
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::<Vec<_>>()
.await
.into_iter()
.map(Result::unwrap)
.collect::<Vec<_>>();
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());
}
}

View file

@ -1,412 +0,0 @@
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::<Vec<_>>();
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"
);
}

View file

@ -1,31 +0,0 @@
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());
}