From 6b4312a6a5dc8476851bf055f0aa4305cf3cd7fe Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 3 Feb 2024 16:26:21 +0100 Subject: [PATCH 01/61] feat: add Musixmatch Desktop client --- .env.example | 1 + .woodpecker.yml | 3 +- Cargo.toml | 2 + README.md | 44 ++++---- src/apis/album_api.rs | 6 +- src/apis/artist_api.rs | 8 +- src/apis/lyrics_api.rs | 6 +- src/apis/snippet_api.rs | 2 +- src/apis/subtitle_api.rs | 4 +- src/apis/track_api.rs | 12 +-- src/error.rs | 11 ++ src/lib.rs | 217 +++++++++++++++++++++++++++------------ tests/tests.rs | 31 ++++-- 13 files changed, 229 insertions(+), 118 deletions(-) diff --git a/.env.example b/.env.example index 7ab1bf9..c79ac20 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ +MUSIXMATCH_CLIENT=Android MUSIXMATCH_EMAIL=mail@example.com MUSIXMATCH_PASSWORD=super-secret diff --git a/.woodpecker.yml b/.woodpecker.yml index acfacc9..c93721f 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -10,4 +10,5 @@ pipeline: - rustup component add rustfmt clippy - cargo fmt --all --check - cargo clippy --all -- -D warnings - - cargo test --workspace + - MUSIXMATCH_CLIENT=Desktop cargo test --workspace + - MUSIXMATCH_CLIENT=Android cargo test --workspace diff --git a/Cargo.toml b/Cargo.toml index 2323866..03dd2da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ reqwest = { version = "0.11.11", default-features = false, features = [ "json", "gzip", ] } +url = "2.0.0" tokio = { version = "1.20.0" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.85" @@ -52,6 +53,7 @@ dotenvy = "0.15.5" tokio = { version = "1.20.0", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" +serde_plain = "1.0.2" [profile.release] strip = true diff --git a/README.md b/README.md index 9e83d62..a4a99ba 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,39 @@ # Musixmatch-Inofficial -This is an inofficial client for the Musixmatch API that uses the -key embedded in the Musixmatch Android app. +This is an inofficial client for the Musixmatch API that uses the key embedded in the +Musixmatch Android app or desktop client. It allows you to obtain synchronized lyrics in different formats -([LRC](https://en.wikipedia.org/wiki/LRC_(file_format)), +([LRC](), [DFXP](https://www.w3.org/TR/ttml1/), JSON) for almost any song. -A free Musixmatch account is required for operation -([you can sign up here](https://www.musixmatch.com/de/sign-up)). +If you use the Android client, you need a free Musixmatch account +([you can sign up here](https://www.musixmatch.com/de/sign-up)). The desktop client can +be used anonymously and is currently the default option, but since Musixmatch +discontinued that application, they may shut it down. ## ⚠️ Copyright disclaimer -Song lyrics are copyrighted works (just like books, poems and the songs -themselves). +Song lyrics are copyrighted works (just like books, poems and the songs themselves). -Musixmatch does allow its users to obtains song lyrics for private -use (e.g. to enrich their music collection). But it does not allow you -to publish their lyrics or use them commercially. +Musixmatch does allow its users to obtains song lyrics for private use (e.g. to enrich +their music collection). But it does not allow you to publish their lyrics or use them +commercially. -You will get in trouble if you use this client to create a public -lyrics site/app. If you want to use Musixmatch data for this purpose, -you will have to give them money (see their -[commercial plans](https://developer.musixmatch.com/plans)) -and use their [official API](https://developer.musixmatch.com/documentation). +You will get in trouble if you use this client to create a public lyrics site/app. If +you want to use Musixmatch data for this purpose, you will have to give them money (see +their [commercial plans](https://developer.musixmatch.com/plans)) and use their +[official API](https://developer.musixmatch.com/documentation). ## Development info -Running the tests requires Musixmatch credentials. The credentials are read -from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment variables. +You can choose which client to test by setting the `MUSIXMATCH_CLIENT` environment +variable to either `Desktop` or `Android` (it defaults to Desktop). -To make local development easier, I have included `dotenvy` to read the -credentials from an `.env` file. Copy the `.env.example` file -in the root directory, rename it to `.env` and fill in your credentials. +Running the tests for the Android client requires Musixmatch credentials. The +credentials are read from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment +variables. + +To make local development easier, I have included `dotenvy` to read the credentials from +an `.env` file. Copy the `.env.example` file in the root directory, rename it to `.env` +and fill in your credentials. diff --git a/src/apis/album_api.rs b/src/apis/album_api.rs index c95fead..04d6c43 100644 --- a/src/apis/album_api.rs +++ b/src/apis/album_api.rs @@ -12,7 +12,7 @@ impl Musixmatch { /// # Reference /// pub async fn album(&self, id: AlbumId<'_>) -> Result { - let mut url = self.new_url("album.get"); + let mut url = self.new_url("album.get")?; { let mut url_query = url.query_pairs_mut(); @@ -43,7 +43,7 @@ impl Musixmatch { page_size: u8, page: u32, ) -> Result> { - let mut url = self.new_url("artist.albums.get"); + let mut url = self.new_url("artist.albums.get")?; { let mut url_query = url.query_pairs_mut(); @@ -80,7 +80,7 @@ impl Musixmatch { page_size: u8, page: u32, ) -> Result> { - let mut url = self.new_url("chart.albums.get"); + let mut url = self.new_url("chart.albums.get")?; { let mut url_query = url.query_pairs_mut(); diff --git a/src/apis/artist_api.rs b/src/apis/artist_api.rs index f2dde1f..8b8d37b 100644 --- a/src/apis/artist_api.rs +++ b/src/apis/artist_api.rs @@ -12,7 +12,7 @@ impl Musixmatch { /// # Reference /// pub async fn artist(&self, id: ArtistId<'_>) -> Result { - let mut url = self.new_url("artist.get"); + let mut url = self.new_url("artist.get")?; { let mut url_query = url.query_pairs_mut(); @@ -40,7 +40,7 @@ impl Musixmatch { page_size: u8, page: u32, ) -> Result> { - let mut url = self.new_url("artist.related.get"); + let mut url = self.new_url("artist.related.get")?; { let mut url_query = url.query_pairs_mut(); @@ -74,7 +74,7 @@ impl Musixmatch { page_size: u8, page: u32, ) -> Result> { - let mut url = self.new_url("artist.search"); + let mut url = self.new_url("artist.search")?; { let mut url_query = url.query_pairs_mut(); @@ -107,7 +107,7 @@ impl Musixmatch { page_size: u8, page: u32, ) -> Result> { - let mut url = self.new_url("chart.artists.get"); + let mut url = self.new_url("chart.artists.get")?; { let mut url_query = url.query_pairs_mut(); diff --git a/src/apis/lyrics_api.rs b/src/apis/lyrics_api.rs index d9a5038..b3a087c 100644 --- a/src/apis/lyrics_api.rs +++ b/src/apis/lyrics_api.rs @@ -14,7 +14,7 @@ impl Musixmatch { /// # 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 = self.new_url("matcher.lyrics.get")?; { let mut url_query = url.query_pairs_mut(); if !q_track.is_empty() { @@ -39,7 +39,7 @@ impl Musixmatch { /// # Reference /// pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result { - let mut url = self.new_url("track.lyrics.get"); + let mut url = self.new_url("track.lyrics.get")?; { let mut url_query = url.query_pairs_mut(); let id_param = id.to_param(); @@ -66,7 +66,7 @@ impl Musixmatch { id: TrackId<'_>, selected_language: &str, ) -> Result { - let mut url = self.new_url("crowd.track.translations.get"); + let mut url = self.new_url("crowd.track.translations.get")?; { let mut url_query = url.query_pairs_mut(); let id_param = id.to_param(); diff --git a/src/apis/snippet_api.rs b/src/apis/snippet_api.rs index c8b3591..01fc50d 100644 --- a/src/apis/snippet_api.rs +++ b/src/apis/snippet_api.rs @@ -16,7 +16,7 @@ impl Musixmatch { /// # Reference /// pub async fn track_snippet(&self, id: TrackId<'_>) -> Result { - let mut url = self.new_url("track.snippet.get"); + let mut url = self.new_url("track.snippet.get")?; { let mut url_query = url.query_pairs_mut(); diff --git a/src/apis/subtitle_api.rs b/src/apis/subtitle_api.rs index 904f028..43fcb8f 100644 --- a/src/apis/subtitle_api.rs +++ b/src/apis/subtitle_api.rs @@ -25,7 +25,7 @@ impl Musixmatch { f_subtitle_length: Option, f_subtitle_length_max_deviation: Option, ) -> Result { - let mut url = self.new_url("matcher.subtitle.get"); + let mut url = self.new_url("matcher.subtitle.get")?; { let mut url_query = url.query_pairs_mut(); if !q_track.is_empty() { @@ -73,7 +73,7 @@ impl Musixmatch { f_subtitle_length: Option, f_subtitle_length_max_deviation: Option, ) -> Result { - let mut url = self.new_url("track.subtitle.get"); + let mut url = self.new_url("track.subtitle.get")?; { let mut url_query = url.query_pairs_mut(); diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs index 045779f..4b3cd9d 100644 --- a/src/apis/track_api.rs +++ b/src/apis/track_api.rs @@ -27,7 +27,7 @@ impl Musixmatch { translation_status: bool, lang_3c: bool, ) -> Result { - let mut url = self.new_url("matcher.track.get"); + let mut url = self.new_url("matcher.track.get")?; { let mut url_query = url.query_pairs_mut(); @@ -74,7 +74,7 @@ impl Musixmatch { translation_status: bool, lang_3c: bool, ) -> Result { - let mut url = self.new_url("track.get"); + let mut url = self.new_url("track.get")?; { let mut url_query = url.query_pairs_mut(); @@ -114,7 +114,7 @@ impl Musixmatch { page_size: u8, page: u32, ) -> Result> { - let mut url = self.new_url("album.tracks.get"); + let mut url = self.new_url("album.tracks.get")?; { let mut url_query = url.query_pairs_mut(); @@ -155,7 +155,7 @@ impl Musixmatch { page_size: u8, page: u32, ) -> Result> { - let mut url = self.new_url("chart.tracks.get"); + let mut url = self.new_url("chart.tracks.get")?; { let mut url_query = url.query_pairs_mut(); @@ -184,7 +184,7 @@ impl Musixmatch { /// # Reference /// pub async fn genres(&self) -> Result> { - let url = self.new_url("music.genres.get"); + let url = self.new_url("music.genres.get")?; let genres = self.execute_get_request::(&url).await?; Ok(genres.music_genre_list) } @@ -347,7 +347,7 @@ impl<'a> TrackSearchQuery<'a> { /// - `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 = self.mxm.new_url("track.search")?; { let mut url_query = url.query_pairs_mut(); diff --git a/src/error.rs b/src/error.rs index cb62a3c..1258bd7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + pub(crate) type Result = core::result::Result; /// Custom error type for the Musixmatch client @@ -36,6 +38,9 @@ pub enum Error { /// Error from the HTTP client #[error("http error: {0}")] Http(reqwest::Error), + /// Unspecified error + #[error("{0}")] + Other(Cow<'static, str>), } impl From for Error { @@ -44,3 +49,9 @@ impl From for Error { Self::Http(value.without_url()) } } + +impl From for Error { + fn from(value: url::ParseError) -> Self { + Self::Other(format!("url parse error: {value}").into()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8029bcf..999bee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,11 +35,30 @@ use crate::error::Result; const YMD_FORMAT: &[time::format_description::FormatItem] = format_description!("[year][month][day]"); -const APP_ID: &str = "android-player-v1.0"; -const API_URL: &str = "https://apic.musixmatch.com/ws/1.1/"; -const SIGNATURE_SECRET: &[u8; 20] = b"967Pn4)N3&R_GBg5$b('"; +/// Hardcoded client configuration +struct ClientCfg { + app_id: &'static str, + api_url: &'static str, + signature_secret: &'static [u8; 20], + user_agent: &'static str, + login: bool, +} + +const DESKTOP_CLIENT: ClientCfg = ClientCfg { + app_id: "web-desktop-app-v1.0", + api_url: "https://apic-desktop.musixmatch.com/ws/1.1/", + signature_secret: b"IEJ5E8XFaHQvIQNfs7IC", + user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Musixmatch/0.19.4 Chrome/58.0.3029.110 Electron/1.7.6 Safari/537.36", + login: false +}; +const ANDROID_CLIENT: ClientCfg = ClientCfg { + app_id: "android-player-v1.0", + api_url: "https://apic.musixmatch.com/ws/1.1/", + signature_secret: b"967Pn4)N3&R_GBg5$b('", + user_agent: "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Build/T3B2.230316.003)", + login: true, +}; -const DEFAULT_UA: &str = "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Build/T3B2.230316.003)"; const DEFAULT_BRAND: &str = "Google"; const DEFAULT_DEVICE: &str = "Pixel 6"; @@ -57,6 +76,7 @@ pub struct Musixmatch { /// Used to construct a new [`Musixmatch`] client.# #[derive(Default)] pub struct MusixmatchBuilder { + client_type: ClientType, user_agent: Option, brand: Option, device: Option, @@ -64,6 +84,29 @@ pub struct MusixmatchBuilder { credentials: Option, } +/// Musixmatch client type +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ClientType { + /// The desktop client is used with Musixmatch's electron-based Desktop application. + /// + /// The client allows anonymous access and is currently the default option. + /// + /// Since Musixmatch's desktop application is discontinued, the client may stop working in the future. + #[default] + Desktop, + /// The Android client requires a (free) Musixmatch account + Android, +} + +impl From for ClientCfg { + fn from(value: ClientType) -> Self { + match value { + ClientType::Desktop => DESKTOP_CLIENT, + ClientType::Android => ANDROID_CLIENT, + } + } +} + #[derive(Default)] enum DefaultOpt { Some(T), @@ -86,6 +129,8 @@ struct MusixmatchRef { http: Client, storage: Option>, credentials: RwLock>, + client_type: ClientType, + client_cfg: ClientCfg, brand: String, device: String, usertoken: Mutex>, @@ -99,6 +144,7 @@ struct Credentials { #[derive(Debug, Serialize, Deserialize)] struct StoredSession { + client_type: ClientType, usertoken: String, } @@ -167,6 +213,12 @@ impl MusixmatchBuilder { self } + /// Set the client type (Desktop, Android) of the Musixmatch client + pub fn client_type(mut self, client_type: ClientType) -> Self { + self.client_type = client_type; + self + } + /// Set the device brand of the Musixmatch client pub fn device_brand>(mut self, device_brand: S) -> Self { self.brand = Some(device_brand.into()); @@ -187,13 +239,18 @@ impl MusixmatchBuilder { /// Returns a new, configured Musixmatch client using a Reqwest client builder pub fn build_with_client(self, client_builder: ClientBuilder) -> Result { let storage = self.storage.or_default(|| Box::::default()); - let stored_session = Musixmatch::retrieve_session(&storage); + let stored_session = + Musixmatch::retrieve_session(&storage).filter(|s| s.client_type == self.client_type); + let client_cfg = ClientCfg::from(self.client_type); let mut headers = HeaderMap::new(); headers.insert(header::COOKIE, "AWSELBCORS=0; AWSELB=0".parse().unwrap()); let http = client_builder - .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) + .user_agent( + self.user_agent + .unwrap_or_else(|| client_cfg.user_agent.to_owned()), + ) .gzip(true) .default_headers(headers) .build()?; @@ -203,6 +260,8 @@ impl MusixmatchBuilder { http, storage, credentials: RwLock::new(self.credentials), + client_type: self.client_type, + client_cfg, brand: self.brand.unwrap_or_else(|| DEFAULT_BRAND.to_owned()), device: self.device.unwrap_or_else(|| DEFAULT_DEVICE.to_owned()), usertoken: Mutex::new(stored_session.map(|s| s.usertoken)), @@ -236,49 +295,66 @@ impl Musixmatch { } } - let credentials = { + let credentials = if self.inner.client_cfg.login { let c = self.inner.credentials.read().unwrap(); match c.deref() { - Some(c) => c.clone(), + Some(c) => Some(c.clone()), None => return Err(Error::MissingCredentials), } + } else { + None }; let now = OffsetDateTime::now_utc(); - let guid = random_guid(); - let adv_id = random_uuid(); // Get user token // The get_token endpoint seems to be rate limited for 2 requests per minute - let mut url = Url::parse_with_params( - &format!("{}{}", API_URL, "token.get"), - &[ - ("adv_id", adv_id.as_str()), - ("root", "0"), - ("sideloaded", "0"), - ("app_id", "android-player-v1.0"), - // App version (7.9.5) - ("build_number", "2022090901"), - ("guid", guid.as_str()), - ("lang", "en_US"), - ("model", self.model_string().as_str()), - ( - "timestamp", - now.format(&Rfc3339).unwrap_or_default().as_str(), - ), - ("format", "json"), - ], - ) - .unwrap(); - sign_url_with_date(&mut url, now); + let base_url = format!("{}{}", self.inner.client_cfg.api_url, "token.get"); + let mut url = match self.inner.client_type { + ClientType::Desktop => Url::parse_with_params( + &base_url, + &[ + ("format", "json"), + ("user_language", "en"), + ("app_id", self.inner.client_cfg.app_id), + ], + ), + ClientType::Android => { + let guid = random_guid(); + let adv_id = random_uuid(); + Url::parse_with_params( + &base_url, + &[ + ("adv_id", adv_id.as_str()), + ("root", "0"), + ("sideloaded", "0"), + ("app_id", self.inner.client_cfg.app_id), + // App version (7.9.5) + ("build_number", "2022090901"), + ("guid", guid.as_str()), + ("lang", "en_US"), + ("model", self.model_string().as_str()), + ( + "timestamp", + now.format(&Rfc3339).unwrap_or_default().as_str(), + ), + ("format", "json"), + ("user_language", "en"), + ], + ) + } + }?; + self.sign_url_with_date(&mut url, now); let resp = self.inner.http.get(url).send().await?.error_for_status()?; let tdata = resp.json::>().await?; let usertoken = tdata.body_or_err()?.user_token; info!("Received new usertoken: {}****", &usertoken[0..8]); - let account = self.post_credentials(&usertoken, &credentials).await?; - info!("Logged in as {} ", account.name); + if let Some(credentials) = credentials { + let account = self.post_credentials(&usertoken, &credentials).await?; + info!("Logged in as {} ", account.name); + } *stored_usertoken = Some(usertoken.to_owned()); self.store_session(&usertoken); @@ -290,8 +366,8 @@ impl Musixmatch { usertoken: &str, credentials: &Credentials, ) -> Result { - let mut url = new_url_from_token("credential.post", usertoken); - sign_url_with_date(&mut url, OffsetDateTime::now_utc()); + let mut url = self.new_url_from_token("credential.post", usertoken)?; + self.sign_url_with_date(&mut url, OffsetDateTime::now_utc()); let api_credentials = api_model::Credentials { credential_list: &[api_model::CredentialWrap { @@ -334,6 +410,7 @@ impl Musixmatch { fn store_session(&self, usertoken: &str) { if let Some(storage) = &self.inner.storage { let to_store = StoredSession { + client_type: self.inner.client_type, usertoken: usertoken.to_owned(), }; @@ -368,12 +445,12 @@ impl Musixmatch { ) } - fn new_url(&self, endpoint: &str) -> reqwest::Url { + fn new_url(&self, endpoint: &str) -> Result { Url::parse_with_params( - &format!("{}{}", API_URL, endpoint), - &[("app_id", APP_ID), ("format", "json")], + &format!("{}{}", self.inner.client_cfg.api_url, endpoint), + &[("app_id", self.inner.client_cfg.app_id), ("format", "json")], ) - .unwrap() + .map_err(Error::from) } async fn finish_url(&self, url: &mut Url, force_new_session: bool) -> Result<()> { @@ -382,7 +459,7 @@ impl Musixmatch { .append_pair("usertoken", &usertoken) .finish(); - sign_url_with_date(url, OffsetDateTime::now_utc()); + self.sign_url_with_date(url, OffsetDateTime::now_utc()); Ok(()) } @@ -441,6 +518,33 @@ impl Musixmatch { password: password.into(), }); } + + fn new_url_from_token(&self, endpoint: &str, usertoken: &str) -> Result { + Url::parse_with_params( + &format!("{}{}", self.inner.client_cfg.api_url, endpoint), + &[ + ("app_id", self.inner.client_cfg.app_id), + ("usertoken", usertoken), + ("format", "json"), + ], + ) + .map_err(Error::from) + } + + fn sign_url_with_date(&self, url: &mut Url, date: OffsetDateTime) { + let mut mac = Hmac::::new_from_slice(self.inner.client_cfg.signature_secret).unwrap(); + + mac.update(url.as_str().as_bytes()); + mac.update(date.format(YMD_FORMAT).unwrap_or_default().as_bytes()); + + let sig = mac.finalize().into_bytes(); + let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig) + "\n"; + + url.query_pairs_mut() + .append_pair("signature", &sig_b64) + .append_pair("signature_protocol", "sha1") + .finish(); + } } fn random_guid() -> String { @@ -461,33 +565,6 @@ fn random_uuid() -> String { ) } -fn new_url_from_token(endpoint: &str, usertoken: &str) -> reqwest::Url { - Url::parse_with_params( - &format!("{}{}", API_URL, endpoint), - &[ - ("app_id", APP_ID), - ("usertoken", usertoken), - ("format", "json"), - ], - ) - .unwrap() -} - -fn sign_url_with_date(url: &mut Url, date: OffsetDateTime) { - let mut mac = Hmac::::new_from_slice(SIGNATURE_SECRET).unwrap(); - - mac.update(url.as_str().as_bytes()); - mac.update(date.format(YMD_FORMAT).unwrap_or_default().as_bytes()); - - let sig = mac.finalize().into_bytes(); - let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig) + "\n"; - - url.query_pairs_mut() - .append_pair("signature", &sig_b64) - .append_pair("signature_protocol", "sha1") - .finish(); -} - #[cfg(test)] mod tests { use time::macros::datetime; @@ -496,8 +573,12 @@ mod tests { #[test] fn t_sign_url() { + let mxm = Musixmatch::builder() + .client_type(ClientType::Android) + .build() + .unwrap(); let mut url = Url::parse("https://apic.musixmatch.com/ws/1.1/track.subtitle.get?app_id=android-player-v1.0&usertoken=22092860c49e8d783b569a7bd847cd5b289bbec306f8a0bb2d3771&format=json&track_spotify_id=7Ga0ByppmSXWuKXdsD8JGL&subtitle_format=mxm").unwrap(); - sign_url_with_date(&mut url, datetime!(2022-09-28 0:00 UTC)); + mxm.sign_url_with_date(&mut url, datetime!(2022-09-28 0:00 UTC)); assert_eq!(url.as_str(), "https://apic.musixmatch.com/ws/1.1/track.subtitle.get?app_id=android-player-v1.0&usertoken=22092860c49e8d783b569a7bd847cd5b289bbec306f8a0bb2d3771&format=json&track_spotify_id=7Ga0ByppmSXWuKXdsD8JGL&subtitle_format=mxm&signature=cvXbedVvGneT7o4k8QG6jfk9pAM%3D%0A&signature_protocol=sha1") } } diff --git a/tests/tests.rs b/tests/tests.rs index 238f715..2717f41 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -6,7 +6,7 @@ use time::macros::{date, datetime}; use musixmatch_inofficial::{ models::{AlbumId, ArtistId, TrackId}, - Error, Musixmatch, + ClientType, Error, Musixmatch, }; #[ctor::ctor] @@ -22,13 +22,20 @@ fn init() { } fn new_mxm() -> Musixmatch { - Musixmatch::builder() - .credentials( - std::env::var("MUSIXMATCH_EMAIL").unwrap(), - std::env::var("MUSIXMATCH_PASSWORD").unwrap(), - ) - .build() - .unwrap() + let client_type = std::env::var("MUSIXMATCH_CLIENT") + .map(|ctype| serde_plain::from_str::(&ctype).expect("valid client type")) + .unwrap_or_default(); + + let mut mxm = Musixmatch::builder().client_type(client_type); + + if let (Ok(email), Ok(password)) = ( + std::env::var("MUSIXMATCH_EMAIL"), + std::env::var("MUSIXMATCH_PASSWORD"), + ) { + mxm = mxm.credentials(email, password); + } + + mxm.build().unwrap() } fn testfile>(name: P) -> PathBuf { @@ -666,7 +673,7 @@ mod track { async fn genres() { let genres = new_mxm().genres().await.unwrap(); assert!(genres.len() > 360); - dbg!(&genres); + // dbg!(&genres); } #[tokio::test] @@ -993,7 +1000,11 @@ mod translation { #[tokio::test] async fn no_credentials() { - let mxm = Musixmatch::builder().no_storage().build().unwrap(); + let mxm = Musixmatch::builder() + .client_type(ClientType::Android) + .no_storage() + .build() + .unwrap(); let err = mxm .track_lyrics(TrackId::TrackId(205688271)) .await From 2300932afc6b7f76cacfd0c16f3684bacb042994 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 3 Feb 2024 16:28:16 +0100 Subject: [PATCH 02/61] chore: use field `steps` instead of `pipeline` for Woodpecker --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index c93721f..cc274e9 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,4 +1,4 @@ -pipeline: +steps: test: image: rust:latest environment: From 9904597d8fad15c5eadde82960a7b9eea113afd7 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 3 Feb 2024 16:32:27 +0100 Subject: [PATCH 03/61] fix: add 60s break to CI workflow --- .woodpecker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker.yml b/.woodpecker.yml index cc274e9..5661a72 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -11,4 +11,5 @@ steps: - cargo fmt --all --check - cargo clippy --all -- -D warnings - MUSIXMATCH_CLIENT=Desktop cargo test --workspace + - sleep 60 # because of Musixmatch rate limit - MUSIXMATCH_CLIENT=Android cargo test --workspace From ad865e1a2601359e184ce06822359ce96eefee58 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 3 Feb 2024 17:20:24 +0100 Subject: [PATCH 04/61] feat: allow usage without credentials --- .woodpecker.yml | 4 +--- README.md | 40 ++++++++++++++++++++-------------------- src/lib.rs | 16 +++++++--------- tests/tests.rs | 27 ++++++++++----------------- 4 files changed, 38 insertions(+), 49 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index acfacc9..a091c3d 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,8 +1,6 @@ -pipeline: +steps: test: image: rust:latest - environment: - - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse secrets: - musixmatch_email - musixmatch_password diff --git a/README.md b/README.md index 9e83d62..f9a5e77 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,35 @@ # Musixmatch-Inofficial -This is an inofficial client for the Musixmatch API that uses the -key embedded in the Musixmatch Android app. +This is an inofficial client for the Musixmatch API that uses the key embedded in the +Musixmatch Android app. It allows you to obtain synchronized lyrics in different formats -([LRC](https://en.wikipedia.org/wiki/LRC_(file_format)), +([LRC](), [DFXP](https://www.w3.org/TR/ttml1/), JSON) for almost any song. -A free Musixmatch account is required for operation -([you can sign up here](https://www.musixmatch.com/de/sign-up)). +The Musixmatch API required a free account on to be used. +However, as of 2024, this requirement was removed and the API can be used anonymously. +The client still allows you to supply credentials if Musixmatch decided to close the API +down again. ## ⚠️ Copyright disclaimer -Song lyrics are copyrighted works (just like books, poems and the songs -themselves). +Song lyrics are copyrighted works (just like books, poems and the songs themselves). -Musixmatch does allow its users to obtains song lyrics for private -use (e.g. to enrich their music collection). But it does not allow you -to publish their lyrics or use them commercially. +Musixmatch does allow its users to obtains song lyrics for private use (e.g. to enrich +their music collection). But it does not allow you to publish their lyrics or use them +commercially. -You will get in trouble if you use this client to create a public -lyrics site/app. If you want to use Musixmatch data for this purpose, -you will have to give them money (see their -[commercial plans](https://developer.musixmatch.com/plans)) -and use their [official API](https://developer.musixmatch.com/documentation). +You will get in trouble if you use this client to create a public lyrics site/app. If +you want to use Musixmatch data for this purpose, you will have to give them money (see +their [commercial plans](https://developer.musixmatch.com/plans)) and use their +[official API](https://developer.musixmatch.com/documentation). ## Development info -Running the tests requires Musixmatch credentials. The credentials are read -from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment variables. +The test suite reads Musixmatch credentials from the `MUSIXMATCH_EMAIL` and +`MUSIXMATCH_PASSWORD` environment variables. -To make local development easier, I have included `dotenvy` to read the -credentials from an `.env` file. Copy the `.env.example` file -in the root directory, rename it to `.env` and fill in your credentials. +To make local development easier, I have included `dotenvy` to read the credentials from +an `.env` file. Copy the `.env.example` file in the root directory, rename it to `.env` +and fill in your credentials. diff --git a/src/lib.rs b/src/lib.rs index 8029bcf..4c6ff32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ pub mod models; pub mod storage; use std::fmt::Debug; -use std::ops::Deref; use std::path::Path; use std::sync::{Arc, RwLock}; @@ -112,8 +111,8 @@ impl MusixmatchBuilder { /// Set the Musixmatch credentials /// - /// You have to create a free account on to use - /// the API. + /// The Musixmatch API required a free account on to be + /// used. However, as of 2024, this requirement was removed. /// /// The Musixmatch client can be constructed without any credentials. /// In this case you rely on the stored session token to authenticate @@ -238,10 +237,7 @@ impl Musixmatch { let credentials = { let c = self.inner.credentials.read().unwrap(); - match c.deref() { - Some(c) => c.clone(), - None => return Err(Error::MissingCredentials), - } + c.clone() }; let now = OffsetDateTime::now_utc(); @@ -277,8 +273,10 @@ impl Musixmatch { let usertoken = tdata.body_or_err()?.user_token; info!("Received new usertoken: {}****", &usertoken[0..8]); - let account = self.post_credentials(&usertoken, &credentials).await?; - info!("Logged in as {} ", account.name); + if let Some(credentials) = credentials { + let account = self.post_credentials(&usertoken, &credentials).await?; + info!("Logged in as {} ", account.name); + } *stored_usertoken = Some(usertoken.to_owned()); self.store_session(&usertoken); diff --git a/tests/tests.rs b/tests/tests.rs index 238f715..b1daddb 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -22,13 +22,16 @@ fn init() { } fn new_mxm() -> Musixmatch { - Musixmatch::builder() - .credentials( - std::env::var("MUSIXMATCH_EMAIL").unwrap(), - std::env::var("MUSIXMATCH_PASSWORD").unwrap(), - ) - .build() - .unwrap() + let mut mxm = Musixmatch::builder(); + + if let (Ok(email), Ok(password)) = ( + std::env::var("MUSIXMATCH_EMAIL"), + std::env::var("MUSIXMATCH_PASSWORD"), + ) { + mxm = mxm.credentials(email, password); + } + + mxm.build().unwrap() } fn testfile>(name: P) -> PathBuf { @@ -990,13 +993,3 @@ mod translation { assert_eq!(subtitles_trans.to_ttml().trim(), expected_ttml.trim()); } } - -#[tokio::test] -async fn no_credentials() { - let mxm = Musixmatch::builder().no_storage().build().unwrap(); - let err = mxm - .track_lyrics(TrackId::TrackId(205688271)) - .await - .unwrap_err(); - assert!(matches!(err, Error::MissingCredentials), "error: {err}"); -} From 6bece6289338e77bc489a7e88e90cd67c939424b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 6 Feb 2024 13:16:30 +0100 Subject: [PATCH 05/61] fix: improved response parsing and errors --- src/api_model.rs | 119 +++++++++++++---------------------------- src/error.rs | 14 +++-- src/lib.rs | 23 +++++--- src/models/subtitle.rs | 4 +- tests/tests.rs | 2 +- 5 files changed, 66 insertions(+), 96 deletions(-) diff --git a/src/api_model.rs b/src/api_model.rs index 378daad..08edfdc 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -1,6 +1,9 @@ use std::{marker::PhantomData, str::FromStr}; -use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; +use serde::{ + de::{DeserializeOwned, Visitor}, + Deserialize, Deserializer, Serialize, +}; use time::OffsetDateTime; use crate::error::{Error, Result as MxmResult}; @@ -9,24 +12,17 @@ use crate::error::{Error, Result as MxmResult}; #[derive(Debug, Deserialize)] pub struct Resp { - pub message: Message, + pub message: T, } #[derive(Debug, Deserialize)] -pub struct Message { +pub struct HeaderMsg { pub header: Header, - pub body: Option>, } #[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum MessageBody { - Some(T), - // "body": [] - EmptyArr(Vec<()>), - // "body": {} - EmptyObj {}, - EmptyStr(String), +pub struct BodyMsg { + pub body: T, } #[derive(Debug, Deserialize)] @@ -37,30 +33,24 @@ pub struct Header { pub hint: String, } -impl Resp { - 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.status_code == 404 { - Err(Error::NotFound) - } else if self.message.header.status_code == 401 - && self.message.header.hint == "renew" - { - Err(Error::TokenExpired) - } else if self.message.header.status_code == 401 - && self.message.header.hint == "captcha" - { - Err(Error::Ratelimit) - } else { - Err(Error::MusixmatchError { - status_code: self.message.header.status_code, - msg: self.message.header.hint, - }) - } - } - } +pub fn parse_body(response: &str) -> MxmResult { + let header = serde_json::from_str::>(response)? + .message + .header; + if header.status_code < 400 { + let body = serde_json::from_str::>>(response)?; + Ok(body.message.body) + } else if header.status_code == 404 { + Err(Error::NotFound) + } else if header.status_code == 401 && header.hint == "renew" { + Err(Error::TokenExpired) + } else if header.status_code == 401 && header.hint == "captcha" { + Err(Error::Ratelimit) + } else { + Err(Error::MusixmatchError { + status_code: header.status_code, + msg: header.hint, + }) } } @@ -565,57 +555,22 @@ mod tests { let json = r#"{"message":{"header":{"status_code":401,"execute_time":0.002,"hint":"fsck"}}}"#; - let res = serde_json::from_str::>(json).unwrap(); + let err = parse_body::(json).unwrap_err(); - assert_eq!(res.message.header.status_code, 401); - assert_eq!(res.message.header.hint, "fsck"); - assert!(res.message.body.is_none()); - - let err = res.body_or_err().unwrap_err(); - assert_eq!( - err.to_string(), - "Error 401 returned by the Musixmatch API. Message: 'fsck'" - ); + if let Error::MusixmatchError { status_code, msg } = err { + assert_eq!(status_code, 401); + assert_eq!(msg, "fsck"); + } else { + panic!("invalid error: {err}"); + } } #[test] - fn deserialize_emptyarr_body() { - let json = r#"{"message":{"header":{"status_code":403,"execute_time":0.0056290626525879},"body":[]}}"#; + fn deserialize_body() { + let json = r#"{"message":{"header":{"status_code":200,"execute_time":0.002},"body":"Hello World"}}"#; - let res = serde_json::from_str::>(json).unwrap(); - - assert_eq!(res.message.header.status_code, 403); - assert_eq!(res.message.header.hint, ""); - assert!(matches!( - res.message.body.as_ref().unwrap(), - MessageBody::EmptyArr(_) - )); - - let err = res.body_or_err().unwrap_err(); - assert_eq!( - err.to_string(), - "Error 403 returned by the Musixmatch API. Message: ''" - ); - } - - #[test] - fn deserialize_emptyobj_body() { - let json = r#"{"message":{"header":{"status_code":403,"execute_time":0.0056290626525879},"body":{}}}"#; - - let res = serde_json::from_str::>(json).unwrap(); - - assert_eq!(res.message.header.status_code, 403); - assert_eq!(res.message.header.hint, ""); - assert!(matches!( - res.message.body.as_ref().unwrap(), - MessageBody::EmptyObj {} - )); - - let err = res.body_or_err().unwrap_err(); - assert_eq!( - err.to_string(), - "Error 403 returned by the Musixmatch API. Message: ''" - ); + let res = parse_body::(json).unwrap(); + assert_eq!(res, "Hello World"); } #[test] diff --git a/src/error.rs b/src/error.rs index cb62a3c..17e03e0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + pub(crate) type Result = core::result::Result; /// Custom error type for the Musixmatch client @@ -18,9 +20,6 @@ pub enum Error { /// Error message msg: String, }, - /// Musixmatch returned no data or the data that could not be deserialized - #[error("Musixmatch returned no data or data that could not be deserialized")] - NoData, /// Client requires credentials, but none were given #[error("You did not input credentials")] MissingCredentials, @@ -33,6 +32,9 @@ pub enum Error { /// Musixmatch content not available #[error("Unfortunately we're not authorized to show these lyrics")] NotAvailable, + /// Musixmatch returned no data or the data that could not be deserialized + #[error("JSON parsing error: {0}")] + InvalidData(Cow<'static, str>), /// Error from the HTTP client #[error("http error: {0}")] Http(reqwest::Error), @@ -44,3 +46,9 @@ impl From for Error { Self::Http(value.without_url()) } } + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::InvalidData(value.to_string().into()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 4c6ff32..b494a13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ use time::macros::format_description; use time::OffsetDateTime; use tokio::sync::Mutex; -use crate::api_model::Resp; +use crate::api_model::parse_body; use crate::error::Result; const YMD_FORMAT: &[time::format_description::FormatItem] = @@ -269,8 +269,8 @@ impl Musixmatch { sign_url_with_date(&mut url, now); let resp = self.inner.http.get(url).send().await?.error_for_status()?; - let tdata = resp.json::>().await?; - let usertoken = tdata.body_or_err()?.user_token; + let resp_txt = resp.text().await?; + let usertoken = parse_body::(&resp_txt)?.user_token; info!("Received new usertoken: {}****", &usertoken[0..8]); if let Some(credentials) = credentials { @@ -311,8 +311,14 @@ impl Musixmatch { .await? .error_for_status()?; - let login = resp.json::>().await?.body_or_err()?; - let credential = login.0.into_iter().next().ok_or(Error::NoData)?.credential; + let resp_txt = resp.text().await?; + let login = parse_body::(&resp_txt)?; + let credential = login + .0 + .into_iter() + .next() + .ok_or(Error::InvalidData("no credentials returned".into()))? + .credential; match credential { api_model::LoginCredential::Account { account } => Ok(account), @@ -398,9 +404,9 @@ impl Musixmatch { .send() .await? .error_for_status()?; - let resp_obj = resp.json::>().await?; + let resp_txt = resp.text().await?; - match resp_obj.body_or_err() { + match parse_body(&resp_txt) { Ok(body) => Ok(body), Err(Error::TokenExpired) => { info!("Usertoken expired, getting a new one"); @@ -416,7 +422,8 @@ impl Musixmatch { .await? .error_for_status()?; - resp.json::>().await?.body_or_err() + let resp_txt = resp.text().await?; + parse_body(&resp_txt) } Err(e) => Err(e), } diff --git a/src/models/subtitle.rs b/src/models/subtitle.rs index 4843127..0ff3b09 100644 --- a/src/models/subtitle.rs +++ b/src/models/subtitle.rs @@ -226,7 +226,7 @@ impl Subtitle { /// Only works with [SubtitleFormat::Json]. pub fn to_lines(&self) -> Result { Ok(SubtitleLines { - lines: serde_json::from_str(&self.subtitle_body).map_err(|_| Error::NoData)?, + lines: serde_json::from_str(&self.subtitle_body)?, lang: self.subtitle_language.to_owned(), length: self.subtitle_length, }) @@ -256,7 +256,7 @@ impl TryFrom for SubtitleLines { impl SubtitleLines { /// Convert subtitles into the [JSON](SubtitleFormat::Json) format pub fn to_json(&self) -> Result { - serde_json::to_string(&self).map_err(|_| Error::NoData) + serde_json::to_string(&self).map_err(Error::from) } /// Convert subtitles into the [LRC](SubtitleFormat::Lrc) format diff --git a/tests/tests.rs b/tests/tests.rs index b1daddb..829e3d1 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -107,7 +107,7 @@ mod album { async fn album_ep() { let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap(); assert_eq!(album.album_name, "Waldbrand EP"); - assert_eq!(album.album_release_type, AlbumType::Ep); + // assert_eq!(album.album_release_type, AlbumType::Ep); assert_eq!(album.album_release_date, Some(date!(2016 - 09 - 30))); } From 9958c57c73ed6488edbccc630766c661f0983246 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:33:31 +0100 Subject: [PATCH 06/61] test: update tests --- tests/tests.rs | 92 ++++++++++++++++++++------------------------------ 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 829e3d1..203100c 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -89,18 +89,9 @@ mod album { ); assert_eq!(album.album_vanity_id, "410698/Gangnam-Style-Single"); assert!(album.updated_time > datetime!(2022-6-3 0:00 UTC)); - assert_eq!( - album.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045.jpg" - ); - assert_eq!( - album.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_350_350.jpg" - ); - assert_eq!( - album.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_500_500.jpg" - ); + assert_imgurl(&album.album_coverart_100x100, "/26544045.jpg"); + assert_imgurl(&album.album_coverart_350x350, "/26544045_350_350.jpg"); + assert_imgurl(&album.album_coverart_500x500, "/26544045_500_500.jpg"); } #[tokio::test] @@ -333,18 +324,9 @@ mod track { "650e7db6-b795-4eb5-a702-5ea2fc46c848" ); assert_eq!(track.artist_name, "Lady Gaga"); - assert_eq!( - track.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg" - ); - assert_eq!( - track.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg" - ); - assert_eq!( - track.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg" - ); + assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg"); + assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg"); + assert_imgurl(&track.album_coverart_500x500, "/26319636_500_500.jpg"); assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); let first_release = track.first_release_date.unwrap(); assert_eq!(first_release.date(), date!(2008 - 1 - 1)); @@ -414,18 +396,9 @@ mod track { assert_eq!(track.album_name, "Black Mamba"); assert_eq!(track.artist_id, 46970441); assert_eq!(track.artist_name, "aespa"); - assert_eq!( - track.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772.jpg" - ); - assert_eq!( - track.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_350_350.jpg" - ); - assert_eq!( - track.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_500_500.jpg" - ); + assert_imgurl(&track.album_coverart_100x100, "/52156772.jpg"); + assert_imgurl(&track.album_coverart_350x350, "/52156772_350_350.jpg"); + assert_imgurl(&track.album_coverart_500x500, "/52156772_500_500.jpg"); assert_eq!(track.commontrack_vanity_id, "aespa/Black-Mamba"); let release_date = track.first_release_date.unwrap(); @@ -491,18 +464,9 @@ mod track { "650e7db6-b795-4eb5-a702-5ea2fc46c848" ); assert_eq!(track.artist_name, "Lady Gaga"); - assert_eq!( - track.album_coverart_100x100.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg" - ); - assert_eq!( - track.album_coverart_350x350.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg" - ); - assert_eq!( - track.album_coverart_500x500.unwrap(), - "https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg" - ); + assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg"); + assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg"); + assert_imgurl(&track.album_coverart_500x500, "/26319636_500_500.jpg"); assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); let first_release = track.first_release_date.unwrap(); assert_eq!(first_release.date(), date!(2008 - 1 - 1)); @@ -702,16 +666,20 @@ mod lyrics { // dbg!(&lyrics); - assert_eq!(lyrics.lyrics_id, 25947036); + assert_eq!(lyrics.lyrics_id, 34583240); assert!(!lyrics.instrumental); assert!(!lyrics.explicit); - assert!(lyrics - .lyrics_body - .starts_with("Eyes, in the sky, gazing far into the night\n")); + assert!( + lyrics + .lyrics_body + .starts_with("Eyes in the sky gazing far into the night\n"), + "got: {}", + lyrics.lyrics_body + ); assert_eq!(lyrics.lyrics_language.unwrap(), "en"); assert_eq!(lyrics.lyrics_language_description.unwrap(), "English"); let copyright = lyrics.lyrics_copyright.unwrap(); - assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",); + assert!(copyright.contains("Jesse Warren"), "copyright: {copyright}",); assert!(lyrics.updated_time > datetime!(2021-6-3 0:00 UTC)); } @@ -851,12 +819,12 @@ mod subtitles { // dbg!(&subtitle); - assert_eq!(subtitle.subtitle_id, 36913312); + assert_eq!(subtitle.subtitle_id, 35340319); assert_eq!(subtitle.subtitle_language.unwrap(), "en"); assert_eq!(subtitle.subtitle_language_description.unwrap(), "English"); let copyright = subtitle.lyrics_copyright.unwrap(); - assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",); - assert_eq!(subtitle.subtitle_length, 315); + assert!(copyright.contains("Jesse Warren"), "copyright: {copyright}",); + assert_eq!(subtitle.subtitle_length, 316); assert!(subtitle.updated_time > datetime!(2021-6-30 0:00 UTC)); } @@ -993,3 +961,15 @@ mod translation { assert_eq!(subtitles_trans.to_ttml().trim(), expected_ttml.trim()); } } + +#[track_caller] +fn assert_imgurl(url: &Option, ends_with: &str) { + assert!( + url.as_deref().is_some_and( + |url| url.starts_with("https://s.mxmcdn.net/images-storage/") + && url.ends_with(ends_with) + ), + "expected url ending with {ends_with}\ngot {:?}", + url + ); +} From 4282889ebe97cb717000ce7a8eae60c3c4838a93 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:35:25 +0100 Subject: [PATCH 07/61] feat: add search function --- cli/src/main.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index b80813f..c0fae2c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -28,6 +28,20 @@ enum Commands { #[command(subcommand)] command: FileCommands, }, + #[group(required = true)] + Search { + /// Track name + #[clap(short, long)] + name: Option, + /// Artist + #[clap(short, long)] + artist: Option, + /// Lyrics + #[clap(short, long)] + lyrics: Option, + /// Search query + query: Option>, + }, } #[derive(Subcommand)] @@ -319,6 +333,42 @@ async fn run(cli: Cli) -> Result<()> { println!("{}", subtitles.subtitle_body); } }, + Commands::Search { + query, + name, + artist, + lyrics, + } => { + let mut sb = mxm + .track_search() + .s_track_rating(musixmatch_inofficial::models::SortOrder::Desc); + let querystr; + if let Some(q) = &query { + querystr = q.join(" "); + sb = sb.q(&querystr); + } + if let Some(n) = &name { + sb = sb.q_track(n); + } + if let Some(a) = &artist { + sb = sb.q_artist(a); + } + if let Some(l) = &lyrics { + sb = sb.q_lyrics(l); + } + + let tracks = sb.send(20, 0).await?; + for t in tracks { + println!( + "{} - {} ({}) ISRC'{}' ", + t.track_name, + t.artist_name, + t.first_release_date.map(|d| d.year()).unwrap_or_default(), + t.track_isrc.unwrap_or_default(), + t.commontrack_vanity_id + ); + } + } }; Ok(()) } From 7d153499ede355abcecd560dbb0ee1bc7e71e56c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:35:46 +0100 Subject: [PATCH 08/61] chore: update dependencies --- Cargo.toml | 4 ++-- Justfile | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 Justfile diff --git a/Cargo.toml b/Cargo.toml index 2323866..a5b6bbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"] rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"] [dependencies] -reqwest = { version = "0.11.11", default-features = false, features = [ +reqwest = { version = "0.12.0", default-features = false, features = [ "json", "gzip", ] } @@ -42,7 +42,7 @@ time = { version = "0.3.15", features = [ hmac = "0.12.1" sha1 = "0.10.5" rand = "0.8.5" -base64 = "0.21.0" +base64 = "0.22.0" [dev-dependencies] ctor = "0.2.0" diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..e3b19b5 --- /dev/null +++ b/Justfile @@ -0,0 +1,44 @@ +test: + cargo test + +release crate="musixmatch-inofficial": + #!/usr/bin/env bash + set -e + + CRATE="{{crate}}" + INCLUDES='--include-path README.md --include-path LICENSE --include-path Cargo.toml' + CHANGELOG="CHANGELOG.md" + + if [ "$CRATE" = "musixmatch-inofficial" ]; then + INCLUDES="$INCLUDES --include-path src/** --include-path tests/** --include-path testfiles/**" + else + if [ ! -d "$CRATE" ]; then + echo "$CRATE does not exist."; exit 1 + fi + INCLUDES="$INCLUDES --include-path $CRATE/**" + CHANGELOG="$CRATE/$CHANGELOG" + CRATE="musixmatch-$CRATE" # Add crate name prefix + fi + + VERSION=$(cargo pkgid --package "$CRATE" | tr '#@' '\n' | tail -n 1) + TAG="${CRATE}/v${VERSION}" + echo "Releasing $TAG:" + + if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi + + CLIFF_ARGS="--tag v${VERSION} --tag-pattern ${CRATE}/* --unreleased $INCLUDES" + echo "git-cliff $CLIFF_ARGS" + if [ -f "$CHANGELOG" ]; then + git-cliff $CLIFF_ARGS --prepend "$CHANGELOG" + else + git-cliff $CLIFF_ARGS --output "$CHANGELOG" + fi + + editor "$CHANGELOG" + + git add "$CHANGELOG" + git commit -m "chore(release): release $CRATE v$VERSION" + + awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG" + + echo "πŸš€ Run 'git push origin $TAG' to publish" From b30b9d7b9f5d5f7e9fc9ccbcd8da22d862ae797d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:36:33 +0100 Subject: [PATCH 09/61] chore: add git-cliff --- cliff.toml | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 cliff.toml diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..32b679a --- /dev/null +++ b/cliff.toml @@ -0,0 +1,99 @@ +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. + +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% set repo_url = "https://code.thetadev.de/ThetaDev/rustypipe" %}\ +{% if version %}\ + {%if previous.version %}\ + ## [{{ version }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\ + {% else %}\ + ## {{ version }}\ + {% endif %} - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% if previous.version %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }} - \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ repo_url }}/commit/{{ commit.id }}))\ + {% endfor %} +{% endfor %}\ +{% else %} +Initial release +{% endif %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true +# postprocessors +postprocessors = [ + # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL +] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit with https://github.com/crate-ci/typos + # If the spelling is incorrect, it will be automatically fixed. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "πŸš€ Features" }, + { message = "^fix", group = "πŸ› Bug Fixes" }, + { message = "^doc", group = "πŸ“š Documentation" }, + { message = "^perf", group = "⚑ Performance" }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", group = "πŸ§ͺ Testing" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore", group = "βš™οΈ Miscellaneous Tasks" }, + { message = "^ci", skip = true }, + { body = ".*security", group = "πŸ›‘οΈ Security" }, + { message = "^revert", group = "◀️ Revert" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# regex for matching git tags +# tag_pattern = "v[0-9].*" +# regex for skipping tags +# skip_tags = "" +# regex for ignoring tags +# ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" +# limit the number of commits included in the changelog. +# limit_commits = 42 From 8168566498b3340642e1d38cd03ae8080629cba8 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:42:41 +0100 Subject: [PATCH 10/61] ci: add workflows --- .gitea/workflows/ci.yaml | 19 ++++++++++++++++++ .gitea/workflows/release.yaml | 38 +++++++++++++++++++++++++++++++++++ .woodpecker.yml | 11 ---------- Cargo.toml | 3 +++ cli/Cargo.toml | 2 +- 5 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitea/workflows/release.yaml delete mode 100644 .woodpecker.yml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..94f970c --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,19 @@ +name: CI +on: [push, pull_request] + +jobs: + Test: + runs-on: cimaster-latest + steps: + - name: πŸ“¦ Checkout repository + uses: actions/checkout@v3 + - name: πŸ¦€ Setup Rust cache + uses: https://github.com/Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + + - name: πŸ“Ž Clippy + run: cargo clippy --all -- -D warnings + + - name: πŸ§ͺ Test + run: cargo test --workspace diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..fa7432d --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,38 @@ +name: Release +on: + push: + tags: + - "*/v*.*.*" + +jobs: + Release: + runs-on: cimaster-latest + steps: + - name: πŸ“¦ Checkout repository + uses: actions/checkout@v3 + + - name: Get variables + run: | + git fetch --tags --force #the checkout action does not load the tag message + + echo "CRATE=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==1{print}')" >> "$GITHUB_ENV" + echo "CRATE_VERSION=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==2{print}')" >> "$GITHUB_ENV" + { + echo 'CHANGELOG<> "$GITHUB_ENV" + + - name: πŸ“€ Publish crate on code.thetadev.de + run: | + mkdir -p ~/.cargo + printf '[registries.thetadev]\nindex = "https://code.thetadev.de/ThetaDev/_cargo-index.git"\ntoken = "Bearer ${{ secrets.TOKEN_GITEA }}"\n' >> ~/.cargo/config.toml + sed -i "s/^musixmatch-.*=\s*{/\0 registry = \"thetadev\",/g" Cargo.toml + cargo publish --registry thetadev --package "${{ env.CRATE }}" --allow-dirty + git restore Cargo.toml + + - name: πŸŽ‰ Publish release + uses: https://gitea.com/actions/release-action@main + with: + title: "${{ env.CRATE }} ${{ env.CRATE_VERSION }}" + body: "${{ env.CHANGELOG }}" diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index a091c3d..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -1,11 +0,0 @@ -steps: - test: - image: rust:latest - secrets: - - musixmatch_email - - musixmatch_password - commands: - - rustup component add rustfmt clippy - - cargo fmt --all --check - - cargo clippy --all -- -D warnings - - cargo test --workspace diff --git a/Cargo.toml b/Cargo.toml index a5b6bbb..8c998bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ include = ["/src", "README.md", "LICENSE"] [workspace] members = [".", "cli"] +[workspace.dependencies] +musixmatch-inofficial = { version = "0.1.0", path = ".", default-features = false } + [features] default = ["default-tls"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index be21b09..4279f8c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,7 +18,7 @@ rustls-tls-webpki-roots = ["musixmatch-inofficial/rustls-tls-webpki-roots"] rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"] [dependencies] -musixmatch-inofficial = { path = "../" } +musixmatch-inofficial.workspace = true tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] } id3 = "1.3.0" mp3-duration = "0.1.10" From 053702c2f569a634f18e2e4fbee24c3926fddaa8 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:48:27 +0100 Subject: [PATCH 11/61] fix: move package attributes to workspace --- Cargo.toml | 18 ++++++++++++++---- cli/Cargo.toml | 9 +++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c998bf..5e0c2ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,27 @@ [package] name = "musixmatch-inofficial" version = "0.1.0" -edition = "2021" -authors = ["ThetaDev "] -license = "MIT" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true description = "Inofficial client for the Musixmatch API" -keywords = ["music", "lyrics"] + include = ["/src", "README.md", "LICENSE"] [workspace] members = [".", "cli"] +[workspace.package] +edition = "2021" +authors = ["ThetaDev "] +license = "MIT" +repository = "https://code.thetadev.de/ThetaDev/musixmatch-inofficial" +keywords = ["music", "lyrics"] +categories = ["api-bindings", "multimedia"] + [workspace.dependencies] musixmatch-inofficial = { version = "0.1.0", path = ".", default-features = false } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4279f8c..c5afe16 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "musixmatch-cli" version = "0.1.0" -edition = "2021" -authors = ["ThetaDev"] -license = "MIT" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true description = "Inofficial command line interface for the Musixmatch API" -keywords = ["music", "lyrics", "cli"] [features] default = ["rustls-tls-native-roots"] From 9faa2cd6b8906b41823c1bea97db34b1ee724123 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:48:48 +0100 Subject: [PATCH 12/61] chore(release): release musixmatch-inofficial v0.1.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fd43631 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## v0.1.0 - 2024-03-23 + +Initial release + + From 8afc43a097ac31a9bf9bb1be0a76f0ddb220d006 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 23 Mar 2024 02:51:41 +0100 Subject: [PATCH 13/61] chore(release): release musixmatch-cli v0.1.0 --- cli/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 cli/CHANGELOG.md diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md new file mode 100644 index 0000000..fd43631 --- /dev/null +++ b/cli/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## v0.1.0 - 2024-03-23 + +Initial release + + From e72d2b4363a3a9a48dec8f2be9389f6cc239035c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 11 Apr 2024 13:49:36 +0200 Subject: [PATCH 14/61] chore: fix changelogs --- CHANGELOG.md | 2 +- Justfile | 10 +++++----- cli/CHANGELOG.md | 2 +- cliff.toml | 9 +++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd43631..90d5005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## v0.1.0 - 2024-03-23 +## [v0.1.0](https://code.thetadev.de/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-inofficial/v0.1.0) - 2024-03-23 Initial release diff --git a/Justfile b/Justfile index e3b19b5..882ce85 100644 --- a/Justfile +++ b/Justfile @@ -10,12 +10,12 @@ release crate="musixmatch-inofficial": CHANGELOG="CHANGELOG.md" if [ "$CRATE" = "musixmatch-inofficial" ]; then - INCLUDES="$INCLUDES --include-path src/** --include-path tests/** --include-path testfiles/**" + INCLUDES="$INCLUDES --include-path 'src/**' --include-path 'tests/**' --include-path 'testfiles/**'" else if [ ! -d "$CRATE" ]; then echo "$CRATE does not exist."; exit 1 fi - INCLUDES="$INCLUDES --include-path $CRATE/**" + INCLUDES="$INCLUDES --include-path '$CRATE/**'" CHANGELOG="$CRATE/$CHANGELOG" CRATE="musixmatch-$CRATE" # Add crate name prefix fi @@ -26,12 +26,12 @@ release crate="musixmatch-inofficial": if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi - CLIFF_ARGS="--tag v${VERSION} --tag-pattern ${CRATE}/* --unreleased $INCLUDES" + CLIFF_ARGS="--tag '${TAG}' --tag-pattern '${CRATE}/*' --unreleased $INCLUDES" echo "git-cliff $CLIFF_ARGS" if [ -f "$CHANGELOG" ]; then - git-cliff $CLIFF_ARGS --prepend "$CHANGELOG" + eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'" else - git-cliff $CLIFF_ARGS --output "$CHANGELOG" + eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'" fi editor "$CHANGELOG" diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index fd43631..7a75fb4 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## v0.1.0 - 2024-03-23 +## [v0.1.0](https://code.thetadev.de/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-cli/v0.1.0) - 2024-03-23 Initial release diff --git a/cliff.toml b/cliff.toml index 32b679a..435dd76 100644 --- a/cliff.toml +++ b/cliff.toml @@ -14,12 +14,13 @@ All notable changes to this project will be documented in this file.\n # template for the changelog body # https://keats.github.io/tera/docs/#introduction body = """ -{% set repo_url = "https://code.thetadev.de/ThetaDev/rustypipe" %}\ +{% set repo_url = "https://code.thetadev.de/ThetaDev/musixmatch-inofficial" %}\ {% if version %}\ + {%set vname = version | split(pat="/") | last %} {%if previous.version %}\ - ## [{{ version }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\ + ## [{{ vname }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\ {% else %}\ - ## {{ version }}\ + ## [{{ vname }}]({{ repo_url }}/commits/tag/{{ version }})\ {% endif %} - {{ timestamp | date(format="%Y-%m-%d") }} {% else %}\ ## [unreleased] @@ -73,7 +74,7 @@ commit_parsers = [ { message = "^refactor", group = "🚜 Refactor" }, { message = "^style", group = "🎨 Styling" }, { message = "^test", group = "πŸ§ͺ Testing" }, - { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(release\\)", skip = true }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, { message = "^chore", group = "βš™οΈ Miscellaneous Tasks" }, From dc01542515ab24e1561c2844024cea4f5f9fc788 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 12 Apr 2024 03:28:34 +0200 Subject: [PATCH 15/61] ci: fix changelog tag pattern --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 882ce85..da5c71b 100644 --- a/Justfile +++ b/Justfile @@ -26,7 +26,7 @@ release crate="musixmatch-inofficial": if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi - CLIFF_ARGS="--tag '${TAG}' --tag-pattern '${CRATE}/*' --unreleased $INCLUDES" + CLIFF_ARGS="--tag '${TAG}' --tag-pattern '${CRATE}/v*' --unreleased $INCLUDES" echo "git-cliff $CLIFF_ARGS" if [ -f "$CHANGELOG" ]; then eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'" From d2a7aed917bfcec75ce00bb49d380fbc31c47384 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 16:14:05 +0200 Subject: [PATCH 16/61] test: fix tests --- Cargo.toml | 1 - src/api_model.rs | 4 +- tests/tests.rs | 308 +++++++++++++++++++++++++++++------------------ 3 files changed, 190 insertions(+), 123 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e0c2ca..1a98d05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,6 @@ rand = "0.8.5" base64 = "0.22.0" [dev-dependencies] -ctor = "0.2.0" rstest = { version = "0.18.0", default-features = false } env_logger = "0.11.0" dotenvy = "0.15.5" diff --git a/src/api_model.rs b/src/api_model.rs index 08edfdc..463e5d1 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -99,8 +99,8 @@ pub enum LoginCredential { #[derive(Debug, Deserialize)] pub struct Account { - pub id: String, - pub email: String, + // pub id: String, + // pub email: String, pub name: String, } diff --git a/tests/tests.rs b/tests/tests.rs index 203100c..202e271 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use path_macro::path; -use rstest::rstest; +use rstest::{fixture, rstest}; use time::macros::{date, datetime}; use musixmatch_inofficial::{ @@ -9,19 +9,14 @@ use musixmatch_inofficial::{ Error, Musixmatch, }; -#[ctor::ctor] -fn init() { - let _ = dotenvy::dotenv(); - env_logger::init(); - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(new_mxm().login()) - .unwrap(); +fn testfile>(name: P) -> PathBuf { + path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name) } -fn new_mxm() -> Musixmatch { +#[fixture] +async fn mxm() -> Musixmatch { + static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new(); + let mut mxm = Musixmatch::builder(); if let (Ok(email), Ok(password)) = ( @@ -31,11 +26,10 @@ fn new_mxm() -> Musixmatch { mxm = mxm.credentials(email, password); } - mxm.build().unwrap() -} + let mxm = mxm.build().unwrap(); -fn testfile>(name: P) -> PathBuf { - path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name) + LOGIN_LOCK.get_or_try_init(|| mxm.login()).await.unwrap(); + mxm } mod album { @@ -46,8 +40,8 @@ mod album { #[case::id(AlbumId::AlbumId(14248253))] #[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))] #[tokio::test] - async fn by_id(#[case] album_id: AlbumId<'_>) { - let album = new_mxm().album(album_id).await.unwrap(); + async fn by_id(#[case] album_id: AlbumId<'_>, #[future] mxm: Musixmatch) { + let album = mxm.await.album(album_id).await.unwrap(); assert_eq!(album.album_id, 14248253); assert_eq!( @@ -94,17 +88,20 @@ mod album { assert_imgurl(&album.album_coverart_500x500, "/26544045_500_500.jpg"); } + #[rstest] #[tokio::test] - async fn album_ep() { - let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap(); + async fn album_ep(#[future] mxm: Musixmatch) { + let album = mxm.await.album(AlbumId::AlbumId(23976123)).await.unwrap(); assert_eq!(album.album_name, "Waldbrand EP"); // assert_eq!(album.album_release_type, AlbumType::Ep); assert_eq!(album.album_release_date, Some(date!(2016 - 09 - 30))); } + #[rstest] #[tokio::test] - async fn by_id_missing() { - let err = new_mxm() + async fn by_id_missing(#[future] mxm: Musixmatch) { + let err = mxm + .await .album(AlbumId::AlbumId(999999999999)) .await .unwrap_err(); @@ -112,9 +109,11 @@ mod album { assert!(matches!(err, Error::NotFound)); } + #[rstest] #[tokio::test] - async fn artist_albums() { - let albums = new_mxm() + async fn artist_albums(#[future] mxm: Musixmatch) { + let albums = mxm + .await .artist_albums(ArtistId::ArtistId(1039), None, 10, 1) .await .unwrap(); @@ -122,9 +121,11 @@ mod album { assert_eq!(albums.len(), 10); } + #[rstest] #[tokio::test] - async fn artist_albums_missing() { - let err = new_mxm() + async fn artist_albums_missing(#[future] mxm: Musixmatch) { + let err = mxm + .await .artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1) .await .unwrap_err(); @@ -132,9 +133,10 @@ mod album { assert!(matches!(err, Error::NotFound)); } + #[rstest] #[tokio::test] - async fn charts() { - let albums = new_mxm().chart_albums("US", 10, 1).await.unwrap(); + async fn charts(#[future] mxm: Musixmatch) { + let albums = mxm.await.chart_albums("US", 10, 1).await.unwrap(); assert_eq!(albums.len(), 10); } @@ -147,8 +149,8 @@ mod artist { #[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(); + async fn by_id(#[case] artist_id: ArtistId<'_>, #[future] mxm: Musixmatch) { + let artist = mxm.await.artist(artist_id).await.unwrap(); // dbg!(&artist); @@ -190,9 +192,11 @@ mod artist { assert_eq!(artist.end_date, None); } + #[rstest] #[tokio::test] - async fn by_id_missing() { - let err = new_mxm() + async fn by_id_missing(#[future] mxm: Musixmatch) { + let err = mxm + .await .artist(ArtistId::ArtistId(999999999999)) .await .unwrap_err(); @@ -200,9 +204,11 @@ mod artist { assert!(matches!(err, Error::NotFound)); } + #[rstest] #[tokio::test] - async fn related() { - let artists = new_mxm() + async fn related(#[future] mxm: Musixmatch) { + let artists = mxm + .await .artist_related(ArtistId::ArtistId(26485840), 10, 1) .await .unwrap(); @@ -210,9 +216,11 @@ mod artist { assert_eq!(artists.len(), 10); } + #[rstest] #[tokio::test] - async fn related_missing() { - let err = new_mxm() + async fn related_missing(#[future] mxm: Musixmatch) { + let err = mxm + .await .artist_related(ArtistId::ArtistId(999999999999), 10, 1) .await .unwrap_err(); @@ -220,20 +228,23 @@ mod artist { assert!(matches!(err, Error::NotFound)); } + #[rstest] #[tokio::test] - async fn search() { - let artists = new_mxm().artist_search("psy", 5, 1).await.unwrap(); + async fn search(#[future] mxm: Musixmatch) { + let artists = mxm.await.artist_search("psy", 5, 1).await.unwrap(); - assert_eq!(artists.len(), 5); + assert_gte(artists.len(), 4, "artists"); let artist = &artists[0]; - assert_eq!(artist.artist_id, 410698); + assert_eq!(artist.artist_id, 32403027); assert_eq!(artist.artist_name, "PSY"); } + #[rstest] #[tokio::test] - async fn search_empty() { - let artists = new_mxm() + async fn search_empty(#[future] mxm: Musixmatch) { + let artists = mxm + .await .artist_search( "RindfleischettikettierungsΓΌberwachungsaufgabenΓΌbertragungsgesetz", 5, @@ -245,16 +256,18 @@ mod artist { assert_eq!(artists.len(), 0); } + #[rstest] #[tokio::test] - async fn charts() { - let artists = new_mxm().chart_artists("US", 10, 1).await.unwrap(); + async fn charts(#[future] mxm: Musixmatch) { + let artists = mxm.await.chart_artists("US", 10, 1).await.unwrap(); assert_eq!(artists.len(), 10); } + #[rstest] #[tokio::test] - async fn charts_no_country() { - let artists = new_mxm().chart_artists("XY", 10, 1).await.unwrap(); + async fn charts_no_country(#[future] mxm: Musixmatch) { + let artists = mxm.await.chart_artists("XY", 10, 1).await.unwrap(); assert_eq!(artists.len(), 10); } @@ -269,8 +282,13 @@ mod track { #[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() + async fn from_match( + #[case] translation_status: bool, + #[case] lang_3c: bool, + #[future] mxm: Musixmatch, + ) { + let track = mxm + .await .matcher_track( "Poker Face", "Lady Gaga", @@ -283,12 +301,12 @@ mod track { // dbg!(&track); - assert_eq!(track.track_id, 15476784); - assert_eq!( - track.track_mbid.unwrap(), - "080975b0-39b1-493c-ae64-5cb3292409bb" - ); - assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); + assert_eq!(track.track_id, 85213841); + // assert_eq!( + // track.track_mbid.unwrap(), + // "080975b0-39b1-493c-ae64-5cb3292409bb" + // ); + // assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); assert!( track.commontrack_isrcs[0] .iter() @@ -296,7 +314,7 @@ mod track { "commontrack_isrcs: {:?}", &track.commontrack_isrcs[0], ); - assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO"); + assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg"); assert!( track .commontrack_spotify_ids @@ -316,7 +334,7 @@ mod track { assert!(track.num_favourite > 50); assert!(track.lyrics_id.is_some()); assert_eq!(track.subtitle_id.unwrap(), 36450705); - assert_eq!(track.album_id, 13810402); + assert_eq!(track.album_id, 20960801); assert_eq!(track.album_name, "The Fame"); assert_eq!(track.artist_id, 378462); assert_eq!( @@ -324,9 +342,10 @@ mod track { "650e7db6-b795-4eb5-a702-5ea2fc46c848" ); assert_eq!(track.artist_name, "Lady Gaga"); - assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg"); - assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg"); - assert_imgurl(&track.album_coverart_500x500, "/26319636_500_500.jpg"); + assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg"); + assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg"); + assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg"); + assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg"); assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); let first_release = track.first_release_date.unwrap(); assert_eq!(first_release.date(), date!(2008 - 1 - 1)); @@ -374,8 +393,8 @@ mod track { #[case::isrc(TrackId::Isrc("KRA302000590".into()))] #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] #[tokio::test] - async fn from_id(#[case] track_id: TrackId<'_>) { - let track = new_mxm().track(track_id, true, false).await.unwrap(); + async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { + let track = mxm.await.track(track_id, true, false).await.unwrap(); // dbg!(&track); @@ -415,20 +434,25 @@ mod track { #[case::translation_2c(true, false)] #[case::translation_3c(true, true)] #[tokio::test] - async fn from_id_translations(#[case] translation_status: bool, #[case] lang_3c: bool) { - let track = new_mxm() - .track(TrackId::TrackId(15476784), translation_status, lang_3c) + async fn from_id_translations( + #[case] translation_status: bool, + #[case] lang_3c: bool, + #[future] mxm: Musixmatch, + ) { + let track = mxm + .await + .track(TrackId::Commontrack(47672612), translation_status, lang_3c) .await .unwrap(); // dbg!(&track); - assert_eq!(track.track_id, 15476784); - assert_eq!( - track.track_mbid.unwrap(), - "080975b0-39b1-493c-ae64-5cb3292409bb" - ); - assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); + assert_eq!(track.track_id, 85213841); + // assert_eq!( + // track.track_mbid.unwrap(), + // "080975b0-39b1-493c-ae64-5cb3292409bb" + // ); + // assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); assert!( track.commontrack_isrcs[0] .iter() @@ -436,7 +460,7 @@ mod track { "commontrack_isrcs: {:?}", &track.commontrack_isrcs[0], ); - assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO"); + assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg"); assert!( track .commontrack_spotify_ids @@ -456,7 +480,7 @@ mod track { assert!(track.num_favourite > 50); assert!(track.lyrics_id.is_some()); assert_eq!(track.subtitle_id.unwrap(), 36450705); - assert_eq!(track.album_id, 13810402); + assert_eq!(track.album_id, 20960801); assert_eq!(track.album_name, "The Fame"); assert_eq!(track.artist_id, 378462); assert_eq!( @@ -464,9 +488,10 @@ mod track { "650e7db6-b795-4eb5-a702-5ea2fc46c848" ); assert_eq!(track.artist_name, "Lady Gaga"); - assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg"); - assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg"); - assert_imgurl(&track.album_coverart_500x500, "/26319636_500_500.jpg"); + assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg"); + assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg"); + assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg"); + assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg"); assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); let first_release = track.first_release_date.unwrap(); assert_eq!(first_release.date(), date!(2008 - 1 - 1)); @@ -507,9 +532,11 @@ mod track { } } + #[rstest] #[tokio::test] - async fn from_id_missing() { - let err = new_mxm() + async fn from_id_missing(#[future] mxm: Musixmatch) { + let err = mxm + .await .track(TrackId::TrackId(999999999999), false, false) .await .unwrap_err(); @@ -517,9 +544,11 @@ mod track { assert!(matches!(err, Error::NotFound)); } + #[rstest] #[tokio::test] - async fn album_tracks() { - let tracks = new_mxm() + async fn album_tracks(#[future] mxm: Musixmatch) { + let tracks = mxm + .await .album_tracks(AlbumId::AlbumId(17118624), true, 20, 1) .await .unwrap(); @@ -556,9 +585,11 @@ mod track { }); } + #[rstest] #[tokio::test] - async fn album_missing() { - let err = new_mxm() + async fn album_missing(#[future] mxm: Musixmatch) { + let err = mxm + .await .album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1) .await .unwrap_err(); @@ -570,8 +601,9 @@ mod track { #[case::top(ChartName::Top)] #[case::hot(ChartName::Hot)] #[tokio::test] - async fn charts(#[case] chart_name: ChartName) { - let tracks = new_mxm() + async fn charts(#[case] chart_name: ChartName, #[future] mxm: Musixmatch) { + let tracks = mxm + .await .chart_tracks("US", chart_name, true, 20, 1) .await .unwrap(); @@ -579,9 +611,11 @@ mod track { assert_eq!(tracks.len(), 20); } + #[rstest] #[tokio::test] - async fn search() { - let tracks = new_mxm() + async fn search(#[future] mxm: Musixmatch) { + let tracks = mxm + .await .track_search() .q_artist("Lena") .q_track("Satellite") @@ -600,9 +634,11 @@ mod track { assert_eq!(track.artist_name, "Lena"); } + #[rstest] #[tokio::test] - async fn search_lyrics() { - let tracks = new_mxm() + async fn search_lyrics(#[future] mxm: Musixmatch) { + let tracks = mxm + .await .track_search() .q_lyrics("the whole world stops and stares for a while") .s_track_rating(SortOrder::Desc) @@ -610,16 +646,18 @@ mod track { .await .unwrap(); - assert_eq!(tracks.len(), 10); + assert_gte(tracks.len(), 8, "tracks"); let track = &tracks[0]; assert_eq!(track.track_name, "Just the Way You Are"); assert_eq!(track.artist_name, "Bruno Mars"); } + #[rstest] #[tokio::test] - async fn search_empty() { - let artists = new_mxm() + async fn search_empty(#[future] mxm: Musixmatch) { + let artists = mxm + .await .track_search() .q("RindfleischettikettierungsΓΌberwachungsaufgabenΓΌbertragungsgesetz") .send(10, 1) @@ -629,16 +667,19 @@ mod track { assert_eq!(artists.len(), 0); } + #[rstest] #[tokio::test] - async fn genres() { - let genres = new_mxm().genres().await.unwrap(); + async fn genres(#[future] mxm: Musixmatch) { + let genres = mxm.await.genres().await.unwrap(); assert!(genres.len() > 360); dbg!(&genres); } + #[rstest] #[tokio::test] - async fn snippet() { - let snippet = new_mxm() + async fn snippet(#[future] mxm: Musixmatch) { + let snippet = mxm + .await .track_snippet(TrackId::Commontrack(8874280)) .await .unwrap(); @@ -660,9 +701,10 @@ mod lyrics { use super::*; + #[rstest] #[tokio::test] - async fn from_match() { - let lyrics = new_mxm().matcher_lyrics("Shine", "Spektrem").await.unwrap(); + async fn from_match(#[future] mxm: Musixmatch) { + let lyrics = mxm.await.matcher_lyrics("Shine", "Spektrem").await.unwrap(); // dbg!(&lyrics); @@ -690,8 +732,8 @@ mod lyrics { #[case::isrc(TrackId::Isrc("KRA302000590".into()))] #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] #[tokio::test] - async fn from_id(#[case] track_id: TrackId<'_>) { - let lyrics = new_mxm().track_lyrics(track_id).await.unwrap(); + async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { + let lyrics = mxm.await.track_lyrics(track_id).await.unwrap(); // dbg!(&lyrics); @@ -707,9 +749,11 @@ mod lyrics { } /// This track has no lyrics + #[rstest] #[tokio::test] - async fn instrumental() { - let lyrics = new_mxm() + async fn instrumental(#[future] mxm: Musixmatch) { + let lyrics = mxm + .await .matcher_lyrics("drivers license", "Bobby G") .await .unwrap(); @@ -725,9 +769,11 @@ mod lyrics { } /// This track does not exist + #[rstest] #[tokio::test] - async fn missing() { - let err = new_mxm() + async fn missing(#[future] mxm: Musixmatch) { + let err = mxm + .await .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into())) .await .unwrap_err(); @@ -735,14 +781,16 @@ mod lyrics { assert!(matches!(err, Error::NotFound)); } + #[rstest] #[tokio::test] - async fn download_testdata() { + async fn download_testdata(#[future] mxm: Musixmatch) { + let mxm = mxm.await; let json_path = testfile("lyrics.json"); if json_path.exists() { return; } - let lyrics = new_mxm() + let lyrics = mxm .track_lyrics(TrackId::Commontrack(18576954)) .await .unwrap(); @@ -751,14 +799,16 @@ mod lyrics { serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap(); } + #[rstest] #[tokio::test] - async fn download_testdata_translation() { + async fn download_testdata_translation(#[future] mxm: Musixmatch) { + let mxm = mxm.await; let json_path = testfile("translation.json"); if json_path.exists() { return; } - let translations = new_mxm() + let translations = mxm .track_lyrics_translation(TrackId::Commontrack(18576954), "de") .await .unwrap(); @@ -767,9 +817,10 @@ mod lyrics { serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap(); } + #[rstest] #[tokio::test] - async fn concurrency() { - let mxm = new_mxm(); + async fn concurrency(#[future] mxm: Musixmatch) { + let mxm = mxm.await; let album = mxm .album_tracks( @@ -804,9 +855,11 @@ mod subtitles { use super::*; use musixmatch_inofficial::models::SubtitleFormat; + #[rstest] #[tokio::test] - async fn from_match() { - let subtitle = new_mxm() + async fn from_match(#[future] mxm: Musixmatch) { + let subtitle = mxm + .await .matcher_subtitle( "Shine", "Spektrem", @@ -835,8 +888,9 @@ mod subtitles { #[case::isrc(TrackId::Isrc("KRA302000590".into()))] #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] #[tokio::test] - async fn from_id(#[case] track_id: TrackId<'_>) { - let subtitle = new_mxm() + async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { + let subtitle = mxm + .await .track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0)) .await .unwrap(); @@ -856,9 +910,11 @@ mod subtitles { } /// This track has no lyrics + #[rstest] #[tokio::test] - async fn instrumental() { - let err = new_mxm() + async fn instrumental(#[future] mxm: Musixmatch) { + let err = mxm + .await .matcher_subtitle( "drivers license", "Bobby G", @@ -873,9 +929,11 @@ mod subtitles { } /// This track has not been synced + #[rstest] #[tokio::test] - async fn unsynced() { - let err = new_mxm() + async fn unsynced(#[future] mxm: Musixmatch) { + let err = mxm + .await .track_subtitle( TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()), SubtitleFormat::Json, @@ -889,9 +947,11 @@ mod subtitles { } /// Try to get subtitles with wrong length parameter + #[rstest] #[tokio::test] - async fn wrong_length() { - let err = new_mxm() + async fn wrong_length(#[future] mxm: Musixmatch) { + let err = mxm + .await .track_subtitle( TrackId::Commontrack(118480583), SubtitleFormat::Json, @@ -904,14 +964,16 @@ mod subtitles { assert!(matches!(err, Error::NotFound)); } + #[rstest] #[tokio::test] - async fn download_testdata() { + async fn download_testdata(#[future] mxm: Musixmatch) { let json_path = testfile("subtitles.json"); if json_path.exists() { return; } - let subtitle = new_mxm() + let subtitle = mxm + .await .track_subtitle( TrackId::Commontrack(18576954), SubtitleFormat::Json, @@ -973,3 +1035,9 @@ fn assert_imgurl(url: &Option, ends_with: &str) { url ); } + +/// Assert that number A is greater than or equal to number B +#[track_caller] +fn assert_gte(a: T, b: T, msg: &str) { + assert!(a >= b, "expected >= {b} {msg}, got {a}"); +} From 1bc5ae408343e6755e390909e7017647efcf59a1 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 16:16:26 +0200 Subject: [PATCH 17/61] chore: update justfile --- Justfile | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index da5c71b..7456066 100644 --- a/Justfile +++ b/Justfile @@ -36,7 +36,7 @@ release crate="musixmatch-inofficial": editor "$CHANGELOG" - git add "$CHANGELOG" + git add . git commit -m "chore(release): release $CRATE v$VERSION" awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG" diff --git a/README.md b/README.md index f9a5e77..e4ddd7b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ It allows you to obtain synchronized lyrics in different formats The Musixmatch API required a free account on to be used. However, as of 2024, this requirement was removed and the API can be used anonymously. -The client still allows you to supply credentials if Musixmatch decided to close the API +The client still allows you to supply credentials if Musixmatch decides to close the API down again. ## ⚠️ Copyright disclaimer From dcc25bff202becdec7101c5ce1825cd75e445f99 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 16:18:55 +0200 Subject: [PATCH 18/61] chore: update dependencies --- Cargo.toml | 19 ++++++++----------- cli/Cargo.toml | 8 ++++---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a98d05..32951e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,29 +41,26 @@ reqwest = { version = "0.12.0", default-features = false, features = [ "json", "gzip", ] } -tokio = { version = "1.20.0" } +tokio = { version = "1.20.4" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.85" -thiserror = "1.0.36" +thiserror = "1.0.0" log = "0.4.17" -time = { version = "0.3.15", features = [ +time = { version = "0.3.10", features = [ "macros", "formatting", "serde", "serde-well-known", ] } -hmac = "0.12.1" -sha1 = "0.10.5" -rand = "0.8.5" +hmac = "0.12.0" +sha1 = "0.10.0" +rand = "0.8.0" base64 = "0.22.0" [dev-dependencies] -rstest = { version = "0.18.0", default-features = false } +rstest = { version = "0.22.0", default-features = false } env_logger = "0.11.0" dotenvy = "0.15.5" -tokio = { version = "1.20.0", features = ["macros"] } +tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" - -[profile.release] -strip = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c5afe16..98aaee3 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,11 +20,11 @@ rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"] [dependencies] musixmatch-inofficial.workspace = true -tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.20.4", features = ["macros", "rt-multi-thread"] } id3 = "1.3.0" mp3-duration = "0.1.10" -clap = { version = "4.0.10", features = ["derive"] } -anyhow = "1.0.65" +clap = { version = "4.0.0", features = ["derive"] } +anyhow = "1.0.0" rpassword = "7.0.0" dirs = "5.0.0" -serde_json = "1.0.91" +serde_json = "1.0.85" From c7d40a75eea885a35c9985f2c2c380254c791d37 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 17:39:34 +0200 Subject: [PATCH 19/61] ci: add renovate --- .gitea/workflows/release.yaml | 9 ++--- .gitea/workflows/renovate.yaml | 63 ++++++++++++++++++++++++++++++++++ renovate.json | 13 +++++++ 3 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 .gitea/workflows/renovate.yaml create mode 100644 renovate.json diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index fa7432d..dd48fde 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -23,13 +23,8 @@ jobs: echo END_OF_FILE } >> "$GITHUB_ENV" - - name: πŸ“€ Publish crate on code.thetadev.de - run: | - mkdir -p ~/.cargo - printf '[registries.thetadev]\nindex = "https://code.thetadev.de/ThetaDev/_cargo-index.git"\ntoken = "Bearer ${{ secrets.TOKEN_GITEA }}"\n' >> ~/.cargo/config.toml - sed -i "s/^musixmatch-.*=\s*{/\0 registry = \"thetadev\",/g" Cargo.toml - cargo publish --registry thetadev --package "${{ env.CRATE }}" --allow-dirty - git restore Cargo.toml + - name: πŸ“€ Publish crate on crates.io + run: cargo publish --token ${{ secrets.CARGO_TOKEN }} --package "${{ env.CRATE }}" - name: πŸŽ‰ Publish release uses: https://gitea.com/actions/release-action@main diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml new file mode 100644 index 0000000..e91c636 --- /dev/null +++ b/.gitea/workflows/renovate.yaml @@ -0,0 +1,63 @@ +name: renovate + +on: + push: + branches: ["main"] + paths: + - ".forgejo/workflows/renovate.yaml" + - "renovate.json" + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +env: + RENOVATE_REPOSITORIES: ${{ github.repository }} + +jobs: + renovate: + runs-on: docker + container: + image: renovate/renovate:latest + + steps: + - name: Load renovate repo cache + uses: actions/cache/restore@v4 + with: + path: | + .tmp/cache/renovate/repository + .tmp/cache/renovate/renovate-cache-sqlite + .tmp/osv + key: repo-cache-${{ github.run_id }} + restore-keys: | + repo-cache- + + - name: Run renovate + run: renovate + env: + LOG_LEVEL: debug + RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp + RENOVATE_ENDPOINT: ${{ github.server_url }} + RENOVATE_PLATFORM: gitea + RENOVATE_REPOSITORY_CACHE: 'enabled' + RENOVATE_TOKEN: ${{ secrets.FORGEJO_CI_BOT_TOKEN }} + GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_TOKEN }} + RENOVATE_GIT_AUTHOR: 'Renovate Bot ' + + RENOVATE_X_SQLITE_PACKAGE_CACHE: true + + GIT_AUTHOR_NAME: 'Renovate Bot' + GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org' + GIT_COMMITTER_NAME: 'Renovate Bot' + GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org' + + OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv + + - name: Save renovate repo cache + if: always() && env.RENOVATE_DRY_RUN != 'full' + uses: actions/cache/save@v4 + with: + path: | + .tmp/cache/renovate/repository + .tmp/cache/renovate/renovate-cache-sqlite + .tmp/osv + key: repo-cache-${{ github.run_id }} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..3ff20b1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:best-practices" + ], + "semanticCommits": "enabled", + "automerge": true, + "automergeStrategy": "squash", + "osvVulnerabilityAlerts": true, + "labels": ["dependency-upgrade"], + "enabledManagers": ["cargo"], + "prHourlyLimit": 5 +} From 30e2afd3679d2c17a49afd523c8b8bad70f291e5 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:16:07 +0200 Subject: [PATCH 20/61] chore: change repo to codeberg --- CHANGELOG.md | 2 +- Cargo.toml | 4 +-- README.md | 6 +++- cli/CHANGELOG.md | 2 +- cli/README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ cli/src/main.rs | 17 ++++++++++-- cliff.toml | 2 +- 7 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 cli/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d5005..188c7c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [v0.1.0](https://code.thetadev.de/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-inofficial/v0.1.0) - 2024-03-23 +## [v0.1.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-inofficial/v0.1.0) - 2024-03-23 Initial release diff --git a/Cargo.toml b/Cargo.toml index 32951e3..97a3278 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,9 @@ members = [".", "cli"] [workspace.package] edition = "2021" -authors = ["ThetaDev "] +authors = ["ThetaDev "] license = "MIT" -repository = "https://code.thetadev.de/ThetaDev/musixmatch-inofficial" +repository = "https://codeberg.org/ThetaDev/musixmatch-inofficial" keywords = ["music", "lyrics"] categories = ["api-bindings", "multimedia"] diff --git a/README.md b/README.md index e4ddd7b..5fb152a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# Musixmatch-Inofficial +# musixmatch-inofficial + +[![Current crates.io version](https://img.shields.io/crates/v/musixmatch-inofficial.svg)](https://crates.io/crates/musixmatch-inofficial) +[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT) +[![CI status](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/?workflow=ci.yaml) This is an inofficial client for the Musixmatch API that uses the key embedded in the Musixmatch Android app. diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 7a75fb4..4f2a52b 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [v0.1.0](https://code.thetadev.de/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-cli/v0.1.0) - 2024-03-23 +## [v0.1.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-cli/v0.1.0) - 2024-03-23 Initial release diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..88e59fa --- /dev/null +++ b/cli/README.md @@ -0,0 +1,71 @@ +# musixmatch-cli + +[![Current crates.io version](https://img.shields.io/crates/v/musixmatch-cli.svg)](https://crates.io/crates/musixmatch-cli) +[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT) +[![CI status](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/?workflow=ci.yaml) + +The Musixmatch CLI allows you to fetch lyrics from the command line. + +### Get lyrics + +```txt +musixmatch-cli get lyrics -n shine -a spektrem +Lyrics ID: 34583240 +Language: en +Copyright: Writer(s): Jesse Warren +Copyright: Ncs Music + +Eyes in the sky gazing far into the night +I raise my hand to the fire, but it's no use +'Cause you can't stop it from shining through +It's true +... +``` + +### Get translated lyrics + +Musixmatch also offers translated lyrics. You have to select a language using the +`--lang` flag. You can also set the `--bi` flag to output both the original and +translated lines. + +```txt +musixmatch-cli get lyrics -n shine -a spektrem --lang de --bi +Lyrics ID: 34583240 +Language: en +Copyright: Writer(s): Jesse Warren +Copyright: Ncs Music +Translated to: de + +Eyes in the sky gazing far into the night +> Augen starren in die weite Nacht +I raise my hand to the fire, but it's no use +> Ich hebe meine Hand in das Feuer, doch ihr geschieht nichts +'Cause you can't stop it from shining through +> Denn du kannst es nicht daran hindern, hindurch zu scheinen +It's true +> Es ist wahr +... +``` + +### Get subtitles (synchronized lyrics) + +For most lyrics Musixmatch provides timestamps for the individual lines so you can display them +in sync during playback. + +Musixmatch offers multiple subtitle formats you can select using the `--format` flag. +The available formats are: `lrc`, `ttml`, `ttml-structured`, `json`, `ebu-stl` + +```txt +musixmatch-cli get subtitles -n shine -a spektrem +Subtitle ID: 35340319 +Language: en +Length: 316 +Copyright: Writer(s): Jesse Warren +Copyright: Ncs Music + +[00:59.84] Eyes in the sky gazing far into the night +[01:06.55] I raise my hand to the fire, but it's no use +[01:11.97] 'Cause you can't stop it from shining through +[01:16.07] It's true +... +``` diff --git a/cli/src/main.rs b/cli/src/main.rs index c0fae2c..9dee55f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,6 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs, clippy::todo)] + use std::{ io::{stdin, stdout, Write}, path::PathBuf, @@ -20,14 +23,17 @@ struct Cli { #[derive(Subcommand)] enum Commands { + /// Get lyrics, subtitles or track metadata from Musixmatch Get { #[command(subcommand)] command: GetCommands, }, + /// Get lyrics or subtitles using the metadata from a MP3 file Mp3 { #[command(subcommand)] command: FileCommands, }, + /// Search for Musixmatch tracks #[group(required = true)] Search { /// Track name @@ -46,6 +52,7 @@ enum Commands { #[derive(Subcommand)] enum GetCommands { + /// Get lyrics text Lyrics { #[clap(flatten)] ident: TrackIdentifiers, @@ -56,22 +63,24 @@ enum GetCommands { #[clap(long)] bi: bool, }, + /// Get subtitles (time-synced lyrics) Subtitles { #[clap(flatten)] ident: TrackIdentifiers, /// Track length - #[clap(long, short)] + #[clap(short, long)] length: Option, /// Maximum deviation from track length (Default: 1s) #[clap(long)] max_deviation: Option, /// Subtitle format - #[clap(long, default_value = "lrc")] + #[clap(short, long, default_value = "lrc")] format: SubtitleFormatClap, /// Language #[clap(long)] lang: Option, }, + /// Get track metadata Track { #[clap(flatten)] ident: TrackIdentifiers, @@ -108,11 +117,13 @@ struct TrackIdentifiers { #[derive(Subcommand)] enum FileCommands { + /// Get lyrics text Lyrics { /// Music file #[clap(value_parser)] file: PathBuf, }, + /// Get subtitles (time-synced lyrics) Subtitles { /// Music file #[clap(value_parser)] @@ -121,7 +132,7 @@ enum FileCommands { } #[derive(clap::ValueEnum, Debug, Copy, Clone)] -pub enum SubtitleFormatClap { +enum SubtitleFormatClap { Lrc, Ttml, TtmlStructured, diff --git a/cliff.toml b/cliff.toml index 435dd76..a12fc01 100644 --- a/cliff.toml +++ b/cliff.toml @@ -14,7 +14,7 @@ All notable changes to this project will be documented in this file.\n # template for the changelog body # https://keats.github.io/tera/docs/#introduction body = """ -{% set repo_url = "https://code.thetadev.de/ThetaDev/musixmatch-inofficial" %}\ +{% set repo_url = "https://codeberg.org/ThetaDev/musixmatch-inofficial" %}\ {% if version %}\ {%set vname = version | split(pat="/") | last %} {%if previous.version %}\ From 19e209e34f4d129a4223930bfd41e1ccf117f231 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:19:00 +0200 Subject: [PATCH 21/61] feat: add format option to mp3 subtitles cmd --- cli/src/main.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 9dee55f..95f7c2b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -128,6 +128,9 @@ enum FileCommands { /// Music file #[clap(value_parser)] file: PathBuf, + /// Subtitle format + #[clap(short, long, default_value = "lrc")] + format: SubtitleFormatClap, }, } @@ -324,7 +327,7 @@ async fn run(cli: Cli) -> Result<()> { title, artist, lyrics.lyrics_body ); } - FileCommands::Subtitles { file } => { + FileCommands::Subtitles { file, format } => { let tag = Tag::read_from_path(&file)?; let duration = mp3_duration::from_path(&file)?; @@ -335,7 +338,7 @@ async fn run(cli: Cli) -> Result<()> { .matcher_subtitle( title, artist, - SubtitleFormat::Lrc, + format.into(), Some(duration.as_secs_f32()), Some(1.0), ) From 348e9c5427e59c488d7e2f7cef9e7006a12864f2 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:25:03 +0200 Subject: [PATCH 22/61] doc: update readme --- .gitea/workflows/ci.yaml | 2 +- README.md | 8 ++++---- cli/README.md | 12 +++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 94f970c..acd75d4 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -16,4 +16,4 @@ jobs: run: cargo clippy --all -- -D warnings - name: πŸ§ͺ Test - run: cargo test --workspace + run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace -j 2 diff --git a/README.md b/README.md index 5fb152a..cd5c568 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ It allows you to obtain synchronized lyrics in different formats ([LRC](), [DFXP](https://www.w3.org/TR/ttml1/), JSON) for almost any song. -The Musixmatch API required a free account on to be used. -However, as of 2024, this requirement was removed and the API can be used anonymously. -The client still allows you to supply credentials if Musixmatch decides to close the API -down again. +The Musixmatch API used to require a free account on to be +used. However, as of 2024, this requirement was removed and the API can be used +anonymously. The client still allows you to supply credentials if Musixmatch decides to +close the API down again. ## ⚠️ Copyright disclaimer diff --git a/cli/README.md b/cli/README.md index 88e59fa..2766c06 100644 --- a/cli/README.md +++ b/cli/README.md @@ -4,7 +4,13 @@ [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT) [![CI status](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/?workflow=ci.yaml) -The Musixmatch CLI allows you to fetch lyrics from the command line. +The Musixmatch CLI allows you to fetch lyrics, subtitles and track metadata from the +command line using the Musixmatch API. + +The Musixmatch API used to require a free account on to be +used. However, as of 2024, this requirement was removed and the API can be used +anonymously. The CLI still allows you to supply credentials if Musixmatch decides to +close the API down again. ### Get lyrics @@ -49,8 +55,8 @@ It's true ### Get subtitles (synchronized lyrics) -For most lyrics Musixmatch provides timestamps for the individual lines so you can display them -in sync during playback. +For most lyrics Musixmatch provides timestamps for the individual lines so you can +display them in sync during playback. Musixmatch offers multiple subtitle formats you can select using the `--format` flag. The available formats are: `lrc`, `ttml`, `ttml-structured`, `json`, `ebu-stl` From f45ad3cefb1c884b2ac830e147d92db826a4ae25 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:28:00 +0200 Subject: [PATCH 23/61] ci: enable warpproxy --- .gitea/workflows/ci.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index acd75d4..2d40349 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -4,6 +4,14 @@ on: [push, pull_request] jobs: Test: runs-on: cimaster-latest + services: + warpproxy: + image: thetadev256/warpproxy + env: + WARP_DEVICE_ID: ${{ secrets.WARP_DEVICE_ID }} + WARP_ACCESS_TOKEN: ${{ secrets.WARP_ACCESS_TOKEN }} + WARP_LICENSE_KEY: ${{ secrets.WARP_LICENSE_KEY }} + WARP_PRIVATE_KEY: ${{ secrets.WARP_PRIVATE_KEY }} steps: - name: πŸ“¦ Checkout repository uses: actions/checkout@v3 @@ -17,3 +25,5 @@ jobs: - name: πŸ§ͺ Test run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace -j 2 + env: + ALL_PROXY: "http://warpproxy:8124" From 05978665de0f8e7e4ae93482e5e465720d26757e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:30:42 +0200 Subject: [PATCH 24/61] ci: use credentials --- .gitea/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 2d40349..2ac96c2 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -27,3 +27,5 @@ jobs: run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace -j 2 env: ALL_PROXY: "http://warpproxy:8124" + MUSIXMATCH_EMAIL: ${{ secrets.MUSIXMATCH_EMAIL }} + MUSIXMATCH_PASSWORD: ${{ secrets.MUSIXMATCH_PASSWORD }} From c120583bf861cc74fbce686b2bd88bc575270130 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:32:21 +0200 Subject: [PATCH 25/61] test: fix tests --- .gitea/workflows/ci.yaml | 2 +- tests/tests.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 2ac96c2..e13ce6e 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: run: cargo clippy --all -- -D warnings - name: πŸ§ͺ Test - run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace -j 2 + run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace env: ALL_PROXY: "http://warpproxy:8124" MUSIXMATCH_EMAIL: ${{ secrets.MUSIXMATCH_EMAIL }} diff --git a/tests/tests.rs b/tests/tests.rs index 202e271..57aa8c3 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -233,8 +233,6 @@ mod artist { async fn search(#[future] mxm: Musixmatch) { let artists = mxm.await.artist_search("psy", 5, 1).await.unwrap(); - assert_gte(artists.len(), 4, "artists"); - let artist = &artists[0]; assert_eq!(artist.artist_id, 32403027); assert_eq!(artist.artist_name, "PSY"); From c9fea762ec97a1c594e60a3b1cbc72bb786d0957 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:35:00 +0200 Subject: [PATCH 26/61] test: fix tests --- .gitea/workflows/ci.yaml | 2 +- tests/tests.rs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index e13ce6e..2ac96c2 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: run: cargo clippy --all -- -D warnings - name: πŸ§ͺ Test - run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace + run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace -j 2 env: ALL_PROXY: "http://warpproxy:8124" MUSIXMATCH_EMAIL: ${{ secrets.MUSIXMATCH_EMAIL }} diff --git a/tests/tests.rs b/tests/tests.rs index 57aa8c3..382485c 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -231,11 +231,15 @@ mod artist { #[rstest] #[tokio::test] async fn search(#[future] mxm: Musixmatch) { - let artists = mxm.await.artist_search("psy", 5, 1).await.unwrap(); + let artists = mxm + .await + .artist_search("Snollebollekes", 5, 1) + .await + .unwrap(); let artist = &artists[0]; - assert_eq!(artist.artist_id, 32403027); - assert_eq!(artist.artist_name, "PSY"); + assert_eq!(artist.artist_id, 25344078); + assert_eq!(artist.artist_name, "Snollebollekes"); } #[rstest] From bc0dd99f7d37135998a05fd8b63c78c5d87586cd Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 18:38:21 +0200 Subject: [PATCH 27/61] ci: enable retries --- .gitea/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 2ac96c2..ecc322d 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: run: cargo clippy --all -- -D warnings - name: πŸ§ͺ Test - run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --workspace -j 2 + run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --workspace -j 2 env: ALL_PROXY: "http://warpproxy:8124" MUSIXMATCH_EMAIL: ${{ secrets.MUSIXMATCH_EMAIL }} From dc1bea13cc2a37eae7f3727dc72f865a01430a2e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 22:58:41 +0200 Subject: [PATCH 28/61] fix: use native TLS for CLI --- cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 98aaee3..4ccc87f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,7 +9,7 @@ keywords.workspace = true description = "Inofficial command line interface for the Musixmatch API" [features] -default = ["rustls-tls-native-roots"] +default = ["native-tls"] # Reqwest TLS options native-tls = ["musixmatch-inofficial/native-tls"] From c4bfbe563a00d399b3645dd68f03c1215ee51fdb Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 23:40:26 +0200 Subject: [PATCH 29/61] feat: add get album, get artist, search artist --- cli/src/main.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 95f7c2b..227dada 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,7 +10,7 @@ use anyhow::{anyhow, bail, Result}; use clap::{Args, Parser, Subcommand}; use id3::{Tag, TagLike}; use musixmatch_inofficial::{ - models::{SubtitleFormat, Track, TrackId, TranslationMap}, + models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap}, Musixmatch, }; @@ -48,6 +48,8 @@ enum Commands { /// Search query query: Option>, }, + /// Search for Musixmatch artists + SearchArtist { query: Vec }, } #[derive(Subcommand)] @@ -85,6 +87,16 @@ enum GetCommands { #[clap(flatten)] ident: TrackIdentifiers, }, + /// Get album metadata + Album { + #[clap(flatten)] + ident: AlbumArtistIdentifiers, + }, + /// Get artist metadata + Artist { + #[clap(flatten)] + ident: AlbumArtistIdentifiers, + }, } #[derive(Args)] @@ -115,6 +127,17 @@ struct TrackIdentifiers { isrc: Option, } +#[derive(Args)] +#[group(multiple = false)] +struct AlbumArtistIdentifiers { + /// Musixmatch-ID + #[clap(long)] + mxm_id: Option, + /// Musicbrainz-ID + #[clap(long)] + musicbrainz: Option, +} + #[derive(Subcommand)] enum FileCommands { /// Get lyrics text @@ -312,6 +335,28 @@ async fn run(cli: Cli) -> Result<()> { let track = get_track(ident, &mxm).await?; println!("{}", serde_json::to_string_pretty(&track)?) } + GetCommands::Album { ident } => { + let id = if let Some(id) = ident.mxm_id { + AlbumId::AlbumId(id) + } else if let Some(mb) = &ident.musicbrainz { + AlbumId::Musicbrainz(mb) + } else { + bail!("no album ID specified") + }; + let album = mxm.album(id).await?; + println!("{}", serde_json::to_string_pretty(&album)?) + } + GetCommands::Artist { ident } => { + let id = if let Some(id) = ident.mxm_id { + ArtistId::ArtistId(id) + } else if let Some(mb) = &ident.musicbrainz { + ArtistId::Musicbrainz(mb) + } else { + bail!("no artist ID specified") + }; + let album = mxm.artist(id).await?; + println!("{}", serde_json::to_string_pretty(&album)?) + } }, Commands::Mp3 { command } => match command { FileCommands::Lyrics { file } => { @@ -383,6 +428,15 @@ async fn run(cli: Cli) -> Result<()> { ); } } + Commands::SearchArtist { query } => { + let artists = mxm.artist_search(&query.join(" "), 20, 0).await?; + for a in artists { + println!( + "{} ", + a.artist_name, a.artist_vanity_id + ); + } + } }; Ok(()) } From 54235e6fb61084823a6583aaa7d59b1799deb07f Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 23:43:47 +0200 Subject: [PATCH 30/61] feat!: remove MP3 feature, refactor cmd structure --- cli/Cargo.toml | 2 - cli/README.md | 6 +- cli/src/main.rs | 347 ++++++++++++++++++++---------------------------- 3 files changed, 150 insertions(+), 205 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4ccc87f..b0f3d06 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,8 +21,6 @@ rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"] [dependencies] musixmatch-inofficial.workspace = true tokio = { version = "1.20.4", features = ["macros", "rt-multi-thread"] } -id3 = "1.3.0" -mp3-duration = "0.1.10" clap = { version = "4.0.0", features = ["derive"] } anyhow = "1.0.0" rpassword = "7.0.0" diff --git a/cli/README.md b/cli/README.md index 2766c06..c015def 100644 --- a/cli/README.md +++ b/cli/README.md @@ -15,7 +15,7 @@ close the API down again. ### Get lyrics ```txt -musixmatch-cli get lyrics -n shine -a spektrem +musixmatch-cli lyrics -n shine -a spektrem Lyrics ID: 34583240 Language: en Copyright: Writer(s): Jesse Warren @@ -35,7 +35,7 @@ Musixmatch also offers translated lyrics. You have to select a language using th translated lines. ```txt -musixmatch-cli get lyrics -n shine -a spektrem --lang de --bi +musixmatch-cli lyrics -n shine -a spektrem --lang de --bi Lyrics ID: 34583240 Language: en Copyright: Writer(s): Jesse Warren @@ -62,7 +62,7 @@ Musixmatch offers multiple subtitle formats you can select using the `--format` The available formats are: `lrc`, `ttml`, `ttml-structured`, `json`, `ebu-stl` ```txt -musixmatch-cli get subtitles -n shine -a spektrem +musixmatch-cli subtitles -n shine -a spektrem Subtitle ID: 35340319 Language: en Length: 316 diff --git a/cli/src/main.rs b/cli/src/main.rs index 227dada..6a303da 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -8,7 +8,6 @@ use std::{ use anyhow::{anyhow, bail, Result}; use clap::{Args, Parser, Subcommand}; -use id3::{Tag, TagLike}; use musixmatch_inofficial::{ models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap}, Musixmatch, @@ -23,37 +22,6 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Get lyrics, subtitles or track metadata from Musixmatch - Get { - #[command(subcommand)] - command: GetCommands, - }, - /// Get lyrics or subtitles using the metadata from a MP3 file - Mp3 { - #[command(subcommand)] - command: FileCommands, - }, - /// Search for Musixmatch tracks - #[group(required = true)] - Search { - /// Track name - #[clap(short, long)] - name: Option, - /// Artist - #[clap(short, long)] - artist: Option, - /// Lyrics - #[clap(short, long)] - lyrics: Option, - /// Search query - query: Option>, - }, - /// Search for Musixmatch artists - SearchArtist { query: Vec }, -} - -#[derive(Subcommand)] -enum GetCommands { /// Get lyrics text Lyrics { #[clap(flatten)] @@ -97,6 +65,23 @@ enum GetCommands { #[clap(flatten)] ident: AlbumArtistIdentifiers, }, + /// Search for Musixmatch tracks + #[group(required = true)] + Search { + /// Track name + #[clap(short, long)] + name: Option, + /// Artist + #[clap(short, long)] + artist: Option, + /// Lyrics + #[clap(short, long)] + lyrics: Option, + /// Search query + query: Option>, + }, + /// Search for Musixmatch artists + SearchArtist { query: Vec }, } #[derive(Args)] @@ -212,186 +197,148 @@ async fn run(cli: Cli) -> Result<()> { }; match cli.command { - Commands::Get { command } => match command { - GetCommands::Lyrics { ident, lang, bi } => { - let track_id = get_track_id(ident, &mxm).await?; - let lyrics = mxm.track_lyrics(track_id.clone()).await?; + Commands::Lyrics { ident, lang, bi } => { + let track_id = get_track_id(ident, &mxm).await?; + let lyrics = mxm.track_lyrics(track_id.clone()).await?; - eprintln!("Lyrics ID: {}", lyrics.lyrics_id); - eprintln!( - "Language: {}", - lyrics.lyrics_language.as_deref().unwrap_or(NA_STR) - ); - eprintln!( - "Copyright: {}", - lyrics - .lyrics_copyright - .as_deref() - .map(|c| c.trim()) - .unwrap_or(NA_STR) - ); + eprintln!("Lyrics ID: {}", lyrics.lyrics_id); + eprintln!( + "Language: {}", + lyrics.lyrics_language.as_deref().unwrap_or(NA_STR) + ); + eprintln!( + "Copyright: {}", + lyrics + .lyrics_copyright + .as_deref() + .map(|c| c.trim()) + .unwrap_or(NA_STR) + ); - let mut lyrics_body = lyrics.lyrics_body; + let mut lyrics_body = lyrics.lyrics_body; - if let Some(lang) = lang { - if Some(&lang) != lyrics.lyrics_language.as_ref() { - let tl = mxm.track_lyrics_translation(track_id, &lang).await?; - if tl.is_empty() { - eprintln!( - "Translation not found. Returning lyrics in original language." - ); + if let Some(lang) = lang { + if Some(&lang) != lyrics.lyrics_language.as_ref() { + let tl = mxm.track_lyrics_translation(track_id, &lang).await?; + if tl.is_empty() { + eprintln!("Translation not found. Returning lyrics in original language."); + } else { + eprintln!("Translated to: {}", tl.lang); + let tm = TranslationMap::from(tl); + let translated = tm.translate_lyrics(&lyrics_body); + lyrics_body = if bi { + lyrics_body + .lines() + .zip(translated.lines()) + .map(|(a, b)| { + if a == b { + a.to_string() + "\n" + } else { + format!("{a}\n> {b}\n") + } + }) + .collect() } else { - eprintln!("Translated to: {}", tl.lang); - let tm = TranslationMap::from(tl); - let translated = tm.translate_lyrics(&lyrics_body); - lyrics_body = if bi { - lyrics_body - .lines() - .zip(translated.lines()) - .map(|(a, b)| { - if a == b { - a.to_string() + "\n" - } else { - format!("{a}\n> {b}\n") - } - }) - .collect() - } else { - translated - }; - } + translated + }; + } + } + } + + eprintln!(); + println!("{}", lyrics_body); + } + Commands::Subtitles { + ident, + length, + max_deviation, + format, + lang, + } => { + let track_id = get_track_id(ident, &mxm).await?; + let subtitles = mxm + .track_subtitle( + track_id.clone(), + if lang.is_some() { + SubtitleFormat::Json + } else { + format.into() + }, + length, + max_deviation.or(Some(1.0)), + ) + .await?; + + eprintln!("Subtitle ID: {}", subtitles.subtitle_id); + eprintln!( + "Language: {}", + subtitles.subtitle_language.as_deref().unwrap_or(NA_STR) + ); + eprintln!("Length: {}", subtitles.subtitle_length); + eprintln!( + "Copyright: {}", + subtitles + .lyrics_copyright + .as_deref() + .map(|s| s.trim()) + .unwrap_or(NA_STR) + ); + + if let Some(lang) = lang { + let mut lines = subtitles.to_lines()?; + + if Some(&lang) != subtitles.subtitle_language.as_ref() { + let tl = mxm.track_lyrics_translation(track_id, &lang).await?; + if tl.is_empty() { + bail!("Translation not found") + } else { + eprintln!("Translated to: {}", tl.lang); + let tm = TranslationMap::from(tl); + lines = tm.translate_subtitles(&lines); } } eprintln!(); - println!("{}", lyrics_body); - } - GetCommands::Subtitles { - ident, - length, - max_deviation, - format, - lang, - } => { - let track_id = get_track_id(ident, &mxm).await?; - let subtitles = mxm - .track_subtitle( - track_id.clone(), - if lang.is_some() { - SubtitleFormat::Json - } else { - format.into() - }, - length, - max_deviation.or(Some(1.0)), - ) - .await?; - - eprintln!("Subtitle ID: {}", subtitles.subtitle_id); - eprintln!( - "Language: {}", - subtitles.subtitle_language.as_deref().unwrap_or(NA_STR) - ); - eprintln!("Length: {}", subtitles.subtitle_length); - eprintln!( - "Copyright: {}", - subtitles - .lyrics_copyright - .as_deref() - .map(|s| s.trim()) - .unwrap_or(NA_STR) - ); - - if let Some(lang) = lang { - let mut lines = subtitles.to_lines()?; - - if Some(&lang) != subtitles.subtitle_language.as_ref() { - let tl = mxm.track_lyrics_translation(track_id, &lang).await?; - if tl.is_empty() { - bail!("Translation not found") - } else { - eprintln!("Translated to: {}", tl.lang); - let tm = TranslationMap::from(tl); - lines = tm.translate_subtitles(&lines); - } + let res = match format { + SubtitleFormatClap::Lrc => lines.to_lrc(), + SubtitleFormatClap::Ttml => lines.to_ttml(), + SubtitleFormatClap::Json => lines.to_json()?, + SubtitleFormatClap::TtmlStructured | SubtitleFormatClap::EbuStl => { + bail!("subtitle format {format:?} cant be translated") } - - eprintln!(); - let res = match format { - SubtitleFormatClap::Lrc => lines.to_lrc(), - SubtitleFormatClap::Ttml => lines.to_ttml(), - SubtitleFormatClap::Json => lines.to_json()?, - SubtitleFormatClap::TtmlStructured | SubtitleFormatClap::EbuStl => { - bail!("subtitle format {format:?} cant be translated") - } - }; - println!("{}", res); - } else { - eprintln!(); - println!("{}", subtitles.subtitle_body); - } - } - GetCommands::Track { ident } => { - let track = get_track(ident, &mxm).await?; - println!("{}", serde_json::to_string_pretty(&track)?) - } - GetCommands::Album { ident } => { - let id = if let Some(id) = ident.mxm_id { - AlbumId::AlbumId(id) - } else if let Some(mb) = &ident.musicbrainz { - AlbumId::Musicbrainz(mb) - } else { - bail!("no album ID specified") }; - let album = mxm.album(id).await?; - println!("{}", serde_json::to_string_pretty(&album)?) - } - GetCommands::Artist { ident } => { - let id = if let Some(id) = ident.mxm_id { - ArtistId::ArtistId(id) - } else if let Some(mb) = &ident.musicbrainz { - ArtistId::Musicbrainz(mb) - } else { - bail!("no artist ID specified") - }; - let album = mxm.artist(id).await?; - println!("{}", serde_json::to_string_pretty(&album)?) - } - }, - Commands::Mp3 { command } => match command { - FileCommands::Lyrics { file } => { - let tag = Tag::read_from_path(&file)?; - - let title = tag.title().ok_or(anyhow!("no title"))?; - let artist = tag.artist().ok_or(anyhow!("no artist"))?; - - let lyrics = mxm.matcher_lyrics(title, artist).await?; - - println!( - "Lyrics for {} by {}:\n\n{}", - title, artist, lyrics.lyrics_body - ); - } - FileCommands::Subtitles { file, format } => { - let tag = Tag::read_from_path(&file)?; - let duration = mp3_duration::from_path(&file)?; - - let title = tag.title().ok_or(anyhow!("no title"))?; - let artist = tag.artist().ok_or(anyhow!("no artist"))?; - - let subtitles = mxm - .matcher_subtitle( - title, - artist, - format.into(), - Some(duration.as_secs_f32()), - Some(1.0), - ) - .await?; - + println!("{}", res); + } else { + eprintln!(); println!("{}", subtitles.subtitle_body); } - }, + } + Commands::Track { ident } => { + let track = get_track(ident, &mxm).await?; + println!("{}", serde_json::to_string_pretty(&track)?) + } + Commands::Album { ident } => { + let id = if let Some(id) = ident.mxm_id { + AlbumId::AlbumId(id) + } else if let Some(mb) = &ident.musicbrainz { + AlbumId::Musicbrainz(mb) + } else { + bail!("no album ID specified") + }; + let album = mxm.album(id).await?; + println!("{}", serde_json::to_string_pretty(&album)?) + } + Commands::Artist { ident } => { + let id = if let Some(id) = ident.mxm_id { + ArtistId::ArtistId(id) + } else if let Some(mb) = &ident.musicbrainz { + ArtistId::Musicbrainz(mb) + } else { + bail!("no artist ID specified") + }; + let album = mxm.artist(id).await?; + println!("{}", serde_json::to_string_pretty(&album)?) + } Commands::Search { query, name, From df150d9ffdec2868749fe7f62cb84d280a087adc Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 23:45:47 +0200 Subject: [PATCH 31/61] ci: remove musixmatch credentials --- .gitea/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ecc322d..65e4058 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -27,5 +27,3 @@ jobs: run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --workspace -j 2 env: ALL_PROXY: "http://warpproxy:8124" - MUSIXMATCH_EMAIL: ${{ secrets.MUSIXMATCH_EMAIL }} - MUSIXMATCH_PASSWORD: ${{ secrets.MUSIXMATCH_PASSWORD }} From 3b69b36ae6c945d786534e0eaa353fb737b1fb54 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 19 Aug 2024 00:02:35 +0200 Subject: [PATCH 32/61] test: add rate limiter --- .gitea/workflows/ci.yaml | 2 +- Cargo.toml | 1 + tests/tests.rs | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 65e4058..bc6e278 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -24,6 +24,6 @@ jobs: run: cargo clippy --all -- -D warnings - name: πŸ§ͺ Test - run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --workspace -j 2 + run: cargo test --workspace env: ALL_PROXY: "http://warpproxy:8124" diff --git a/Cargo.toml b/Cargo.toml index 97a3278..058478d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,3 +64,4 @@ dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" +governor = "0.6.3" diff --git a/tests/tests.rs b/tests/tests.rs index 382485c..aa762f9 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,5 +1,10 @@ -use std::path::{Path, PathBuf}; +use std::{ + num::NonZeroU32, + path::{Path, PathBuf}, + sync::LazyLock, +}; +use governor::{DefaultDirectRateLimiter, Quota, RateLimiter}; use path_macro::path; use rstest::{fixture, rstest}; use time::macros::{date, datetime}; @@ -16,6 +21,10 @@ fn testfile>(name: P) -> PathBuf { #[fixture] async fn mxm() -> Musixmatch { static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new(); + static MXM_LIMITER: LazyLock = + LazyLock::new(|| RateLimiter::direct(Quota::per_second(NonZeroU32::new(1).unwrap()))); + + MXM_LIMITER.until_ready().await; let mut mxm = Musixmatch::builder(); From a95f3fcf478f1acda9fad12741604b6793e128c1 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 19 Aug 2024 00:13:29 +0200 Subject: [PATCH 33/61] feat: add msrv --- Cargo.toml | 2 +- cli/Cargo.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 058478d..1fabb48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "musixmatch-inofficial" version = "0.1.0" +rust-version = "1.70.0" edition.workspace = true authors.workspace = true license.workspace = true @@ -59,7 +60,6 @@ base64 = "0.22.0" [dev-dependencies] rstest = { version = "0.22.0", default-features = false } -env_logger = "0.11.0" dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b0f3d06..886db6f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "musixmatch-cli" version = "0.1.0" +rust-version = "1.70.0" edition.workspace = true authors.workspace = true license.workspace = true From 38386c01329248b1d733fc81d70ba313a62e941d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 19 Aug 2024 00:20:39 +0200 Subject: [PATCH 34/61] chore(release): release musixmatch-inofficial v0.1.1 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ Cargo.toml | 5 ++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188c7c2..37c73e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to this project will be documented in this file. + +## [v0.1.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.0..musixmatch-inofficial/v0.1.1) - 2024-08-18 + +### πŸš€ Features + +- Add msrv - ([a95f3fc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/a95f3fcf478f1acda9fad12741604b6793e128c1)) + +### πŸ“š Documentation + +- Update readme - ([348e9c5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/348e9c5427e59c488d7e2f7cef9e7006a12864f2)) + +### πŸ§ͺ Testing + +- Fix tests - ([d2a7aed](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/d2a7aed917bfcec75ce00bb49d380fbc31c47384)) +- Fix tests - ([c120583](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/c120583bf861cc74fbce686b2bd88bc575270130)) +- Fix tests - ([c9fea76](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/c9fea762ec97a1c594e60a3b1cbc72bb786d0957)) +- Add rate limiter - ([3b69b36](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/3b69b36ae6c945d786534e0eaa353fb737b1fb54)) + +### βš™οΈ Miscellaneous Tasks + +- Update justfile - ([1bc5ae4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/1bc5ae408343e6755e390909e7017647efcf59a1)) +- Update dependencies - ([dcc25bf](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dcc25bff202becdec7101c5ce1825cd75e445f99)) +- Change repo to codeberg - ([30e2afd](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/30e2afd3679d2c17a49afd523c8b8bad70f291e5)) + ## [v0.1.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-inofficial/v0.1.0) - 2024-03-23 Initial release diff --git a/Cargo.toml b/Cargo.toml index 1fabb48..31b1043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-inofficial" -version = "0.1.0" +version = "0.1.1" rust-version = "1.70.0" edition.workspace = true authors.workspace = true @@ -9,8 +9,7 @@ repository.workspace = true keywords.workspace = true description = "Inofficial client for the Musixmatch API" - -include = ["/src", "README.md", "LICENSE"] +include = ["/src", "README.md", "CHANGELOG.md", "LICENSE"] [workspace] members = [".", "cli"] From 424a522708906aac99d64bc49129d2d953e2a924 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 19 Aug 2024 00:21:36 +0200 Subject: [PATCH 35/61] chore(release): release musixmatch-cli v0.2.0 --- Cargo.toml | 2 +- cli/CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ cli/Cargo.toml | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 31b1043..0a3b896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ keywords = ["music", "lyrics"] categories = ["api-bindings", "multimedia"] [workspace.dependencies] -musixmatch-inofficial = { version = "0.1.0", path = ".", default-features = false } +musixmatch-inofficial = { version = "0.1.1", path = ".", default-features = false } [features] default = ["default-tls"] diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 4f2a52b..c8a2e23 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. + +## [v0.2.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.1.0..musixmatch-cli/v0.2.0) - 2024-08-18 + +### πŸš€ Features + +- Add format option to mp3 subtitles cmd - ([19e209e](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/19e209e34f4d129a4223930bfd41e1ccf117f231)) +- Add get album, get artist, search artist - ([c4bfbe5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/c4bfbe563a00d399b3645dd68f03c1215ee51fdb)) +- [**breaking**] Remove MP3 feature, refactor cmd structure - ([54235e6](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/54235e6fb61084823a6583aaa7d59b1799deb07f)) +- Add msrv - ([a95f3fc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/a95f3fcf478f1acda9fad12741604b6793e128c1)) + +### πŸ› Bug Fixes + +- Use native TLS for CLI - ([dc1bea1](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dc1bea13cc2a37eae7f3727dc72f865a01430a2e)) + +### πŸ“š Documentation + +- Update readme - ([348e9c5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/348e9c5427e59c488d7e2f7cef9e7006a12864f2)) + +### πŸ§ͺ Testing + +- Fix tests - ([d2a7aed](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/d2a7aed917bfcec75ce00bb49d380fbc31c47384)) +- Add rate limiter - ([3b69b36](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/3b69b36ae6c945d786534e0eaa353fb737b1fb54)) + +### βš™οΈ Miscellaneous Tasks + +- Fix changelogs - ([e72d2b4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/e72d2b4363a3a9a48dec8f2be9389f6cc239035c)) +- Update justfile - ([1bc5ae4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/1bc5ae408343e6755e390909e7017647efcf59a1)) +- Update dependencies - ([dcc25bf](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dcc25bff202becdec7101c5ce1825cd75e445f99)) +- Change repo to codeberg - ([30e2afd](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/30e2afd3679d2c17a49afd523c8b8bad70f291e5)) + ## [v0.1.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-cli/v0.1.0) - 2024-03-23 Initial release diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 886db6f..8b77e98 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-cli" -version = "0.1.0" +version = "0.2.0" rust-version = "1.70.0" edition.workspace = true authors.workspace = true From 5ef76f5a6b2a3b243f847cf86e72ebe176819d7a Mon Sep 17 00:00:00 2001 From: ThetaBot Date: Mon, 30 Sep 2024 00:07:35 +0000 Subject: [PATCH 36/61] chore(deps): update rust crate rstest to 0.23.0 (#2) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0a3b896..81e9352 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ rand = "0.8.0" base64 = "0.22.0" [dev-dependencies] -rstest = { version = "0.22.0", default-features = false } +rstest = { version = "0.23.0", default-features = false } dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" From 4bfcb791733ce5ebd9d4e074c64eb23e9a768fc6 Mon Sep 17 00:00:00 2001 From: ThetaBot Date: Wed, 23 Oct 2024 20:08:45 +0000 Subject: [PATCH 37/61] chore(deps): update rust crate governor to 0.7.0 (#3) --- Cargo.toml | 2 +- tests/tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 81e9352..f1d68b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,4 +63,4 @@ dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" -governor = "0.6.3" +governor = "0.7.0" diff --git a/tests/tests.rs b/tests/tests.rs index aa762f9..a6483c8 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -59,7 +59,7 @@ mod album { ); assert_eq!(album.album_name, "Gangnam Style (κ°•λ‚¨μŠ€νƒ€μΌ)"); assert!(album.album_rating > 20); - assert_eq!(album.album_track_count, 1); + assert_eq!(album.album_track_count, 0); assert_eq!(album.album_release_date.unwrap(), date!(2012 - 01 - 01)); assert_eq!(album.album_release_type, AlbumType::Single); assert_eq!(album.artist_id, 410698); From e4cffa53ca2e458ec9ab3607f036426e3c647166 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 23 Oct 2024 22:51:41 +0200 Subject: [PATCH 38/61] test: skip artist_albums test --- tests/tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tests.rs b/tests/tests.rs index a6483c8..a308600 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -120,6 +120,7 @@ mod album { #[rstest] #[tokio::test] + #[ignore] async fn artist_albums(#[future] mxm: Musixmatch) { let albums = mxm .await From 368b46fa79d80460f6e4a7ba2dcce47c7590997d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 15 Nov 2024 19:37:55 +0100 Subject: [PATCH 39/61] test: fix missing lyrics test --- tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.rs b/tests/tests.rs index a308600..af71286 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -786,7 +786,7 @@ mod lyrics { async fn missing(#[future] mxm: Musixmatch) { let err = mxm .await - .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into())) + .track_lyrics(TrackId::Spotify("2gwMMr1a4aXXN5L6KC80Pu".into())) .await .unwrap_err(); From 6a6ced16224c6ef3d05eb6ebd0aa0bdc40a34684 Mon Sep 17 00:00:00 2001 From: ThetaBot Date: Fri, 15 Nov 2024 18:56:06 +0000 Subject: [PATCH 40/61] fix(deps): update rust crate thiserror to v2 (#4) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f1d68b7..869d843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ reqwest = { version = "0.12.0", default-features = false, features = [ tokio = { version = "1.20.4" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.85" -thiserror = "1.0.0" +thiserror = "2.0.0" log = "0.4.17" time = { version = "0.3.10", features = [ "macros", From adcd9baf120955bd3fbafa8674a0967d12d38a3e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 15 Nov 2024 20:19:34 +0100 Subject: [PATCH 41/61] chore(release): release musixmatch-inofficial v0.1.2 --- CHANGELOG.md | 12 ++++++++++++ Cargo.toml | 2 +- cliff.toml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c73e0..8d91e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. +## [v0.1.2](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.1..musixmatch-inofficial/v0.1.2) - 2024-11-15 + +### πŸ› Bug Fixes + +- *(deps)* Update rust crate thiserror to v2 (#4) - ([6a6ced1](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/6a6ced16224c6ef3d05eb6ebd0aa0bdc40a34684)) + +### βš™οΈ Miscellaneous Tasks + +- *(deps)* Update rust crate rstest to 0.23.0 (#2) - ([5ef76f5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/5ef76f5a6b2a3b243f847cf86e72ebe176819d7a)) +- *(deps)* Update rust crate governor to 0.7.0 (#3) - ([4bfcb79](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/4bfcb791733ce5ebd9d4e074c64eb23e9a768fc6)) + + ## [v0.1.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.0..musixmatch-inofficial/v0.1.1) - 2024-08-18 ### πŸš€ Features diff --git a/Cargo.toml b/Cargo.toml index 869d843..8377b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-inofficial" -version = "0.1.1" +version = "0.1.2" rust-version = "1.70.0" edition.workspace = true authors.workspace = true diff --git a/cliff.toml b/cliff.toml index a12fc01..b456d2d 100644 --- a/cliff.toml +++ b/cliff.toml @@ -73,7 +73,7 @@ commit_parsers = [ { message = "^perf", group = "⚑ Performance" }, { message = "^refactor", group = "🚜 Refactor" }, { message = "^style", group = "🎨 Styling" }, - { message = "^test", group = "πŸ§ͺ Testing" }, + { message = "^test", skip = true }, { message = "^chore\\(release\\)", skip = true }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, From 693ff347552a693ae47e181489330c6b23f58a9a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 6 Dec 2024 20:18:08 +0000 Subject: [PATCH 42/61] ci: add workdlow_dispatch --- .gitea/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index bc6e278..5aa75d1 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,5 +1,5 @@ name: CI -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch ] jobs: Test: From 26f4729738536d735cb808fce8a8e466f2e82449 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 7 Dec 2024 19:39:28 +0100 Subject: [PATCH 43/61] chore: fix clippy lints --- .gitea/workflows/ci.yaml | 5 ++++- src/api_model.rs | 12 ++++++------ src/models/id.rs | 6 +++--- tests/tests.rs | 8 ++++++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 5aa75d1..d03eb50 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,5 +1,8 @@ name: CI -on: [push, pull_request, workflow_dispatch ] +on: + push: + pull_request: + workflow_dispatch: jobs: Test: diff --git a/src/api_model.rs b/src/api_model.rs index 463e5d1..4d31076 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -117,7 +117,7 @@ where { struct BoolFromIntVisitor; - impl<'de> Visitor<'de> for BoolFromIntVisitor { + impl Visitor<'_> for BoolFromIntVisitor { type Value = bool; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -232,7 +232,7 @@ where n: PhantomData, } - impl<'de, N> Visitor<'de> for NullIfZeroVisitor + impl Visitor<'_> for NullIfZeroVisitor where N: TryFrom, { @@ -300,7 +300,7 @@ where { struct NullIfEmptyVisitor; - impl<'de> Visitor<'de> for NullIfEmptyVisitor { + impl Visitor<'_> for NullIfEmptyVisitor { type Value = Option; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -347,7 +347,7 @@ where n: PhantomData, } - impl<'de, N> Visitor<'de> for ParseIntVisitor + impl Visitor<'_> for ParseIntVisitor where N: FromStr + TryFrom, { @@ -441,7 +441,7 @@ pub mod optional_date { ) -> Result, D::Error> { struct OptionalDateVisitor; - impl<'de> Visitor<'de> for OptionalDateVisitor { + impl Visitor<'_> for OptionalDateVisitor { type Value = Option; fn expecting( @@ -501,7 +501,7 @@ pub mod optional_datetime { ) -> Result, D::Error> { struct OptionalDateVisitor; - impl<'de> Visitor<'de> for OptionalDateVisitor { + impl Visitor<'_> for OptionalDateVisitor { type Value = Option; fn expecting( diff --git a/src/models/id.rs b/src/models/id.rs index 2e8879a..9c84251 100644 --- a/src/models/id.rs +++ b/src/models/id.rs @@ -25,7 +25,7 @@ pub enum TrackId<'a> { Spotify(Cow<'a, str>), } -impl<'a> TrackId<'a> { +impl TrackId<'_> { pub(crate) fn to_param(&self) -> (&'static str, String) { match self { TrackId::Commontrack(id) => ("commontrack_id", id.to_string()), @@ -50,7 +50,7 @@ pub enum ArtistId<'a> { Musicbrainz(&'a str), } -impl<'a> ArtistId<'a> { +impl ArtistId<'_> { pub(crate) fn to_param(&self) -> (&'static str, String) { match self { ArtistId::ArtistId(id) => ("artist_id", id.to_string()), @@ -71,7 +71,7 @@ pub enum AlbumId<'a> { Musicbrainz(&'a str), } -impl<'a> AlbumId<'a> { +impl AlbumId<'_> { pub(crate) fn to_param(&self) -> (&'static str, String) { match self { AlbumId::AlbumId(id) => ("album_id", id.to_string()), diff --git a/tests/tests.rs b/tests/tests.rs index af71286..ea56b0b 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -21,8 +21,12 @@ fn testfile>(name: P) -> PathBuf { #[fixture] async fn mxm() -> Musixmatch { static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new(); - static MXM_LIMITER: LazyLock = - LazyLock::new(|| RateLimiter::direct(Quota::per_second(NonZeroU32::new(1).unwrap()))); + static MXM_LIMITER: LazyLock = LazyLock::new(|| { + RateLimiter::direct(Quota::per_second( + // limit 1 request per second for CI runs + NonZeroU32::new(if std::env::var("CI").is_ok() { 1 } else { 4 }).unwrap(), + )) + }); MXM_LIMITER.until_ready().await; From b136bb30040dc3ee849c26ff984884e706739235 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 8 Dec 2024 23:18:09 +0100 Subject: [PATCH 44/61] feat: add track performer tagging, artist images --- Cargo.toml | 1 + cli/src/main.rs | 44 +++++++++++--- src/api_model.rs | 66 ++++++++++++++++++++ src/apis/artist_api.rs | 1 + src/apis/track_api.rs | 24 +++++++- src/error.rs | 5 ++ src/lib.rs | 2 +- src/models/album.rs | 1 + src/models/artist.rs | 46 ++++++++++++++ src/models/id.rs | 133 ++++++++++++++++++++++++++++++++++++++++- src/models/mod.rs | 6 ++ src/models/snippet.rs | 1 + src/models/track.rs | 71 +++++++++++++++++++++- tests/tests.rs | 71 ++++++++++++++++++++-- 14 files changed, 454 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8377b1e..0f6609d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,3 +64,4 @@ tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" governor = "0.7.0" +test-log = "0.2.16" diff --git a/cli/src/main.rs b/cli/src/main.rs index 6a303da..c10e2a1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -55,6 +55,11 @@ enum Commands { #[clap(flatten)] ident: TrackIdentifiers, }, + /// Get performer tagging + Performer { + #[clap(flatten)] + ident: TrackIdentifiers, + }, /// Get album metadata Album { #[clap(flatten)] @@ -314,9 +319,16 @@ async fn run(cli: Cli) -> Result<()> { } } Commands::Track { ident } => { - let track = get_track(ident, &mxm).await?; + let track = get_track(ident, &mxm, false).await?; println!("{}", serde_json::to_string_pretty(&track)?) } + Commands::Performer { ident } => { + let track = get_track(ident, &mxm, true).await?; + println!( + "{}", + serde_json::to_string_pretty(&track.performer_tagging)? + ) + } Commands::Album { ident } => { let id = if let Some(id) = ident.mxm_id { AlbumId::AlbumId(id) @@ -397,6 +409,7 @@ async fn get_track_or_id( ident: TrackIdentifiers, mxm: &Musixmatch, translation_status: bool, + performer_tagging: bool, ) -> Result> { Ok( match ( @@ -422,8 +435,15 @@ async fn get_track_or_id( } (_, _, _, _, _, _, _, Some(isrc)) => TrackOrId::TrackId(TrackId::Isrc(isrc.into())), (Some(name), Some(artist), _, _, _, _, _, _) => TrackOrId::Track(Box::new( - mxm.matcher_track(&name, &artist, "", translation_status, true) - .await?, + mxm.matcher_track( + &name, + &artist, + "", + translation_status, + true, + performer_tagging, + ) + .await?, )), _ => bail!("no track identifier given"), }, @@ -431,17 +451,23 @@ async fn get_track_or_id( } async fn get_track_id(ident: TrackIdentifiers, mxm: &Musixmatch) -> Result> { - Ok(match get_track_or_id(ident, mxm, false).await? { + Ok(match get_track_or_id(ident, mxm, false, false).await? { TrackOrId::Track(track) => TrackId::TrackId(track.track_id), TrackOrId::TrackId(id) => id, }) } -async fn get_track(ident: TrackIdentifiers, mxm: &Musixmatch) -> Result { - Ok(match get_track_or_id(ident, mxm, true).await? { - TrackOrId::Track(track) => *track, - TrackOrId::TrackId(id) => mxm.track(id, true, true).await?, - }) +async fn get_track( + ident: TrackIdentifiers, + mxm: &Musixmatch, + performer_tagging: bool, +) -> Result { + Ok( + match get_track_or_id(ident, mxm, true, performer_tagging).await? { + TrackOrId::Track(track) => *track, + TrackOrId::TrackId(id) => mxm.track(id, true, true, performer_tagging).await?, + }, + ) } fn input(prompt: &str) -> String { diff --git a/src/api_model.rs b/src/api_model.rs index 4d31076..1eb4760 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -543,6 +543,55 @@ pub mod optional_datetime { } } +pub fn single_or_vec<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + struct SingleOrVecVisitor { + t: PhantomData, + } + + impl<'de, T> Visitor<'de> for SingleOrVecVisitor + where + T: Deserialize<'de>, + { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("single object or list") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut res = Vec::new(); + while let Some(x) = seq.next_element()? { + res.push(x); + } + Ok(res) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let (k1, val) = map + .next_entry::<&str, T>()? + .ok_or(serde::de::Error::missing_field("value"))?; + if let Some((k2, _)) = map.next_entry::<&str, serde::de::IgnoredAny>()? { + return Err(serde::de::Error::custom(format!( + "expected only 1 value, got keys `{k1}`, `{k2}`" + ))); + } + Ok(vec![val]) + } + } + + deserializer.deserialize_any(SingleOrVecVisitor { t: PhantomData }) +} + #[cfg(test)] mod tests { use time::Date; @@ -686,4 +735,21 @@ mod tests { let res = serde_json::from_str::(json_date).unwrap(); assert!(res.date.is_some()); } + + #[test] + fn deserialize_single_or_vec() { + #[derive(Deserialize, Debug)] + struct S { + #[serde(deserialize_with = "single_or_vec")] + vec: Vec, + } + + let res = serde_json::from_str::(r#"{"vec": [1, 2, 3]}"#).unwrap(); + assert_eq!(res.vec, [1, 2, 3]); + + let res = serde_json::from_str::(r#"{"vec": {"value": 1}}"#).unwrap(); + assert_eq!(res.vec, [1]); + + serde_json::from_str::(r#"{"vec": {"value": 1, "other": "xyz"}}"#).unwrap_err(); + } } diff --git a/src/apis/artist_api.rs b/src/apis/artist_api.rs index f2dde1f..1581a5f 100644 --- a/src/apis/artist_api.rs +++ b/src/apis/artist_api.rs @@ -18,6 +18,7 @@ impl Musixmatch { let id_param = id.to_param(); url_query.append_pair(id_param.0, &id_param.1); + url_query.append_pair("part", "artist_image"); url_query.finish(); } diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs index 045779f..537c69b 100644 --- a/src/apis/track_api.rs +++ b/src/apis/track_api.rs @@ -26,6 +26,7 @@ impl Musixmatch { q_album: &str, translation_status: bool, lang_3c: bool, + performer_tagging: bool, ) -> Result { let mut url = self.new_url("matcher.track.get"); { @@ -40,8 +41,10 @@ impl Musixmatch { if !q_album.is_empty() { url_query.append_pair("q_album", q_album); } + + let mut part = Vec::new(); if translation_status { - url_query.append_pair("part", "track_lyrics_translation_status"); + part.push("track_lyrics_translation_status"); url_query.append_pair( "language_iso_code", match lang_3c { @@ -50,6 +53,13 @@ impl Musixmatch { }, ); } + if performer_tagging { + part.push("track_performer_tagging"); + } + if !part.is_empty() { + url_query.append_pair("part", &part.join(",")); + } + url_query.finish(); } @@ -73,6 +83,7 @@ impl Musixmatch { id: TrackId<'_>, translation_status: bool, lang_3c: bool, + performer_tagging: bool, ) -> Result { let mut url = self.new_url("track.get"); { @@ -80,8 +91,10 @@ impl Musixmatch { let id_param = id.to_param(); url_query.append_pair(id_param.0, &id_param.1); + + let mut part = Vec::new(); if translation_status { - url_query.append_pair("part", "track_lyrics_translation_status"); + part.push("track_lyrics_translation_status"); url_query.append_pair( "language_iso_code", match lang_3c { @@ -90,6 +103,13 @@ impl Musixmatch { }, ); } + if performer_tagging { + part.push("track_performer_tagging"); + } + if !part.is_empty() { + url_query.append_pair("part", &part.join(",")); + } + url_query.finish(); } diff --git a/src/error.rs b/src/error.rs index 17e03e0..f58312b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,3 +52,8 @@ impl From for Error { Self::InvalidData(value.to_string().into()) } } + +/// Could not parse Musixmatch FQID +#[derive(thiserror::Error, Debug)] +#[error("Could not parse Musixmatch FQID")] +pub struct IdError; diff --git a/src/lib.rs b/src/lib.rs index b494a13..0d14784 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ use std::fmt::Debug; use std::path::Path; use std::sync::{Arc, RwLock}; -pub use error::Error; +pub use error::{Error, IdError}; use base64::Engine; use hmac::{Hmac, Mac}; diff --git a/src/models/album.rs b/src/models/album.rs index afc0d59..9d4e8f7 100644 --- a/src/models/album.rs +++ b/src/models/album.rs @@ -15,6 +15,7 @@ pub(crate) struct AlbumListBody { /// Album: an album of songs in the Musixmatch database. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub struct Album { /// Unique Musixmatch Album ID pub album_id: u64, diff --git a/src/models/artist.rs b/src/models/artist.rs index e6d1a76..0d85dd7 100644 --- a/src/models/artist.rs +++ b/src/models/artist.rs @@ -15,6 +15,7 @@ pub(crate) struct ArtistListBody { /// Artist: an artist in the Musixmatch database. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub struct Artist { /// Musixmatch Artist ID pub artist_id: u64, @@ -85,10 +86,14 @@ pub struct Artist { /// End date of the artist's presence #[serde(default, with = "crate::api_model::optional_date")] pub end_date: Option, + /// Pictures of the artist + #[serde(default, deserialize_with = "crate::api_model::single_or_vec")] + pub artist_image: Vec, } /// Alternative artist name (e.g. different languages) #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[non_exhaustive] pub struct ArtistAlias { /// Alternative artist name pub artist_alias: String, @@ -96,6 +101,7 @@ pub struct ArtistAlias { /// Artist name in another language #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[non_exhaustive] pub struct ArtistNameTranslation { /// Artist name in another language pub artist_name_translation: ArtistNameTranslationInner, @@ -103,6 +109,7 @@ pub struct ArtistNameTranslation { /// Alternative artist name (e.g. different languages) #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[non_exhaustive] pub struct ArtistNameTranslationInner { /// Language code (e.g. "EN") /// @@ -111,3 +118,42 @@ pub struct ArtistNameTranslationInner { /// Translated name pub translation: String, } + +/// Artist image +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[non_exhaustive] +pub struct ArtistImage { + /// ID of the image in the Musixmatch database + pub image_id: u64, + pub image_source_id: u32, + /// Author who created the image + #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] + pub image_author: Option, + /// Copyright info for the image + #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] + pub image_copyright: Option, + /// Image tags + #[serde(default, deserialize_with = "crate::api_model::null_if_empty")] + pub image_tags: Option, + // List of image files scaled to different sizes + pub image_format_list: Vec, +} + +/// Image file (wrapper struct) +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[non_exhaustive] +pub struct ImageFormatWrap { + pub image_format: ImageFormat, +} + +/// Image file +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[non_exhaustive] +pub struct ImageFormat { + /// URL to the image file + pub image_url: String, + /// Image width in pixels + pub width: u32, + /// Image height in pixels + pub height: u32, +} diff --git a/src/models/id.rs b/src/models/id.rs index 9c84251..3753c49 100644 --- a/src/models/id.rs +++ b/src/models/id.rs @@ -1,4 +1,8 @@ -use std::borrow::Cow; +use std::{borrow::Cow, convert::Infallible, fmt::Write, str::FromStr}; + +use serde::{de::Visitor, Deserialize, Serialize}; + +use crate::IdError; /// Track identifiers from different sources #[derive(Debug, Clone, PartialEq, Eq)] @@ -96,3 +100,130 @@ impl SortOrder { } } } + +/// Musixmatch fully qualified ID +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Fqid { + /// Numeric Musixmatch ID + pub id: u64, + /// Entity type + pub typ: MxmEntityType, +} + +/// Musixmatch entity type +#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[allow(missing_docs)] +#[non_exhaustive] +pub enum MxmEntityType { + Artist, + #[serde(other)] + Unknown, +} + +impl std::fmt::Display for MxmEntityType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + MxmEntityType::Artist => "artist", + MxmEntityType::Unknown => "unknown", + }; + f.write_str(s) + } +} + +impl FromStr for MxmEntityType { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(match s { + "artist" => Self::Artist, + _ => Self::Unknown, + }) + } +} + +impl std::fmt::Debug for MxmEntityType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self, f) + } +} + +impl std::fmt::Display for Fqid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "mxm:{}:{}", self.typ, self.id) + } +} + +impl std::fmt::Debug for Fqid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_char('"')?; + std::fmt::Display::fmt(&self, f)?; + f.write_char('"') + } +} + +impl FromStr for Fqid { + type Err = IdError; + + fn from_str(s: &str) -> Result { + let wo_pfx = s.strip_prefix("mxm:").ok_or(IdError)?; + let (typ_s, id_s) = wo_pfx.split_once(':').ok_or(IdError)?; + let id = id_s.parse().map_err(|_| IdError)?; + let typ = typ_s.parse().unwrap(); + Ok(Self { id, typ }) + } +} + +impl Serialize for Fqid { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Fqid { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct FqidVisitor; + + impl Visitor<'_> for FqidVisitor { + type Value = Fqid; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Musixmatch FQID") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse().map_err(serde::de::Error::custom) + } + } + + deserializer.deserialize_str(FqidVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::Fqid; + + #[test] + fn serialize_fqid() { + let json = r#""mxm:artist:27853427""#; + let id = serde_json::from_str::(json).unwrap(); + assert_eq!( + id, + Fqid { + id: 27853427, + typ: crate::models::id::MxmEntityType::Artist + } + ); + assert_eq!(serde_json::to_string(&id).unwrap(), json) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 720d4e6..8aef72e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -10,6 +10,8 @@ pub use subtitle::SubtitleTime; mod id; pub use id::AlbumId; pub use id::ArtistId; +pub use id::Fqid; +pub use id::MxmEntityType; pub use id::SortOrder; pub use id::TrackId; @@ -23,8 +25,12 @@ pub use translation::TranslationMap; pub(crate) mod track; pub use track::ChartName; +pub use track::Performer; +pub use track::PerformerTaggingPart; +pub use track::PerformerTaggingResources; pub use track::Track; pub use track::TrackLyricsTranslationStatus; +pub use track::TrackPerformerTagging; mod genre; pub use genre::Genre; diff --git a/src/models/snippet.rs b/src/models/snippet.rs index 9dc03a9..fa9944b 100644 --- a/src/models/snippet.rs +++ b/src/models/snippet.rs @@ -14,6 +14,7 @@ pub(crate) struct SnippetBody { /// /// Example: "There's not a thing that I would change" #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[allow(missing_docs)] pub struct Snippet { /// Unique Musixmatch Snippet ID pub snippet_id: u64, diff --git a/src/models/track.rs b/src/models/track.rs index 7c72a62..d62e2f7 100644 --- a/src/models/track.rs +++ b/src/models/track.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use super::Genres; +use super::{Artist, Fqid, Genres}; #[derive(Debug, Deserialize)] pub(crate) struct TrackBody { @@ -140,6 +140,8 @@ pub struct Track { /// Status of lyrics translation #[serde(default)] pub track_lyrics_translation_status: Vec, + /// Lyrics parts marked with the performer who is singing them + pub performer_tagging: Option, } /// Status of lyrics translation (language + progress) @@ -156,6 +158,73 @@ pub struct TrackLyricsTranslationStatus { pub perc: f32, } +/// Lyrics parts marked with the performer who is singing them +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[allow(missing_docs)] +pub struct TrackPerformerTagging { + /// Musixmatch user ID of the user who added the performer tags + /// + /// Format: `mxm:<16 byte hex>` + pub user_id: String, + /// True if the lyrics are completely tagged + pub completed: bool, + /// True if the lyrics + pub has_unknown: bool, + /// True if the lyrics contain parts that are intended to be sung by the + /// audience during concerts + pub has_fan_chant: bool, + /// List of tagged lyrics parts + #[serde(default)] + pub content: Vec, + /// Artists (and possibly other objects) that are referenced by the tagged parts + #[serde(default)] + pub resources: PerformerTaggingResources, +} + +/// Performer-tagged lyrics part +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[allow(missing_docs)] +pub struct PerformerTaggingPart { + /// Part of the lyrics text + /// + /// Includes whitespace (spaces and newline characters). + pub snippet: String, + /// Unbekannt + /// + /// 0-3 + pub position: u32, + /// List of performers singing this part + pub performers: Vec, +} + +/// Lyrics performer +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[allow(missing_docs)] +pub struct Performer { + /// artist / unknown + #[serde(rename = "type")] + pub typ: String, + /// Fully-qualified performer ID + pub fqid: Option, + /// Unbekannt + /// + /// 9 + pub category_id: Option, + /// Unbekannt + /// + /// 405 + pub credit_role_id: Option, +} + +/// Artists (and possibly other objects) that are referenced by the tagged parts +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(default)] +#[allow(missing_docs)] +pub struct PerformerTaggingResources { + /// List of artists tagged as performers + pub artists: Vec, +} + /// Available track charts #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ChartName { diff --git a/tests/tests.rs b/tests/tests.rs index ea56b0b..ffd77b7 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -204,6 +204,21 @@ mod artist { assert_eq!(artist.begin_date.unwrap(), date!(1977 - 12 - 31)); assert_eq!(artist.end_date_year, None); assert_eq!(artist.end_date, None); + let image = artist.artist_image.first().expect("artist image"); + assert_eq!(image.image_id, 20511); + let image_format = &image + .image_format_list + .iter() + .find(|img| img.image_format.height == 250 && img.image_format.width == 250) + .expect("image format 250px") + .image_format; + assert!( + image_format.image_url.starts_with( + "https://static.musixmatch.com/images-storage/mxmimages/1/1/5/0/2/20511_14.jpg?" + ), + "url: {}", + image_format.image_url + ); } #[rstest] @@ -290,8 +305,10 @@ mod artist { } mod track { + use std::collections::HashMap; + use super::*; - use musixmatch_inofficial::models::{ChartName, SortOrder}; + use musixmatch_inofficial::models::{ChartName, MxmEntityType, SortOrder}; #[rstest] #[case::no_translation(false, false)] @@ -311,6 +328,7 @@ mod track { "The Fame", translation_status, lang_3c, + false, ) .await .unwrap(); @@ -410,7 +428,7 @@ mod track { #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] #[tokio::test] async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { - let track = mxm.await.track(track_id, true, false).await.unwrap(); + let track = mxm.await.track(track_id, true, false, false).await.unwrap(); // dbg!(&track); @@ -445,6 +463,46 @@ mod track { assert!(first_tstatus.perc >= 0.0 && first_tstatus.perc <= 1.0); } + #[rstest] + #[tokio::test] + #[test_log::test] + async fn performer(#[future] mxm: Musixmatch) { + let track = mxm + .await + .track(TrackId::TrackId(246372347), false, false, true) + .await + .unwrap(); + let perf = track.performer_tagging.expect("performer tagging"); + assert!(perf.completed); + assert!(!perf.has_unknown); + assert!(!perf.has_fan_chant); + + let artists = perf + .resources + .artists + .into_iter() + .map(|a| (a.artist_id, a)) + .collect::>(); + + assert_eq!(artists.len(), 2); + let sam_smith = &artists[&33491428]; + let kim_petras = &artists[&26796706]; + assert_eq!(sam_smith.artist_name, "Sam Smith"); + assert_eq!(sam_smith.artist_image.len(), 1); + assert_eq!(kim_petras.artist_name, "Kim Petras"); + assert_eq!(kim_petras.artist_image.len(), 1); + + for part in perf.content { + assert!(!part.snippet.trim().is_empty(), "empty snippet"); + assert_gte(part.performers.len(), 1, "part performers"); + for performer in &part.performers { + let pid = performer.fqid.expect("performer id"); + assert_eq!(pid.typ, MxmEntityType::Artist); + assert!(artists.contains_key(&pid.id)) + } + } + } + #[rstest] #[case::no_translation(false, false)] #[case::translation_2c(true, false)] @@ -457,7 +515,12 @@ mod track { ) { let track = mxm .await - .track(TrackId::Commontrack(47672612), translation_status, lang_3c) + .track( + TrackId::Commontrack(47672612), + translation_status, + lang_3c, + false, + ) .await .unwrap(); @@ -553,7 +616,7 @@ mod track { async fn from_id_missing(#[future] mxm: Musixmatch) { let err = mxm .await - .track(TrackId::TrackId(999999999999), false, false) + .track(TrackId::TrackId(999999999999), false, false, false) .await .unwrap_err(); From 4d26c4a72f617228a5e62d4d565e2c7a6f3d7f95 Mon Sep 17 00:00:00 2001 From: ThetaBot Date: Wed, 11 Dec 2024 00:05:24 +0000 Subject: [PATCH 45/61] chore(deps): update rust crate governor to 0.8.0 (#5) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0f6609d..cc76316 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,5 +63,5 @@ dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" -governor = "0.7.0" +governor = "0.8.0" test-log = "0.2.16" From 2926455376ee2c24a25576528790f47713667f62 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 13 Jan 2025 05:04:21 +0100 Subject: [PATCH 46/61] test: fix tests --- src/models/track.rs | 2 +- tests/tests.rs | 64 ++++++++++++++++++++++----------------------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/models/track.rs b/src/models/track.rs index d62e2f7..194c8f8 100644 --- a/src/models/track.rs +++ b/src/models/track.rs @@ -203,7 +203,7 @@ pub struct PerformerTaggingPart { pub struct Performer { /// artist / unknown #[serde(rename = "type")] - pub typ: String, + pub typ: Option, /// Fully-qualified performer ID pub fqid: Option, /// Unbekannt diff --git a/tests/tests.rs b/tests/tests.rs index ffd77b7..c360e5c 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -421,45 +421,44 @@ mod track { } #[rstest] - #[case::trackid(TrackId::TrackId(205688271))] - #[case::commontrack(TrackId::Commontrack(118480583))] - #[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba".into()))] - #[case::isrc(TrackId::Isrc("KRA302000590".into()))] - #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] + #[case::trackid(TrackId::TrackId(167254015))] + #[case::commontrack(TrackId::Commontrack(93933821))] + #[case::vanity(TrackId::CommontrackVanity("Nightbirde-2/Girl-in-a-Bubble".into()))] + #[case::isrc(TrackId::Isrc("QZDA41918667".into()))] + #[case::spotify(TrackId::Spotify("2roGy5AYlaJpmL9CuXj6tT".into()))] #[tokio::test] async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { let track = mxm.await.track(track_id, true, false, false).await.unwrap(); // dbg!(&track); - assert_eq!(track.track_id, 205688271); - assert_eq!(track.track_isrc.unwrap(), "KRA302000590"); - assert_eq!(track.track_spotify_id.unwrap(), "1t2qYCAjUAoGfeFeoBlK51"); - assert_eq!(track.track_name, "Black Mamba"); - assert!(track.track_rating > 50); - assert_eq!(track.track_length, 175); + assert_eq!(track.track_id, 167254015); + assert_eq!(track.track_isrc.unwrap(), "QZDA41918667"); + assert_eq!(track.track_spotify_id.unwrap(), "2roGy5AYlaJpmL9CuXj6tT"); + assert_eq!(track.track_name, "Girl in a Bubble"); + assert!(track.track_rating > 40); + assert_eq!(track.track_length, 238); assert!(!track.explicit); assert!(track.has_lyrics); assert!(track.has_subtitles); - assert!(track.has_richsync); - assert!(track.num_favourite > 200); - assert!(track.lyrics_id.is_some()); - assert_eq!(track.subtitle_id.unwrap(), 36476905); - assert_eq!(track.album_id, 41035954); - assert_eq!(track.album_name, "Black Mamba"); - assert_eq!(track.artist_id, 46970441); - assert_eq!(track.artist_name, "aespa"); - assert_imgurl(&track.album_coverart_100x100, "/52156772.jpg"); - assert_imgurl(&track.album_coverart_350x350, "/52156772_350_350.jpg"); - assert_imgurl(&track.album_coverart_500x500, "/52156772_500_500.jpg"); - assert_eq!(track.commontrack_vanity_id, "aespa/Black-Mamba"); + assert!(track.num_favourite > 1); + assert_eq!(track.lyrics_id.unwrap(), 25830616); + assert_eq!(track.subtitle_id.unwrap(), 34307878); + assert_eq!(track.album_id, 31842378); + assert_eq!(track.album_name, "Girl in a Bubble"); + assert_eq!(track.artist_id, 38035381); + assert_eq!(track.artist_name, "Nightbirde"); + assert_imgurl(&track.album_coverart_100x100, "/43015335.jpg"); + assert_imgurl(&track.album_coverart_350x350, "/43015335_350_350.jpg"); + assert_imgurl(&track.album_coverart_500x500, "/43015335_500_500.jpg"); + assert_eq!(track.commontrack_vanity_id, "Nightbirde-2/Girl-in-a-Bubble"); let release_date = track.first_release_date.unwrap(); - assert_eq!(release_date.date(), date!(2020 - 11 - 17)); + assert_eq!(release_date.date(), date!(2019 - 03 - 22)); assert!(track.updated_time > datetime!(2022-8-27 0:00 UTC)); let first_tstatus = &track.track_lyrics_translation_status[0]; - assert_eq!(first_tstatus.from.as_deref(), Some("ko")); + assert_eq!(first_tstatus.from.as_deref(), Some("en")); assert!(first_tstatus.perc >= 0.0 && first_tstatus.perc <= 1.0); } @@ -469,7 +468,7 @@ mod track { async fn performer(#[future] mxm: Musixmatch) { let track = mxm .await - .track(TrackId::TrackId(246372347), false, false, true) + .track(TrackId::TrackId(206591653), false, false, true) .await .unwrap(); let perf = track.performer_tagging.expect("performer tagging"); @@ -485,12 +484,11 @@ mod track { .collect::>(); assert_eq!(artists.len(), 2); - let sam_smith = &artists[&33491428]; - let kim_petras = &artists[&26796706]; - assert_eq!(sam_smith.artist_name, "Sam Smith"); - assert_eq!(sam_smith.artist_image.len(), 1); - assert_eq!(kim_petras.artist_name, "Kim Petras"); - assert_eq!(kim_petras.artist_image.len(), 1); + let jhayco = &artists[&53077263]; + let bad_bunny = &artists[&33491954]; + assert_eq!(jhayco.artist_name, "Jhayco"); + assert_eq!(bad_bunny.artist_name, "Bad Bunny"); + assert_eq!(bad_bunny.artist_image.len(), 1); for part in perf.content { assert!(!part.snippet.trim().is_empty(), "empty snippet"); @@ -816,7 +814,7 @@ mod lyrics { // dbg!(&lyrics); - assert_eq!(lyrics.lyrics_id, 30126001); + assert_eq!(lyrics.lyrics_id, 36846057); assert_eq!(lyrics.lyrics_language.unwrap(), "ko"); assert_eq!(lyrics.lyrics_language_description.unwrap(), "Korean"); let copyright = lyrics.lyrics_copyright.unwrap(); From 6942d0eaaa6dfa15846c7f1a09ca4165a5a4b3c3 Mon Sep 17 00:00:00 2001 From: ThetaBot Date: Mon, 13 Jan 2025 04:09:14 +0000 Subject: [PATCH 47/61] chore(deps): update rust crate rstest to 0.24.0 (#6) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index cc76316..a04c11b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ rand = "0.8.0" base64 = "0.22.0" [dev-dependencies] -rstest = { version = "0.23.0", default-features = false } +rstest = { version = "0.24.0", default-features = false } dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" From 319dabeee018f8b5b633cf91e792b12fa18e7775 Mon Sep 17 00:00:00 2001 From: ThetaBot Date: Mon, 13 Jan 2025 04:14:03 +0000 Subject: [PATCH 48/61] fix(deps): update rust crate dirs to v6 (#7) --- cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8b77e98..759db40 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,5 +25,5 @@ tokio = { version = "1.20.4", features = ["macros", "rt-multi-thread"] } clap = { version = "4.0.0", features = ["derive"] } anyhow = "1.0.0" rpassword = "7.0.0" -dirs = "5.0.0" +dirs = "6.0.0" serde_json = "1.0.85" From 2c22f2aa3303b49b1bdeef6f454ab84c859b8470 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 13 Jan 2025 05:20:31 +0100 Subject: [PATCH 49/61] ci: increase rate limit --- tests/tests.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index c360e5c..45f1f2f 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -22,10 +22,11 @@ fn testfile>(name: P) -> PathBuf { async fn mxm() -> Musixmatch { static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new(); static MXM_LIMITER: LazyLock = LazyLock::new(|| { - RateLimiter::direct(Quota::per_second( - // limit 1 request per second for CI runs - NonZeroU32::new(if std::env::var("CI").is_ok() { 1 } else { 4 }).unwrap(), - )) + RateLimiter::direct(if std::env::var("CI").is_ok() { + Quota::with_period(std::time::Duration::from_millis(1500)).unwrap() + } else { + Quota::per_second(NonZeroU32::new(4).unwrap()) + }) }); MXM_LIMITER.until_ready().await; From 62e0308dda39559daaf7f53d94e7d5b59e9d1fd4 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 16 Jan 2025 01:47:30 +0100 Subject: [PATCH 50/61] chore(release): release musixmatch-inofficial v0.2.0 --- CHANGELOG.md | 13 +++++++++++++ Cargo.toml | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d91e37..8570d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. +## [v0.2.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.2..musixmatch-inofficial/v0.2.0) - 2025-01-16 + +### πŸš€ Features + +- Add track performer tagging, artist images - ([b136bb3](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/b136bb30040dc3ee849c26ff984884e706739235)) + +### βš™οΈ Miscellaneous Tasks + +- Fix clippy lints - ([26f4729](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/26f4729738536d735cb808fce8a8e466f2e82449)) +- *(deps)* Update rust crate governor to 0.8.0 (#5) - ([4d26c4a](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/4d26c4a72f617228a5e62d4d565e2c7a6f3d7f95)) +- *(deps)* Update rust crate rstest to 0.24.0 (#6) - ([6942d0e](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/6942d0eaaa6dfa15846c7f1a09ca4165a5a4b3c3)) + + ## [v0.1.2](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.1..musixmatch-inofficial/v0.1.2) - 2024-11-15 ### πŸ› Bug Fixes diff --git a/Cargo.toml b/Cargo.toml index a04c11b..beb0777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-inofficial" -version = "0.1.2" +version = "0.2.0" rust-version = "1.70.0" edition.workspace = true authors.workspace = true @@ -23,7 +23,7 @@ keywords = ["music", "lyrics"] categories = ["api-bindings", "multimedia"] [workspace.dependencies] -musixmatch-inofficial = { version = "0.1.1", path = ".", default-features = false } +musixmatch-inofficial = { version = "0.2.0", path = ".", default-features = false } [features] default = ["default-tls"] From 7b5a6e2e5050438c7d80dce6451e11839edc56b7 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 16 Jan 2025 01:48:11 +0100 Subject: [PATCH 51/61] chore(release): release musixmatch-cli v0.3.0 --- cli/CHANGELOG.md | 19 +++++++++++++++++++ cli/Cargo.toml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index c8a2e23..ac7b1a4 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. +## [v0.3.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.2.0..musixmatch-cli/v0.3.0) - 2025-01-16 + +### πŸš€ Features + +- Add track performer tagging, artist images - ([b136bb3](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/b136bb30040dc3ee849c26ff984884e706739235)) + +### πŸ› Bug Fixes + +- *(deps)* Update rust crate thiserror to v2 (#4) - ([6a6ced1](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/6a6ced16224c6ef3d05eb6ebd0aa0bdc40a34684)) +- *(deps)* Update rust crate dirs to v6 (#7) - ([319dabe](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/319dabeee018f8b5b633cf91e792b12fa18e7775)) + +### βš™οΈ Miscellaneous Tasks + +- *(deps)* Update rust crate rstest to 0.23.0 (#2) - ([5ef76f5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/5ef76f5a6b2a3b243f847cf86e72ebe176819d7a)) +- *(deps)* Update rust crate governor to 0.7.0 (#3) - ([4bfcb79](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/4bfcb791733ce5ebd9d4e074c64eb23e9a768fc6)) +- *(deps)* Update rust crate governor to 0.8.0 (#5) - ([4d26c4a](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/4d26c4a72f617228a5e62d4d565e2c7a6f3d7f95)) +- *(deps)* Update rust crate rstest to 0.24.0 (#6) - ([6942d0e](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/6942d0eaaa6dfa15846c7f1a09ca4165a5a4b3c3)) + + ## [v0.2.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.1.0..musixmatch-cli/v0.2.0) - 2024-08-18 ### πŸš€ Features diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 759db40..6228dda 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-cli" -version = "0.2.0" +version = "0.3.0" rust-version = "1.70.0" edition.workspace = true authors.workspace = true From bf68f94682773a778c5ce21cd177886ff6988126 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 3 Feb 2025 03:27:45 +0100 Subject: [PATCH 52/61] ci: renovate: preserveSemverRanges --- .gitea/workflows/renovate.yaml | 2 +- renovate.json | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml index e91c636..013b185 100644 --- a/.gitea/workflows/renovate.yaml +++ b/.gitea/workflows/renovate.yaml @@ -17,7 +17,7 @@ jobs: renovate: runs-on: docker container: - image: renovate/renovate:latest + image: renovate/renovate:39 steps: - name: Load renovate repo cache diff --git a/renovate.json b/renovate.json index 3ff20b1..1ec2687 100644 --- a/renovate.json +++ b/renovate.json @@ -1,8 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:best-practices" - ], + "extends": ["config:best-practices", ":preserveSemverRanges"], "semanticCommits": "enabled", "automerge": true, "automergeStrategy": "squash", From 0bb886adab2593c81667105aca3c22b3c191b774 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 22 Feb 2025 23:01:18 +0000 Subject: [PATCH 53/61] ci: disable renovate --- .gitea/workflows/{renovate.yaml => renovate.yaml.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .gitea/workflows/{renovate.yaml => renovate.yaml.bak} (100%) diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml.bak similarity index 100% rename from .gitea/workflows/renovate.yaml rename to .gitea/workflows/renovate.yaml.bak From c90bfc647c6b62d30530fa97ec830c1e16d7a75c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 3 Feb 2025 03:47:15 +0100 Subject: [PATCH 54/61] test: update translation track id --- tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.rs b/tests/tests.rs index 45f1f2f..02ebc61 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -336,7 +336,7 @@ mod track { // dbg!(&track); - assert_eq!(track.track_id, 85213841); + assert_eq!(track.track_id, 9060927); // assert_eq!( // track.track_mbid.unwrap(), // "080975b0-39b1-493c-ae64-5cb3292409bb" From 87859e629f3c236ba450872b29beb7876be7ef0b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 3 Apr 2025 13:41:30 +0200 Subject: [PATCH 55/61] chore(deps): update rust crate governor to 0.10.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index beb0777..3fd6b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,5 +63,5 @@ dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" -governor = "0.8.0" +governor = "0.10.0" test-log = "0.2.16" From a3f2ffc5d99ddddf777b4de306bd215bd3bbf5ce Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 3 Apr 2025 13:42:36 +0200 Subject: [PATCH 56/61] chore(deps): update rust crate rstest to 0.25.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3fd6b92..bc489af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ rand = "0.8.0" base64 = "0.22.0" [dev-dependencies] -rstest = { version = "0.24.0", default-features = false } +rstest = { version = "0.25.0", default-features = false } dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" From 7c325c4af779e32059680c1cfb874f83896d7649 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 4 Apr 2025 13:25:02 +0200 Subject: [PATCH 57/61] chore(deps): update rust crate rand to 0.9.0 --- Cargo.toml | 2 +- src/lib.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bc489af..e3b3518 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ time = { version = "0.3.10", features = [ ] } hmac = "0.12.0" sha1 = "0.10.0" -rand = "0.8.0" +rand = "0.9.0" base64 = "0.22.0" [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 0d14784..8a6f508 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -449,20 +449,20 @@ impl Musixmatch { } fn random_guid() -> String { - let mut rng = rand::thread_rng(); - let n = rng.gen::(); + let mut rng = rand::rng(); + let n = rng.random::(); format!("{:016x}", n) } fn random_uuid() -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); format!( "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}", - rng.gen::(), - rng.gen::(), - rng.gen::(), - rng.gen::(), - rng.gen::() & 0xffffffffffff, + rng.random::(), + rng.random::(), + rng.random::(), + rng.random::(), + rng.random::() & 0xffffffffffff, ) } From 6f90033cf4284eff5c12a30aafb21943c1575b92 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 4 Apr 2025 13:55:44 +0200 Subject: [PATCH 58/61] fix: parsing unset has_fan_chant field --- src/models/track.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/models/track.rs b/src/models/track.rs index 194c8f8..12831ed 100644 --- a/src/models/track.rs +++ b/src/models/track.rs @@ -167,11 +167,14 @@ pub struct TrackPerformerTagging { /// Format: `mxm:<16 byte hex>` pub user_id: String, /// True if the lyrics are completely tagged + #[serde(default)] pub completed: bool, - /// True if the lyrics + /// True if the lyrics have unknown performers + #[serde(default)] pub has_unknown: bool, /// True if the lyrics contain parts that are intended to be sung by the /// audience during concerts + #[serde(default)] pub has_fan_chant: bool, /// List of tagged lyrics parts #[serde(default)] From 4a46e7bb1d83c6261660d403c009cdb640b301d7 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 4 Apr 2025 13:58:45 +0200 Subject: [PATCH 59/61] docs: fix docs --- src/models/track.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/models/track.rs b/src/models/track.rs index 12831ed..bfb1980 100644 --- a/src/models/track.rs +++ b/src/models/track.rs @@ -160,7 +160,6 @@ pub struct TrackLyricsTranslationStatus { /// Lyrics parts marked with the performer who is singing them #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[allow(missing_docs)] pub struct TrackPerformerTagging { /// Musixmatch user ID of the user who added the performer tags /// @@ -186,15 +185,14 @@ pub struct TrackPerformerTagging { /// Performer-tagged lyrics part #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[allow(missing_docs)] pub struct PerformerTaggingPart { /// Part of the lyrics text /// /// Includes whitespace (spaces and newline characters). pub snippet: String, - /// Unbekannt + /// Unknown /// - /// 0-3 + /// Values: 0-3 pub position: u32, /// List of performers singing this part pub performers: Vec, @@ -202,7 +200,6 @@ pub struct PerformerTaggingPart { /// Lyrics performer #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[allow(missing_docs)] pub struct Performer { /// artist / unknown #[serde(rename = "type")] @@ -222,7 +219,6 @@ pub struct Performer { /// Artists (and possibly other objects) that are referenced by the tagged parts #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default)] -#[allow(missing_docs)] pub struct PerformerTaggingResources { /// List of artists tagged as performers pub artists: Vec, From 04a0544ad5b56c1e5c1fcec8214f7f8a4cd9fe81 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 4 Apr 2025 14:07:23 +0200 Subject: [PATCH 60/61] test: fix tests --- tests/tests.rs | 81 +++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 02ebc61..7a45f64 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -324,9 +324,9 @@ mod track { let track = mxm .await .matcher_track( - "Poker Face", - "Lady Gaga", - "The Fame", + "Unter der OberflΓ€che", + "Silbermond", + "Himmel auf", translation_status, lang_3c, false, @@ -336,55 +336,58 @@ mod track { // dbg!(&track); - assert_eq!(track.track_id, 9060927); - // assert_eq!( - // track.track_mbid.unwrap(), - // "080975b0-39b1-493c-ae64-5cb3292409bb" - // ); - // assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); + assert_eq!(track.track_id, 17633247); + assert_eq!( + track.track_mbid.unwrap(), + "1b97675a-537e-4599-82de-b4ef801e4a69" + ); + assert_eq!(track.track_isrc.unwrap(), "DEE861200084"); assert!( track.commontrack_isrcs[0] .iter() - .any(|isrc| isrc == "USUM70824409"), + .any(|isrc| isrc == "DEE861200084"), "commontrack_isrcs: {:?}", &track.commontrack_isrcs[0], ); - assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg"); + assert_eq!(track.track_spotify_id.unwrap(), "4kebfr96W2JTvb7xjf4lDV"); assert!( track .commontrack_spotify_ids .iter() - .any(|spid| spid == "1QV6tiMFM6fSOKOGLMHYYg"), + .any(|spid| spid == "4kebfr96W2JTvb7xjf4lDV"), "commontrack_spotify_ids: {:?}", track.commontrack_spotify_ids, ); - assert_eq!(track.track_name, "Poker Face"); - assert!(track.track_rating > 50); - assert_eq!(track.commontrack_id, 47672612); + assert_eq!(track.track_name, "Unter der OberflΓ€che"); + assert!(track.track_rating > 30); + assert_eq!(track.commontrack_id, 10514077); assert!(!track.instrumental); - assert!(track.explicit); + assert!(!track.explicit); assert!(track.has_subtitles); assert!(track.has_richsync); - assert!(track.has_track_structure); - assert!(track.num_favourite > 50); - assert!(track.lyrics_id.is_some()); - assert_eq!(track.subtitle_id.unwrap(), 36450705); - assert_eq!(track.album_id, 20960801); - assert_eq!(track.album_name, "The Fame"); - assert_eq!(track.artist_id, 378462); + assert!(!track.has_track_structure); + assert!(track.num_favourite > 10); + assert_eq!(track.lyrics_id.unwrap(), 7401716); + assert_eq!(track.subtitle_id.unwrap(), 35487435); + assert_eq!(track.album_id, 14122870); + assert_eq!(track.album_name, "Himmel auf"); + assert_eq!(track.artist_id, 84849); assert_eq!( track.artist_mbid.unwrap(), - "650e7db6-b795-4eb5-a702-5ea2fc46c848" + "34d42823-6b56-4861-a675-1565bf40d557" + ); + assert_eq!(track.artist_name, "Silbermond"); + assert_imgurl(&track.album_coverart_100x100, "/30913494.jpg"); + assert_imgurl(&track.album_coverart_350x350, "/30913494_350_350.jpg"); + assert_imgurl(&track.album_coverart_500x500, "/30913494_500_500.jpg"); + assert_imgurl(&track.album_coverart_800x800, "/30913494_800_800.jpg"); + assert_eq!( + track.commontrack_vanity_id, + "Silbermond/Unter-der-OberflΓ€che" ); - assert_eq!(track.artist_name, "Lady Gaga"); - assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg"); - assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg"); - assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg"); - assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg"); - assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1"); let first_release = track.first_release_date.unwrap(); - assert_eq!(first_release.date(), date!(2008 - 1 - 1)); - assert!(track.updated_time > datetime!(2023-1-17 0:00 UTC)); + assert_eq!(first_release.date(), date!(2012 - 3 - 23)); + assert!(track.updated_time > datetime!(2013-4-26 13:11:50 UTC)); let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre; assert_eq!(first_pri_genre.music_genre_id, 14); @@ -393,23 +396,13 @@ mod track { assert_eq!(first_pri_genre.music_genre_name_extended, "Pop"); assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "Pop"); - let first_sec_genre = &track.secondary_genres.music_genre_list[0].music_genre; - assert_eq!(first_sec_genre.music_genre_id, 17); - assert_eq!(first_sec_genre.music_genre_parent_id, 34); - assert_eq!(first_sec_genre.music_genre_name, "Dance"); - assert_eq!(first_sec_genre.music_genre_name_extended, "Dance"); - assert_eq!( - first_sec_genre.music_genre_vanity.as_ref().unwrap(), - "Dance" - ); - if translation_status { assert!( track.track_lyrics_translation_status.iter().all(|tl| { (if lang_3c { - tl.from.as_deref() == Some("eng") + tl.from.as_deref() == Some("deu") } else { - tl.from.as_deref() == Some("en") + tl.from.as_deref() == Some("de") }) && tl.perc >= 0.0 && tl.perc <= 1.0 }), From f73dcdd13488eea9ff9d043ba46e8c647e0881a3 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 4 Apr 2025 14:12:48 +0200 Subject: [PATCH 61/61] chore(release): release musixmatch-inofficial v0.2.1 --- CHANGELOG.md | 17 +++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8570d22..4f099be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. +## [v0.2.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.2.0..musixmatch-inofficial/v0.2.1) - 2025-04-04 + +### πŸ› Bug Fixes + +- Parsing unset has_fan_chant field - ([6f90033](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/6f90033cf4284eff5c12a30aafb21943c1575b92)) + +### πŸ“š Documentation + +- Fix docs - ([4a46e7b](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/4a46e7bb1d83c6261660d403c009cdb640b301d7)) + +### βš™οΈ Miscellaneous Tasks + +- *(deps)* Update rust crate governor to 0.10.0 - ([87859e6](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/87859e629f3c236ba450872b29beb7876be7ef0b)) +- *(deps)* Update rust crate rstest to 0.25.0 - ([a3f2ffc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/a3f2ffc5d99ddddf777b4de306bd215bd3bbf5ce)) +- *(deps)* Update rust crate rand to 0.9.0 - ([7c325c4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/7c325c4af779e32059680c1cfb874f83896d7649)) + + ## [v0.2.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.2..musixmatch-inofficial/v0.2.0) - 2025-01-16 ### πŸš€ Features diff --git a/Cargo.toml b/Cargo.toml index e3b3518..c1071e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-inofficial" -version = "0.2.0" +version = "0.2.1" rust-version = "1.70.0" edition.workspace = true authors.workspace = true