diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 5ebadac..bc6e278 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,8 +1,5 @@ name: CI -on: - push: - pull_request: - workflow_dispatch: +on: [push, pull_request] jobs: Test: @@ -27,25 +24,6 @@ jobs: run: cargo clippy --all -- -D warnings - name: ๐Ÿงช Test - run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 -j 1 --workspace + run: cargo test --workspace env: ALL_PROXY: "http://warpproxy:8124" - - - name: Move test report - if: always() - run: mv target/nextest/ci/junit.xml junit.xml || true - - - name: ๐Ÿ’Œ Upload test report - if: always() - uses: https://code.forgejo.org/forgejo/upload-artifact@v4 - with: - name: test - path: | - junit.xml - - - name: ๐Ÿ”— Artifactview PR comment - if: ${{ always() && github.event_name == 'pull_request' }} - run: | - if [[ "$GITEA_ACTIONS" == "true" ]]; then RUN_NUMBER="$GITHUB_RUN_NUMBER"; else RUN_NUMBER="$GITHUB_RUN_ID"; fi - curl -SsL --fail-with-body -w "\n" -X POST https://av.thetadev.de/.well-known/api/prComment -H "Content-Type: application/json" \ - --data '{"url": "'"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_NUMBER"'", "pr": ${{ github.event.number }}, "artifact_titles": {"test":"๐Ÿงช Test report"}, "artifact_paths": {"test":"/junit.xml?viewer=1"}}' diff --git a/.gitea/workflows/renovate.yaml.bak b/.gitea/workflows/renovate.yaml similarity index 97% rename from .gitea/workflows/renovate.yaml.bak rename to .gitea/workflows/renovate.yaml index 013b185..e91c636 100644 --- a/.gitea/workflows/renovate.yaml.bak +++ b/.gitea/workflows/renovate.yaml @@ -17,7 +17,7 @@ jobs: renovate: runs-on: docker container: - image: renovate/renovate:39 + image: renovate/renovate:latest steps: - name: Load renovate repo cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 0801b22..37c73e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,62 +3,6 @@ All notable changes to this project will be documented in this file. -## [v0.3.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.2.1..musixmatch-inofficial/v0.3.0) - 2025-12-08 - -### ๐Ÿš€ Features - -- [**breaking**] Removed artist_related endpoint (discontinued) - ([74d5359](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/74d5359547e0b3ffa75f5f7384ed3b7ab0e2ea11)) - -### ๐Ÿ› Bug Fixes - -- Update signature secret - ([59dee61](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/59dee61a2fdbda6a7643aa209b9baaf25a6de0f8)) -- Clippy lints - ([bfe6fec](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/bfe6fec115b9ba3b58f1e949ac7b583a95041f8f)) -- Clippy lints (2) - ([09c6004](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/09c6004ed13c2f9039490c083b0e6b61dccaa884)) -- Remove serde::__private::fmt - ([e087752](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/e087752dd0d331093a3434e18b5ff276bf4dc7c1)) - - -## [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 - -- 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 - -- *(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 0d3ead1..0a3b896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-inofficial" -version = "0.3.0" +version = "0.1.1" 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.3.0", path = ".", default-features = false } +musixmatch-inofficial = { version = "0.1.1", path = ".", default-features = false } [features] default = ["default-tls"] @@ -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 = "2.0.0" +thiserror = "1.0.0" log = "0.4.17" time = { version = "0.3.10", features = [ "macros", @@ -54,14 +54,13 @@ time = { version = "0.3.10", features = [ ] } hmac = "0.12.0" sha1 = "0.10.0" -rand = "0.9.0" +rand = "0.8.0" base64 = "0.22.0" [dev-dependencies] -rstest = { version = "0.26.0", default-features = false } +rstest = { version = "0.22.0", default-features = false } dotenvy = "0.15.5" tokio = { version = "1.20.4", features = ["macros"] } futures = "0.3.21" path_macro = "1.0.0" -governor = "0.10.0" -test-log = "0.2.16" +governor = "0.6.3" diff --git a/Justfile b/Justfile index f288b43..7456066 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,5 @@ test: - cargo nextest run --workspace --no-fail-fast --retries 1 -j 1 + cargo test release crate="musixmatch-inofficial": #!/usr/bin/env bash diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 9c882bc..c8a2e23 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,40 +3,6 @@ All notable changes to this project will be documented in this file. -## [v0.3.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.3.0..musixmatch-cli/v0.3.1) - 2025-12-08 - -### ๐Ÿ› Bug Fixes - -- Clippy lints - ([bfe6fec](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/bfe6fec115b9ba3b58f1e949ac7b583a95041f8f)) -- Clippy lints (3) - ([39bbd50](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/39bbd5070a62d865c0e276904047bb687f1ba8a8)) - -### โš™๏ธ 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)) -- *(deps)* Update musixmatch to 0.3.0 - - -## [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 6543bc5..8b77e98 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-cli" -version = "0.3.1" +version = "0.2.0" rust-version = "1.70.0" edition.workspace = true authors.workspace = true @@ -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 = "6.0.0" +dirs = "5.0.0" serde_json = "1.0.85" diff --git a/cli/src/main.rs b/cli/src/main.rs index 84e26b9..6a303da 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -55,11 +55,6 @@ enum Commands { #[clap(flatten)] ident: TrackIdentifiers, }, - /// Get performer tagging - Performer { - #[clap(flatten)] - ident: TrackIdentifiers, - }, /// Get album metadata Album { #[clap(flatten)] @@ -175,7 +170,7 @@ async fn main() { let cli = Cli::parse(); run(cli).await.unwrap_or_else(|e| { - eprintln!("Error: {e}"); + println!("Error: {}", e); std::process::exit(1); }); } @@ -251,7 +246,7 @@ async fn run(cli: Cli) -> Result<()> { } eprintln!(); - println!("{lyrics_body}"); + println!("{}", lyrics_body); } Commands::Subtitles { ident, @@ -312,23 +307,16 @@ async fn run(cli: Cli) -> Result<()> { bail!("subtitle format {format:?} cant be translated") } }; - println!("{res}"); + println!("{}", res); } else { eprintln!(); println!("{}", subtitles.subtitle_body); } } Commands::Track { ident } => { - let track = get_track(ident, &mxm, false).await?; + let track = get_track(ident, &mxm).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) @@ -409,7 +397,6 @@ async fn get_track_or_id( ident: TrackIdentifiers, mxm: &Musixmatch, translation_status: bool, - performer_tagging: bool, ) -> Result> { Ok( match ( @@ -435,15 +422,8 @@ 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, - performer_tagging, - ) - .await?, + mxm.matcher_track(&name, &artist, "", translation_status, true) + .await?, )), _ => bail!("no track identifier given"), }, @@ -451,27 +431,21 @@ 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, false).await? { + Ok(match get_track_or_id(ident, mxm, false).await? { TrackOrId::Track(track) => TrackId::TrackId(track.track_id), TrackOrId::TrackId(id) => id, }) } -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?, - }, - ) +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?, + }) } fn input(prompt: &str) -> String { - print!("{prompt}"); + print!("{}", prompt); stdout().flush().expect("Failed to flush stdout!"); @@ -483,7 +457,7 @@ fn input(prompt: &str) -> String { } fn input_pwd(prompt: &str) -> String { - print!("{prompt}"); + print!("{}", prompt); stdout().flush().expect("Failed to flush stdout!"); rpassword::read_password().expect("Failed to read password") diff --git a/cliff.toml b/cliff.toml index b456d2d..a12fc01 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", skip = true }, + { message = "^test", group = "๐Ÿงช Testing" }, { message = "^chore\\(release\\)", skip = true }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, diff --git a/renovate.json b/renovate.json index 1ec2687..3ff20b1 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,8 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:best-practices", ":preserveSemverRanges"], + "extends": [ + "config:best-practices" + ], "semanticCommits": "enabled", "automerge": true, "automergeStrategy": "squash", diff --git a/src/api_model.rs b/src/api_model.rs index d69aaa8..463e5d1 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -117,7 +117,7 @@ where { struct BoolFromIntVisitor; - impl Visitor<'_> for BoolFromIntVisitor { + impl<'de> Visitor<'de> for BoolFromIntVisitor { type Value = bool; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -232,7 +232,7 @@ where n: PhantomData, } - impl Visitor<'_> for NullIfZeroVisitor + impl<'de, N> Visitor<'de> for NullIfZeroVisitor where N: TryFrom, { @@ -300,7 +300,7 @@ where { struct NullIfEmptyVisitor; - impl Visitor<'_> for NullIfEmptyVisitor { + impl<'de> Visitor<'de> for NullIfEmptyVisitor { type Value = Option; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -347,7 +347,7 @@ where n: PhantomData, } - impl Visitor<'_> for ParseIntVisitor + impl<'de, N> Visitor<'de> for ParseIntVisitor where N: FromStr + TryFrom, { @@ -441,10 +441,13 @@ pub mod optional_date { ) -> Result, D::Error> { struct OptionalDateVisitor; - impl Visitor<'_> for OptionalDateVisitor { + impl<'de> Visitor<'de> for OptionalDateVisitor { type Value = Option; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + fn expecting( + &self, + formatter: &mut serde::__private::fmt::Formatter, + ) -> serde::__private::fmt::Result { formatter.write_str("date or empty string") } @@ -498,10 +501,13 @@ pub mod optional_datetime { ) -> Result, D::Error> { struct OptionalDateVisitor; - impl Visitor<'_> for OptionalDateVisitor { + impl<'de> Visitor<'de> for OptionalDateVisitor { type Value = Option; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + fn expecting( + &self, + formatter: &mut serde::__private::fmt::Formatter, + ) -> serde::__private::fmt::Result { formatter.write_str("timestamp or empty string") } @@ -537,55 +543,6 @@ 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; @@ -729,21 +686,4 @@ 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 024c6bc..f2dde1f 100644 --- a/src/apis/artist_api.rs +++ b/src/apis/artist_api.rs @@ -18,7 +18,6 @@ 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(); } @@ -26,6 +25,40 @@ impl Musixmatch { Ok(artist_body.artist) } + /// Get a list of artists somehow related to the one specified by its ID. + /// + /// # Parameters + /// - `id`: [Artist ID](crate::models::ArtistId) + /// - `page_size`: Define the page size for paginated results. Range is 1 to 100. + /// - `page`: Define the page number for paginated results, starting from 1. + /// + /// # Reference + /// + pub async fn artist_related( + &self, + id: ArtistId<'_>, + page_size: u8, + page: u32, + ) -> Result> { + let mut url = self.new_url("artist.related.get"); + { + let mut url_query = url.query_pairs_mut(); + + let id_param = id.to_param(); + url_query.append_pair(id_param.0, &id_param.1); + url_query.append_pair("page_size", &page_size.to_string()); + url_query.append_pair("page", &page.to_string()); + url_query.finish(); + } + + let artist_list_body = self.execute_get_request::(&url).await?; + Ok(artist_list_body + .artist_list + .into_iter() + .map(|a| a.artist) + .collect()) + } + /// Search for artists in the Musixmatch database. /// /// # Parameters diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs index 1c25975..045779f 100644 --- a/src/apis/track_api.rs +++ b/src/apis/track_api.rs @@ -26,7 +26,6 @@ impl Musixmatch { q_album: &str, translation_status: bool, lang_3c: bool, - performer_tagging: bool, ) -> Result { let mut url = self.new_url("matcher.track.get"); { @@ -41,10 +40,8 @@ impl Musixmatch { if !q_album.is_empty() { url_query.append_pair("q_album", q_album); } - - let mut part = Vec::new(); if translation_status { - part.push("track_lyrics_translation_status"); + url_query.append_pair("part", "track_lyrics_translation_status"); url_query.append_pair( "language_iso_code", match lang_3c { @@ -53,13 +50,6 @@ impl Musixmatch { }, ); } - if performer_tagging { - part.push("track_performer_tagging"); - } - if !part.is_empty() { - url_query.append_pair("part", &part.join(",")); - } - url_query.finish(); } @@ -83,7 +73,6 @@ impl Musixmatch { id: TrackId<'_>, translation_status: bool, lang_3c: bool, - performer_tagging: bool, ) -> Result { let mut url = self.new_url("track.get"); { @@ -91,10 +80,8 @@ 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 { - part.push("track_lyrics_translation_status"); + url_query.append_pair("part", "track_lyrics_translation_status"); url_query.append_pair( "language_iso_code", match lang_3c { @@ -103,13 +90,6 @@ impl Musixmatch { }, ); } - if performer_tagging { - part.push("track_performer_tagging"); - } - if !part.is_empty() { - url_query.append_pair("part", &part.join(",")); - } - url_query.finish(); } @@ -226,7 +206,7 @@ impl Musixmatch { /// /// # Reference /// - pub fn track_search(&self) -> TrackSearchQuery<'_> { + pub fn track_search(&self) -> TrackSearchQuery { TrackSearchQuery { mxm: self.clone(), q_track: None, diff --git a/src/error.rs b/src/error.rs index f58312b..17e03e0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,8 +52,3 @@ 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 448d8d4..b494a13 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, IdError}; +pub use error::Error; use base64::Engine; use hmac::{Hmac, Mac}; @@ -36,7 +36,7 @@ const YMD_FORMAT: &[time::format_description::FormatItem] = const APP_ID: &str = "android-player-v1.0"; const API_URL: &str = "https://apic.musixmatch.com/ws/1.1/"; -const SIGNATURE_SECRET: &[u8; 29] = b"mNdca@6W7TeEcFn6*3.s97sJ*yPMd"; +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"; @@ -247,7 +247,7 @@ impl Musixmatch { // 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"), + &format!("{}{}", API_URL, "token.get"), &[ ("adv_id", adv_id.as_str()), ("root", "0"), @@ -343,7 +343,7 @@ impl Musixmatch { match serde_json::to_string(&to_store) { Ok(json) => storage.write(&json), - Err(e) => error!("Could not serialize session. Error: {e}"), + Err(e) => error!("Could not serialize session. Error: {}", e), } } } @@ -355,7 +355,7 @@ impl Musixmatch { .and_then(|json| match serde_json::from_str::(&json) { Ok(session) => Some(session), Err(e) => { - error!("Could not deserialize session. Error: {e}"); + error!("Could not deserialize session. Error: {}", e); None } }) @@ -374,7 +374,7 @@ impl Musixmatch { fn new_url(&self, endpoint: &str) -> reqwest::Url { Url::parse_with_params( - &format!("{API_URL}{endpoint}"), + &format!("{}{}", API_URL, endpoint), &[("app_id", APP_ID), ("format", "json")], ) .unwrap() @@ -449,26 +449,26 @@ impl Musixmatch { } fn random_guid() -> String { - let mut rng = rand::rng(); - let n = rng.random::(); - format!("{n:016x}") + let mut rng = rand::thread_rng(); + let n = rng.gen::(); + format!("{:016x}", n) } fn random_uuid() -> String { - let mut rng = rand::rng(); + let mut rng = rand::thread_rng(); format!( "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}", - rng.random::(), - rng.random::(), - rng.random::(), - rng.random::(), - rng.random::() & 0xffffffffffff, + rng.gen::(), + rng.gen::(), + rng.gen::(), + rng.gen::(), + rng.gen::() & 0xffffffffffff, ) } fn new_url_from_token(endpoint: &str, usertoken: &str) -> reqwest::Url { Url::parse_with_params( - &format!("{API_URL}{endpoint}"), + &format!("{}{}", API_URL, endpoint), &[ ("app_id", APP_ID), ("usertoken", usertoken), @@ -503,6 +503,6 @@ mod tests { fn t_sign_url() { 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)); - 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=78ywxkeXlazpevI%2BbD8E3YluLPc%3D%0A&signature_protocol=sha1") + 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 9d4e8f7..afc0d59 100644 --- a/src/models/album.rs +++ b/src/models/album.rs @@ -15,7 +15,6 @@ 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 0d85dd7..e6d1a76 100644 --- a/src/models/artist.rs +++ b/src/models/artist.rs @@ -15,7 +15,6 @@ 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, @@ -86,14 +85,10 @@ 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, @@ -101,7 +96,6 @@ 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, @@ -109,7 +103,6 @@ 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") /// @@ -118,42 +111,3 @@ 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 3753c49..2e8879a 100644 --- a/src/models/id.rs +++ b/src/models/id.rs @@ -1,8 +1,4 @@ -use std::{borrow::Cow, convert::Infallible, fmt::Write, str::FromStr}; - -use serde::{de::Visitor, Deserialize, Serialize}; - -use crate::IdError; +use std::borrow::Cow; /// Track identifiers from different sources #[derive(Debug, Clone, PartialEq, Eq)] @@ -29,7 +25,7 @@ pub enum TrackId<'a> { Spotify(Cow<'a, str>), } -impl TrackId<'_> { +impl<'a> TrackId<'a> { pub(crate) fn to_param(&self) -> (&'static str, String) { match self { TrackId::Commontrack(id) => ("commontrack_id", id.to_string()), @@ -54,7 +50,7 @@ pub enum ArtistId<'a> { Musicbrainz(&'a str), } -impl ArtistId<'_> { +impl<'a> ArtistId<'a> { pub(crate) fn to_param(&self) -> (&'static str, String) { match self { ArtistId::ArtistId(id) => ("artist_id", id.to_string()), @@ -75,7 +71,7 @@ pub enum AlbumId<'a> { Musicbrainz(&'a str), } -impl AlbumId<'_> { +impl<'a> AlbumId<'a> { pub(crate) fn to_param(&self) -> (&'static str, String) { match self { AlbumId::AlbumId(id) => ("album_id", id.to_string()), @@ -100,130 +96,3 @@ 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 8aef72e..720d4e6 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -10,8 +10,6 @@ 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; @@ -25,12 +23,8 @@ 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 fa9944b..9dc03a9 100644 --- a/src/models/snippet.rs +++ b/src/models/snippet.rs @@ -14,7 +14,6 @@ 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 bfb1980..7c72a62 100644 --- a/src/models/track.rs +++ b/src/models/track.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use super::{Artist, Fqid, Genres}; +use super::Genres; #[derive(Debug, Deserialize)] pub(crate) struct TrackBody { @@ -140,8 +140,6 @@ 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) @@ -158,72 +156,6 @@ pub struct TrackLyricsTranslationStatus { pub perc: f32, } -/// Lyrics parts marked with the performer who is singing them -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -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 - #[serde(default)] - pub completed: bool, - /// 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)] - 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)] -pub struct PerformerTaggingPart { - /// Part of the lyrics text - /// - /// Includes whitespace (spaces and newline characters). - pub snippet: String, - /// Unknown - /// - /// Values: 0-3 - pub position: u32, - /// List of performers singing this part - pub performers: Vec, -} - -/// Lyrics performer -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Performer { - /// artist / unknown - #[serde(rename = "type")] - pub typ: Option, - /// 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)] -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 70d1fc8..aa762f9 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,7 +1,7 @@ use std::{ + num::NonZeroU32, path::{Path, PathBuf}, sync::LazyLock, - time::Duration, }; use governor::{DefaultDirectRateLimiter, Quota, RateLimiter}; @@ -21,13 +21,8 @@ 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(if std::env::var("CI").is_ok() { - Quota::with_period(Duration::from_millis(2000)).unwrap() - } else { - Quota::with_period(Duration::from_millis(500)).unwrap() - }) - }); + static MXM_LIMITER: LazyLock = + LazyLock::new(|| RateLimiter::direct(Quota::per_second(NonZeroU32::new(1).unwrap()))); MXM_LIMITER.until_ready().await; @@ -52,19 +47,18 @@ mod album { #[rstest] #[case::id(AlbumId::AlbumId(14248253))] - // #[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))] + #[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))] #[tokio::test] async fn by_id(#[case] album_id: AlbumId<'_>, #[future] mxm: Musixmatch) { let album = mxm.await.album(album_id).await.unwrap(); - // dbg!(&album); assert_eq!(album.album_id, 14248253); - // assert_eq!( - // album.album_mbid.expect("mbid"), - // "6c3cf9d8-88a8-43ed-850b-55813f01e451" - // ); + assert_eq!( + album.album_mbid.unwrap(), + "6c3cf9d8-88a8-43ed-850b-55813f01e451" + ); assert_eq!(album.album_name, "Gangnam Style (๊ฐ•๋‚จ์Šคํƒ€์ผ)"); - assert!(album.album_rating > 12); + assert!(album.album_rating > 20); assert_eq!(album.album_track_count, 1); assert_eq!(album.album_release_date.unwrap(), date!(2012 - 01 - 01)); assert_eq!(album.album_release_type, AlbumType::Single); @@ -121,12 +115,11 @@ mod album { .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } #[rstest] #[tokio::test] - #[ignore] async fn artist_albums(#[future] mxm: Musixmatch) { let albums = mxm .await @@ -146,7 +139,7 @@ mod album { .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } #[rstest] @@ -163,7 +156,7 @@ mod artist { #[rstest] #[case::id(ArtistId::ArtistId(410698))] - // #[case::musicbrainz(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))] + #[case::musicbrainz(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))] #[tokio::test] async fn by_id(#[case] artist_id: ArtistId<'_>, #[future] mxm: Musixmatch) { let artist = mxm.await.artist(artist_id).await.unwrap(); @@ -172,7 +165,7 @@ mod artist { assert_eq!(artist.artist_id, 410698); assert_eq!( - artist.artist_mbid.expect("mbid"), + artist.artist_mbid.unwrap(), "f99b7d67-4e63-4678-aa66-4c6ac0f7d24a" ); assert_eq!(artist.artist_name, "PSY"); @@ -185,11 +178,7 @@ mod artist { artist.artist_name_translation_list ); assert_eq!(artist.artist_country.unwrap(), "KR"); - assert!( - artist.artist_rating > 10, - "rating: {}", - artist.artist_rating - ); + assert!(artist.artist_rating > 50); let first_genre = &artist.primary_genres.music_genre_list[0].music_genre; assert_eq!(first_genre.music_genre_id, 14); assert_eq!(first_genre.music_genre_parent_id, 34); @@ -210,21 +199,6 @@ 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] @@ -236,7 +210,31 @@ mod artist { .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); + } + + #[rstest] + #[tokio::test] + async fn related(#[future] mxm: Musixmatch) { + let artists = mxm + .await + .artist_related(ArtistId::ArtistId(26485840), 10, 1) + .await + .unwrap(); + + assert_eq!(artists.len(), 10); + } + + #[rstest] + #[tokio::test] + async fn related_missing(#[future] mxm: Musixmatch) { + let err = mxm + .await + .artist_related(ArtistId::ArtistId(999999999999), 10, 1) + .await + .unwrap_err(); + + assert!(matches!(err, Error::NotFound)); } #[rstest] @@ -287,10 +285,8 @@ mod artist { } mod track { - use std::collections::HashMap; - use super::*; - use musixmatch_inofficial::models::{ChartName, MxmEntityType, SortOrder}; + use musixmatch_inofficial::models::{ChartName, SortOrder}; #[rstest] #[case::no_translation(false, false)] @@ -305,65 +301,66 @@ mod track { let track = mxm .await .matcher_track( - "Du fehlst hier", - "Silbermond", - "Himmel auf", + "Poker Face", + "Lady Gaga", + "The Fame", translation_status, lang_3c, - false, ) .await .unwrap(); // dbg!(&track); - assert_eq!(track.track_id, 17633259); - assert_eq!( - track.track_mbid.unwrap(), - "746af8c4-703e-4461-a40d-74ecdbcd755e" - ); - assert_eq!(track.track_isrc.unwrap(), "DEE861200095"); + 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() - .any(|isrc| isrc == "DEE861200095"), + .any(|isrc| isrc == "USUM70824409"), "commontrack_isrcs: {:?}", &track.commontrack_isrcs[0], ); - assert_eq!(track.track_spotify_id.unwrap(), "3wZwbYSozyMLnJJcT3e51Q"); + assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg"); assert!( track .commontrack_spotify_ids .iter() - .any(|spid| spid == "7lsi0kAFXHUCRjJzKlCZdA"), + .any(|spid| spid == "1QV6tiMFM6fSOKOGLMHYYg"), "commontrack_spotify_ids: {:?}", track.commontrack_spotify_ids, ); - assert_eq!(track.track_name, "Du fehlst hier"); - assert!(track.track_rating > 25); - assert_eq!(track.commontrack_id, 10514065); + assert_eq!(track.track_name, "Poker Face"); + assert!(track.track_rating > 50); + assert_eq!(track.commontrack_id, 47672612); assert!(!track.instrumental); - assert!(!track.explicit); - assert!(track.has_lyrics); + assert!(track.explicit); assert!(track.has_subtitles); - assert_eq!(track.lyrics_id.unwrap(), 34727375); - assert_eq!(track.subtitle_id.unwrap(), 15014656); - assert_eq!(track.album_id, 14122870); - assert_eq!(track.album_name, "Himmel auf"); - assert_eq!(track.artist_id, 84849); + 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_eq!( track.artist_mbid.unwrap(), - "34d42823-6b56-4861-a675-1565bf40d557" + "650e7db6-b795-4eb5-a702-5ea2fc46c848" ); - 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/Du-fehlst-hier"); + 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!(2012 - 3 - 23)); - assert!(track.updated_time > datetime!(2024-2-22 11:00 UTC)); + assert_eq!(first_release.date(), date!(2008 - 1 - 1)); + assert!(track.updated_time > datetime!(2023-1-17 0:00 UTC)); let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre; assert_eq!(first_pri_genre.music_genre_id, 14); @@ -372,13 +369,23 @@ 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("deu") + tl.from.as_deref() == Some("eng") } else { - tl.from.as_deref() == Some("de") + tl.from.as_deref() == Some("en") }) && tl.perc >= 0.0 && tl.perc <= 1.0 }), @@ -391,86 +398,48 @@ mod track { } #[rstest] - #[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()))] + #[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()))] #[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(); + let track = mxm.await.track(track_id, true, false).await.unwrap(); // dbg!(&track); - 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_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!(!track.explicit); assert!(track.has_lyrics); assert!(track.has_subtitles); - 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"); + 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"); let release_date = track.first_release_date.unwrap(); - assert_eq!(release_date.date(), date!(2019 - 03 - 22)); + assert_eq!(release_date.date(), date!(2020 - 11 - 17)); 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("en")); + assert_eq!(first_tstatus.from.as_deref(), Some("ko")); 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(206591653), 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 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"); - 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)] @@ -483,12 +452,7 @@ mod track { ) { let track = mxm .await - .track( - TrackId::Commontrack(47672612), - translation_status, - lang_3c, - false, - ) + .track(TrackId::Commontrack(47672612), translation_status, lang_3c) .await .unwrap(); @@ -584,11 +548,11 @@ mod track { async fn from_id_missing(#[future] mxm: Musixmatch) { let err = mxm .await - .track(TrackId::TrackId(999999999999), false, false, false) + .track(TrackId::TrackId(999999999999), false, false) .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } #[rstest] @@ -641,7 +605,7 @@ mod track { .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } #[rstest] @@ -784,7 +748,7 @@ mod lyrics { // dbg!(&lyrics); - assert_eq!(lyrics.lyrics_id, 36846057); + assert_eq!(lyrics.lyrics_id, 30126001); assert_eq!(lyrics.lyrics_language.unwrap(), "ko"); assert_eq!(lyrics.lyrics_language_description.unwrap(), "Korean"); let copyright = lyrics.lyrics_copyright.unwrap(); @@ -821,11 +785,11 @@ mod lyrics { async fn missing(#[future] mxm: Musixmatch) { let err = mxm .await - .track_lyrics(TrackId::Spotify("2gwMMr1a4aXXN5L6KC80Pu".into())) + .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into())) .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } #[rstest] @@ -972,7 +936,7 @@ mod subtitles { .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } /// This track has not been synced @@ -990,7 +954,7 @@ mod subtitles { .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } /// Try to get subtitles with wrong length parameter @@ -1008,7 +972,7 @@ mod subtitles { .await .unwrap_err(); - assert!(matches!(err, Error::NotFound), "got: {err:?}"); + assert!(matches!(err, Error::NotFound)); } #[rstest]