diff --git a/.env.example b/.env.example index c79ac20..7ab1bf9 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,2 @@ -MUSIXMATCH_CLIENT=Android MUSIXMATCH_EMAIL=mail@example.com MUSIXMATCH_PASSWORD=super-secret diff --git a/.woodpecker.yml b/.woodpecker.yml index c93721f..acfacc9 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -10,5 +10,4 @@ pipeline: - rustup component add rustfmt clippy - cargo fmt --all --check - cargo clippy --all -- -D warnings - - MUSIXMATCH_CLIENT=Desktop cargo test --workspace - - MUSIXMATCH_CLIENT=Android cargo test --workspace + - cargo test --workspace diff --git a/Cargo.toml b/Cargo.toml index 03dd2da..fdcb98c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ 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" @@ -47,13 +46,12 @@ base64 = "0.21.0" [dev-dependencies] ctor = "0.2.0" -rstest = { version = "0.18.0", default-features = false } -env_logger = "0.11.0" +rstest = { version = "0.17.0", default-features = false } +env_logger = "0.10.0" 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 a4a99ba..9e83d62 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,35 @@ # Musixmatch-Inofficial -This is an inofficial client for the Musixmatch API that uses the key embedded in the -Musixmatch Android app or desktop client. +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](), +([LRC](https://en.wikipedia.org/wiki/LRC_(file_format)), [DFXP](https://www.w3.org/TR/ttml1/), JSON) for almost any song. -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. +A free Musixmatch account is required for operation +([you can sign up here](https://www.musixmatch.com/de/sign-up)). ## ⚠️ 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 -You can choose which client to test by setting the `MUSIXMATCH_CLIENT` environment -variable to either `Desktop` or `Android` (it defaults to Desktop). +Running the tests requires Musixmatch credentials. The credentials are read +from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment variables. -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. +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 04d6c43..c95fead 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 8b8d37b..f2dde1f 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 b3a087c..d9a5038 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 01fc50d..c8b3591 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 43fcb8f..904f028 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 4b3cd9d..045779f 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 1258bd7..cb62a3c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - pub(crate) type Result = core::result::Result; /// Custom error type for the Musixmatch client @@ -38,9 +36,6 @@ 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 { @@ -49,9 +44,3 @@ 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 999bee3..8029bcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,30 +35,11 @@ use crate::error::Result; const YMD_FORMAT: &[time::format_description::FormatItem] = format_description!("[year][month][day]"); -/// 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 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('"; +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"; @@ -76,7 +57,6 @@ 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, @@ -84,29 +64,6 @@ 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), @@ -129,8 +86,6 @@ struct MusixmatchRef { http: Client, storage: Option>, credentials: RwLock>, - client_type: ClientType, - client_cfg: ClientCfg, brand: String, device: String, usertoken: Mutex>, @@ -144,7 +99,6 @@ struct Credentials { #[derive(Debug, Serialize, Deserialize)] struct StoredSession { - client_type: ClientType, usertoken: String, } @@ -213,12 +167,6 @@ 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()); @@ -239,18 +187,13 @@ 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).filter(|s| s.client_type == self.client_type); - let client_cfg = ClientCfg::from(self.client_type); + let stored_session = Musixmatch::retrieve_session(&storage); 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(|| client_cfg.user_agent.to_owned()), - ) + .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) .gzip(true) .default_headers(headers) .build()?; @@ -260,8 +203,6 @@ 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)), @@ -295,66 +236,49 @@ impl Musixmatch { } } - let credentials = if self.inner.client_cfg.login { + let credentials = { let c = self.inner.credentials.read().unwrap(); match c.deref() { - Some(c) => Some(c.clone()), + Some(c) => 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 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 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 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]); - if let Some(credentials) = credentials { - let account = self.post_credentials(&usertoken, &credentials).await?; - info!("Logged in as {} ", account.name); - } + let account = self.post_credentials(&usertoken, &credentials).await?; + info!("Logged in as {} ", account.name); *stored_usertoken = Some(usertoken.to_owned()); self.store_session(&usertoken); @@ -366,8 +290,8 @@ impl Musixmatch { usertoken: &str, credentials: &Credentials, ) -> Result { - let mut url = self.new_url_from_token("credential.post", usertoken)?; - self.sign_url_with_date(&mut url, OffsetDateTime::now_utc()); + let mut url = new_url_from_token("credential.post", usertoken); + sign_url_with_date(&mut url, OffsetDateTime::now_utc()); let api_credentials = api_model::Credentials { credential_list: &[api_model::CredentialWrap { @@ -410,7 +334,6 @@ 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(), }; @@ -445,12 +368,12 @@ impl Musixmatch { ) } - fn new_url(&self, endpoint: &str) -> Result { + fn new_url(&self, endpoint: &str) -> reqwest::Url { Url::parse_with_params( - &format!("{}{}", self.inner.client_cfg.api_url, endpoint), - &[("app_id", self.inner.client_cfg.app_id), ("format", "json")], + &format!("{}{}", API_URL, endpoint), + &[("app_id", APP_ID), ("format", "json")], ) - .map_err(Error::from) + .unwrap() } async fn finish_url(&self, url: &mut Url, force_new_session: bool) -> Result<()> { @@ -459,7 +382,7 @@ impl Musixmatch { .append_pair("usertoken", &usertoken) .finish(); - self.sign_url_with_date(url, OffsetDateTime::now_utc()); + sign_url_with_date(url, OffsetDateTime::now_utc()); Ok(()) } @@ -518,33 +441,6 @@ 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 { @@ -565,6 +461,33 @@ 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; @@ -573,12 +496,8 @@ 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(); - mxm.sign_url_with_date(&mut url, datetime!(2022-09-28 0:00 UTC)); + 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/src/models/album.rs b/src/models/album.rs index afc0d59..24abbf7 100644 --- a/src/models/album.rs +++ b/src/models/album.rs @@ -88,7 +88,6 @@ pub struct Album { pub enum AlbumType { #[default] Album, - Ep, Single, Compilation, Remix, diff --git a/src/models/subtitle.rs b/src/models/subtitle.rs index 4843127..d2b0e9e 100644 --- a/src/models/subtitle.rs +++ b/src/models/subtitle.rs @@ -1,5 +1,4 @@ use serde::{Deserialize, Serialize}; -use std::fmt::Write; use time::OffsetDateTime; use crate::{error::Result, Error}; @@ -261,11 +260,12 @@ impl SubtitleLines { /// Convert subtitles into the [LRC](SubtitleFormat::Lrc) format pub fn to_lrc(&self) -> String { - let mut t = self.lines.iter().fold(String::new(), |mut acc, line| { - _ = writeln!(acc, "[{}] {}", line.time.format_lrc(), line.text); - acc - }); - t.pop(); // trailing newline + let mut t = self + .lines + .iter() + .map(|line| format!("[{}] {}\n", line.time.format_lrc(), line.text)) + .collect::(); + t.pop(); t } diff --git a/tests/tests.rs b/tests/tests.rs index 2717f41..e351bbe 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}, - ClientType, Error, Musixmatch, + Error, Musixmatch, }; #[ctor::ctor] @@ -22,20 +22,13 @@ fn init() { } fn new_mxm() -> Musixmatch { - 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() + Musixmatch::builder() + .credentials( + std::env::var("MUSIXMATCH_EMAIL").unwrap(), + std::env::var("MUSIXMATCH_PASSWORD").unwrap(), + ) + .build() + .unwrap() } fn testfile>(name: P) -> PathBuf { @@ -108,11 +101,10 @@ mod album { } #[tokio::test] - async fn album_ep() { + async fn album_tst() { 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_date, Some(date!(2016 - 09 - 30))); + println!("type: {:?}", album.album_release_type); + println!("date: {:?}", album.album_release_date); } #[tokio::test] @@ -415,7 +407,7 @@ mod track { 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.album_name, "Black Mamba - Single"); assert_eq!(track.artist_id, 46970441); assert_eq!(track.artist_name, "aespa"); assert_eq!( @@ -673,7 +665,7 @@ mod track { async fn genres() { let genres = new_mxm().genres().await.unwrap(); assert!(genres.len() > 360); - // dbg!(&genres); + dbg!(&genres); } #[tokio::test] @@ -1000,11 +992,7 @@ mod translation { #[tokio::test] async fn no_credentials() { - let mxm = Musixmatch::builder() - .client_type(ClientType::Android) - .no_storage() - .build() - .unwrap(); + let mxm = Musixmatch::builder().no_storage().build().unwrap(); let err = mxm .track_lyrics(TrackId::TrackId(205688271)) .await