Compare commits

...

5 commits

Author SHA1 Message Date
41216c56a9 fix: clippy warnings 2022-10-02 01:43:34 +02:00
4cd73d8ae1 feat: add album API 2022-10-02 01:41:58 +02:00
ca438da4de feat: add genres, snippet API 2022-10-02 00:12:33 +02:00
e9e3bdc26d feat: add track and artist API 2022-10-01 23:38:12 +02:00
692e112292 feat: add lyrics API 2022-10-01 02:22:51 +02:00
25 changed files with 2417 additions and 66 deletions

View file

@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
use crate::error::{Error, Result};
use crate::error::{Error, Result as MxmResult};
//#COMMON
@ -23,6 +24,7 @@ pub enum MessageBody<T> {
EmptyArr(Vec<()>),
// "body": {}
EmptyObj {},
EmptyStr(String),
}
#[derive(Debug, Deserialize)]
@ -32,17 +34,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<T> Resp<T> {
pub fn body_or_err(self) -> Result<T> {
pub fn body_or_err(self) -> MxmResult<T> {
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 +124,145 @@ pub struct ErrorMxm {
pub description: String,
}
pub fn bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
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<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v)
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0)
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0.0)
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v != 0.0)
}
}
deserializer.deserialize_any(BoolFromIntVisitor)
}
pub fn optional_date<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
struct OptionalDateVisitor;
impl<'de> Visitor<'de> for OptionalDateVisitor {
type Value = Option<DateTime<Utc>>;
fn expecting(
&self,
formatter: &mut serde::__private::fmt::Formatter,
) -> serde::__private::fmt::Result {
formatter.write_str("timestamp or empty string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
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::*;
@ -184,4 +325,51 @@ 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::<Resp<SubtitleBody>>(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::<Resp<SubtitleBody>>(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_optional_date() {
#[derive(Deserialize)]
struct S {
#[serde(deserialize_with = "optional_date")]
date: Option<DateTime<Utc>>,
}
let json_empty_string = r#"{"date": ""}"#;
let json_date = r#"{"date": "2022-08-27T23:47:20Z"}"#;
let res = serde_json::from_str::<S>(json_empty_string).unwrap();
assert!(res.date.is_none());
let res = serde_json::from_str::<S>(json_date).unwrap();
assert!(res.date.is_some());
}
}

101
src/apis/album_api.rs Normal file
View file

@ -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
/// <https://developer.musixmatch.com/documentation/api-reference/album-get>
pub async fn album(&self, id: AlbumId<'_>) -> Result<Album> {
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::<AlbumBody>(&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
/// <https://developer.musixmatch.com/documentation/api-reference/artist-albums-get>
pub async fn artist_albums(
&self,
artist_id: ArtistId<'_>,
s_release_date: Option<SortOrder>,
page_size: u8,
page: u32,
) -> Result<Vec<Album>> {
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::<AlbumListBody>(&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<Vec<Album>> {
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::<AlbumListBody>(&url).await?;
Ok(album_list_body
.album_list
.into_iter()
.map(|a| a.album)
.collect())
}
}

129
src/apis/artist_api.rs Normal file
View file

@ -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
/// <https://developer.musixmatch.com/documentation/api-reference/artist-get>
pub async fn artist(&self, id: ArtistId<'_>) -> Result<Artist> {
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::<ArtistBody>(&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
/// <https://developer.musixmatch.com/documentation/api-reference/artist-related-get>
pub async fn artist_related(
&self,
id: ArtistId<'_>,
page_size: u8,
page: u32,
) -> Result<Vec<Artist>> {
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::<ArtistListBody>(&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
/// <https://developer.musixmatch.com/documentation/api-reference/artist-search>
pub async fn artist_search(
&self,
q_artist: &str,
page_size: u8,
page: u32,
) -> Result<Vec<Artist>> {
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::<ArtistListBody>(&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
/// <https://developer.musixmatch.com/documentation/api-reference/artist-chart-get>
pub async fn chart_artists(
&self,
country: &str,
page_size: u8,
page: u32,
) -> Result<Vec<Artist>> {
let mut url = self.new_url("chart.artists.get");
{
let mut url_query = url.query_pairs_mut();
if !country.is_empty() {
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::<ArtistListBody>(&url).await?;
Ok(artist_list_body
.artist_list
.into_iter()
.map(|a| a.artist)
.collect())
}
}

51
src/apis/lyrics_api.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::error::Result;
use crate::models::lyrics::{Lyrics, LyricsBody};
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
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-lyrics-get>
pub async fn matcher_lyrics(&self, q_track: &str, q_artist: &str) -> Result<Lyrics> {
let mut url = self.new_url("matcher.lyrics.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);
}
url_query.finish();
}
let lyrics_body = self.execute_get_request::<LyricsBody>(&url).await?;
Ok(lyrics_body.lyrics)
}
/// Get the lyrics for a track specified by its ID.
///
/// # Parameters
/// - `id`: [Track ID](crate::models::TrackId)
///
/// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-lyrics-get>
pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result<Lyrics> {
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::<LyricsBody>(&url).await?;
Ok(lyrics_body.lyrics)
}
}

View file

@ -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;

20
src/apis/snippet_api.rs Normal file
View file

@ -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<Snippet> {
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::<SnippetBody>(&url).await?;
Ok(snippet_body.snippet)
}
}

View file

@ -16,7 +16,7 @@ impl Musixmatch {
/// so this should be the recommended value.
///
/// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-subtitle-get>
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-subtitle-get>
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());
@ -44,7 +48,6 @@ impl Musixmatch {
}
let subtitle_body = self.execute_get_request::<SubtitleBody>(&url).await?;
Ok(subtitle_body.subtitle)
}
@ -87,7 +90,6 @@ impl Musixmatch {
}
let subtitle_body = self.execute_get_request::<SubtitleBody>(&url).await?;
Ok(subtitle_body.subtitle)
}
}

426
src/apis/track_api.rs Normal file
View file

@ -0,0 +1,426 @@
use chrono::NaiveDate;
use crate::error::Result;
use crate::models::track::{Track, TrackBody, TrackListBody};
use crate::models::{AlbumId, ChartName, Genre, Genres, 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 6392](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) 3-letter language codes
/// instead of [ISO 6391](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes.
///
/// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-track-get>
pub async fn matcher_track(
&self,
q_track: &str,
q_artist: &str,
q_album: &str,
translation_status: bool,
lang_3c: bool,
) -> Result<Track> {
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::<TrackBody>(&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 6392](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) 3-letter language codes
/// instead of [ISO 6391](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes.
///
/// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-get>
pub async fn track(
&self,
id: TrackId<'_>,
translation_status: bool,
lang_3c: bool,
) -> Result<Track> {
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::<TrackBody>(&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
/// <https://developer.musixmatch.com/documentation/api-reference/album-tracks-get>
pub async fn album_tracks(
&self,
id: AlbumId<'_>,
f_has_lyrics: bool,
page_size: u8,
page: u32,
) -> Result<Vec<Track>> {
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::<TrackListBody>(&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
/// <https://developer.musixmatch.com/documentation/api-reference/track-chart-get>
pub async fn chart_tracks(
&self,
country: &str,
chart_name: ChartName,
f_has_lyrics: bool,
page_size: u8,
page: u32,
) -> Result<Vec<Track>> {
let mut url = self.new_url("chart.tracks.get");
{
let mut url_query = url.query_pairs_mut();
if !country.is_empty() {
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::<TrackListBody>(&url).await?;
Ok(track_list_body
.track_list
.into_iter()
.map(|t| t.track)
.collect())
}
/// Get the list of the music genres the Musixmatch catalogue.
///
/// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/music-genres-get>
pub async fn genres(&self) -> Result<Vec<Genre>> {
let url = self.new_url("music.genres.get");
let genres = self.execute_get_request::<Genres>(&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
/// 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
/// <https://developer.musixmatch.com/documentation/api-reference/track-search>
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<u64>,
f_music_genre_id: Option<u64>,
f_lyrics_language: Option<&'a str>,
f_has_lyrics: bool,
f_track_release_group_first_release_date_min: Option<NaiveDate>,
f_track_release_group_first_release_date_max: Option<NaiveDate>,
s_artist_rating: Option<SortOrder>,
s_track_rating: Option<SortOrder>,
quorum_factor: Option<f32>,
}
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<Vec<Track>> {
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::<TrackListBody>(&url).await?;
Ok(track_list_body
.track_list
.into_iter()
.map(|t| t.track)
.collect())
}
}

View file

@ -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,

View file

@ -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<MusixmatchRef>,
}
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,
},
}),
}
}

86
src/models/album.rs Normal file
View file

@ -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<AlbumBody>,
}
/// Album: an album of songs in the Musixmatch database.
#[derive(Clone, Debug, PartialEq, Eq, 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<Utc>,
/// 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,
}

111
src/models/artist.rs Normal file
View file

@ -0,0 +1,111 @@
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<ArtistBody>,
}
/// 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<ArtistNameTranslation>,
/// 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<ArtistAlias>,
/// 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<Utc>,
/// 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,
}

30
src/models/genre.rs Normal file
View file

@ -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<Genre>,
}
#[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<String>,
}

79
src/models/id.rs Normal file
View file

@ -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()),
}
}
}

