From 692e11229299339bccb373fd8a93775414281dc4 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 1 Oct 2022 02:22:51 +0200 Subject: [PATCH 1/5] feat: add lyrics API --- src/api_model.rs | 149 +++++++++++++++++++++++- src/apis/lyrics_api.rs | 32 +++++ src/apis/mod.rs | 12 +- src/apis/subtitle_api.rs | 2 - src/models/lyrics.rs | 42 +++++++ src/models/mod.rs | 3 + src/models/subtitle.rs | 6 +- tests/lyrics_test.rs | 102 ++++++++++++++++ tests/{subtitle.rs => subtitle_test.rs} | 40 ++++++- 9 files changed, 368 insertions(+), 20 deletions(-) create mode 100644 src/apis/lyrics_api.rs create mode 100644 src/models/lyrics.rs create mode 100644 tests/lyrics_test.rs rename tests/{subtitle.rs => subtitle_test.rs} (71%) diff --git a/src/api_model.rs b/src/api_model.rs index 89dcc2f..dd34a05 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -1,6 +1,6 @@ -use serde::{Deserialize, Serialize}; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; -use crate::error::{Error, Result}; +use crate::error::{Error, Result as MxmResult}; //#COMMON @@ -32,17 +32,17 @@ pub struct Header { #[serde(default)] pub hint: String, /// Is the requested track instrumental? - #[serde(default)] - pub instrumental: u8, + #[serde(default, deserialize_with = "bool_from_int")] + pub instrumental: bool, } impl Resp { - pub fn body_or_err(self) -> Result { + pub fn body_or_err(self) -> MxmResult { match (self.message.body, self.message.header.status_code < 400) { (Some(MessageBody::Some(body)), true) => Ok(body), (_, true) => Err(Error::NoData), (_, false) => { - if self.message.header.instrumental > 0 { + if self.message.header.instrumental { Err(Error::Instrumental) } else if self.message.header.status_code == 404 { Err(Error::NotFound) @@ -122,6 +122,114 @@ pub struct ErrorMxm { pub description: String, } +pub fn bool_from_int<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct BoolFromIntVisitor; + + impl<'de> Visitor<'de> for BoolFromIntVisitor { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a number or boolean") + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + Ok(v) + } + + fn visit_u8(self, v: u8) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_u16(self, v: u16) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_u32(self, v: u32) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_u128(self, v: u128) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_i8(self, v: i8) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_i16(self, v: i16) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_i32(self, v: i32) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_i128(self, v: i128) -> Result + where + E: serde::de::Error, + { + Ok(v != 0) + } + + fn visit_f32(self, v: f32) -> Result + where + E: serde::de::Error, + { + Ok(v != 0.0) + } + + fn visit_f64(self, v: f64) -> Result + where + E: serde::de::Error, + { + Ok(v != 0.0) + } + } + + deserializer.deserialize_any(BoolFromIntVisitor) +} + #[cfg(test)] mod tests { use super::*; @@ -184,4 +292,33 @@ mod tests { "Error 403 returned by the Musixmatch API. Message: ''" ); } + + #[test] + fn deserialize_instrumental() { + let json = r#"{"message":{"header":{"status_code":404,"execute_time":0.002,"instrumental":true}}}"#; + + let res = serde_json::from_str::>(json).unwrap(); + + assert_eq!(res.message.header.status_code, 404); + assert!(res.message.header.instrumental); + assert!(res.message.body.is_none()); + + let err = res.body_or_err().unwrap_err(); + assert!(matches!(err, Error::Instrumental)); + } + + #[test] + fn deserialize_instrumental2() { + let json = + r#"{"message":{"header":{"status_code":404,"execute_time":0.002,"instrumental":1}}}"#; + + let res = serde_json::from_str::>(json).unwrap(); + + assert_eq!(res.message.header.status_code, 404); + assert!(res.message.header.instrumental); + assert!(res.message.body.is_none()); + + let err = res.body_or_err().unwrap_err(); + assert!(matches!(err, Error::Instrumental)); + } } diff --git a/src/apis/lyrics_api.rs b/src/apis/lyrics_api.rs new file mode 100644 index 0000000..3d384ae --- /dev/null +++ b/src/apis/lyrics_api.rs @@ -0,0 +1,32 @@ +use crate::error::Result; +use crate::models::lyrics::{Lyrics, LyricsBody}; +use crate::models::TrackId; +use crate::Musixmatch; + +impl Musixmatch { + pub async fn matcher_lyrics(&self, q_track: &str, q_artist: &str) -> Result { + let mut url = self.new_url("matcher.lyrics.get"); + { + let mut url_query = url.query_pairs_mut(); + url_query.append_pair("q_track", q_track); + url_query.append_pair("q_artist", q_artist); + url_query.finish(); + } + + let lyrics_body = self.execute_get_request::(&url).await?; + Ok(lyrics_body.lyrics) + } + + pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result { + let mut url = self.new_url("track.lyrics.get"); + { + let mut url_query = url.query_pairs_mut(); + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + url_query.finish(); + } + + let lyrics_body = self.execute_get_request::(&url).await?; + Ok(lyrics_body.lyrics) + } +} diff --git a/src/apis/mod.rs b/src/apis/mod.rs index 6dd6146..b1518f2 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,6 +1,6 @@ -// pub mod album_api; -// pub mod artist_api; -// pub mod lyrics_api; -// pub mod snippet_api; -pub mod subtitle_api; -// pub mod track_api; +// mod album_api; +// mod artist_api; +mod lyrics_api; +// mod snippet_api; +mod subtitle_api; +// mod track_api; diff --git a/src/apis/subtitle_api.rs b/src/apis/subtitle_api.rs index 625c237..fccdac7 100644 --- a/src/apis/subtitle_api.rs +++ b/src/apis/subtitle_api.rs @@ -44,7 +44,6 @@ impl Musixmatch { } let subtitle_body = self.execute_get_request::(&url).await?; - Ok(subtitle_body.subtitle) } @@ -87,7 +86,6 @@ impl Musixmatch { } let subtitle_body = self.execute_get_request::(&url).await?; - Ok(subtitle_body.subtitle) } } diff --git a/src/models/lyrics.rs b/src/models/lyrics.rs new file mode 100644 index 0000000..3c2da5d --- /dev/null +++ b/src/models/lyrics.rs @@ -0,0 +1,42 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub(crate) struct LyricsBody { + pub lyrics: Lyrics, +} + +/// Lyrics from the Musixmatch database. +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[non_exhaustive] +pub struct Lyrics { + /// Unique Musixmatch lyrics ID + pub lyrics_id: u64, + /// True if the track is instrumental + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub instrumental: bool, + /// True if the lyrics contain explicit language + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub explicit: bool, + /// Lyrics text + pub lyrics_body: String, + /// Language code (e.g. "en") + #[serde(default)] + pub lyrics_language: String, + /// Language name (e.g. "English") + #[serde(default)] + pub lyrics_language_description: String, + /// Copyright text of the lyrics + /// + /// Ends with a newline. + /// + /// Example: + /// ```text + /// Writer(s): David Hodges + /// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p. + /// ``` + #[serde(default)] + pub lyrics_copyright: String, + /// Date and time when the lyrics were last updated + pub updated_time: DateTime, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index abde616..afeba33 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -6,3 +6,6 @@ pub use subtitle::SubtitleTime; mod track_id; pub use track_id::TrackId; + +pub(crate) mod lyrics; +pub use lyrics::Lyrics; diff --git a/src/models/subtitle.rs b/src/models/subtitle.rs index 86a58f4..559c9dd 100644 --- a/src/models/subtitle.rs +++ b/src/models/subtitle.rs @@ -158,9 +158,11 @@ pub struct Subtitle { /// Subtitle / synchronized lyrics in the requested format pub subtitle_body: String, /// Language code (e.g. "en") - pub subtitle_language: Option, + #[serde(default)] + pub subtitle_language: String, /// Language name (e.g. "English") - pub subtitle_language_description: Option, + #[serde(default)] + pub subtitle_language_description: String, /// Copyright text of the lyrics /// /// Ends with a newline. diff --git a/tests/lyrics_test.rs b/tests/lyrics_test.rs new file mode 100644 index 0000000..a0d5bfe --- /dev/null +++ b/tests/lyrics_test.rs @@ -0,0 +1,102 @@ +use chrono::TimeZone; +use dotenv::dotenv; +use musixmatch_inofficial::{models::TrackId, storage::FileStorage, Error, Musixmatch}; +use rstest::rstest; + +#[ctor::ctor] +fn init() { + let _ = dotenv(); + env_logger::init(); +} + +fn new_mxm() -> Musixmatch { + Musixmatch::new( + &std::env::var("MUSIXMATCH_EMAIL").unwrap(), + &std::env::var("MUSIXMATCH_PASSWORD").unwrap(), + Some(Box::new(FileStorage::default())), + ) +} + +#[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")); + assert!( + lyrics.updated_time + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2021, 6, 30), + chrono::NaiveTime::default(), + )) + ); +} + +#[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, 29401691); + assert_eq!(lyrics.lyrics_language, "ko"); + assert_eq!(lyrics.lyrics_language_description, "Korean"); + assert!(lyrics.lyrics_copyright.contains("Michael Fonseca")); + assert!( + lyrics.updated_time + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2022, 8, 27), + chrono::NaiveTime::default(), + )) + ); +} + +/// 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 + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2021, 6, 21), + chrono::NaiveTime::default(), + )) + ); +} + +/// This track has no lyrics +#[tokio::test] +async fn missing() { + let err = new_mxm() + .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn")) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); +} diff --git a/tests/subtitle.rs b/tests/subtitle_test.rs similarity index 71% rename from tests/subtitle.rs rename to tests/subtitle_test.rs index 9b1e4c9..d227d12 100644 --- a/tests/subtitle.rs +++ b/tests/subtitle_test.rs @@ -37,8 +37,8 @@ async fn from_match() { // dbg!(&subtitle); assert_eq!(subtitle.subtitle_id, 35340319); - assert_eq!(subtitle.subtitle_language.unwrap(), "en"); - assert_eq!(subtitle.subtitle_language_description.unwrap(), "English"); + assert_eq!(subtitle.subtitle_language, "en"); + assert_eq!(subtitle.subtitle_language_description, "English"); assert!(subtitle.lyrics_copyright.contains("Kim Jeffeson")); assert_eq!(subtitle.subtitle_length, 316); assert!( @@ -66,8 +66,8 @@ async fn from_id(#[case] track_id: TrackId<'_>) { // dbg!(&subtitle); assert_eq!(subtitle.subtitle_id, 36476905); - assert_eq!(subtitle.subtitle_language.unwrap(), "ko"); - assert_eq!(subtitle.subtitle_language_description.unwrap(), "Korean"); + assert_eq!(subtitle.subtitle_language, "ko"); + assert_eq!(subtitle.subtitle_language_description, "Korean"); assert!(subtitle.lyrics_copyright.contains("Michael Fonseca")); assert_eq!(subtitle.subtitle_length, 175); assert!( @@ -95,3 +95,35 @@ async fn instrumental() { 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)); +} From e9e3bdc26dc8bc49481fc1ae3e67aa19779ea756 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 1 Oct 2022 23:37:33 +0200 Subject: [PATCH 2/5] feat: add track and artist API --- src/api_model.rs | 51 +++++ src/apis/artist_api.rs | 129 ++++++++++++ src/apis/lyrics_api.rs | 23 ++- src/apis/mod.rs | 4 +- src/apis/subtitle_api.rs | 10 +- src/apis/track_api.rs | 416 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 10 +- src/models/artist.rs | 107 ++++++++++ src/models/genre.rs | 30 +++ src/models/id.rs | 79 +++++++ src/models/lyrics.rs | 2 +- src/models/mod.rs | 29 ++- src/models/snippet.rs | 28 +++ src/models/track.rs | 183 +++++++++++++++++ src/models/track_id.rs | 37 ---- tests/artist_test.rs | 150 ++++++++++++++ tests/track_test.rs | 429 +++++++++++++++++++++++++++++++++++++++ 17 files changed, 1667 insertions(+), 50 deletions(-) create mode 100644 src/apis/artist_api.rs create mode 100644 src/apis/track_api.rs create mode 100644 src/models/artist.rs create mode 100644 src/models/genre.rs create mode 100644 src/models/id.rs create mode 100644 src/models/snippet.rs create mode 100644 src/models/track.rs delete mode 100644 src/models/track_id.rs create mode 100644 tests/artist_test.rs create mode 100644 tests/track_test.rs diff --git a/src/api_model.rs b/src/api_model.rs index dd34a05..a8e697a 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use crate::error::{Error, Result as MxmResult}; @@ -23,6 +24,7 @@ pub enum MessageBody { EmptyArr(Vec<()>), // "body": {} EmptyObj {}, + EmptyStr(String), } #[derive(Debug, Deserialize)] @@ -230,6 +232,37 @@ where deserializer.deserialize_any(BoolFromIntVisitor) } +pub fn optional_date<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + 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("timestamp or empty string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.is_empty() { + Ok(None) + } else { + v.parse().map_err(E::custom).map(Some) + } + } + } + + deserializer.deserialize_str(OptionalDateVisitor) +} + #[cfg(test)] mod tests { use super::*; @@ -321,4 +354,22 @@ mod tests { let err = res.body_or_err().unwrap_err(); assert!(matches!(err, Error::Instrumental)); } + + #[test] + fn deserialize_optional_date() { + #[derive(Deserialize)] + struct S { + #[serde(deserialize_with = "optional_date")] + date: Option>, + } + + let json_empty_string = r#"{"date": ""}"#; + let json_date = r#"{"date": "2022-08-27T23:47:20Z"}"#; + + let res = serde_json::from_str::(json_empty_string).unwrap(); + assert!(res.date.is_none()); + + let res = serde_json::from_str::(json_date).unwrap(); + assert!(res.date.is_some()); + } } diff --git a/src/apis/artist_api.rs b/src/apis/artist_api.rs new file mode 100644 index 0000000..7ae1e23 --- /dev/null +++ b/src/apis/artist_api.rs @@ -0,0 +1,129 @@ +use crate::error::Result; +use crate::models::artist::{Artist, ArtistBody, ArtistListBody}; +use crate::models::ArtistId; +use crate::Musixmatch; + +impl Musixmatch { + /// Get the metadata for an artist specified by its ID. + /// + /// # Parameters + /// - `id`: [Artist ID](crate::models::ArtistId) + /// + /// # Reference + /// + pub async fn artist(&self, id: ArtistId<'_>) -> Result { + let mut url = self.new_url("artist.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + url_query.finish(); + } + + let artist_body = self.execute_get_request::(&url).await?; + Ok(artist_body.artist) + } + + /// Get a list of artists somehow related to the one specified by its ID. + /// + /// # Parameters + /// - `id`: [Artist ID](crate::models::ArtistId) + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// + pub async fn artist_related( + &self, + id: ArtistId<'_>, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("artist.related.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let artist_list_body = self.execute_get_request::(&url).await?; + Ok(artist_list_body + .artist_list + .into_iter() + .map(|a| a.artist) + .collect()) + } + + /// Search for artists in the Musixmatch database. + /// + /// # Parameters + /// - `q_artist`: Search term + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// + pub async fn artist_search( + &self, + q_artist: &str, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("artist.search"); + { + let mut url_query = url.query_pairs_mut(); + + url_query.append_pair("q_artist", q_artist); + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let artist_list_body = self.execute_get_request::(&url).await?; + Ok(artist_list_body + .artist_list + .into_iter() + .map(|a| a.artist) + .collect()) + } + + /// This api provides you the list of the top artists of a given country. + /// + /// # Parameters + /// - `country`: A valid country code (default: "US") + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// + pub async fn chart_artists( + &self, + country: &str, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("chart.artists.get"); + { + let mut url_query = url.query_pairs_mut(); + + if country != "" { + url_query.append_pair("country", country); + } + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let artist_list_body = self.execute_get_request::(&url).await?; + Ok(artist_list_body + .artist_list + .into_iter() + .map(|a| a.artist) + .collect()) + } +} diff --git a/src/apis/lyrics_api.rs b/src/apis/lyrics_api.rs index 3d384ae..d395a2d 100644 --- a/src/apis/lyrics_api.rs +++ b/src/apis/lyrics_api.rs @@ -4,12 +4,24 @@ use crate::models::TrackId; use crate::Musixmatch; impl Musixmatch { + /// Get the lyrics for a track specified by its name and artist. + /// + /// # Parameters + /// - `q_track`: Title of the track + /// - `q_artist`: Artist of the track + /// + /// # Reference + /// pub async fn matcher_lyrics(&self, q_track: &str, q_artist: &str) -> Result { let mut url = self.new_url("matcher.lyrics.get"); { let mut url_query = url.query_pairs_mut(); - url_query.append_pair("q_track", q_track); - url_query.append_pair("q_artist", q_artist); + if !q_track.is_empty() { + url_query.append_pair("q_track", q_track); + } + if !q_artist.is_empty() { + url_query.append_pair("q_artist", q_artist); + } url_query.finish(); } @@ -17,6 +29,13 @@ impl Musixmatch { Ok(lyrics_body.lyrics) } + /// Get the lyrics for a track specified by its ID. + /// + /// # Parameters + /// - `id`: [Track ID](crate::models::TrackId) + /// + /// # Reference + /// pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result { let mut url = self.new_url("track.lyrics.get"); { diff --git a/src/apis/mod.rs b/src/apis/mod.rs index b1518f2..c89bdcf 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,6 +1,6 @@ // mod album_api; -// mod artist_api; +mod artist_api; mod lyrics_api; // mod snippet_api; mod subtitle_api; -// mod track_api; +mod track_api; diff --git a/src/apis/subtitle_api.rs b/src/apis/subtitle_api.rs index fccdac7..d4b2421 100644 --- a/src/apis/subtitle_api.rs +++ b/src/apis/subtitle_api.rs @@ -16,7 +16,7 @@ impl Musixmatch { /// so this should be the recommended value. /// /// # Reference - /// + /// pub async fn matcher_subtitle( &self, q_track: &str, @@ -28,8 +28,12 @@ impl Musixmatch { let mut url = self.new_url("matcher.subtitle.get"); { let mut url_query = url.query_pairs_mut(); - url_query.append_pair("q_track", q_track); - url_query.append_pair("q_artist", q_artist); + if !q_track.is_empty() { + url_query.append_pair("q_track", q_track); + } + if !q_artist.is_empty() { + url_query.append_pair("q_artist", q_artist); + } url_query.append_pair("subtitle_format", subtitle_format.to_param()); if let Some(f_subtitle_length) = f_subtitle_length { url_query.append_pair("f_subtitle_length", &f_subtitle_length.to_string()); diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs new file mode 100644 index 0000000..92c2dcd --- /dev/null +++ b/src/apis/track_api.rs @@ -0,0 +1,416 @@ +use chrono::NaiveDate; + +use crate::error::Result; +use crate::models::track::{Track, TrackBody, TrackListBody}; +use crate::models::{AlbumId, ChartName, SortOrder, TrackId}; +use crate::Musixmatch; + +impl Musixmatch { + /// Get the metadata for a track specified by its name, artist and album. + /// + /// # Parameters + /// - `q_track`: Title of the track + /// - `q_artist`: Artist of the track + /// - `q_album`: Album name of the track + /// - `translation_status`: Get the lyrics [translation status](crate::models::TrackLyricsTranslationStatus) + /// - `lang_3c`: Output the lyrics translation status with + /// [ISO 639‑2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) 3-letter language codes + /// instead of [ISO 639‑1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes. + /// + /// # Reference + /// + pub async fn matcher_track( + &self, + q_track: &str, + q_artist: &str, + q_album: &str, + translation_status: bool, + lang_3c: bool, + ) -> Result { + let mut url = self.new_url("matcher.track.get"); + { + let mut url_query = url.query_pairs_mut(); + + if !q_track.is_empty() { + url_query.append_pair("q_track", q_track); + } + if !q_artist.is_empty() { + url_query.append_pair("q_artist", q_artist); + } + if !q_album.is_empty() { + url_query.append_pair("q_album", q_album); + } + if translation_status { + url_query.append_pair("part", "track_lyrics_translation_status"); + url_query.append_pair( + "language_iso_code", + match lang_3c { + true => "0", + false => "1", + }, + ); + } + url_query.finish(); + } + + let track_body = self.execute_get_request::(&url).await?; + Ok(track_body.track) + } + + /// Get the metadata for a track specified by its id. + /// + /// # Parameters + /// - `id`: [Track ID](crate::models::TrackId) + /// - `translation_status`: Get the lyrics [translation status](crate::models::TrackLyricsTranslationStatus) + /// - `lang_3c`: Output the lyrics translation status with + /// [ISO 639‑2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) 3-letter language codes + /// instead of [ISO 639‑1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes. + /// + /// # Reference + /// + pub async fn track( + &self, + id: TrackId<'_>, + translation_status: bool, + lang_3c: bool, + ) -> Result { + let mut url = self.new_url("track.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + if translation_status { + url_query.append_pair("part", "track_lyrics_translation_status"); + url_query.append_pair( + "language_iso_code", + match lang_3c { + true => "0", + false => "1", + }, + ); + } + url_query.finish(); + } + + let track_body = self.execute_get_request::(&url).await?; + Ok(track_body.track) + } + + /// Get the list of songs of an album. + /// + /// # Parameters + /// - `id`: [Album ID](crate::models::AlbumId) + /// - `f_has_lyrics`: When true, filter only contents with lyrics + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// + pub async fn album_tracks( + &self, + id: AlbumId<'_>, + f_has_lyrics: bool, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("album.tracks.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + if f_has_lyrics { + url_query.append_pair("f_has_lyrics", "1"); + } + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let track_list_body = self.execute_get_request::(&url).await?; + Ok(track_list_body + .track_list + .into_iter() + .map(|t| t.track) + .collect()) + } + + /// Get a list of the top songs of a given country. + /// + /// # Parameters + /// - `country`: A valid country code (default: "US") + /// - `chart_name`: Select among [available charts](crate::models::ChartName) + /// - `f_has_lyrics`: When true, filter only contents with lyrics + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// + pub async fn chart_tracks( + &self, + country: &str, + chart_name: ChartName, + f_has_lyrics: bool, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("chart.tracks.get"); + { + let mut url_query = url.query_pairs_mut(); + + if country != "" { + url_query.append_pair("country", country); + } + url_query.append_pair("chart_name", chart_name.as_str()); + if f_has_lyrics { + url_query.append_pair("f_has_lyrics", "1"); + } + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let track_list_body = self.execute_get_request::(&url).await?; + Ok(track_list_body + .track_list + .into_iter() + .map(|t| t.track) + .collect()) + } + + /// Create a new query builder for searching tracks in the Musixmatch database. + /// + /// **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 + /// ```ignore + /// let tracks = musixmatch + /// .track_search() + /// .q_lyrics("Never gonna run around and desert you") + /// .s_track_rating(SortOrder::Desc) + /// .send(10, 1) + /// .await?; + /// ``` + /// + /// # Reference + /// + pub fn track_search(&self) -> TrackSearchQuery { + TrackSearchQuery { + mxm: self.clone(), + q_track: None, + q_artist: None, + q_lyrics: None, + q_track_artist: None, + q_writer: None, + q: None, + f_artist_id: None, + f_music_genre_id: None, + f_lyrics_language: None, + f_has_lyrics: false, + f_track_release_group_first_release_date_min: None, + f_track_release_group_first_release_date_max: None, + s_artist_rating: None, + s_track_rating: None, + quorum_factor: None, + } + } +} + +pub struct TrackSearchQuery<'a> { + mxm: Musixmatch, + + q_track: Option<&'a str>, + q_artist: Option<&'a str>, + q_lyrics: Option<&'a str>, + q_track_artist: Option<&'a str>, + q_writer: Option<&'a str>, + q: Option<&'a str>, + f_artist_id: Option, + f_music_genre_id: Option, + f_lyrics_language: Option<&'a str>, + f_has_lyrics: bool, + f_track_release_group_first_release_date_min: Option, + f_track_release_group_first_release_date_max: Option, + s_artist_rating: Option, + s_track_rating: Option, + quorum_factor: Option, +} + +impl<'a> TrackSearchQuery<'a> { + /// Track title + pub fn q_track(mut self, q_track: &'a str) -> Self { + self.q_track = Some(q_track); + self + } + + /// Track artist + pub fn q_artist(mut self, q_artist: &'a str) -> Self { + self.q_artist = Some(q_artist); + self + } + + /// Any word in the lyrics + pub fn q_lyrics(mut self, q_lyrics: &'a str) -> Self { + self.q_lyrics = Some(q_lyrics); + self + } + + /// Any word in the song title or artist name + pub fn q_track_artist(mut self, q_track_artist: &'a str) -> Self { + self.q_track_artist = Some(q_track_artist); + self + } + + /// Track lyrics writer + pub fn q_writer(mut self, q_writer: &'a str) -> Self { + self.q_writer = Some(q_writer); + self + } + + /// Any word in the song title or artist name or lyrics + pub fn q(mut self, q: &'a str) -> Self { + self.q = Some(q); + self + } + + /// When set, filter by this artist ID + pub fn f_artist_id(mut self, f_artist_id: u64) -> Self { + self.f_artist_id = Some(f_artist_id); + self + } + + /// When set, filter by this genre ID + pub fn f_music_genre_id(mut self, f_music_genre_id: u64) -> Self { + self.f_music_genre_id = Some(f_music_genre_id); + self + } + + /// Filter by the lyrics language ("en", "it", ...) + pub fn f_lyrics_language(mut self, f_lyrics_language: &'a str) -> Self { + self.f_lyrics_language = Some(f_lyrics_language); + self + } + + /// When set, filter only contents with lyrics + pub fn f_has_lyrics(mut self) -> Self { + self.f_has_lyrics = true; + self + } + + /// When set, filter the tracks with release date newer than the given value + pub fn f_track_release_date_min(mut self, f_track_release_date_min: NaiveDate) -> Self { + self.f_track_release_group_first_release_date_min = Some(f_track_release_date_min); + self + } + + /// When set, filter the tracks with release date older than the given value + pub fn f_track_release_date_max(mut self, f_track_release_date_max: NaiveDate) -> Self { + self.f_track_release_group_first_release_date_max = Some(f_track_release_date_max); + self + } + + /// Sort by artist popularity (asc|desc) + pub fn s_artist_rating(mut self, s_artist_rating: SortOrder) -> Self { + self.s_artist_rating = Some(s_artist_rating); + self + } + + /// Sort by track popularity (asc|desc) + pub fn s_track_rating(mut self, s_track_rating: SortOrder) -> Self { + self.s_track_rating = Some(s_track_rating); + self + } + + /// Search only a part of the given query string. + /// + /// Allowed range is (0.1 – 0.9) + pub fn quorum_factor(mut self, quorum_factor: f32) -> Self { + self.quorum_factor = Some(quorum_factor); + self + } + + /// Execute the search query. + /// + /// # Parameters + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + pub async fn send(&self, page_size: u8, page: u32) -> Result> { + let mut url = self.mxm.new_url("track.search"); + { + let mut url_query = url.query_pairs_mut(); + + if let Some(q_track) = self.q_track { + url_query.append_pair("q_track", q_track); + } + if let Some(q_artist) = self.q_artist { + url_query.append_pair("q_artist", q_artist); + } + if let Some(q_lyrics) = self.q_lyrics { + url_query.append_pair("q_lyrics", q_lyrics); + } + if let Some(q_track_artist) = self.q_track_artist { + url_query.append_pair("q_track_artist", q_track_artist); + } + if let Some(q_writer) = self.q_writer { + url_query.append_pair("q_writer", q_writer); + } + if let Some(q) = self.q { + url_query.append_pair("q", q); + } + if let Some(f_artist_id) = self.f_artist_id { + url_query.append_pair("f_artist_id", &f_artist_id.to_string()); + } + if let Some(f_music_genre_id) = self.f_music_genre_id { + url_query.append_pair("f_music_genre_id", &f_music_genre_id.to_string()); + } + if let Some(f_lyrics_language) = self.f_lyrics_language { + url_query.append_pair("f_lyrics_language", f_lyrics_language); + } + if self.f_has_lyrics { + url_query.append_pair("f_has_lyrics", "1"); + } + if let Some(f_track_release_group_first_release_date_min) = + self.f_track_release_group_first_release_date_min + { + url_query.append_pair( + "f_track_release_group_first_release_date_min", + &f_track_release_group_first_release_date_min + .format("%Y%m%d") + .to_string(), + ); + } + if let Some(f_track_release_group_first_release_date_max) = + self.f_track_release_group_first_release_date_max + { + url_query.append_pair( + "f_track_release_group_first_release_date_max", + &f_track_release_group_first_release_date_max + .format("%Y%m%d") + .to_string(), + ); + } + if let Some(s_artist_rating) = &self.s_artist_rating { + url_query.append_pair("s_artist_rating", s_artist_rating.as_str()); + } + if let Some(s_track_rating) = &self.s_track_rating { + url_query.append_pair("s_track_rating", s_track_rating.as_str()); + } + if let Some(quorum_factor) = self.quorum_factor { + url_query.append_pair("quorum_factor", &quorum_factor.to_string()); + } + + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let track_list_body = self.mxm.execute_get_request::(&url).await?; + Ok(track_list_body + .track_list + .into_iter() + .map(|t| t.track) + .collect()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 45c1a62..8eb93b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,9 @@ mod error; pub mod models; pub mod storage; +use std::fmt::Debug; +use std::sync::Arc; + pub use error::Error; use chrono::{Datelike, Local}; @@ -49,8 +52,9 @@ use tokio::sync::Mutex; use crate::api_model::Resp; use crate::error::Result; +#[derive(Clone)] pub struct Musixmatch { - inner: MusixmatchRef, + inner: Arc, } struct MusixmatchRef { @@ -113,7 +117,7 @@ impl Musixmatch { .unwrap(); Self { - inner: MusixmatchRef { + inner: Arc::new(MusixmatchRef { http, storage, email: email.to_owned(), @@ -122,7 +126,7 @@ impl Musixmatch { brand, device, ua, - }, + }), } } diff --git a/src/models/artist.rs b/src/models/artist.rs new file mode 100644 index 0000000..08d041b --- /dev/null +++ b/src/models/artist.rs @@ -0,0 +1,107 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::Genres; + +#[derive(Debug, Deserialize)] +pub(crate) struct ArtistBody { + pub artist: Artist, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct ArtistListBody { + pub artist_list: Vec, +} + +/// Artist: an artist in the Musixmatch database. +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct Artist { + /// Musixmatch Artist ID + pub artist_id: u64, + /// Musicbrainz Artist ID + /// + /// **Note:** most tracks dont have this entry set + #[serde(default)] + pub artist_mbid: String, + /// Artist name + pub artist_name: String, + /// Artist name in different languages + #[serde(default)] + pub artist_name_translation_list: Vec, + /// Artist origin as a 2-letter country code (e.g. "US") + #[serde(default)] + pub artist_country: String, + // Alternative names for the artist (e.g. in different languages) + #[serde(default)] + pub artist_alias_list: Vec, + /// Popularity of the artist from 0 to 100 + #[serde(default)] + pub artist_rating: u8, + /// Primary genres of the artist + #[serde(default)] + pub primary_genres: Genres, + /// 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, + #[serde(default)] + pub artist_twitter_url: String, + #[serde(default)] + pub artist_website_url: String, + #[serde(default)] + pub artist_instagram_url: String, + #[serde(default)] + pub artist_tiktok_url: String, + #[serde(default)] + pub artist_facebook_url: String, + /// URL-safe human-readable artist ID + /// + /// Example: "aespa" + pub artist_vanity_id: String, + /// Date and time when the artist was last updated + pub updated_time: DateTime, + /// 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) +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct ArtistAlias { + pub artist_alias: String, +} + +/// Alternative artist name (e.g. different languages) +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct ArtistNameTranslation { + pub artist_name_translation: ArtistNameTranslationInner, +} + +/// Alternative artist name (e.g. different languages) +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct ArtistNameTranslationInner { + /// Language code (e.g. "EN") + /// + /// **Note:** the language code is uppercase for some reason + pub language: String, + /// Translated name + pub translation: String, +} diff --git a/src/models/genre.rs b/src/models/genre.rs new file mode 100644 index 0000000..6a22c10 --- /dev/null +++ b/src/models/genre.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct Genres { + pub music_genre_list: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct Genre { + pub music_genre: GenreInner, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct GenreInner { + /// Unique Musixmatch Genre ID + pub music_genre_id: u64, + /// ID of supergenre + pub music_genre_parent_id: u64, + /// Genre name + pub music_genre_name: String, + /// Long genre name + pub music_genre_name_extended: String, + /// URL-safe genre name + /// + /// **Note:** Jazz / Bebop (ID: 1291) has this set to None for some reason. + pub music_genre_vanity: Option, +} diff --git a/src/models/id.rs b/src/models/id.rs new file mode 100644 index 0000000..73969b2 --- /dev/null +++ b/src/models/id.rs @@ -0,0 +1,79 @@ +/// Track identifiers from different sources +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum TrackId<'a> { + /// Musixmatch ID that is the same for equivalent tracks (e.g. same track on different albums) + Commontrack(u64), + /// Unique Musixmatch track ID + TrackId(u64), + /// Human-readable Musixmatch ID + /// + /// Used in the URLs on the Musixmatch website. + /// + /// Example: `aespa/Black-Mamba` + CommontrackVanity(&'a str), + /// [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) + Isrc(&'a str), + /// Musicbrainz ID + /// + /// **Note:** Musicbrainz IDs are not present for most tracks in + /// the Musixmatch database, so use them only with a fallback. + Musicbrainz(&'a str), + /// Spotify track ID + Spotify(&'a str), +} + +impl<'a> TrackId<'a> { + pub(crate) fn to_param(&self) -> (&'static str, String) { + match self { + TrackId::Commontrack(id) => ("commontrack_id", id.to_string()), + TrackId::TrackId(id) => ("track_id", id.to_string()), + TrackId::CommontrackVanity(id) => ("commontrack_vanity_id", id.to_string()), + TrackId::Isrc(id) => ("track_isrc", id.to_string()), + TrackId::Musicbrainz(id) => ("track_mbid", id.to_string()), + TrackId::Spotify(id) => ("track_spotify_id", id.to_string()), + } + } +} + +/// Artist identifiers from different sources +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ArtistId<'a> { + /// Unique Musixmatch artist ID + ArtistId(u64), + /// Musicbrainz ID + /// + /// **Note:** Musicbrainz IDs are not present for most artists in + /// the Musixmatch database, so use them only with a fallback. + Musicbrainz(&'a str), +} + +impl<'a> ArtistId<'a> { + pub(crate) fn to_param(&self) -> (&'static str, String) { + match self { + ArtistId::ArtistId(id) => ("artist_id", id.to_string()), + ArtistId::Musicbrainz(id) => ("artist_mbid", id.to_string()), + } + } +} + +/// Album identifiers from different sources +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AlbumId<'a> { + /// Unique Musixmatch artist ID + AlbumId(u64), + /// Musicbrainz ID + /// + /// **Note:** Musicbrainz IDs are not present for most artists in + /// the Musixmatch database, so use them only with a fallback. + Musicbrainz(&'a str), +} + +impl<'a> AlbumId<'a> { + pub(crate) fn to_param(&self) -> (&'static str, String) { + match self { + AlbumId::AlbumId(id) => ("album_id", id.to_string()), + AlbumId::Musicbrainz(id) => ("album_mbid", id.to_string()), + } + } +} diff --git a/src/models/lyrics.rs b/src/models/lyrics.rs index 3c2da5d..956c2f1 100644 --- a/src/models/lyrics.rs +++ b/src/models/lyrics.rs @@ -7,7 +7,7 @@ pub(crate) struct LyricsBody { } /// Lyrics from the Musixmatch database. -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct Lyrics { /// Unique Musixmatch lyrics ID diff --git a/src/models/mod.rs b/src/models/mod.rs index afeba33..afb91c3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -4,8 +4,33 @@ pub use subtitle::SubtitleFormat; pub use subtitle::SubtitleLine; pub use subtitle::SubtitleTime; -mod track_id; -pub use track_id::TrackId; +mod id; +pub use id::AlbumId; +pub use id::ArtistId; +pub use id::TrackId; pub(crate) mod lyrics; pub use lyrics::Lyrics; + +pub(crate) mod track; +pub use track::ChartName; +pub use track::SortOrder; +pub use track::Track; +pub use track::TrackLyricsTranslationStatus; + +mod genre; +pub use genre::Genre; +pub use genre::GenreInner; +pub use genre::Genres; + +pub(crate) mod artist; +pub use artist::Artist; +pub use artist::ArtistAlias; +pub use artist::ArtistNameTranslation; +pub use artist::ArtistNameTranslationInner; + +// pub(crate) mod album; +// pub use album::Album; + +pub(crate) mod snippet; +pub use snippet::Snippet; diff --git a/src/models/snippet.rs b/src/models/snippet.rs new file mode 100644 index 0000000..898eeca --- /dev/null +++ b/src/models/snippet.rs @@ -0,0 +1,28 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub(crate) struct SnippetBody { + pub snippet: Snippet, +} + +/// Snippet: Snippet of lyrics text in the Musixmatch database. +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct Snippet { + /// Unique Musixmatch Snippet ID + pub snippet_id: u64, + /// Snippet language code (e.g. "en") + #[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, + /// Date and time when the snippet was last updated + pub updated_time: DateTime, + /// Snippet text + /// + /// Example: + /// + /// TODO: example + pub snippet_body: String, +} diff --git a/src/models/track.rs b/src/models/track.rs new file mode 100644 index 0000000..daa6038 --- /dev/null +++ b/src/models/track.rs @@ -0,0 +1,183 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::Genres; + +#[derive(Debug, Deserialize)] +pub(crate) struct TrackBody { + pub track: Track, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct TrackListBody { + pub track_list: Vec, +} + +/// Track: a song in the Musixmatch database +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct Track { + /// Unique Musixmatch Track ID + pub track_id: u64, + /// Track Musicbrainz ID + /// + /// **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)] + 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)] + 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)] + pub track_soundcloud_id: u64, + /// Track ID on XBox Music + #[serde(default)] + pub track_xboxmusic_id: String, + /// Title of the track + pub track_name: String, + /// Popularity of the track from 0 to 100 + #[serde(default)] + pub track_rating: u8, + /// Length of the track in seconds + #[serde(default)] + pub track_length: u32, + /// Musixmatch ID that is the same for equivalent tracks (e.g. same track on different albums) + pub commontrack_id: u64, + /// True if the track is instrumental + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub instrumental: bool, + /// True if the lyrics contain explicit language + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub explicit: bool, + /// True if lyrics are available + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub has_lyrics: bool, + /// True if subtitles (synchronized lyrics) are available + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub has_subtitles: bool, + /// True if richsync lyrics (synchronized by word) are available + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub has_richsync: bool, + /// True if the track structure is available + #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] + pub has_track_structure: bool, + /// Amount of users that favorited the track on Musixmatch + #[serde(default)] + pub num_favourite: u32, + /// Musixmatch lyrics ID + #[serde(default)] + pub lyrics_id: u64, + /// Musixmatch subtitle ID + #[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, + /// Musicbrainz artist ID + /// + /// **Note:** most artists 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)] + pub album_coverart_100x100: String, + /// Album cover URL (350x350px) + #[serde(default)] + pub album_coverart_350x350: String, + /// Album cover URL (500x500px) + #[serde(default)] + pub album_coverart_500x500: String, + /// Album cover URL (800x800px) + /// + /// **Note:** not present on a lot of albums + #[serde(default)] + pub album_coverart_800x800: String, + /// Human-readable Musixmatch ID + /// + /// Used in the URLs on the Musixmatch website. + /// + /// Example: `aespa/Black-Mamba` + pub commontrack_vanity_id: String, + /// Track release date + #[serde(default, deserialize_with = "crate::api_model::optional_date")] + pub first_release_date: Option>, + /// Date and time when the track was last updated + pub updated_time: DateTime, + /// List of primary genres + #[serde(default)] + pub primary_genres: Genres, + /// 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, + /// Status of lyrics translation + #[serde(default)] + pub track_lyrics_translation_status: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct TrackLyricsTranslationStatus { + /// Source language code (e.g. "ko") + pub from: String, + /// Target language code (e.g. "en") + pub to: String, + /// Translation ratio from 0 (untranslated) - 1 (fully translated) + /// + /// **NOT** the percentage + pub perc: f32, +} + +/// Available track charts +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ChartName { + /// Editorial chart + Top, + /// Most viewed lyrics in the last 2 hours + Hot, +} + +impl ChartName { + pub(crate) fn as_str(&self) -> &str { + match self { + ChartName::Top => "top", + ChartName::Hot => "hot", + } + } +} + +pub enum SortOrder { + Asc, + Desc, +} + +impl SortOrder { + pub(crate) fn as_str(&self) -> &str { + match self { + SortOrder::Asc => "asc", + SortOrder::Desc => "desc", + } + } +} diff --git a/src/models/track_id.rs b/src/models/track_id.rs deleted file mode 100644 index f1ab535..0000000 --- a/src/models/track_id.rs +++ /dev/null @@ -1,37 +0,0 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub enum TrackId<'a> { - /// Musixmatch ID for a unified track - Commontrack(u64), - /// Unique Musixmatch track ID - TrackId(u64), - /// Human-readable Musixmatch ID - /// - /// Used in the URLs on the Musixmatch website. - /// - /// Example: `aespa/Black-Mamba` - CommontrackVanity(&'a str), - /// [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) - Isrc(&'a str), - /// Musicbrainz ID - /// - /// **Note:** Musicbrainz IDs are not present for most tracks in - /// the Musixmatch database, so use them only with a fallback. - Musicbrainz(&'a str), - /// Spotify track ID - Spotify(&'a str), -} - -impl<'a> TrackId<'a> { - pub(crate) fn to_param(&self) -> (&'static str, String) { - match &self { - // &["commontrack_id", &id.to_string()] - TrackId::Commontrack(id) => ("commontrack_id", id.to_string()), - TrackId::TrackId(id) => ("track_id", id.to_string()), - TrackId::CommontrackVanity(id) => ("commontrack_vanity_id", id.to_string()), - TrackId::Isrc(id) => ("track_isrc", id.to_string()), - TrackId::Musicbrainz(id) => ("track_mbid", id.to_string()), - TrackId::Spotify(id) => ("track_spotify_id", id.to_string()), - } - } -} diff --git a/tests/artist_test.rs b/tests/artist_test.rs new file mode 100644 index 0000000..89ff363 --- /dev/null +++ b/tests/artist_test.rs @@ -0,0 +1,150 @@ +use chrono::TimeZone; +use dotenv::dotenv; +use musixmatch_inofficial::{models::ArtistId, storage::FileStorage, Error, Musixmatch}; +use rstest::rstest; + +#[ctor::ctor] +fn init() { + let _ = dotenv(); + env_logger::init(); +} + +fn new_mxm() -> Musixmatch { + Musixmatch::new( + &std::env::var("MUSIXMATCH_EMAIL").unwrap(), + &std::env::var("MUSIXMATCH_PASSWORD").unwrap(), + Some(Box::new(FileStorage::default())), + ) +} + +#[rstest] +#[case::artist(ArtistId::ArtistId(410698))] +#[case::artist(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_eq!( + artist.artist_name_translation_list[0] + .artist_name_translation + .language, + "EN" + ); + assert_eq!( + artist.artist_name_translation_list[0] + .artist_name_translation + .translation, + "PSY" + ); + assert_eq!( + artist.artist_name_translation_list[1] + .artist_name_translation + .language, + "KO" + ); + assert_eq!( + artist.artist_name_translation_list[1] + .artist_name_translation + .translation, + "싸이" + ); + 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 + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2016, 6, 30), + chrono::NaiveTime::default(), + )) + ); + 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/track_test.rs b/tests/track_test.rs new file mode 100644 index 0000000..112caa9 --- /dev/null +++ b/tests/track_test.rs @@ -0,0 +1,429 @@ +use chrono::{Datelike, TimeZone}; +use dotenv::dotenv; +use musixmatch_inofficial::{ + models::{AlbumId, ChartName, SortOrder, TrackId}, + storage::FileStorage, + Error, Musixmatch, +}; +use rstest::rstest; + +#[ctor::ctor] +fn init() { + let _ = dotenv(); + env_logger::init(); +} + +fn new_mxm() -> Musixmatch { + Musixmatch::new( + &std::env::var("MUSIXMATCH_EMAIL").unwrap(), + &std::env::var("MUSIXMATCH_PASSWORD").unwrap(), + Some(Box::new(FileStorage::default())), + ) +} + +#[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("Gangnam Style", "PSY", "", translation_status, lang_3c) + .await + .unwrap(); + + dbg!(&track); + + assert_eq!(track.track_id, 19737449); + assert_eq!(track.track_mbid, "882ce25d-51b9-4fe5-bbdf-16e661df0822"); + assert_eq!(track.track_isrc, "USUM71210283"); + assert_eq!( + track.commontrack_isrcs, + vec![vec![ + "USUM71210283", + "USHM91235871", + "KRA341205652", + "DEN061202418" + ]] + ); + assert_eq!(track.track_spotify_id, "1PKnaWkakd5CBjNv8NyaSK"); + assert_eq!( + track.commontrack_spotify_ids, + vec![ + "1PKnaWkakd5CBjNv8NyaSK", + "0TN8agMRmMu9oh2UbUbmMr", + "4htXSyLAH1wcHuLA5PKHSk", + "1d6RiDRVLe2RS5N3faTm4A", + "3KfAWiIGR5jaihyB7cMZtg", + "291iUZHZVkDUTs6rHXJ1bx", + "3HtU8IlHuHbL37fOB60sQ1", + ] + ); + assert_eq!( + track.track_xboxmusic_id, + "music.7B42BF07-0100-11DB-89CA-0019B92A3933" + ); + assert_eq!(track.track_name, "Gangnam Style"); + assert!(track.track_rating > 50); + assert_eq!(track.commontrack_id, 86989384); + assert!(!track.instrumental); + assert!(track.explicit); + assert!(track.has_subtitles); + assert!(track.has_track_structure); + assert!(track.num_favourite > 140); + assert_eq!(track.lyrics_id, 29727716); + assert_eq!(track.subtitle_id, 36671981); + assert_eq!(track.album_id, 14323438); + assert_eq!(track.album_name, "Gangnam Style - Single"); + assert_eq!(track.artist_id, 410698); + assert_eq!(track.artist_mbid, "f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"); + assert_eq!(track.artist_name, "Psy"); + assert_eq!( + track.album_coverart_100x100, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035.jpg" + ); + assert_eq!( + track.album_coverart_350x350, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035_350_350.jpg" + ); + assert_eq!( + track.album_coverart_500x500, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035_500_500.jpg" + ); + assert_eq!(track.commontrack_vanity_id, "410698/gangnam-style"); + // assert_eq!(track.first_release_date.day(), 1); + // assert_eq!(track.first_release_date.month(), 1); + // assert_eq!(track.first_release_date.year(), 2012); + assert!( + track.updated_time + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2022, 9, 25), + chrono::NaiveTime::default(), + )) + ); + + 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, 51); + assert_eq!(first_sec_genre.music_genre_parent_id, 34); + assert_eq!(first_sec_genre.music_genre_name, "K-Pop"); + assert_eq!(first_sec_genre.music_genre_name_extended, "K-Pop"); + assert_eq!( + first_sec_genre.music_genre_vanity.as_ref().unwrap(), + "K-Pop" + ); + + if translation_status { + let first_tstatus = &track.track_lyrics_translation_status[0]; + if lang_3c { + assert_eq!(first_tstatus.from, "kor"); + } else { + assert_eq!(first_tstatus.from, "ko"); + } + assert!(first_tstatus.perc >= 0.0 && first_tstatus.perc <= 1.0); + } 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, 29401691); + 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.day(), 17); + assert_eq!(release_date.month(), 11); + assert_eq!(release_date.year(), 2020); + assert!( + track.updated_time + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2022, 8, 27), + chrono::NaiveTime::default(), + )) + ); + + 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(19737449), translation_status, lang_3c) + .await + .unwrap(); + + dbg!(&track); + + assert_eq!(track.track_id, 19737449); + assert_eq!(track.track_mbid, "882ce25d-51b9-4fe5-bbdf-16e661df0822"); + assert_eq!(track.track_isrc, "USUM71210283"); + assert_eq!( + track.commontrack_isrcs, + vec![vec![ + "USUM71210283", + "USHM91235871", + "KRA341205652", + "DEN061202418" + ]] + ); + assert_eq!(track.track_spotify_id, "1PKnaWkakd5CBjNv8NyaSK"); + assert_eq!( + track.commontrack_spotify_ids, + vec![ + "1PKnaWkakd5CBjNv8NyaSK", + "0TN8agMRmMu9oh2UbUbmMr", + "4htXSyLAH1wcHuLA5PKHSk", + "1d6RiDRVLe2RS5N3faTm4A", + "3KfAWiIGR5jaihyB7cMZtg", + "291iUZHZVkDUTs6rHXJ1bx", + "3HtU8IlHuHbL37fOB60sQ1", + ] + ); + assert_eq!( + track.track_xboxmusic_id, + "music.7B42BF07-0100-11DB-89CA-0019B92A3933" + ); + assert_eq!(track.track_name, "Gangnam Style"); + assert!(track.track_rating > 50); + assert_eq!(track.commontrack_id, 86989384); + assert!(!track.instrumental); + assert!(track.explicit); + assert!(track.has_subtitles); + assert!(track.has_track_structure); + assert!(track.num_favourite > 140); + assert_eq!(track.lyrics_id, 29727716); + assert_eq!(track.subtitle_id, 36671981); + assert_eq!(track.album_id, 14323438); + assert_eq!(track.album_name, "Gangnam Style - Single"); + assert_eq!(track.artist_id, 410698); + assert_eq!(track.artist_mbid, "f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"); + assert_eq!(track.artist_name, "Psy"); + assert_eq!( + track.album_coverart_100x100, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035.jpg" + ); + assert_eq!( + track.album_coverart_350x350, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035_350_350.jpg" + ); + assert_eq!( + track.album_coverart_500x500, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035_500_500.jpg" + ); + assert_eq!(track.commontrack_vanity_id, "410698/gangnam-style"); + + let first_release = track.first_release_date.unwrap(); + assert_eq!(first_release.day(), 1); + assert_eq!(first_release.month(), 1); + assert_eq!(first_release.year(), 2012); + assert!( + track.updated_time + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2022, 9, 25), + chrono::NaiveTime::default(), + )) + ); + + 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, 51); + assert_eq!(first_sec_genre.music_genre_parent_id, 34); + assert_eq!(first_sec_genre.music_genre_name, "K-Pop"); + assert_eq!(first_sec_genre.music_genre_name_extended, "K-Pop"); + assert_eq!( + first_sec_genre.music_genre_vanity.as_ref().unwrap(), + "K-Pop" + ); + + if translation_status { + let first_tstatus = &track.track_lyrics_translation_status[0]; + if lang_3c { + assert_eq!(first_tstatus.from, "kor"); + } else { + assert_eq!(first_tstatus.from, "ko"); + } + assert!(first_tstatus.perc >= 0.0 && first_tstatus.perc <= 1.0); + } 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("not a thing that i would change") + .s_track_rating(SortOrder::Desc) + .send(10, 1) + .await + .unwrap(); + + assert_eq!(tracks.len(), 10); + + let track = &tracks[0]; + assert_eq!(track.commontrack_id, 8874280); + 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); +} From ca438da4de5ebca5161acb4479bfd96fbf3d6ec8 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 2 Oct 2022 00:09:47 +0200 Subject: [PATCH 3/5] feat: add genres, snippet API --- src/apis/mod.rs | 2 +- src/apis/snippet_api.rs | 20 ++++++++++++++++++++ src/apis/track_api.rs | 12 +++++++++++- src/models/snippet.rs | 12 +++++++----- tests/track_test.rs | 30 +++++++++++++++++++++++++++++- 5 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 src/apis/snippet_api.rs diff --git a/src/apis/mod.rs b/src/apis/mod.rs index c89bdcf..b04fd1c 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,6 +1,6 @@ // mod album_api; mod artist_api; mod lyrics_api; -// mod snippet_api; +mod snippet_api; mod subtitle_api; mod track_api; diff --git a/src/apis/snippet_api.rs b/src/apis/snippet_api.rs new file mode 100644 index 0000000..f6ff8ff --- /dev/null +++ b/src/apis/snippet_api.rs @@ -0,0 +1,20 @@ +use crate::error::Result; +use crate::models::snippet::{Snippet, SnippetBody}; +use crate::models::TrackId; +use crate::Musixmatch; + +impl Musixmatch { + pub async fn track_snippet(&self, id: TrackId<'_>) -> Result { + let mut url = self.new_url("track.snippet.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + url_query.finish(); + } + + let snippet_body = self.execute_get_request::(&url).await?; + Ok(snippet_body.snippet) + } +} diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs index 92c2dcd..a3b4247 100644 --- a/src/apis/track_api.rs +++ b/src/apis/track_api.rs @@ -2,7 +2,7 @@ use chrono::NaiveDate; use crate::error::Result; use crate::models::track::{Track, TrackBody, TrackListBody}; -use crate::models::{AlbumId, ChartName, SortOrder, TrackId}; +use crate::models::{AlbumId, ChartName, Genre, Genres, SortOrder, TrackId}; use crate::Musixmatch; impl Musixmatch { @@ -179,6 +179,16 @@ impl Musixmatch { .collect()) } + /// Get the list of the music genres the Musixmatch catalogue. + /// + /// # Reference + /// + pub async fn genres(&self) -> Result> { + let url = self.new_url("music.genres.get"); + let genres = self.execute_get_request::(&url).await?; + Ok(genres.music_genre_list) + } + /// Create a new query builder for searching tracks in the Musixmatch database. /// /// **Note:** The search results are unsorted the by default. You probably want diff --git a/src/models/snippet.rs b/src/models/snippet.rs index 898eeca..48b5767 100644 --- a/src/models/snippet.rs +++ b/src/models/snippet.rs @@ -6,7 +6,13 @@ pub(crate) struct SnippetBody { pub snippet: Snippet, } -/// Snippet: Snippet of lyrics text in the Musixmatch database. +/// Snippet of lyrics text in the Musixmatch database. +/// +/// A lyrics snippet is a very short representation of a song lyrics. +/// It’s usually twenty to a hundred characters long and it’s calculated +/// extracting a sequence of words from the lyrics. +/// +/// Example: "There's not a thing that I would change" #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Snippet { /// Unique Musixmatch Snippet ID @@ -20,9 +26,5 @@ pub struct Snippet { /// Date and time when the snippet was last updated pub updated_time: DateTime, /// Snippet text - /// - /// Example: - /// - /// TODO: example pub snippet_body: String, } diff --git a/tests/track_test.rs b/tests/track_test.rs index 112caa9..131036b 100644 --- a/tests/track_test.rs +++ b/tests/track_test.rs @@ -411,7 +411,6 @@ async fn search_lyrics() { assert_eq!(tracks.len(), 10); let track = &tracks[0]; - assert_eq!(track.commontrack_id, 8874280); assert_eq!(track.track_name, "Just the Way You Are"); assert_eq!(track.artist_name, "Bruno Mars"); } @@ -427,3 +426,32 @@ async fn search_empty() { 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 + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2022, 8, 29), + chrono::NaiveTime::default(), + )) + ); + assert_eq!( + snippet.snippet_body, + "There's not a thing that I would change" + ); +} From 4cd73d8ae1cad42f36cd68f4a7c50caace108825 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 2 Oct 2022 01:41:31 +0200 Subject: [PATCH 4/5] feat: add album API --- src/apis/album_api.rs | 101 ++++++++++++++++++++++++++++++++++ src/apis/mod.rs | 2 +- src/error.rs | 2 +- src/models/album.rs | 86 +++++++++++++++++++++++++++++ src/models/artist.rs | 4 ++ src/models/mod.rs | 4 +- src/models/track.rs | 13 ++++- tests/album_test.rs | 123 ++++++++++++++++++++++++++++++++++++++++++ tests/artist_test.rs | 4 +- tests/track_test.rs | 7 +-- 10 files changed, 336 insertions(+), 10 deletions(-) create mode 100644 src/apis/album_api.rs create mode 100644 src/models/album.rs create mode 100644 tests/album_test.rs diff --git a/src/apis/album_api.rs b/src/apis/album_api.rs new file mode 100644 index 0000000..c95fead --- /dev/null +++ b/src/apis/album_api.rs @@ -0,0 +1,101 @@ +use crate::error::Result; +use crate::models::album::{Album, AlbumBody, AlbumListBody}; +use crate::models::{AlbumId, ArtistId, SortOrder}; +use crate::Musixmatch; + +impl Musixmatch { + /// Get the metadata for an album specified by its ID. + /// + /// # Parameters + /// - `id`: [Album ID](crate::models::AlbumId) + /// + /// # Reference + /// + pub async fn album(&self, id: AlbumId<'_>) -> Result { + let mut url = self.new_url("album.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + url_query.finish(); + } + + let album_body = self.execute_get_request::(&url).await?; + Ok(album_body.album) + } + + /// Get the album discography of an artist specified by its ID. + /// + /// # Parameters + /// - `id`: [Artist ID](crate::models::ArtistId) + /// - `s_release_date`: Sort the albums by release date. If None, the albums are + /// sorted by popularity + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// + pub async fn artist_albums( + &self, + artist_id: ArtistId<'_>, + s_release_date: Option, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("artist.albums.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = artist_id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + if let Some(s_release_date) = s_release_date { + url_query.append_pair("s_release_date", s_release_date.as_str()); + } + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let album_list_body = self.execute_get_request::(&url).await?; + Ok(album_list_body + .album_list + .into_iter() + .map(|a| a.album) + .collect()) + } + + /// This api provides you the list of the top albums of a given country. + /// + /// # Parameters + /// - `country`: A valid country code (default: "US") + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// None - please update your documentation, Musixmatch! + pub async fn chart_albums( + &self, + country: &str, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("chart.albums.get"); + { + let mut url_query = url.query_pairs_mut(); + + url_query.append_pair("country", country); + url_query.append_pair("chart_name", "new_releases"); + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let album_list_body = self.execute_get_request::(&url).await?; + Ok(album_list_body + .album_list + .into_iter() + .map(|a| a.album) + .collect()) + } +} diff --git a/src/apis/mod.rs b/src/apis/mod.rs index b04fd1c..fcde0df 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,4 +1,4 @@ -// mod album_api; +mod album_api; mod artist_api; mod lyrics_api; mod snippet_api; diff --git a/src/error.rs b/src/error.rs index 8857d77..b06c0f5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,7 +10,7 @@ pub enum Error { TokenExpired, #[error("Error {status_code} returned by the Musixmatch API. Message: '{msg}'")] MusixmatchError { status_code: u16, msg: String }, - #[error("Musixmatch returned no data")] + #[error("Musixmatch returned no data or data that could not be deserialized")] NoData, #[error("You entered wrong credentials")] WrongCredentials, diff --git a/src/models/album.rs b/src/models/album.rs new file mode 100644 index 0000000..cbeaa90 --- /dev/null +++ b/src/models/album.rs @@ -0,0 +1,86 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::Genres; + +#[derive(Debug, Deserialize)] +pub(crate) struct AlbumBody { + pub album: Album, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct AlbumListBody { + pub album_list: Vec, +} + +/// Album: an album of songs in the Musixmatch database. +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct Album { + /// Unique Musixmatch Album ID + pub album_id: u64, + /// Musicbrainz album ID + /// + /// **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 + #[serde(default)] + pub album_rating: u8, + /// Number of tracks on the album + pub album_track_count: u16, + /// Album release date (e.g. "2009-07-07") + #[serde(default)] + 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, + + /// List of primary genres + #[serde(default)] + pub primary_genres: Genres, + /// 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)] + pub album_copyright: String, + /// Album label / recording company + #[serde(default)] + pub album_label: String, + /// Human-readable URL-safe Album ID + /// + /// Example: `LMFAO/Party-Rock-5` + pub album_vanity_id: String, + + /// Date and time when the album was last updated + pub updated_time: DateTime, + + /// Album cover URL (100x100px) + #[serde(default)] + pub album_coverart_100x100: String, + /// Album cover URL (350x350px) + #[serde(default)] + pub album_coverart_350x350: String, + /// Album cover URL (500x500px) + #[serde(default)] + pub album_coverart_500x500: String, + /// Album cover URL (800x800px) + /// + /// **Note:** not present on a lot of albums + #[serde(default)] + pub album_coverart_800x800: String, +} diff --git a/src/models/artist.rs b/src/models/artist.rs index 08d041b..f6b1b12 100644 --- a/src/models/artist.rs +++ b/src/models/artist.rs @@ -23,6 +23,7 @@ pub struct Artist { /// **Note:** most tracks dont have this entry set #[serde(default)] pub artist_mbid: String, + /// Artist name pub artist_name: String, /// Artist name in different languages @@ -37,6 +38,7 @@ pub struct Artist { /// Popularity of the artist from 0 to 100 #[serde(default)] pub artist_rating: u8, + /// Primary genres of the artist #[serde(default)] pub primary_genres: Genres, @@ -49,6 +51,7 @@ pub struct Artist { /// tagged as primary genre and this field empty. #[serde(default)] pub secondary_genres: Genres, + #[serde(default)] pub artist_twitter_url: String, #[serde(default)] @@ -59,6 +62,7 @@ pub struct Artist { pub artist_tiktok_url: String, #[serde(default)] pub artist_facebook_url: String, + /// URL-safe human-readable artist ID /// /// Example: "aespa" diff --git a/src/models/mod.rs b/src/models/mod.rs index afb91c3..0e60ecf 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -29,8 +29,8 @@ pub use artist::ArtistAlias; pub use artist::ArtistNameTranslation; pub use artist::ArtistNameTranslationInner; -// pub(crate) mod album; -// pub use album::Album; +pub(crate) mod album; +pub use album::Album; pub(crate) mod snippet; pub use snippet::Snippet; diff --git a/src/models/track.rs b/src/models/track.rs index daa6038..352915e 100644 --- a/src/models/track.rs +++ b/src/models/track.rs @@ -42,6 +42,7 @@ pub struct Track { /// Track ID on XBox Music #[serde(default)] pub track_xboxmusic_id: String, + /// Title of the track pub track_name: String, /// Popularity of the track from 0 to 100 @@ -52,6 +53,7 @@ pub struct Track { pub track_length: u32, /// Musixmatch ID that is the same for equivalent tracks (e.g. same track on different albums) pub commontrack_id: u64, + /// True if the track is instrumental #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] pub instrumental: bool, @@ -70,31 +72,36 @@ pub struct Track { /// True if the track structure is available #[serde(default, deserialize_with = "crate::api_model::bool_from_int")] pub has_track_structure: bool, + /// Amount of users that favorited the track on Musixmatch #[serde(default)] pub num_favourite: u32, + /// Musixmatch lyrics ID #[serde(default)] pub lyrics_id: u64, /// Musixmatch subtitle ID #[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, /// Musicbrainz artist ID /// - /// **Note:** most artists dont have this entry set + /// **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)] pub album_coverart_100x100: String, @@ -109,17 +116,20 @@ pub struct Track { /// **Note:** not present on a lot of albums #[serde(default)] pub album_coverart_800x800: String, + /// Human-readable Musixmatch ID /// /// Used in the URLs on the Musixmatch website. /// /// Example: `aespa/Black-Mamba` pub commontrack_vanity_id: String, + /// Track release date #[serde(default, deserialize_with = "crate::api_model::optional_date")] pub first_release_date: Option>, /// Date and time when the track was last updated pub updated_time: DateTime, + /// List of primary genres #[serde(default)] pub primary_genres: Genres, @@ -132,6 +142,7 @@ pub struct Track { /// tagged as primary genre and this field empty. #[serde(default)] pub secondary_genres: Genres, + /// Status of lyrics translation #[serde(default)] pub track_lyrics_translation_status: Vec, diff --git a/tests/album_test.rs b/tests/album_test.rs new file mode 100644 index 0000000..98aadb5 --- /dev/null +++ b/tests/album_test.rs @@ -0,0 +1,123 @@ +use chrono::TimeZone; +use dotenv::dotenv; +use musixmatch_inofficial::{ + models::{AlbumId, ArtistId}, + storage::FileStorage, + Error, Musixmatch, +}; +use rstest::rstest; + +#[ctor::ctor] +fn init() { + let _ = dotenv(); + env_logger::init(); +} + +fn new_mxm() -> Musixmatch { + Musixmatch::new( + &std::env::var("MUSIXMATCH_EMAIL").unwrap(), + &std::env::var("MUSIXMATCH_PASSWORD").unwrap(), + Some(Box::new(FileStorage::default())), + ) +} + +#[rstest] +#[case::id(AlbumId::AlbumId(14323438))] +#[case::musicbrainz(AlbumId::Musicbrainz("0fa2a7db-d26b-412e-beb0-df9855818b54"))] +#[tokio::test] +async fn by_id(#[case] album_id: AlbumId<'_>) { + let album = new_mxm().album(album_id).await.unwrap(); + + assert_eq!(album.album_id, 14323438); + assert_eq!(album.album_mbid, "0fa2a7db-d26b-412e-beb0-df9855818b54"); + assert_eq!(album.album_name, "Gangnam Style - Single"); + assert!(album.album_rating > 50); + 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 + > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd(2022, 6, 3), + chrono::NaiveTime::default(), + )) + ); + assert_eq!( + album.album_coverart_100x100, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035.jpg" + ); + assert_eq!( + album.album_coverart_350x350, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035_350_350.jpg" + ); + assert_eq!( + album.album_coverart_500x500, + "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035_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 index 89ff363..4f71793 100644 --- a/tests/artist_test.rs +++ b/tests/artist_test.rs @@ -18,8 +18,8 @@ fn new_mxm() -> Musixmatch { } #[rstest] -#[case::artist(ArtistId::ArtistId(410698))] -#[case::artist(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))] +#[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(); diff --git a/tests/track_test.rs b/tests/track_test.rs index 131036b..bed0399 100644 --- a/tests/track_test.rs +++ b/tests/track_test.rs @@ -91,9 +91,10 @@ async fn from_match(#[case] translation_status: bool, #[case] lang_3c: bool) { "https://s.mxmcdn.net/images-storage/albums/5/3/0/5/4/2/14245035_500_500.jpg" ); assert_eq!(track.commontrack_vanity_id, "410698/gangnam-style"); - // assert_eq!(track.first_release_date.day(), 1); - // assert_eq!(track.first_release_date.month(), 1); - // assert_eq!(track.first_release_date.year(), 2012); + let first_release = track.first_release_date.unwrap(); + assert_eq!(first_release.day(), 1); + assert_eq!(first_release.month(), 1); + assert_eq!(first_release.year(), 2012); assert!( track.updated_time > chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::new( From 41216c56a9b4a93c3632559d335d1af168bae20f Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 2 Oct 2022 01:43:34 +0200 Subject: [PATCH 5/5] fix: clippy warnings --- src/apis/artist_api.rs | 2 +- src/apis/track_api.rs | 2 +- src/models/album.rs | 2 +- src/models/snippet.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apis/artist_api.rs b/src/apis/artist_api.rs index 7ae1e23..f2dde1f 100644 --- a/src/apis/artist_api.rs +++ b/src/apis/artist_api.rs @@ -111,7 +111,7 @@ impl Musixmatch { { let mut url_query = url.query_pairs_mut(); - if country != "" { + if !country.is_empty() { url_query.append_pair("country", country); } url_query.append_pair("page_size", &page_size.to_string()); diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs index a3b4247..3f2b740 100644 --- a/src/apis/track_api.rs +++ b/src/apis/track_api.rs @@ -159,7 +159,7 @@ impl Musixmatch { { let mut url_query = url.query_pairs_mut(); - if country != "" { + if !country.is_empty() { url_query.append_pair("country", country); } url_query.append_pair("chart_name", chart_name.as_str()); diff --git a/src/models/album.rs b/src/models/album.rs index cbeaa90..dd7fcf2 100644 --- a/src/models/album.rs +++ b/src/models/album.rs @@ -14,7 +14,7 @@ pub(crate) struct AlbumListBody { } /// Album: an album of songs in the Musixmatch database. -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Album { /// Unique Musixmatch Album ID pub album_id: u64, diff --git a/src/models/snippet.rs b/src/models/snippet.rs index 48b5767..099c49f 100644 --- a/src/models/snippet.rs +++ b/src/models/snippet.rs @@ -13,7 +13,7 @@ pub(crate) struct SnippetBody { /// extracting a sequence of words from the lyrics. /// /// Example: "There's not a thing that I would change" -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Snippet { /// Unique Musixmatch Snippet ID pub snippet_id: u64,