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..5661a72 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,4 +1,4 @@ -pipeline: +steps: test: image: rust:latest environment: @@ -10,4 +10,6 @@ pipeline: - rustup component add rustfmt clippy - cargo fmt --all --check - cargo clippy --all -- -D warnings - - cargo test --workspace + - MUSIXMATCH_CLIENT=Desktop cargo test --workspace + - sleep 60 # because of Musixmatch rate limit + - 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