42
src/models/lyrics.rs Normal file
View file

@ -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, Eq, 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<Utc>,
}

View file

@ -4,5 +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;

30
src/models/snippet.rs Normal file
View file

@ -0,0 +1,30 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub(crate) struct SnippetBody {
pub snippet: Snippet,
}
/// Snippet of lyrics text in the Musixmatch database.
///
/// A lyrics snippet is a very short representation of a song lyrics.
/// Its usually twenty to a hundred characters long and its calculated
/// extracting a sequence of words from the lyrics.
///
/// Example: "There's not a thing that I would change"
#[derive(Clone, Debug, PartialEq, Eq, 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<Utc>,
/// Snippet text
pub snippet_body: String,
}

View file

@ -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<String>,
#[serde(default)]
pub subtitle_language: String,
/// Language name (e.g. "English")
pub subtitle_language_description: Option<String>,
#[serde(default)]
pub subtitle_language_description: String,
/// Copyright text of the lyrics
///
/// Ends with a newline.

194
src/models/track.rs Normal file
View file

@ -0,0 +1,194 @@
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<TrackBody>,
}
/// 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<Vec<String>>,
/// 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<String>,
/// 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 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,
/// 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<DateTime<Utc>>,
/// Date and time when the track was last updated
pub updated_time: DateTime<Utc>,
/// 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<TrackLyricsTranslationStatus>,
}
#[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",
}
}
}

View file

@ -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()),
}
}
}

123
tests/album_test.rs Normal file
View file

@ -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);
}

150
tests/artist_test.rs Normal file
View file

@ -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::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_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);
}

102
tests/lyrics_test.rs Normal file
View file

@ -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));
}

View file

@ -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));
}

458
tests/track_test.rs Normal file
View file

@ -0,0 +1,458 @@
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");
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())
}
}
#[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::<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("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.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
> 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"
);
}