Compare commits
38 commits
musixmatch
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
5e5942beb8 |
|||
|
f6fe442e3c |
|||
|
4782f4905e |
|||
|
c6b3382948 |
|||
|
e087752dd0 |
|||
|
80c3c38c41 |
|||
|
39bbd5070a |
|||
|
09c6004ed1 |
|||
|
bfe6fec115 |
|||
|
528c6f7eb1 |
|||
|
74d5359547 |
|||
|
59dee61a2f |
|||
|
f73dcdd134 |
|||
|
04a0544ad5 |
|||
|
4a46e7bb1d |
|||
|
6f90033cf4 |
|||
|
7c325c4af7 |
|||
|
a3f2ffc5d9 |
|||
|
87859e629f |
|||
|
c90bfc647c |
|||
| 0bb886adab | |||
|
bf68f94682 |
|||
|
7b5a6e2e50 |
|||
|
62e0308dda |
|||
|
2c22f2aa33 |
|||
|
|
319dabeee0 | ||
|
|
6942d0eaaa | ||
|
2926455376 |
|||
|
|
4d26c4a72f | ||
|
b136bb3004 |
|||
|
26f4729738 |
|||
| 693ff34755 | |||
|
adcd9baf12 |
|||
|
|
6a6ced1622 | ||
|
368b46fa79 |
|||
|
e4cffa53ca |
|||
|
|
4bfcb79173 | ||
|
|
5ef76f5a6b |
22 changed files with 702 additions and 224 deletions
|
|
@ -1,5 +1,8 @@
|
||||||
name: CI
|
name: CI
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Test:
|
Test:
|
||||||
|
|
@ -24,6 +27,25 @@ jobs:
|
||||||
run: cargo clippy --all -- -D warnings
|
run: cargo clippy --all -- -D warnings
|
||||||
|
|
||||||
- name: 🧪 Test
|
- name: 🧪 Test
|
||||||
run: cargo test --workspace
|
run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 -j 1 --workspace
|
||||||
env:
|
env:
|
||||||
ALL_PROXY: "http://warpproxy:8124"
|
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"}}'
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: renovate/renovate:latest
|
image: renovate/renovate:39
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Load renovate repo cache
|
- name: Load renovate repo cache
|
||||||
56
CHANGELOG.md
56
CHANGELOG.md
|
|
@ -3,6 +3,62 @@
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [v0.1.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.0..musixmatch-inofficial/v0.1.1) - 2024-08-18
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
|
||||||
13
Cargo.toml
13
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "musixmatch-inofficial"
|
name = "musixmatch-inofficial"
|
||||||
version = "0.1.1"
|
version = "0.3.0"
|
||||||
rust-version = "1.70.0"
|
rust-version = "1.70.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
@ -23,7 +23,7 @@ keywords = ["music", "lyrics"]
|
||||||
categories = ["api-bindings", "multimedia"]
|
categories = ["api-bindings", "multimedia"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
musixmatch-inofficial = { version = "0.1.1", path = ".", default-features = false }
|
musixmatch-inofficial = { version = "0.3.0", path = ".", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["default-tls"]
|
default = ["default-tls"]
|
||||||
|
|
@ -44,7 +44,7 @@ reqwest = { version = "0.12.0", default-features = false, features = [
|
||||||
tokio = { version = "1.20.4" }
|
tokio = { version = "1.20.4" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.85"
|
serde_json = "1.0.85"
|
||||||
thiserror = "1.0.0"
|
thiserror = "2.0.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
time = { version = "0.3.10", features = [
|
time = { version = "0.3.10", features = [
|
||||||
"macros",
|
"macros",
|
||||||
|
|
@ -54,13 +54,14 @@ time = { version = "0.3.10", features = [
|
||||||
] }
|
] }
|
||||||
hmac = "0.12.0"
|
hmac = "0.12.0"
|
||||||
sha1 = "0.10.0"
|
sha1 = "0.10.0"
|
||||||
rand = "0.8.0"
|
rand = "0.9.0"
|
||||||
base64 = "0.22.0"
|
base64 = "0.22.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = { version = "0.22.0", default-features = false }
|
rstest = { version = "0.26.0", default-features = false }
|
||||||
dotenvy = "0.15.5"
|
dotenvy = "0.15.5"
|
||||||
tokio = { version = "1.20.4", features = ["macros"] }
|
tokio = { version = "1.20.4", features = ["macros"] }
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
path_macro = "1.0.0"
|
path_macro = "1.0.0"
|
||||||
governor = "0.6.3"
|
governor = "0.10.0"
|
||||||
|
test-log = "0.2.16"
|
||||||
|
|
|
||||||
2
Justfile
2
Justfile
|
|
@ -1,5 +1,5 @@
|
||||||
test:
|
test:
|
||||||
cargo test
|
cargo nextest run --workspace --no-fail-fast --retries 1 -j 1
|
||||||
|
|
||||||
release crate="musixmatch-inofficial":
|
release crate="musixmatch-inofficial":
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,40 @@
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [v0.2.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.1.0..musixmatch-cli/v0.2.0) - 2024-08-18
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "musixmatch-cli"
|
name = "musixmatch-cli"
|
||||||
version = "0.2.0"
|
version = "0.3.1"
|
||||||
rust-version = "1.70.0"
|
rust-version = "1.70.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.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"] }
|
clap = { version = "4.0.0", features = ["derive"] }
|
||||||
anyhow = "1.0.0"
|
anyhow = "1.0.0"
|
||||||
rpassword = "7.0.0"
|
rpassword = "7.0.0"
|
||||||
dirs = "5.0.0"
|
dirs = "6.0.0"
|
||||||
serde_json = "1.0.85"
|
serde_json = "1.0.85"
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,11 @@ enum Commands {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
ident: TrackIdentifiers,
|
ident: TrackIdentifiers,
|
||||||
},
|
},
|
||||||
|
/// Get performer tagging
|
||||||
|
Performer {
|
||||||
|
#[clap(flatten)]
|
||||||
|
ident: TrackIdentifiers,
|
||||||
|
},
|
||||||
/// Get album metadata
|
/// Get album metadata
|
||||||
Album {
|
Album {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
|
|
@ -170,7 +175,7 @@ async fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
run(cli).await.unwrap_or_else(|e| {
|
run(cli).await.unwrap_or_else(|e| {
|
||||||
println!("Error: {}", e);
|
eprintln!("Error: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +251,7 @@ async fn run(cli: Cli) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!();
|
eprintln!();
|
||||||
println!("{}", lyrics_body);
|
println!("{lyrics_body}");
|
||||||
}
|
}
|
||||||
Commands::Subtitles {
|
Commands::Subtitles {
|
||||||
ident,
|
ident,
|
||||||
|
|
@ -307,16 +312,23 @@ async fn run(cli: Cli) -> Result<()> {
|
||||||
bail!("subtitle format {format:?} cant be translated")
|
bail!("subtitle format {format:?} cant be translated")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
println!("{}", res);
|
println!("{res}");
|
||||||
} else {
|
} else {
|
||||||
eprintln!();
|
eprintln!();
|
||||||
println!("{}", subtitles.subtitle_body);
|
println!("{}", subtitles.subtitle_body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Track { ident } => {
|
Commands::Track { ident } => {
|
||||||
let track = get_track(ident, &mxm).await?;
|
let track = get_track(ident, &mxm, false).await?;
|
||||||
println!("{}", serde_json::to_string_pretty(&track)?)
|
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 } => {
|
Commands::Album { ident } => {
|
||||||
let id = if let Some(id) = ident.mxm_id {
|
let id = if let Some(id) = ident.mxm_id {
|
||||||
AlbumId::AlbumId(id)
|
AlbumId::AlbumId(id)
|
||||||
|
|
@ -397,6 +409,7 @@ async fn get_track_or_id(
|
||||||
ident: TrackIdentifiers,
|
ident: TrackIdentifiers,
|
||||||
mxm: &Musixmatch,
|
mxm: &Musixmatch,
|
||||||
translation_status: bool,
|
translation_status: bool,
|
||||||
|
performer_tagging: bool,
|
||||||
) -> Result<TrackOrId<'static>> {
|
) -> Result<TrackOrId<'static>> {
|
||||||
Ok(
|
Ok(
|
||||||
match (
|
match (
|
||||||
|
|
@ -422,7 +435,14 @@ async fn get_track_or_id(
|
||||||
}
|
}
|
||||||
(_, _, _, _, _, _, _, Some(isrc)) => TrackOrId::TrackId(TrackId::Isrc(isrc.into())),
|
(_, _, _, _, _, _, _, Some(isrc)) => TrackOrId::TrackId(TrackId::Isrc(isrc.into())),
|
||||||
(Some(name), Some(artist), _, _, _, _, _, _) => TrackOrId::Track(Box::new(
|
(Some(name), Some(artist), _, _, _, _, _, _) => TrackOrId::Track(Box::new(
|
||||||
mxm.matcher_track(&name, &artist, "", translation_status, true)
|
mxm.matcher_track(
|
||||||
|
&name,
|
||||||
|
&artist,
|
||||||
|
"",
|
||||||
|
translation_status,
|
||||||
|
true,
|
||||||
|
performer_tagging,
|
||||||
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)),
|
)),
|
||||||
_ => bail!("no track identifier given"),
|
_ => bail!("no track identifier given"),
|
||||||
|
|
@ -431,21 +451,27 @@ async fn get_track_or_id(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_track_id(ident: TrackIdentifiers, mxm: &Musixmatch) -> Result<TrackId<'static>> {
|
async fn get_track_id(ident: TrackIdentifiers, mxm: &Musixmatch) -> Result<TrackId<'static>> {
|
||||||
Ok(match get_track_or_id(ident, mxm, false).await? {
|
Ok(match get_track_or_id(ident, mxm, false, false).await? {
|
||||||
TrackOrId::Track(track) => TrackId::TrackId(track.track_id),
|
TrackOrId::Track(track) => TrackId::TrackId(track.track_id),
|
||||||
TrackOrId::TrackId(id) => id,
|
TrackOrId::TrackId(id) => id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_track(ident: TrackIdentifiers, mxm: &Musixmatch) -> Result<Track> {
|
async fn get_track(
|
||||||
Ok(match get_track_or_id(ident, mxm, true).await? {
|
ident: TrackIdentifiers,
|
||||||
|
mxm: &Musixmatch,
|
||||||
|
performer_tagging: bool,
|
||||||
|
) -> Result<Track> {
|
||||||
|
Ok(
|
||||||
|
match get_track_or_id(ident, mxm, true, performer_tagging).await? {
|
||||||
TrackOrId::Track(track) => *track,
|
TrackOrId::Track(track) => *track,
|
||||||
TrackOrId::TrackId(id) => mxm.track(id, true, true).await?,
|
TrackOrId::TrackId(id) => mxm.track(id, true, true, performer_tagging).await?,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input(prompt: &str) -> String {
|
fn input(prompt: &str) -> String {
|
||||||
print!("{}", prompt);
|
print!("{prompt}");
|
||||||
|
|
||||||
stdout().flush().expect("Failed to flush stdout!");
|
stdout().flush().expect("Failed to flush stdout!");
|
||||||
|
|
||||||
|
|
@ -457,7 +483,7 @@ fn input(prompt: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_pwd(prompt: &str) -> String {
|
fn input_pwd(prompt: &str) -> String {
|
||||||
print!("{}", prompt);
|
print!("{prompt}");
|
||||||
stdout().flush().expect("Failed to flush stdout!");
|
stdout().flush().expect("Failed to flush stdout!");
|
||||||
|
|
||||||
rpassword::read_password().expect("Failed to read password")
|
rpassword::read_password().expect("Failed to read password")
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ commit_parsers = [
|
||||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
{ message = "^test", skip = true },
|
||||||
{ message = "^chore\\(release\\)", skip = true },
|
{ message = "^chore\\(release\\)", skip = true },
|
||||||
{ message = "^chore\\(pr\\)", skip = true },
|
{ message = "^chore\\(pr\\)", skip = true },
|
||||||
{ message = "^chore\\(pull\\)", skip = true },
|
{ message = "^chore\\(pull\\)", skip = true },
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:best-practices", ":preserveSemverRanges"],
|
||||||
"config:best-practices"
|
|
||||||
],
|
|
||||||
"semanticCommits": "enabled",
|
"semanticCommits": "enabled",
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
"automergeStrategy": "squash",
|
"automergeStrategy": "squash",
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ where
|
||||||
{
|
{
|
||||||
struct BoolFromIntVisitor;
|
struct BoolFromIntVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for BoolFromIntVisitor {
|
impl Visitor<'_> for BoolFromIntVisitor {
|
||||||
type Value = bool;
|
type Value = bool;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
|
@ -232,7 +232,7 @@ where
|
||||||
n: PhantomData<N>,
|
n: PhantomData<N>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de, N> Visitor<'de> for NullIfZeroVisitor<N>
|
impl<N> Visitor<'_> for NullIfZeroVisitor<N>
|
||||||
where
|
where
|
||||||
N: TryFrom<u64>,
|
N: TryFrom<u64>,
|
||||||
{
|
{
|
||||||
|
|
@ -300,7 +300,7 @@ where
|
||||||
{
|
{
|
||||||
struct NullIfEmptyVisitor;
|
struct NullIfEmptyVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for NullIfEmptyVisitor {
|
impl Visitor<'_> for NullIfEmptyVisitor {
|
||||||
type Value = Option<String>;
|
type Value = Option<String>;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
|
@ -347,7 +347,7 @@ where
|
||||||
n: PhantomData<N>,
|
n: PhantomData<N>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de, N> Visitor<'de> for ParseIntVisitor<N>
|
impl<N> Visitor<'_> for ParseIntVisitor<N>
|
||||||
where
|
where
|
||||||
N: FromStr + TryFrom<u64>,
|
N: FromStr + TryFrom<u64>,
|
||||||
{
|
{
|
||||||
|
|
@ -441,13 +441,10 @@ pub mod optional_date {
|
||||||
) -> Result<Option<Date>, D::Error> {
|
) -> Result<Option<Date>, D::Error> {
|
||||||
struct OptionalDateVisitor;
|
struct OptionalDateVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for OptionalDateVisitor {
|
impl Visitor<'_> for OptionalDateVisitor {
|
||||||
type Value = Option<Date>;
|
type Value = Option<Date>;
|
||||||
|
|
||||||
fn expecting(
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
&self,
|
|
||||||
formatter: &mut serde::__private::fmt::Formatter,
|
|
||||||
) -> serde::__private::fmt::Result {
|
|
||||||
formatter.write_str("date or empty string")
|
formatter.write_str("date or empty string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,13 +498,10 @@ pub mod optional_datetime {
|
||||||
) -> Result<Option<OffsetDateTime>, D::Error> {
|
) -> Result<Option<OffsetDateTime>, D::Error> {
|
||||||
struct OptionalDateVisitor;
|
struct OptionalDateVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for OptionalDateVisitor {
|
impl Visitor<'_> for OptionalDateVisitor {
|
||||||
type Value = Option<OffsetDateTime>;
|
type Value = Option<OffsetDateTime>;
|
||||||
|
|
||||||
fn expecting(
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
&self,
|
|
||||||
formatter: &mut serde::__private::fmt::Formatter,
|
|
||||||
) -> serde::__private::fmt::Result {
|
|
||||||
formatter.write_str("timestamp or empty string")
|
formatter.write_str("timestamp or empty string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -543,6 +537,55 @@ pub mod optional_datetime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn single_or_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
struct SingleOrVecVisitor<T> {
|
||||||
|
t: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, T> Visitor<'de> for SingleOrVecVisitor<T>
|
||||||
|
where
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
type Value = Vec<T>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("single object or list")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||||
|
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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use time::Date;
|
use time::Date;
|
||||||
|
|
@ -686,4 +729,21 @@ mod tests {
|
||||||
let res = serde_json::from_str::<S>(json_date).unwrap();
|
let res = serde_json::from_str::<S>(json_date).unwrap();
|
||||||
assert!(res.date.is_some());
|
assert!(res.date.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_single_or_vec() {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct S {
|
||||||
|
#[serde(deserialize_with = "single_or_vec")]
|
||||||
|
vec: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = serde_json::from_str::<S>(r#"{"vec": [1, 2, 3]}"#).unwrap();
|
||||||
|
assert_eq!(res.vec, [1, 2, 3]);
|
||||||
|
|
||||||
|
let res = serde_json::from_str::<S>(r#"{"vec": {"value": 1}}"#).unwrap();
|
||||||
|
assert_eq!(res.vec, [1]);
|
||||||
|
|
||||||
|
serde_json::from_str::<S>(r#"{"vec": {"value": 1, "other": "xyz"}}"#).unwrap_err();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ impl Musixmatch {
|
||||||
|
|
||||||
let id_param = id.to_param();
|
let id_param = id.to_param();
|
||||||
url_query.append_pair(id_param.0, &id_param.1);
|
url_query.append_pair(id_param.0, &id_param.1);
|
||||||
|
url_query.append_pair("part", "artist_image");
|
||||||
url_query.finish();
|
url_query.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,40 +26,6 @@ impl Musixmatch {
|
||||||
Ok(artist_body.artist)
|
Ok(artist_body.artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a list of artists somehow related to the one specified by its ID.
|
|
||||||
///
|
|
||||||
/// # Parameters
|
|
||||||
/// - `id`: [Artist ID](crate::models::ArtistId)
|
|
||||||
/// - `page_size`: Define the page size for paginated results. Range is 1 to 100.
|
|
||||||
/// - `page`: Define the page number for paginated results, starting from 1.
|
|
||||||
///
|
|
||||||
/// # Reference
|
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/artist-related-get>
|
|
||||||
pub async fn artist_related(
|
|
||||||
&self,
|
|
||||||
id: ArtistId<'_>,
|
|
||||||
page_size: u8,
|
|
||||||
page: u32,
|
|
||||||
) -> Result<Vec<Artist>> {
|
|
||||||
let mut url = self.new_url("artist.related.get");
|
|
||||||
{
|
|
||||||
let mut url_query = url.query_pairs_mut();
|
|
||||||
|
|
||||||
let id_param = id.to_param();
|
|
||||||
url_query.append_pair(id_param.0, &id_param.1);
|
|
||||||
url_query.append_pair("page_size", &page_size.to_string());
|
|
||||||
url_query.append_pair("page", &page.to_string());
|
|
||||||
url_query.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
let artist_list_body = self.execute_get_request::<ArtistListBody>(&url).await?;
|
|
||||||
Ok(artist_list_body
|
|
||||||
.artist_list
|
|
||||||
.into_iter()
|
|
||||||
.map(|a| a.artist)
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search for artists in the Musixmatch database.
|
/// Search for artists in the Musixmatch database.
|
||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ impl Musixmatch {
|
||||||
q_album: &str,
|
q_album: &str,
|
||||||
translation_status: bool,
|
translation_status: bool,
|
||||||
lang_3c: bool,
|
lang_3c: bool,
|
||||||
|
performer_tagging: bool,
|
||||||
) -> Result<Track> {
|
) -> Result<Track> {
|
||||||
let mut url = self.new_url("matcher.track.get");
|
let mut url = self.new_url("matcher.track.get");
|
||||||
{
|
{
|
||||||
|
|
@ -40,8 +41,10 @@ impl Musixmatch {
|
||||||
if !q_album.is_empty() {
|
if !q_album.is_empty() {
|
||||||
url_query.append_pair("q_album", q_album);
|
url_query.append_pair("q_album", q_album);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut part = Vec::new();
|
||||||
if translation_status {
|
if translation_status {
|
||||||
url_query.append_pair("part", "track_lyrics_translation_status");
|
part.push("track_lyrics_translation_status");
|
||||||
url_query.append_pair(
|
url_query.append_pair(
|
||||||
"language_iso_code",
|
"language_iso_code",
|
||||||
match lang_3c {
|
match lang_3c {
|
||||||
|
|
@ -50,6 +53,13 @@ impl Musixmatch {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if performer_tagging {
|
||||||
|
part.push("track_performer_tagging");
|
||||||
|
}
|
||||||
|
if !part.is_empty() {
|
||||||
|
url_query.append_pair("part", &part.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
url_query.finish();
|
url_query.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +83,7 @@ impl Musixmatch {
|
||||||
id: TrackId<'_>,
|
id: TrackId<'_>,
|
||||||
translation_status: bool,
|
translation_status: bool,
|
||||||
lang_3c: bool,
|
lang_3c: bool,
|
||||||
|
performer_tagging: bool,
|
||||||
) -> Result<Track> {
|
) -> Result<Track> {
|
||||||
let mut url = self.new_url("track.get");
|
let mut url = self.new_url("track.get");
|
||||||
{
|
{
|
||||||
|
|
@ -80,8 +91,10 @@ impl Musixmatch {
|
||||||
|
|
||||||
let id_param = id.to_param();
|
let id_param = id.to_param();
|
||||||
url_query.append_pair(id_param.0, &id_param.1);
|
url_query.append_pair(id_param.0, &id_param.1);
|
||||||
|
|
||||||
|
let mut part = Vec::new();
|
||||||
if translation_status {
|
if translation_status {
|
||||||
url_query.append_pair("part", "track_lyrics_translation_status");
|
part.push("track_lyrics_translation_status");
|
||||||
url_query.append_pair(
|
url_query.append_pair(
|
||||||
"language_iso_code",
|
"language_iso_code",
|
||||||
match lang_3c {
|
match lang_3c {
|
||||||
|
|
@ -90,6 +103,13 @@ impl Musixmatch {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if performer_tagging {
|
||||||
|
part.push("track_performer_tagging");
|
||||||
|
}
|
||||||
|
if !part.is_empty() {
|
||||||
|
url_query.append_pair("part", &part.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
url_query.finish();
|
url_query.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,7 +226,7 @@ impl Musixmatch {
|
||||||
///
|
///
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/track-search>
|
/// <https://developer.musixmatch.com/documentation/api-reference/track-search>
|
||||||
pub fn track_search(&self) -> TrackSearchQuery {
|
pub fn track_search(&self) -> TrackSearchQuery<'_> {
|
||||||
TrackSearchQuery {
|
TrackSearchQuery {
|
||||||
mxm: self.clone(),
|
mxm: self.clone(),
|
||||||
q_track: None,
|
q_track: None,
|
||||||
|
|
|
||||||
|
|
@ -52,3 +52,8 @@ impl From<serde_json::Error> for Error {
|
||||||
Self::InvalidData(value.to_string().into())
|
Self::InvalidData(value.to_string().into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Could not parse Musixmatch FQID
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[error("Could not parse Musixmatch FQID")]
|
||||||
|
pub struct IdError;
|
||||||
|
|
|
||||||
34
src/lib.rs
34
src/lib.rs
|
|
@ -11,7 +11,7 @@ use std::fmt::Debug;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::{Error, IdError};
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
|
|
@ -36,7 +36,7 @@ const YMD_FORMAT: &[time::format_description::FormatItem] =
|
||||||
|
|
||||||
const APP_ID: &str = "android-player-v1.0";
|
const APP_ID: &str = "android-player-v1.0";
|
||||||
const API_URL: &str = "https://apic.musixmatch.com/ws/1.1/";
|
const API_URL: &str = "https://apic.musixmatch.com/ws/1.1/";
|
||||||
const SIGNATURE_SECRET: &[u8; 20] = b"967Pn4)N3&R_GBg5$b('";
|
const SIGNATURE_SECRET: &[u8; 29] = b"mNdca@6W7TeEcFn6*3.s97sJ*yPMd";
|
||||||
|
|
||||||
const DEFAULT_UA: &str = "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Build/T3B2.230316.003)";
|
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_BRAND: &str = "Google";
|
||||||
|
|
@ -247,7 +247,7 @@ impl Musixmatch {
|
||||||
// Get user token
|
// Get user token
|
||||||
// The get_token endpoint seems to be rate limited for 2 requests per minute
|
// The get_token endpoint seems to be rate limited for 2 requests per minute
|
||||||
let mut url = Url::parse_with_params(
|
let mut url = Url::parse_with_params(
|
||||||
&format!("{}{}", API_URL, "token.get"),
|
&format!("{API_URL}token.get"),
|
||||||
&[
|
&[
|
||||||
("adv_id", adv_id.as_str()),
|
("adv_id", adv_id.as_str()),
|
||||||
("root", "0"),
|
("root", "0"),
|
||||||
|
|
@ -343,7 +343,7 @@ impl Musixmatch {
|
||||||
|
|
||||||
match serde_json::to_string(&to_store) {
|
match serde_json::to_string(&to_store) {
|
||||||
Ok(json) => storage.write(&json),
|
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::<StoredSession>(&json) {
|
.and_then(|json| match serde_json::from_str::<StoredSession>(&json) {
|
||||||
Ok(session) => Some(session),
|
Ok(session) => Some(session),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Could not deserialize session. Error: {}", e);
|
error!("Could not deserialize session. Error: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -374,7 +374,7 @@ impl Musixmatch {
|
||||||
|
|
||||||
fn new_url(&self, endpoint: &str) -> reqwest::Url {
|
fn new_url(&self, endpoint: &str) -> reqwest::Url {
|
||||||
Url::parse_with_params(
|
Url::parse_with_params(
|
||||||
&format!("{}{}", API_URL, endpoint),
|
&format!("{API_URL}{endpoint}"),
|
||||||
&[("app_id", APP_ID), ("format", "json")],
|
&[("app_id", APP_ID), ("format", "json")],
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -449,26 +449,26 @@ impl Musixmatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_guid() -> String {
|
fn random_guid() -> String {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::rng();
|
||||||
let n = rng.gen::<u64>();
|
let n = rng.random::<u64>();
|
||||||
format!("{:016x}", n)
|
format!("{n:016x}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_uuid() -> String {
|
fn random_uuid() -> String {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::rng();
|
||||||
format!(
|
format!(
|
||||||
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
|
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
|
||||||
rng.gen::<u32>(),
|
rng.random::<u32>(),
|
||||||
rng.gen::<u16>(),
|
rng.random::<u16>(),
|
||||||
rng.gen::<u16>(),
|
rng.random::<u16>(),
|
||||||
rng.gen::<u16>(),
|
rng.random::<u16>(),
|
||||||
rng.gen::<u64>() & 0xffffffffffff,
|
rng.random::<u64>() & 0xffffffffffff,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_url_from_token(endpoint: &str, usertoken: &str) -> reqwest::Url {
|
fn new_url_from_token(endpoint: &str, usertoken: &str) -> reqwest::Url {
|
||||||
Url::parse_with_params(
|
Url::parse_with_params(
|
||||||
&format!("{}{}", API_URL, endpoint),
|
&format!("{API_URL}{endpoint}"),
|
||||||
&[
|
&[
|
||||||
("app_id", APP_ID),
|
("app_id", APP_ID),
|
||||||
("usertoken", usertoken),
|
("usertoken", usertoken),
|
||||||
|
|
@ -503,6 +503,6 @@ mod tests {
|
||||||
fn t_sign_url() {
|
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();
|
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));
|
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")
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ pub(crate) struct AlbumListBody {
|
||||||
|
|
||||||
/// Album: an album of songs in the Musixmatch database.
|
/// Album: an album of songs in the Musixmatch database.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
/// Unique Musixmatch Album ID
|
/// Unique Musixmatch Album ID
|
||||||
pub album_id: u64,
|
pub album_id: u64,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ pub(crate) struct ArtistListBody {
|
||||||
|
|
||||||
/// Artist: an artist in the Musixmatch database.
|
/// Artist: an artist in the Musixmatch database.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
/// Musixmatch Artist ID
|
/// Musixmatch Artist ID
|
||||||
pub artist_id: u64,
|
pub artist_id: u64,
|
||||||
|
|
@ -85,10 +86,14 @@ pub struct Artist {
|
||||||
/// End date of the artist's presence
|
/// End date of the artist's presence
|
||||||
#[serde(default, with = "crate::api_model::optional_date")]
|
#[serde(default, with = "crate::api_model::optional_date")]
|
||||||
pub end_date: Option<Date>,
|
pub end_date: Option<Date>,
|
||||||
|
/// Pictures of the artist
|
||||||
|
#[serde(default, deserialize_with = "crate::api_model::single_or_vec")]
|
||||||
|
pub artist_image: Vec<ArtistImage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alternative artist name (e.g. different languages)
|
/// Alternative artist name (e.g. different languages)
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct ArtistAlias {
|
pub struct ArtistAlias {
|
||||||
/// Alternative artist name
|
/// Alternative artist name
|
||||||
pub artist_alias: String,
|
pub artist_alias: String,
|
||||||
|
|
@ -96,6 +101,7 @@ pub struct ArtistAlias {
|
||||||
|
|
||||||
/// Artist name in another language
|
/// Artist name in another language
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct ArtistNameTranslation {
|
pub struct ArtistNameTranslation {
|
||||||
/// Artist name in another language
|
/// Artist name in another language
|
||||||
pub artist_name_translation: ArtistNameTranslationInner,
|
pub artist_name_translation: ArtistNameTranslationInner,
|
||||||
|
|
@ -103,6 +109,7 @@ pub struct ArtistNameTranslation {
|
||||||
|
|
||||||
/// Alternative artist name (e.g. different languages)
|
/// Alternative artist name (e.g. different languages)
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct ArtistNameTranslationInner {
|
pub struct ArtistNameTranslationInner {
|
||||||
/// Language code (e.g. "EN")
|
/// Language code (e.g. "EN")
|
||||||
///
|
///
|
||||||
|
|
@ -111,3 +118,42 @@ pub struct ArtistNameTranslationInner {
|
||||||
/// Translated name
|
/// Translated name
|
||||||
pub translation: String,
|
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<String>,
|
||||||
|
/// Copyright info for the image
|
||||||
|
#[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
|
||||||
|
pub image_copyright: Option<String>,
|
||||||
|
/// Image tags
|
||||||
|
#[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
|
||||||
|
pub image_tags: Option<String>,
|
||||||
|
// List of image files scaled to different sizes
|
||||||
|
pub image_format_list: Vec<ImageFormatWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
|
||||||
139
src/models/id.rs
139
src/models/id.rs
|
|
@ -1,4 +1,8 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, convert::Infallible, fmt::Write, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{de::Visitor, Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::IdError;
|
||||||
|
|
||||||
/// Track identifiers from different sources
|
/// Track identifiers from different sources
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
@ -25,7 +29,7 @@ pub enum TrackId<'a> {
|
||||||
Spotify(Cow<'a, str>),
|
Spotify(Cow<'a, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TrackId<'a> {
|
impl TrackId<'_> {
|
||||||
pub(crate) fn to_param(&self) -> (&'static str, String) {
|
pub(crate) fn to_param(&self) -> (&'static str, String) {
|
||||||
match self {
|
match self {
|
||||||
TrackId::Commontrack(id) => ("commontrack_id", id.to_string()),
|
TrackId::Commontrack(id) => ("commontrack_id", id.to_string()),
|
||||||
|
|
@ -50,7 +54,7 @@ pub enum ArtistId<'a> {
|
||||||
Musicbrainz(&'a str),
|
Musicbrainz(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ArtistId<'a> {
|
impl ArtistId<'_> {
|
||||||
pub(crate) fn to_param(&self) -> (&'static str, String) {
|
pub(crate) fn to_param(&self) -> (&'static str, String) {
|
||||||
match self {
|
match self {
|
||||||
ArtistId::ArtistId(id) => ("artist_id", id.to_string()),
|
ArtistId::ArtistId(id) => ("artist_id", id.to_string()),
|
||||||
|
|
@ -71,7 +75,7 @@ pub enum AlbumId<'a> {
|
||||||
Musicbrainz(&'a str),
|
Musicbrainz(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AlbumId<'a> {
|
impl AlbumId<'_> {
|
||||||
pub(crate) fn to_param(&self) -> (&'static str, String) {
|
pub(crate) fn to_param(&self) -> (&'static str, String) {
|
||||||
match self {
|
match self {
|
||||||
AlbumId::AlbumId(id) => ("album_id", id.to_string()),
|
AlbumId::AlbumId(id) => ("album_id", id.to_string()),
|
||||||
|
|
@ -96,3 +100,130 @@ impl SortOrder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Musixmatch fully qualified ID
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Fqid {
|
||||||
|
/// Numeric Musixmatch ID
|
||||||
|
pub id: u64,
|
||||||
|
/// Entity type
|
||||||
|
pub typ: MxmEntityType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Musixmatch entity type
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum MxmEntityType {
|
||||||
|
Artist,
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for MxmEntityType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
MxmEntityType::Artist => "artist",
|
||||||
|
MxmEntityType::Unknown => "unknown",
|
||||||
|
};
|
||||||
|
f.write_str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MxmEntityType {
|
||||||
|
type Err = Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
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<Self, Self::Err> {
|
||||||
|
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Fqid {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
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::<Fqid>(json).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
id,
|
||||||
|
Fqid {
|
||||||
|
id: 27853427,
|
||||||
|
typ: crate::models::id::MxmEntityType::Artist
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(serde_json::to_string(&id).unwrap(), json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ pub use subtitle::SubtitleTime;
|
||||||
mod id;
|
mod id;
|
||||||
pub use id::AlbumId;
|
pub use id::AlbumId;
|
||||||
pub use id::ArtistId;
|
pub use id::ArtistId;
|
||||||
|
pub use id::Fqid;
|
||||||
|
pub use id::MxmEntityType;
|
||||||
pub use id::SortOrder;
|
pub use id::SortOrder;
|
||||||
pub use id::TrackId;
|
pub use id::TrackId;
|
||||||
|
|
||||||
|
|
@ -23,8 +25,12 @@ pub use translation::TranslationMap;
|
||||||
|
|
||||||
pub(crate) mod track;
|
pub(crate) mod track;
|
||||||
pub use track::ChartName;
|
pub use track::ChartName;
|
||||||
|
pub use track::Performer;
|
||||||
|
pub use track::PerformerTaggingPart;
|
||||||
|
pub use track::PerformerTaggingResources;
|
||||||
pub use track::Track;
|
pub use track::Track;
|
||||||
pub use track::TrackLyricsTranslationStatus;
|
pub use track::TrackLyricsTranslationStatus;
|
||||||
|
pub use track::TrackPerformerTagging;
|
||||||
|
|
||||||
mod genre;
|
mod genre;
|
||||||
pub use genre::Genre;
|
pub use genre::Genre;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub(crate) struct SnippetBody {
|
||||||
///
|
///
|
||||||
/// Example: "There's not a thing that I would change"
|
/// Example: "There's not a thing that I would change"
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
pub struct Snippet {
|
pub struct Snippet {
|
||||||
/// Unique Musixmatch Snippet ID
|
/// Unique Musixmatch Snippet ID
|
||||||
pub snippet_id: u64,
|
pub snippet_id: u64,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use super::Genres;
|
use super::{Artist, Fqid, Genres};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(crate) struct TrackBody {
|
pub(crate) struct TrackBody {
|
||||||
|
|
@ -140,6 +140,8 @@ pub struct Track {
|
||||||
/// Status of lyrics translation
|
/// Status of lyrics translation
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub track_lyrics_translation_status: Vec<TrackLyricsTranslationStatus>,
|
pub track_lyrics_translation_status: Vec<TrackLyricsTranslationStatus>,
|
||||||
|
/// Lyrics parts marked with the performer who is singing them
|
||||||
|
pub performer_tagging: Option<TrackPerformerTagging>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Status of lyrics translation (language + progress)
|
/// Status of lyrics translation (language + progress)
|
||||||
|
|
@ -156,6 +158,72 @@ pub struct TrackLyricsTranslationStatus {
|
||||||
pub perc: f32,
|
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<PerformerTaggingPart>,
|
||||||
|
/// 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<Performer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lyrics performer
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Performer {
|
||||||
|
/// artist / unknown
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub typ: Option<String>,
|
||||||
|
/// Fully-qualified performer ID
|
||||||
|
pub fqid: Option<Fqid>,
|
||||||
|
/// Unbekannt
|
||||||
|
///
|
||||||
|
/// 9
|
||||||
|
pub category_id: Option<std::num::NonZeroU32>,
|
||||||
|
/// Unbekannt
|
||||||
|
///
|
||||||
|
/// 405
|
||||||
|
pub credit_role_id: Option<std::num::NonZeroU32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Artist>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Available track charts
|
/// Available track charts
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum ChartName {
|
pub enum ChartName {
|
||||||
|
|
|
||||||
278
tests/tests.rs
278
tests/tests.rs
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
num::NonZeroU32,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::LazyLock,
|
sync::LazyLock,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
|
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
|
||||||
|
|
@ -21,8 +21,13 @@ fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
|
||||||
#[fixture]
|
#[fixture]
|
||||||
async fn mxm() -> Musixmatch {
|
async fn mxm() -> Musixmatch {
|
||||||
static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
|
static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
|
||||||
static MXM_LIMITER: LazyLock<DefaultDirectRateLimiter> =
|
static MXM_LIMITER: LazyLock<DefaultDirectRateLimiter> = LazyLock::new(|| {
|
||||||
LazyLock::new(|| RateLimiter::direct(Quota::per_second(NonZeroU32::new(1).unwrap())));
|
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()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
MXM_LIMITER.until_ready().await;
|
MXM_LIMITER.until_ready().await;
|
||||||
|
|
||||||
|
|
@ -47,18 +52,19 @@ mod album {
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::id(AlbumId::AlbumId(14248253))]
|
#[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]
|
#[tokio::test]
|
||||||
async fn by_id(#[case] album_id: AlbumId<'_>, #[future] mxm: Musixmatch) {
|
async fn by_id(#[case] album_id: AlbumId<'_>, #[future] mxm: Musixmatch) {
|
||||||
let album = mxm.await.album(album_id).await.unwrap();
|
let album = mxm.await.album(album_id).await.unwrap();
|
||||||
|
// dbg!(&album);
|
||||||
|
|
||||||
assert_eq!(album.album_id, 14248253);
|
assert_eq!(album.album_id, 14248253);
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
album.album_mbid.unwrap(),
|
// album.album_mbid.expect("mbid"),
|
||||||
"6c3cf9d8-88a8-43ed-850b-55813f01e451"
|
// "6c3cf9d8-88a8-43ed-850b-55813f01e451"
|
||||||
);
|
// );
|
||||||
assert_eq!(album.album_name, "Gangnam Style (강남스타일)");
|
assert_eq!(album.album_name, "Gangnam Style (강남스타일)");
|
||||||
assert!(album.album_rating > 20);
|
assert!(album.album_rating > 12);
|
||||||
assert_eq!(album.album_track_count, 1);
|
assert_eq!(album.album_track_count, 1);
|
||||||
assert_eq!(album.album_release_date.unwrap(), date!(2012 - 01 - 01));
|
assert_eq!(album.album_release_date.unwrap(), date!(2012 - 01 - 01));
|
||||||
assert_eq!(album.album_release_type, AlbumType::Single);
|
assert_eq!(album.album_release_type, AlbumType::Single);
|
||||||
|
|
@ -115,11 +121,12 @@ mod album {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
async fn artist_albums(#[future] mxm: Musixmatch) {
|
async fn artist_albums(#[future] mxm: Musixmatch) {
|
||||||
let albums = mxm
|
let albums = mxm
|
||||||
.await
|
.await
|
||||||
|
|
@ -139,7 +146,7 @@ mod album {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
@ -156,7 +163,7 @@ mod artist {
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::id(ArtistId::ArtistId(410698))]
|
#[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]
|
#[tokio::test]
|
||||||
async fn by_id(#[case] artist_id: ArtistId<'_>, #[future] mxm: Musixmatch) {
|
async fn by_id(#[case] artist_id: ArtistId<'_>, #[future] mxm: Musixmatch) {
|
||||||
let artist = mxm.await.artist(artist_id).await.unwrap();
|
let artist = mxm.await.artist(artist_id).await.unwrap();
|
||||||
|
|
@ -165,7 +172,7 @@ mod artist {
|
||||||
|
|
||||||
assert_eq!(artist.artist_id, 410698);
|
assert_eq!(artist.artist_id, 410698);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
artist.artist_mbid.unwrap(),
|
artist.artist_mbid.expect("mbid"),
|
||||||
"f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"
|
"f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"
|
||||||
);
|
);
|
||||||
assert_eq!(artist.artist_name, "PSY");
|
assert_eq!(artist.artist_name, "PSY");
|
||||||
|
|
@ -178,7 +185,11 @@ mod artist {
|
||||||
artist.artist_name_translation_list
|
artist.artist_name_translation_list
|
||||||
);
|
);
|
||||||
assert_eq!(artist.artist_country.unwrap(), "KR");
|
assert_eq!(artist.artist_country.unwrap(), "KR");
|
||||||
assert!(artist.artist_rating > 50);
|
assert!(
|
||||||
|
artist.artist_rating > 10,
|
||||||
|
"rating: {}",
|
||||||
|
artist.artist_rating
|
||||||
|
);
|
||||||
let first_genre = &artist.primary_genres.music_genre_list[0].music_genre;
|
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_id, 14);
|
||||||
assert_eq!(first_genre.music_genre_parent_id, 34);
|
assert_eq!(first_genre.music_genre_parent_id, 34);
|
||||||
|
|
@ -199,6 +210,21 @@ mod artist {
|
||||||
assert_eq!(artist.begin_date.unwrap(), date!(1977 - 12 - 31));
|
assert_eq!(artist.begin_date.unwrap(), date!(1977 - 12 - 31));
|
||||||
assert_eq!(artist.end_date_year, None);
|
assert_eq!(artist.end_date_year, None);
|
||||||
assert_eq!(artist.end_date, 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]
|
#[rstest]
|
||||||
|
|
@ -210,31 +236,7 @@ mod artist {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
|
||||||
|
|
||||||
#[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]
|
#[rstest]
|
||||||
|
|
@ -285,8 +287,10 @@ mod artist {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod track {
|
mod track {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use musixmatch_inofficial::models::{ChartName, SortOrder};
|
use musixmatch_inofficial::models::{ChartName, MxmEntityType, SortOrder};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::no_translation(false, false)]
|
#[case::no_translation(false, false)]
|
||||||
|
|
@ -301,66 +305,65 @@ mod track {
|
||||||
let track = mxm
|
let track = mxm
|
||||||
.await
|
.await
|
||||||
.matcher_track(
|
.matcher_track(
|
||||||
"Poker Face",
|
"Du fehlst hier",
|
||||||
"Lady Gaga",
|
"Silbermond",
|
||||||
"The Fame",
|
"Himmel auf",
|
||||||
translation_status,
|
translation_status,
|
||||||
lang_3c,
|
lang_3c,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// dbg!(&track);
|
// dbg!(&track);
|
||||||
|
|
||||||
assert_eq!(track.track_id, 85213841);
|
assert_eq!(track.track_id, 17633259);
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// track.track_mbid.unwrap(),
|
track.track_mbid.unwrap(),
|
||||||
// "080975b0-39b1-493c-ae64-5cb3292409bb"
|
"746af8c4-703e-4461-a40d-74ecdbcd755e"
|
||||||
// );
|
);
|
||||||
// assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
|
assert_eq!(track.track_isrc.unwrap(), "DEE861200095");
|
||||||
assert!(
|
assert!(
|
||||||
track.commontrack_isrcs[0]
|
track.commontrack_isrcs[0]
|
||||||
.iter()
|
.iter()
|
||||||
.any(|isrc| isrc == "USUM70824409"),
|
.any(|isrc| isrc == "DEE861200095"),
|
||||||
"commontrack_isrcs: {:?}",
|
"commontrack_isrcs: {:?}",
|
||||||
&track.commontrack_isrcs[0],
|
&track.commontrack_isrcs[0],
|
||||||
);
|
);
|
||||||
assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg");
|
assert_eq!(track.track_spotify_id.unwrap(), "3wZwbYSozyMLnJJcT3e51Q");
|
||||||
assert!(
|
assert!(
|
||||||
track
|
track
|
||||||
.commontrack_spotify_ids
|
.commontrack_spotify_ids
|
||||||
.iter()
|
.iter()
|
||||||
.any(|spid| spid == "1QV6tiMFM6fSOKOGLMHYYg"),
|
.any(|spid| spid == "7lsi0kAFXHUCRjJzKlCZdA"),
|
||||||
"commontrack_spotify_ids: {:?}",
|
"commontrack_spotify_ids: {:?}",
|
||||||
track.commontrack_spotify_ids,
|
track.commontrack_spotify_ids,
|
||||||
);
|
);
|
||||||
assert_eq!(track.track_name, "Poker Face");
|
assert_eq!(track.track_name, "Du fehlst hier");
|
||||||
assert!(track.track_rating > 50);
|
assert!(track.track_rating > 25);
|
||||||
assert_eq!(track.commontrack_id, 47672612);
|
assert_eq!(track.commontrack_id, 10514065);
|
||||||
assert!(!track.instrumental);
|
assert!(!track.instrumental);
|
||||||
assert!(track.explicit);
|
assert!(!track.explicit);
|
||||||
|
assert!(track.has_lyrics);
|
||||||
assert!(track.has_subtitles);
|
assert!(track.has_subtitles);
|
||||||
assert!(track.has_richsync);
|
assert_eq!(track.lyrics_id.unwrap(), 34727375);
|
||||||
assert!(track.has_track_structure);
|
assert_eq!(track.subtitle_id.unwrap(), 15014656);
|
||||||
assert!(track.num_favourite > 50);
|
assert_eq!(track.album_id, 14122870);
|
||||||
assert!(track.lyrics_id.is_some());
|
assert_eq!(track.album_name, "Himmel auf");
|
||||||
assert_eq!(track.subtitle_id.unwrap(), 36450705);
|
assert_eq!(track.artist_id, 84849);
|
||||||
assert_eq!(track.album_id, 20960801);
|
|
||||||
assert_eq!(track.album_name, "The Fame");
|
|
||||||
assert_eq!(track.artist_id, 378462);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
track.artist_mbid.unwrap(),
|
track.artist_mbid.unwrap(),
|
||||||
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
|
"34d42823-6b56-4861-a675-1565bf40d557"
|
||||||
);
|
);
|
||||||
assert_eq!(track.artist_name, "Lady Gaga");
|
assert_eq!(track.artist_name, "Silbermond");
|
||||||
assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg");
|
assert_imgurl(&track.album_coverart_100x100, "/30913494.jpg");
|
||||||
assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg");
|
assert_imgurl(&track.album_coverart_350x350, "/30913494_350_350.jpg");
|
||||||
assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg");
|
assert_imgurl(&track.album_coverart_500x500, "/30913494_500_500.jpg");
|
||||||
assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg");
|
assert_imgurl(&track.album_coverart_800x800, "/30913494_800_800.jpg");
|
||||||
assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1");
|
assert_eq!(track.commontrack_vanity_id, "Silbermond/Du-fehlst-hier");
|
||||||
let first_release = track.first_release_date.unwrap();
|
let first_release = track.first_release_date.unwrap();
|
||||||
assert_eq!(first_release.date(), date!(2008 - 1 - 1));
|
assert_eq!(first_release.date(), date!(2012 - 3 - 23));
|
||||||
assert!(track.updated_time > datetime!(2023-1-17 0:00 UTC));
|
assert!(track.updated_time > datetime!(2024-2-22 11:00 UTC));
|
||||||
|
|
||||||
let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre;
|
let first_pri_genre = &track.primary_genres.music_genre_list[0].music_genre;
|
||||||
assert_eq!(first_pri_genre.music_genre_id, 14);
|
assert_eq!(first_pri_genre.music_genre_id, 14);
|
||||||
|
|
@ -369,23 +372,13 @@ mod track {
|
||||||
assert_eq!(first_pri_genre.music_genre_name_extended, "Pop");
|
assert_eq!(first_pri_genre.music_genre_name_extended, "Pop");
|
||||||
assert_eq!(first_pri_genre.music_genre_vanity.as_ref().unwrap(), "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 {
|
if translation_status {
|
||||||
assert!(
|
assert!(
|
||||||
track.track_lyrics_translation_status.iter().all(|tl| {
|
track.track_lyrics_translation_status.iter().all(|tl| {
|
||||||
(if lang_3c {
|
(if lang_3c {
|
||||||
tl.from.as_deref() == Some("eng")
|
tl.from.as_deref() == Some("deu")
|
||||||
} else {
|
} else {
|
||||||
tl.from.as_deref() == Some("en")
|
tl.from.as_deref() == Some("de")
|
||||||
}) && tl.perc >= 0.0
|
}) && tl.perc >= 0.0
|
||||||
&& tl.perc <= 1.0
|
&& tl.perc <= 1.0
|
||||||
}),
|
}),
|
||||||
|
|
@ -398,48 +391,86 @@ mod track {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::trackid(TrackId::TrackId(205688271))]
|
#[case::trackid(TrackId::TrackId(167254015))]
|
||||||
#[case::commontrack(TrackId::Commontrack(118480583))]
|
#[case::commontrack(TrackId::Commontrack(93933821))]
|
||||||
#[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba".into()))]
|
#[case::vanity(TrackId::CommontrackVanity("Nightbirde-2/Girl-in-a-Bubble".into()))]
|
||||||
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
|
#[case::isrc(TrackId::Isrc("QZDA41918667".into()))]
|
||||||
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
|
#[case::spotify(TrackId::Spotify("2roGy5AYlaJpmL9CuXj6tT".into()))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
|
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
|
||||||
let track = mxm.await.track(track_id, true, false).await.unwrap();
|
let track = mxm.await.track(track_id, true, false, false).await.unwrap();
|
||||||
|
|
||||||
// dbg!(&track);
|
// dbg!(&track);
|
||||||
|
|
||||||
assert_eq!(track.track_id, 205688271);
|
assert_eq!(track.track_id, 167254015);
|
||||||
assert_eq!(track.track_isrc.unwrap(), "KRA302000590");
|
assert_eq!(track.track_isrc.unwrap(), "QZDA41918667");
|
||||||
assert_eq!(track.track_spotify_id.unwrap(), "1t2qYCAjUAoGfeFeoBlK51");
|
assert_eq!(track.track_spotify_id.unwrap(), "2roGy5AYlaJpmL9CuXj6tT");
|
||||||
assert_eq!(track.track_name, "Black Mamba");
|
assert_eq!(track.track_name, "Girl in a Bubble");
|
||||||
assert!(track.track_rating > 50);
|
assert!(track.track_rating > 40);
|
||||||
assert_eq!(track.track_length, 175);
|
assert_eq!(track.track_length, 238);
|
||||||
assert!(!track.explicit);
|
assert!(!track.explicit);
|
||||||
assert!(track.has_lyrics);
|
assert!(track.has_lyrics);
|
||||||
assert!(track.has_subtitles);
|
assert!(track.has_subtitles);
|
||||||
assert!(track.has_richsync);
|
assert!(track.num_favourite > 1);
|
||||||
assert!(track.num_favourite > 200);
|
assert_eq!(track.lyrics_id.unwrap(), 25830616);
|
||||||
assert!(track.lyrics_id.is_some());
|
assert_eq!(track.subtitle_id.unwrap(), 34307878);
|
||||||
assert_eq!(track.subtitle_id.unwrap(), 36476905);
|
assert_eq!(track.album_id, 31842378);
|
||||||
assert_eq!(track.album_id, 41035954);
|
assert_eq!(track.album_name, "Girl in a Bubble");
|
||||||
assert_eq!(track.album_name, "Black Mamba");
|
assert_eq!(track.artist_id, 38035381);
|
||||||
assert_eq!(track.artist_id, 46970441);
|
assert_eq!(track.artist_name, "Nightbirde");
|
||||||
assert_eq!(track.artist_name, "aespa");
|
assert_imgurl(&track.album_coverart_100x100, "/43015335.jpg");
|
||||||
assert_imgurl(&track.album_coverart_100x100, "/52156772.jpg");
|
assert_imgurl(&track.album_coverart_350x350, "/43015335_350_350.jpg");
|
||||||
assert_imgurl(&track.album_coverart_350x350, "/52156772_350_350.jpg");
|
assert_imgurl(&track.album_coverart_500x500, "/43015335_500_500.jpg");
|
||||||
assert_imgurl(&track.album_coverart_500x500, "/52156772_500_500.jpg");
|
assert_eq!(track.commontrack_vanity_id, "Nightbirde-2/Girl-in-a-Bubble");
|
||||||
assert_eq!(track.commontrack_vanity_id, "aespa/Black-Mamba");
|
|
||||||
|
|
||||||
let release_date = track.first_release_date.unwrap();
|
let release_date = track.first_release_date.unwrap();
|
||||||
assert_eq!(release_date.date(), date!(2020 - 11 - 17));
|
assert_eq!(release_date.date(), date!(2019 - 03 - 22));
|
||||||
assert!(track.updated_time > datetime!(2022-8-27 0:00 UTC));
|
assert!(track.updated_time > datetime!(2022-8-27 0:00 UTC));
|
||||||
|
|
||||||
let first_tstatus = &track.track_lyrics_translation_status[0];
|
let first_tstatus = &track.track_lyrics_translation_status[0];
|
||||||
assert_eq!(first_tstatus.from.as_deref(), Some("ko"));
|
assert_eq!(first_tstatus.from.as_deref(), Some("en"));
|
||||||
assert!(first_tstatus.perc >= 0.0 && first_tstatus.perc <= 1.0);
|
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::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
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]
|
#[rstest]
|
||||||
#[case::no_translation(false, false)]
|
#[case::no_translation(false, false)]
|
||||||
#[case::translation_2c(true, false)]
|
#[case::translation_2c(true, false)]
|
||||||
|
|
@ -452,7 +483,12 @@ mod track {
|
||||||
) {
|
) {
|
||||||
let track = mxm
|
let track = mxm
|
||||||
.await
|
.await
|
||||||
.track(TrackId::Commontrack(47672612), translation_status, lang_3c)
|
.track(
|
||||||
|
TrackId::Commontrack(47672612),
|
||||||
|
translation_status,
|
||||||
|
lang_3c,
|
||||||
|
false,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -548,11 +584,11 @@ mod track {
|
||||||
async fn from_id_missing(#[future] mxm: Musixmatch) {
|
async fn from_id_missing(#[future] mxm: Musixmatch) {
|
||||||
let err = mxm
|
let err = mxm
|
||||||
.await
|
.await
|
||||||
.track(TrackId::TrackId(999999999999), false, false)
|
.track(TrackId::TrackId(999999999999), false, false, false)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
@ -605,7 +641,7 @@ mod track {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
@ -748,7 +784,7 @@ mod lyrics {
|
||||||
|
|
||||||
// dbg!(&lyrics);
|
// dbg!(&lyrics);
|
||||||
|
|
||||||
assert_eq!(lyrics.lyrics_id, 30126001);
|
assert_eq!(lyrics.lyrics_id, 36846057);
|
||||||
assert_eq!(lyrics.lyrics_language.unwrap(), "ko");
|
assert_eq!(lyrics.lyrics_language.unwrap(), "ko");
|
||||||
assert_eq!(lyrics.lyrics_language_description.unwrap(), "Korean");
|
assert_eq!(lyrics.lyrics_language_description.unwrap(), "Korean");
|
||||||
let copyright = lyrics.lyrics_copyright.unwrap();
|
let copyright = lyrics.lyrics_copyright.unwrap();
|
||||||
|
|
@ -785,11 +821,11 @@ mod lyrics {
|
||||||
async fn missing(#[future] mxm: Musixmatch) {
|
async fn missing(#[future] mxm: Musixmatch) {
|
||||||
let err = mxm
|
let err = mxm
|
||||||
.await
|
.await
|
||||||
.track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into()))
|
.track_lyrics(TrackId::Spotify("2gwMMr1a4aXXN5L6KC80Pu".into()))
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
@ -936,7 +972,7 @@ mod subtitles {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This track has not been synced
|
/// This track has not been synced
|
||||||
|
|
@ -954,7 +990,7 @@ mod subtitles {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to get subtitles with wrong length parameter
|
/// Try to get subtitles with wrong length parameter
|
||||||
|
|
@ -972,7 +1008,7 @@ mod subtitles {
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue