Compare commits
3 commits
main
...
feat/deskt
Author | SHA1 | Date | |
---|---|---|---|
9904597d8f | |||
2300932afc | |||
6b4312a6a5 |
26 changed files with 692 additions and 1109 deletions
|
@ -1,2 +1,3 @@
|
||||||
|
MUSIXMATCH_CLIENT=Android
|
||||||
MUSIXMATCH_EMAIL=mail@example.com
|
MUSIXMATCH_EMAIL=mail@example.com
|
||||||
MUSIXMATCH_PASSWORD=super-secret
|
MUSIXMATCH_PASSWORD=super-secret
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
name: CI
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Test:
|
|
||||||
runs-on: cimaster-latest
|
|
||||||
services:
|
|
||||||
warpproxy:
|
|
||||||
image: thetadev256/warpproxy
|
|
||||||
env:
|
|
||||||
WARP_DEVICE_ID: ${{ secrets.WARP_DEVICE_ID }}
|
|
||||||
WARP_ACCESS_TOKEN: ${{ secrets.WARP_ACCESS_TOKEN }}
|
|
||||||
WARP_LICENSE_KEY: ${{ secrets.WARP_LICENSE_KEY }}
|
|
||||||
WARP_PRIVATE_KEY: ${{ secrets.WARP_PRIVATE_KEY }}
|
|
||||||
steps:
|
|
||||||
- name: 📦 Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: 🦀 Setup Rust cache
|
|
||||||
uses: https://github.com/Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
cache-on-failure: "true"
|
|
||||||
|
|
||||||
- name: 📎 Clippy
|
|
||||||
run: cargo clippy --all -- -D warnings
|
|
||||||
|
|
||||||
- name: 🧪 Test
|
|
||||||
run: cargo test --workspace
|
|
||||||
env:
|
|
||||||
ALL_PROXY: "http://warpproxy:8124"
|
|
|
@ -1,33 +0,0 @@
|
||||||
name: Release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*/v*.*.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Release:
|
|
||||||
runs-on: cimaster-latest
|
|
||||||
steps:
|
|
||||||
- name: 📦 Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Get variables
|
|
||||||
run: |
|
|
||||||
git fetch --tags --force #the checkout action does not load the tag message
|
|
||||||
|
|
||||||
echo "CRATE=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==1{print}')" >> "$GITHUB_ENV"
|
|
||||||
echo "CRATE_VERSION=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==2{print}')" >> "$GITHUB_ENV"
|
|
||||||
{
|
|
||||||
echo 'CHANGELOG<<END_OF_FILE'
|
|
||||||
git show -s --format=%N "${{ github.ref_name }}" | tail -n +4 | awk 'BEGIN{RS="-----BEGIN PGP SIGNATURE-----"} NR==1{printf $0}'
|
|
||||||
echo END_OF_FILE
|
|
||||||
} >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: 📤 Publish crate on crates.io
|
|
||||||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }} --package "${{ env.CRATE }}"
|
|
||||||
|
|
||||||
- name: 🎉 Publish release
|
|
||||||
uses: https://gitea.com/actions/release-action@main
|
|
||||||
with:
|
|
||||||
title: "${{ env.CRATE }} ${{ env.CRATE_VERSION }}"
|
|
||||||
body: "${{ env.CHANGELOG }}"
|
|
|
@ -1,63 +0,0 @@
|
||||||
name: renovate
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
paths:
|
|
||||||
- ".forgejo/workflows/renovate.yaml"
|
|
||||||
- "renovate.json"
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RENOVATE_REPOSITORIES: ${{ github.repository }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
renovate:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: renovate/renovate:latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Load renovate repo cache
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
.tmp/cache/renovate/repository
|
|
||||||
.tmp/cache/renovate/renovate-cache-sqlite
|
|
||||||
.tmp/osv
|
|
||||||
key: repo-cache-${{ github.run_id }}
|
|
||||||
restore-keys: |
|
|
||||||
repo-cache-
|
|
||||||
|
|
||||||
- name: Run renovate
|
|
||||||
run: renovate
|
|
||||||
env:
|
|
||||||
LOG_LEVEL: debug
|
|
||||||
RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp
|
|
||||||
RENOVATE_ENDPOINT: ${{ github.server_url }}
|
|
||||||
RENOVATE_PLATFORM: gitea
|
|
||||||
RENOVATE_REPOSITORY_CACHE: 'enabled'
|
|
||||||
RENOVATE_TOKEN: ${{ secrets.FORGEJO_CI_BOT_TOKEN }}
|
|
||||||
GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_TOKEN }}
|
|
||||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <forgejo-renovate-action@forgejo.org>'
|
|
||||||
|
|
||||||
RENOVATE_X_SQLITE_PACKAGE_CACHE: true
|
|
||||||
|
|
||||||
GIT_AUTHOR_NAME: 'Renovate Bot'
|
|
||||||
GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org'
|
|
||||||
GIT_COMMITTER_NAME: 'Renovate Bot'
|
|
||||||
GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org'
|
|
||||||
|
|
||||||
OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv
|
|
||||||
|
|
||||||
- name: Save renovate repo cache
|
|
||||||
if: always() && env.RENOVATE_DRY_RUN != 'full'
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
.tmp/cache/renovate/repository
|
|
||||||
.tmp/cache/renovate/renovate-cache-sqlite
|
|
||||||
.tmp/osv
|
|
||||||
key: repo-cache-${{ github.run_id }}
|
|
15
.woodpecker.yml
Normal file
15
.woodpecker.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
steps:
|
||||||
|
test:
|
||||||
|
image: rust:latest
|
||||||
|
environment:
|
||||||
|
- CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||||
|
secrets:
|
||||||
|
- musixmatch_email
|
||||||
|
- musixmatch_password
|
||||||
|
commands:
|
||||||
|
- rustup component add rustfmt clippy
|
||||||
|
- cargo fmt --all --check
|
||||||
|
- cargo clippy --all -- -D warnings
|
||||||
|
- MUSIXMATCH_CLIENT=Desktop cargo test --workspace
|
||||||
|
- sleep 60 # because of Musixmatch rate limit
|
||||||
|
- MUSIXMATCH_CLIENT=Android cargo test --workspace
|
45
CHANGELOG.md
45
CHANGELOG.md
|
@ -1,45 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.1.2](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.1..musixmatch-inofficial/v0.1.2) - 2024-11-15
|
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
|
||||||
|
|
||||||
- *(deps)* Update rust crate thiserror to v2 (#4) - ([6a6ced1](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/6a6ced16224c6ef3d05eb6ebd0aa0bdc40a34684))
|
|
||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
|
||||||
|
|
||||||
- *(deps)* Update rust crate rstest to 0.23.0 (#2) - ([5ef76f5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/5ef76f5a6b2a3b243f847cf86e72ebe176819d7a))
|
|
||||||
- *(deps)* Update rust crate governor to 0.7.0 (#3) - ([4bfcb79](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/4bfcb791733ce5ebd9d4e074c64eb23e9a768fc6))
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.1.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.1.0..musixmatch-inofficial/v0.1.1) - 2024-08-18
|
|
||||||
|
|
||||||
### 🚀 Features
|
|
||||||
|
|
||||||
- Add msrv - ([a95f3fc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/a95f3fcf478f1acda9fad12741604b6793e128c1))
|
|
||||||
|
|
||||||
### 📚 Documentation
|
|
||||||
|
|
||||||
- Update readme - ([348e9c5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/348e9c5427e59c488d7e2f7cef9e7006a12864f2))
|
|
||||||
|
|
||||||
### 🧪 Testing
|
|
||||||
|
|
||||||
- Fix tests - ([d2a7aed](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/d2a7aed917bfcec75ce00bb49d380fbc31c47384))
|
|
||||||
- Fix tests - ([c120583](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/c120583bf861cc74fbce686b2bd88bc575270130))
|
|
||||||
- Fix tests - ([c9fea76](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/c9fea762ec97a1c594e60a3b1cbc72bb786d0957))
|
|
||||||
- Add rate limiter - ([3b69b36](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/3b69b36ae6c945d786534e0eaa353fb737b1fb54))
|
|
||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Update justfile - ([1bc5ae4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/1bc5ae408343e6755e390909e7017647efcf59a1))
|
|
||||||
- Update dependencies - ([dcc25bf](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dcc25bff202becdec7101c5ce1825cd75e445f99))
|
|
||||||
- Change repo to codeberg - ([30e2afd](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/30e2afd3679d2c17a49afd523c8b8bad70f291e5))
|
|
||||||
|
|
||||||
## [v0.1.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-inofficial/v0.1.0) - 2024-03-23
|
|
||||||
|
|
||||||
Initial release
|
|
||||||
|
|
||||||
<!-- generated by git-cliff -->
|
|
53
Cargo.toml
53
Cargo.toml
|
@ -1,30 +1,17 @@
|
||||||
[package]
|
[package]
|
||||||
name = "musixmatch-inofficial"
|
name = "musixmatch-inofficial"
|
||||||
version = "0.1.2"
|
version = "0.1.0"
|
||||||
rust-version = "1.70.0"
|
edition = "2021"
|
||||||
edition.workspace = true
|
authors = ["ThetaDev <t.testboy@gmail.com>"]
|
||||||
authors.workspace = true
|
license = "MIT"
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
keywords.workspace = true
|
|
||||||
description = "Inofficial client for the Musixmatch API"
|
description = "Inofficial client for the Musixmatch API"
|
||||||
|
keywords = ["music", "lyrics"]
|
||||||
|
|
||||||
include = ["/src", "README.md", "CHANGELOG.md", "LICENSE"]
|
include = ["/src", "README.md", "LICENSE"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "cli"]
|
members = [".", "cli"]
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["ThetaDev <thetadev@magenta.de>"]
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://codeberg.org/ThetaDev/musixmatch-inofficial"
|
|
||||||
keywords = ["music", "lyrics"]
|
|
||||||
categories = ["api-bindings", "multimedia"]
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
musixmatch-inofficial = { version = "0.1.1", path = ".", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["default-tls"]
|
default = ["default-tls"]
|
||||||
|
|
||||||
|
@ -37,30 +24,36 @@ rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
|
||||||
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
|
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.12.0", default-features = false, features = [
|
reqwest = { version = "0.11.11", default-features = false, features = [
|
||||||
"json",
|
"json",
|
||||||
"gzip",
|
"gzip",
|
||||||
] }
|
] }
|
||||||
tokio = { version = "1.20.4" }
|
url = "2.0.0"
|
||||||
|
tokio = { version = "1.20.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.85"
|
serde_json = "1.0.85"
|
||||||
thiserror = "2.0.0"
|
thiserror = "1.0.36"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
time = { version = "0.3.10", features = [
|
time = { version = "0.3.15", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"formatting",
|
"formatting",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-well-known",
|
"serde-well-known",
|
||||||
] }
|
] }
|
||||||
hmac = "0.12.0"
|
hmac = "0.12.1"
|
||||||
sha1 = "0.10.0"
|
sha1 = "0.10.5"
|
||||||
rand = "0.8.0"
|
rand = "0.8.5"
|
||||||
base64 = "0.22.0"
|
base64 = "0.21.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = { version = "0.23.0", default-features = false }
|
ctor = "0.2.0"
|
||||||
|
rstest = { version = "0.18.0", default-features = false }
|
||||||
|
env_logger = "0.11.0"
|
||||||
dotenvy = "0.15.5"
|
dotenvy = "0.15.5"
|
||||||
tokio = { version = "1.20.4", features = ["macros"] }
|
tokio = { version = "1.20.0", features = ["macros"] }
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
path_macro = "1.0.0"
|
path_macro = "1.0.0"
|
||||||
governor = "0.7.0"
|
serde_plain = "1.0.2"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
|
44
Justfile
44
Justfile
|
@ -1,44 +0,0 @@
|
||||||
test:
|
|
||||||
cargo test
|
|
||||||
|
|
||||||
release crate="musixmatch-inofficial":
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
CRATE="{{crate}}"
|
|
||||||
INCLUDES='--include-path README.md --include-path LICENSE --include-path Cargo.toml'
|
|
||||||
CHANGELOG="CHANGELOG.md"
|
|
||||||
|
|
||||||
if [ "$CRATE" = "musixmatch-inofficial" ]; then
|
|
||||||
INCLUDES="$INCLUDES --include-path 'src/**' --include-path 'tests/**' --include-path 'testfiles/**'"
|
|
||||||
else
|
|
||||||
if [ ! -d "$CRATE" ]; then
|
|
||||||
echo "$CRATE does not exist."; exit 1
|
|
||||||
fi
|
|
||||||
INCLUDES="$INCLUDES --include-path '$CRATE/**'"
|
|
||||||
CHANGELOG="$CRATE/$CHANGELOG"
|
|
||||||
CRATE="musixmatch-$CRATE" # Add crate name prefix
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION=$(cargo pkgid --package "$CRATE" | tr '#@' '\n' | tail -n 1)
|
|
||||||
TAG="${CRATE}/v${VERSION}"
|
|
||||||
echo "Releasing $TAG:"
|
|
||||||
|
|
||||||
if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi
|
|
||||||
|
|
||||||
CLIFF_ARGS="--tag '${TAG}' --tag-pattern '${CRATE}/v*' --unreleased $INCLUDES"
|
|
||||||
echo "git-cliff $CLIFF_ARGS"
|
|
||||||
if [ -f "$CHANGELOG" ]; then
|
|
||||||
eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'"
|
|
||||||
else
|
|
||||||
eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
editor "$CHANGELOG"
|
|
||||||
|
|
||||||
git add .
|
|
||||||
git commit -m "chore(release): release $CRATE v$VERSION"
|
|
||||||
|
|
||||||
awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG"
|
|
||||||
|
|
||||||
echo "🚀 Run 'git push origin $TAG' to publish"
|
|
24
README.md
24
README.md
|
@ -1,20 +1,16 @@
|
||||||
# musixmatch-inofficial
|
# Musixmatch-Inofficial
|
||||||
|
|
||||||
[![Current crates.io version](https://img.shields.io/crates/v/musixmatch-inofficial.svg)](https://crates.io/crates/musixmatch-inofficial)
|
|
||||||
[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT)
|
|
||||||
[![CI status](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/?workflow=ci.yaml)
|
|
||||||
|
|
||||||
This is an inofficial client for the Musixmatch API that uses the key embedded in the
|
This is an inofficial client for the Musixmatch API that uses the key embedded in the
|
||||||
Musixmatch Android app.
|
Musixmatch Android app or desktop client.
|
||||||
|
|
||||||
It allows you to obtain synchronized lyrics in different formats
|
It allows you to obtain synchronized lyrics in different formats
|
||||||
([LRC](<https://en.wikipedia.org/wiki/LRC_(file_format)>),
|
([LRC](<https://en.wikipedia.org/wiki/LRC_(file_format)>),
|
||||||
[DFXP](https://www.w3.org/TR/ttml1/), JSON) for almost any song.
|
[DFXP](https://www.w3.org/TR/ttml1/), JSON) for almost any song.
|
||||||
|
|
||||||
The Musixmatch API used to require a free account on <https://www.musixmatch.com> to be
|
If you use the Android client, you need a free Musixmatch account
|
||||||
used. However, as of 2024, this requirement was removed and the API can be used
|
([you can sign up here](https://www.musixmatch.com/de/sign-up)). The desktop client can
|
||||||
anonymously. The client still allows you to supply credentials if Musixmatch decides to
|
be used anonymously and is currently the default option, but since Musixmatch
|
||||||
close the API down again.
|
discontinued that application, they may shut it down.
|
||||||
|
|
||||||
## ⚠️ Copyright disclaimer
|
## ⚠️ Copyright disclaimer
|
||||||
|
|
||||||
|
@ -31,8 +27,12 @@ their [commercial plans](https://developer.musixmatch.com/plans)) and use their
|
||||||
|
|
||||||
## Development info
|
## Development info
|
||||||
|
|
||||||
The test suite reads Musixmatch credentials from the `MUSIXMATCH_EMAIL` and
|
You can choose which client to test by setting the `MUSIXMATCH_CLIENT` environment
|
||||||
`MUSIXMATCH_PASSWORD` environment variables.
|
variable to either `Desktop` or `Android` (it defaults to Desktop).
|
||||||
|
|
||||||
|
Running the tests for the Android client requires Musixmatch credentials. The
|
||||||
|
credentials are read from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment
|
||||||
|
variables.
|
||||||
|
|
||||||
To make local development easier, I have included `dotenvy` to read the credentials from
|
To make local development easier, I have included `dotenvy` to read the credentials from
|
||||||
an `.env` file. Copy the `.env.example` file in the root directory, rename it to `.env`
|
an `.env` file. Copy the `.env.example` file in the root directory, rename it to `.env`
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.2.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.1.0..musixmatch-cli/v0.2.0) - 2024-08-18
|
|
||||||
|
|
||||||
### 🚀 Features
|
|
||||||
|
|
||||||
- Add format option to mp3 subtitles cmd - ([19e209e](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/19e209e34f4d129a4223930bfd41e1ccf117f231))
|
|
||||||
- Add get album, get artist, search artist - ([c4bfbe5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/c4bfbe563a00d399b3645dd68f03c1215ee51fdb))
|
|
||||||
- [**breaking**] Remove MP3 feature, refactor cmd structure - ([54235e6](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/54235e6fb61084823a6583aaa7d59b1799deb07f))
|
|
||||||
- Add msrv - ([a95f3fc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/a95f3fcf478f1acda9fad12741604b6793e128c1))
|
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
|
||||||
|
|
||||||
- Use native TLS for CLI - ([dc1bea1](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dc1bea13cc2a37eae7f3727dc72f865a01430a2e))
|
|
||||||
|
|
||||||
### 📚 Documentation
|
|
||||||
|
|
||||||
- Update readme - ([348e9c5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/348e9c5427e59c488d7e2f7cef9e7006a12864f2))
|
|
||||||
|
|
||||||
### 🧪 Testing
|
|
||||||
|
|
||||||
- Fix tests - ([d2a7aed](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/d2a7aed917bfcec75ce00bb49d380fbc31c47384))
|
|
||||||
- Add rate limiter - ([3b69b36](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/3b69b36ae6c945d786534e0eaa353fb737b1fb54))
|
|
||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Fix changelogs - ([e72d2b4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/e72d2b4363a3a9a48dec8f2be9389f6cc239035c))
|
|
||||||
- Update justfile - ([1bc5ae4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/1bc5ae408343e6755e390909e7017647efcf59a1))
|
|
||||||
- Update dependencies - ([dcc25bf](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dcc25bff202becdec7101c5ce1825cd75e445f99))
|
|
||||||
- Change repo to codeberg - ([30e2afd](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/30e2afd3679d2c17a49afd523c8b8bad70f291e5))
|
|
||||||
|
|
||||||
## [v0.1.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-cli/v0.1.0) - 2024-03-23
|
|
||||||
|
|
||||||
Initial release
|
|
||||||
|
|
||||||
<!-- generated by git-cliff -->
|
|
|
@ -1,16 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "musixmatch-cli"
|
name = "musixmatch-cli"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
rust-version = "1.70.0"
|
edition = "2021"
|
||||||
edition.workspace = true
|
authors = ["ThetaDev"]
|
||||||
authors.workspace = true
|
license = "MIT"
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
keywords.workspace = true
|
|
||||||
description = "Inofficial command line interface for the Musixmatch API"
|
description = "Inofficial command line interface for the Musixmatch API"
|
||||||
|
keywords = ["music", "lyrics", "cli"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["native-tls"]
|
default = ["rustls-tls-native-roots"]
|
||||||
|
|
||||||
# Reqwest TLS options
|
# Reqwest TLS options
|
||||||
native-tls = ["musixmatch-inofficial/native-tls"]
|
native-tls = ["musixmatch-inofficial/native-tls"]
|
||||||
|
@ -20,10 +18,12 @@ rustls-tls-webpki-roots = ["musixmatch-inofficial/rustls-tls-webpki-roots"]
|
||||||
rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"]
|
rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
musixmatch-inofficial.workspace = true
|
musixmatch-inofficial = { path = "../" }
|
||||||
tokio = { version = "1.20.4", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
|
||||||
clap = { version = "4.0.0", features = ["derive"] }
|
id3 = "1.3.0"
|
||||||
anyhow = "1.0.0"
|
mp3-duration = "0.1.10"
|
||||||
|
clap = { version = "4.0.10", features = ["derive"] }
|
||||||
|
anyhow = "1.0.65"
|
||||||
rpassword = "7.0.0"
|
rpassword = "7.0.0"
|
||||||
dirs = "5.0.0"
|
dirs = "5.0.0"
|
||||||
serde_json = "1.0.85"
|
serde_json = "1.0.91"
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
# musixmatch-cli
|
|
||||||
|
|
||||||
[![Current crates.io version](https://img.shields.io/crates/v/musixmatch-cli.svg)](https://crates.io/crates/musixmatch-cli)
|
|
||||||
[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT)
|
|
||||||
[![CI status](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/?workflow=ci.yaml)
|
|
||||||
|
|
||||||
The Musixmatch CLI allows you to fetch lyrics, subtitles and track metadata from the
|
|
||||||
command line using the Musixmatch API.
|
|
||||||
|
|
||||||
The Musixmatch API used to require a free account on <https://www.musixmatch.com> to be
|
|
||||||
used. However, as of 2024, this requirement was removed and the API can be used
|
|
||||||
anonymously. The CLI still allows you to supply credentials if Musixmatch decides to
|
|
||||||
close the API down again.
|
|
||||||
|
|
||||||
### Get lyrics
|
|
||||||
|
|
||||||
```txt
|
|
||||||
musixmatch-cli lyrics -n shine -a spektrem
|
|
||||||
Lyrics ID: 34583240
|
|
||||||
Language: en
|
|
||||||
Copyright: Writer(s): Jesse Warren
|
|
||||||
Copyright: Ncs Music
|
|
||||||
|
|
||||||
Eyes in the sky gazing far into the night
|
|
||||||
I raise my hand to the fire, but it's no use
|
|
||||||
'Cause you can't stop it from shining through
|
|
||||||
It's true
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get translated lyrics
|
|
||||||
|
|
||||||
Musixmatch also offers translated lyrics. You have to select a language using the
|
|
||||||
`--lang` flag. You can also set the `--bi` flag to output both the original and
|
|
||||||
translated lines.
|
|
||||||
|
|
||||||
```txt
|
|
||||||
musixmatch-cli lyrics -n shine -a spektrem --lang de --bi
|
|
||||||
Lyrics ID: 34583240
|
|
||||||
Language: en
|
|
||||||
Copyright: Writer(s): Jesse Warren
|
|
||||||
Copyright: Ncs Music
|
|
||||||
Translated to: de
|
|
||||||
|
|
||||||
Eyes in the sky gazing far into the night
|
|
||||||
> Augen starren in die weite Nacht
|
|
||||||
I raise my hand to the fire, but it's no use
|
|
||||||
> Ich hebe meine Hand in das Feuer, doch ihr geschieht nichts
|
|
||||||
'Cause you can't stop it from shining through
|
|
||||||
> Denn du kannst es nicht daran hindern, hindurch zu scheinen
|
|
||||||
It's true
|
|
||||||
> Es ist wahr
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get subtitles (synchronized lyrics)
|
|
||||||
|
|
||||||
For most lyrics Musixmatch provides timestamps for the individual lines so you can
|
|
||||||
display them in sync during playback.
|
|
||||||
|
|
||||||
Musixmatch offers multiple subtitle formats you can select using the `--format` flag.
|
|
||||||
The available formats are: `lrc`, `ttml`, `ttml-structured`, `json`, `ebu-stl`
|
|
||||||
|
|
||||||
```txt
|
|
||||||
musixmatch-cli subtitles -n shine -a spektrem
|
|
||||||
Subtitle ID: 35340319
|
|
||||||
Language: en
|
|
||||||
Length: 316
|
|
||||||
Copyright: Writer(s): Jesse Warren
|
|
||||||
Copyright: Ncs Music
|
|
||||||
|
|
||||||
[00:59.84] Eyes in the sky gazing far into the night
|
|
||||||
[01:06.55] I raise my hand to the fire, but it's no use
|
|
||||||
[01:11.97] 'Cause you can't stop it from shining through
|
|
||||||
[01:16.07] It's true
|
|
||||||
...
|
|
||||||
```
|
|
173
cli/src/main.rs
173
cli/src/main.rs
|
@ -1,6 +1,3 @@
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![warn(missing_docs, clippy::todo)]
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdin, stdout, Write},
|
io::{stdin, stdout, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -8,8 +5,9 @@ use std::{
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use id3::{Tag, TagLike};
|
||||||
use musixmatch_inofficial::{
|
use musixmatch_inofficial::{
|
||||||
models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap},
|
models::{SubtitleFormat, Track, TrackId, TranslationMap},
|
||||||
Musixmatch,
|
Musixmatch,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,7 +20,18 @@ struct Cli {
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Get lyrics text
|
Get {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: GetCommands,
|
||||||
|
},
|
||||||
|
Mp3 {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: FileCommands,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum GetCommands {
|
||||||
Lyrics {
|
Lyrics {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
ident: TrackIdentifiers,
|
ident: TrackIdentifiers,
|
||||||
|
@ -33,55 +42,26 @@ enum Commands {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
bi: bool,
|
bi: bool,
|
||||||
},
|
},
|
||||||
/// Get subtitles (time-synced lyrics)
|
|
||||||
Subtitles {
|
Subtitles {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
ident: TrackIdentifiers,
|
ident: TrackIdentifiers,
|
||||||
/// Track length
|
/// Track length
|
||||||
#[clap(short, long)]
|
#[clap(long, short)]
|
||||||
length: Option<f32>,
|
length: Option<f32>,
|
||||||
/// Maximum deviation from track length (Default: 1s)
|
/// Maximum deviation from track length (Default: 1s)
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
max_deviation: Option<f32>,
|
max_deviation: Option<f32>,
|
||||||
/// Subtitle format
|
/// Subtitle format
|
||||||
#[clap(short, long, default_value = "lrc")]
|
#[clap(long, default_value = "lrc")]
|
||||||
format: SubtitleFormatClap,
|
format: SubtitleFormatClap,
|
||||||
/// Language
|
/// Language
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
lang: Option<String>,
|
lang: Option<String>,
|
||||||
},
|
},
|
||||||
/// Get track metadata
|
|
||||||
Track {
|
Track {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
ident: TrackIdentifiers,
|
ident: TrackIdentifiers,
|
||||||
},
|
},
|
||||||
/// Get album metadata
|
|
||||||
Album {
|
|
||||||
#[clap(flatten)]
|
|
||||||
ident: AlbumArtistIdentifiers,
|
|
||||||
},
|
|
||||||
/// Get artist metadata
|
|
||||||
Artist {
|
|
||||||
#[clap(flatten)]
|
|
||||||
ident: AlbumArtistIdentifiers,
|
|
||||||
},
|
|
||||||
/// Search for Musixmatch tracks
|
|
||||||
#[group(required = true)]
|
|
||||||
Search {
|
|
||||||
/// Track name
|
|
||||||
#[clap(short, long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// Artist
|
|
||||||
#[clap(short, long)]
|
|
||||||
artist: Option<String>,
|
|
||||||
/// Lyrics
|
|
||||||
#[clap(short, long)]
|
|
||||||
lyrics: Option<String>,
|
|
||||||
/// Search query
|
|
||||||
query: Option<Vec<String>>,
|
|
||||||
},
|
|
||||||
/// Search for Musixmatch artists
|
|
||||||
SearchArtist { query: Vec<String> },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
@ -112,38 +92,22 @@ struct TrackIdentifiers {
|
||||||
isrc: Option<String>,
|
isrc: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
#[group(multiple = false)]
|
|
||||||
struct AlbumArtistIdentifiers {
|
|
||||||
/// Musixmatch-ID
|
|
||||||
#[clap(long)]
|
|
||||||
mxm_id: Option<u64>,
|
|
||||||
/// Musicbrainz-ID
|
|
||||||
#[clap(long)]
|
|
||||||
musicbrainz: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum FileCommands {
|
enum FileCommands {
|
||||||
/// Get lyrics text
|
|
||||||
Lyrics {
|
Lyrics {
|
||||||
/// Music file
|
/// Music file
|
||||||
#[clap(value_parser)]
|
#[clap(value_parser)]
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
},
|
},
|
||||||
/// Get subtitles (time-synced lyrics)
|
|
||||||
Subtitles {
|
Subtitles {
|
||||||
/// Music file
|
/// Music file
|
||||||
#[clap(value_parser)]
|
#[clap(value_parser)]
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
/// Subtitle format
|
|
||||||
#[clap(short, long, default_value = "lrc")]
|
|
||||||
format: SubtitleFormatClap,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::ValueEnum, Debug, Copy, Clone)]
|
#[derive(clap::ValueEnum, Debug, Copy, Clone)]
|
||||||
enum SubtitleFormatClap {
|
pub enum SubtitleFormatClap {
|
||||||
Lrc,
|
Lrc,
|
||||||
Ttml,
|
Ttml,
|
||||||
TtmlStructured,
|
TtmlStructured,
|
||||||
|
@ -197,7 +161,8 @@ async fn run(cli: Cli) -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Lyrics { ident, lang, bi } => {
|
Commands::Get { command } => match command {
|
||||||
|
GetCommands::Lyrics { ident, lang, bi } => {
|
||||||
let track_id = get_track_id(ident, &mxm).await?;
|
let track_id = get_track_id(ident, &mxm).await?;
|
||||||
let lyrics = mxm.track_lyrics(track_id.clone()).await?;
|
let lyrics = mxm.track_lyrics(track_id.clone()).await?;
|
||||||
|
|
||||||
|
@ -221,7 +186,9 @@ async fn run(cli: Cli) -> Result<()> {
|
||||||
if Some(&lang) != lyrics.lyrics_language.as_ref() {
|
if Some(&lang) != lyrics.lyrics_language.as_ref() {
|
||||||
let tl = mxm.track_lyrics_translation(track_id, &lang).await?;
|
let tl = mxm.track_lyrics_translation(track_id, &lang).await?;
|
||||||
if tl.is_empty() {
|
if tl.is_empty() {
|
||||||
eprintln!("Translation not found. Returning lyrics in original language.");
|
eprintln!(
|
||||||
|
"Translation not found. Returning lyrics in original language."
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Translated to: {}", tl.lang);
|
eprintln!("Translated to: {}", tl.lang);
|
||||||
let tm = TranslationMap::from(tl);
|
let tm = TranslationMap::from(tl);
|
||||||
|
@ -248,7 +215,7 @@ async fn run(cli: Cli) -> Result<()> {
|
||||||
eprintln!();
|
eprintln!();
|
||||||
println!("{}", lyrics_body);
|
println!("{}", lyrics_body);
|
||||||
}
|
}
|
||||||
Commands::Subtitles {
|
GetCommands::Subtitles {
|
||||||
ident,
|
ident,
|
||||||
length,
|
length,
|
||||||
max_deviation,
|
max_deviation,
|
||||||
|
@ -313,77 +280,45 @@ async fn run(cli: Cli) -> Result<()> {
|
||||||
println!("{}", subtitles.subtitle_body);
|
println!("{}", subtitles.subtitle_body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Track { ident } => {
|
GetCommands::Track { ident } => {
|
||||||
let track = get_track(ident, &mxm).await?;
|
let track = get_track(ident, &mxm).await?;
|
||||||
println!("{}", serde_json::to_string_pretty(&track)?)
|
println!("{}", serde_json::to_string_pretty(&track)?)
|
||||||
}
|
}
|
||||||
Commands::Album { ident } => {
|
},
|
||||||
let id = if let Some(id) = ident.mxm_id {
|
Commands::Mp3 { command } => match command {
|
||||||
AlbumId::AlbumId(id)
|
FileCommands::Lyrics { file } => {
|
||||||
} else if let Some(mb) = &ident.musicbrainz {
|
let tag = Tag::read_from_path(&file)?;
|
||||||
AlbumId::Musicbrainz(mb)
|
|
||||||
} else {
|
let title = tag.title().ok_or(anyhow!("no title"))?;
|
||||||
bail!("no album ID specified")
|
let artist = tag.artist().ok_or(anyhow!("no artist"))?;
|
||||||
};
|
|
||||||
let album = mxm.album(id).await?;
|
let lyrics = mxm.matcher_lyrics(title, artist).await?;
|
||||||
println!("{}", serde_json::to_string_pretty(&album)?)
|
|
||||||
}
|
|
||||||
Commands::Artist { ident } => {
|
|
||||||
let id = if let Some(id) = ident.mxm_id {
|
|
||||||
ArtistId::ArtistId(id)
|
|
||||||
} else if let Some(mb) = &ident.musicbrainz {
|
|
||||||
ArtistId::Musicbrainz(mb)
|
|
||||||
} else {
|
|
||||||
bail!("no artist ID specified")
|
|
||||||
};
|
|
||||||
let album = mxm.artist(id).await?;
|
|
||||||
println!("{}", serde_json::to_string_pretty(&album)?)
|
|
||||||
}
|
|
||||||
Commands::Search {
|
|
||||||
query,
|
|
||||||
name,
|
|
||||||
artist,
|
|
||||||
lyrics,
|
|
||||||
} => {
|
|
||||||
let mut sb = mxm
|
|
||||||
.track_search()
|
|
||||||
.s_track_rating(musixmatch_inofficial::models::SortOrder::Desc);
|
|
||||||
let querystr;
|
|
||||||
if let Some(q) = &query {
|
|
||||||
querystr = q.join(" ");
|
|
||||||
sb = sb.q(&querystr);
|
|
||||||
}
|
|
||||||
if let Some(n) = &name {
|
|
||||||
sb = sb.q_track(n);
|
|
||||||
}
|
|
||||||
if let Some(a) = &artist {
|
|
||||||
sb = sb.q_artist(a);
|
|
||||||
}
|
|
||||||
if let Some(l) = &lyrics {
|
|
||||||
sb = sb.q_lyrics(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tracks = sb.send(20, 0).await?;
|
|
||||||
for t in tracks {
|
|
||||||
println!(
|
println!(
|
||||||
"{} - {} ({}) ISRC'{}' <https://musixmatch.com/lyrics/{}>",
|
"Lyrics for {} by {}:\n\n{}",
|
||||||
t.track_name,
|
title, artist, lyrics.lyrics_body
|
||||||
t.artist_name,
|
|
||||||
t.first_release_date.map(|d| d.year()).unwrap_or_default(),
|
|
||||||
t.track_isrc.unwrap_or_default(),
|
|
||||||
t.commontrack_vanity_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Commands::SearchArtist { query } => {
|
|
||||||
let artists = mxm.artist_search(&query.join(" "), 20, 0).await?;
|
|
||||||
for a in artists {
|
|
||||||
println!(
|
|
||||||
"{} <https://musixmatch.com/artist/{}>",
|
|
||||||
a.artist_name, a.artist_vanity_id
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
FileCommands::Subtitles { file } => {
|
||||||
|
let tag = Tag::read_from_path(&file)?;
|
||||||
|
let duration = mp3_duration::from_path(&file)?;
|
||||||
|
|
||||||
|
let title = tag.title().ok_or(anyhow!("no title"))?;
|
||||||
|
let artist = tag.artist().ok_or(anyhow!("no artist"))?;
|
||||||
|
|
||||||
|
let subtitles = mxm
|
||||||
|
.matcher_subtitle(
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
SubtitleFormat::Lrc,
|
||||||
|
Some(duration.as_secs_f32()),
|
||||||
|
Some(1.0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("{}", subtitles.subtitle_body);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
100
cliff.toml
100
cliff.toml
|
@ -1,100 +0,0 @@
|
||||||
# git-cliff ~ default configuration file
|
|
||||||
# https://git-cliff.org/docs/configuration
|
|
||||||
#
|
|
||||||
# Lines starting with "#" are comments.
|
|
||||||
# Configuration options are organized into tables and keys.
|
|
||||||
# See documentation for more information on available options.
|
|
||||||
|
|
||||||
[changelog]
|
|
||||||
# changelog header
|
|
||||||
header = """
|
|
||||||
# Changelog\n
|
|
||||||
All notable changes to this project will be documented in this file.\n
|
|
||||||
"""
|
|
||||||
# template for the changelog body
|
|
||||||
# https://keats.github.io/tera/docs/#introduction
|
|
||||||
body = """
|
|
||||||
{% set repo_url = "https://codeberg.org/ThetaDev/musixmatch-inofficial" %}\
|
|
||||||
{% if version %}\
|
|
||||||
{%set vname = version | split(pat="/") | last %}
|
|
||||||
{%if previous.version %}\
|
|
||||||
## [{{ vname }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\
|
|
||||||
{% else %}\
|
|
||||||
## [{{ vname }}]({{ repo_url }}/commits/tag/{{ version }})\
|
|
||||||
{% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
||||||
{% else %}\
|
|
||||||
## [unreleased]
|
|
||||||
{% endif %}\
|
|
||||||
{% if previous.version %}\
|
|
||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
### {{ group | striptags | trim | upper_first }}
|
|
||||||
{% for commit in commits %}
|
|
||||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
|
||||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
|
||||||
{{ commit.message | upper_first }} - \
|
|
||||||
([{{ commit.id | truncate(length=7, end="") }}]({{ repo_url }}/commit/{{ commit.id }}))\
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}\
|
|
||||||
{% else %}
|
|
||||||
Initial release
|
|
||||||
{% endif %}\n
|
|
||||||
"""
|
|
||||||
# template for the changelog footer
|
|
||||||
footer = """
|
|
||||||
<!-- generated by git-cliff -->
|
|
||||||
"""
|
|
||||||
# remove the leading and trailing s
|
|
||||||
trim = true
|
|
||||||
# postprocessors
|
|
||||||
postprocessors = [
|
|
||||||
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
|
|
||||||
]
|
|
||||||
|
|
||||||
[git]
|
|
||||||
# parse the commits based on https://www.conventionalcommits.org
|
|
||||||
conventional_commits = true
|
|
||||||
# filter out the commits that are not conventional
|
|
||||||
filter_unconventional = true
|
|
||||||
# process each line of a commit as an individual commit
|
|
||||||
split_commits = false
|
|
||||||
# regex for preprocessing the commit messages
|
|
||||||
commit_preprocessors = [
|
|
||||||
# Replace issue numbers
|
|
||||||
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
|
||||||
# Check spelling of the commit with https://github.com/crate-ci/typos
|
|
||||||
# If the spelling is incorrect, it will be automatically fixed.
|
|
||||||
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
|
||||||
]
|
|
||||||
# regex for parsing and grouping commits
|
|
||||||
commit_parsers = [
|
|
||||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
|
||||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
|
||||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
|
||||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
|
||||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
|
||||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
|
||||||
{ message = "^test", skip = true },
|
|
||||||
{ message = "^chore\\(release\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pr\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pull\\)", skip = true },
|
|
||||||
{ message = "^chore", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
|
||||||
{ message = "^ci", skip = true },
|
|
||||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
|
||||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
|
||||||
]
|
|
||||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
|
||||||
protect_breaking_commits = false
|
|
||||||
# filter out the commits that are not matched by commit parsers
|
|
||||||
filter_commits = false
|
|
||||||
# regex for matching git tags
|
|
||||||
# tag_pattern = "v[0-9].*"
|
|
||||||
# regex for skipping tags
|
|
||||||
# skip_tags = ""
|
|
||||||
# regex for ignoring tags
|
|
||||||
# ignore_tags = ""
|
|
||||||
# sort the tags topologically
|
|
||||||
topo_order = false
|
|
||||||
# sort the commits inside sections by oldest/newest order
|
|
||||||
sort_commits = "oldest"
|
|
||||||
# limit the number of commits included in the changelog.
|
|
||||||
# limit_commits = 42
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:best-practices"
|
|
||||||
],
|
|
||||||
"semanticCommits": "enabled",
|
|
||||||
"automerge": true,
|
|
||||||
"automergeStrategy": "squash",
|
|
||||||
"osvVulnerabilityAlerts": true,
|
|
||||||
"labels": ["dependency-upgrade"],
|
|
||||||
"enabledManagers": ["cargo"],
|
|
||||||
"prHourlyLimit": 5
|
|
||||||
}
|
|
111
src/api_model.rs
111
src/api_model.rs
|
@ -1,9 +1,6 @@
|
||||||
use std::{marker::PhantomData, str::FromStr};
|
use std::{marker::PhantomData, str::FromStr};
|
||||||
|
|
||||||
use serde::{
|
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
|
||||||
de::{DeserializeOwned, Visitor},
|
|
||||||
Deserialize, Deserializer, Serialize,
|
|
||||||
};
|
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::error::{Error, Result as MxmResult};
|
use crate::error::{Error, Result as MxmResult};
|
||||||
|
@ -12,17 +9,24 @@ use crate::error::{Error, Result as MxmResult};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Resp<T> {
|
pub struct Resp<T> {
|
||||||
pub message: T,
|
pub message: Message<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct HeaderMsg {
|
pub struct Message<T> {
|
||||||
pub header: Header,
|
pub header: Header,
|
||||||
|
pub body: Option<MessageBody<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BodyMsg<T> {
|
#[serde(untagged)]
|
||||||
pub body: T,
|
pub enum MessageBody<T> {
|
||||||
|
Some(T),
|
||||||
|
// "body": []
|
||||||
|
EmptyArr(Vec<()>),
|
||||||
|
// "body": {}
|
||||||
|
EmptyObj {},
|
||||||
|
EmptyStr(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -33,25 +37,31 @@ pub struct Header {
|
||||||
pub hint: String,
|
pub hint: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_body<T: DeserializeOwned>(response: &str) -> MxmResult<T> {
|
impl<T> Resp<T> {
|
||||||
let header = serde_json::from_str::<Resp<HeaderMsg>>(response)?
|
pub fn body_or_err(self) -> MxmResult<T> {
|
||||||
.message
|
match (self.message.body, self.message.header.status_code < 400) {
|
||||||
.header;
|
(Some(MessageBody::Some(body)), true) => Ok(body),
|
||||||
if header.status_code < 400 {
|
(_, true) => Err(Error::NoData),
|
||||||
let body = serde_json::from_str::<Resp<BodyMsg<T>>>(response)?;
|
(_, false) => {
|
||||||
Ok(body.message.body)
|
if self.message.header.status_code == 404 {
|
||||||
} else if header.status_code == 404 {
|
|
||||||
Err(Error::NotFound)
|
Err(Error::NotFound)
|
||||||
} else if header.status_code == 401 && header.hint == "renew" {
|
} else if self.message.header.status_code == 401
|
||||||
|
&& self.message.header.hint == "renew"
|
||||||
|
{
|
||||||
Err(Error::TokenExpired)
|
Err(Error::TokenExpired)
|
||||||
} else if header.status_code == 401 && header.hint == "captcha" {
|
} else if self.message.header.status_code == 401
|
||||||
|
&& self.message.header.hint == "captcha"
|
||||||
|
{
|
||||||
Err(Error::Ratelimit)
|
Err(Error::Ratelimit)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::MusixmatchError {
|
Err(Error::MusixmatchError {
|
||||||
status_code: header.status_code,
|
status_code: self.message.header.status_code,
|
||||||
msg: header.hint,
|
msg: self.message.header.hint,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//#SESSION
|
//#SESSION
|
||||||
|
@ -99,8 +109,8 @@ pub enum LoginCredential {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
// pub id: String,
|
pub id: String,
|
||||||
// pub email: String,
|
pub email: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,22 +565,57 @@ mod tests {
|
||||||
let json =
|
let json =
|
||||||
r#"{"message":{"header":{"status_code":401,"execute_time":0.002,"hint":"fsck"}}}"#;
|
r#"{"message":{"header":{"status_code":401,"execute_time":0.002,"hint":"fsck"}}}"#;
|
||||||
|
|
||||||
let err = parse_body::<SubtitleBody>(json).unwrap_err();
|
let res = serde_json::from_str::<Resp<SubtitleBody>>(json).unwrap();
|
||||||
|
|
||||||
if let Error::MusixmatchError { status_code, msg } = err {
|
assert_eq!(res.message.header.status_code, 401);
|
||||||
assert_eq!(status_code, 401);
|
assert_eq!(res.message.header.hint, "fsck");
|
||||||
assert_eq!(msg, "fsck");
|
assert!(res.message.body.is_none());
|
||||||
} else {
|
|
||||||
panic!("invalid error: {err}");
|
let err = res.body_or_err().unwrap_err();
|
||||||
}
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"Error 401 returned by the Musixmatch API. Message: 'fsck'"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_body() {
|
fn deserialize_emptyarr_body() {
|
||||||
let json = r#"{"message":{"header":{"status_code":200,"execute_time":0.002},"body":"Hello World"}}"#;
|
let json = r#"{"message":{"header":{"status_code":403,"execute_time":0.0056290626525879},"body":[]}}"#;
|
||||||
|
|
||||||
let res = parse_body::<String>(json).unwrap();
|
let res = serde_json::from_str::<Resp<SubtitleBody>>(json).unwrap();
|
||||||
assert_eq!(res, "Hello World");
|
|
||||||
|
assert_eq!(res.message.header.status_code, 403);
|
||||||
|
assert_eq!(res.message.header.hint, "");
|
||||||
|
assert!(matches!(
|
||||||
|
res.message.body.as_ref().unwrap(),
|
||||||
|
MessageBody::EmptyArr(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
let err = res.body_or_err().unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"Error 403 returned by the Musixmatch API. Message: ''"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_emptyobj_body() {
|
||||||
|
let json = r#"{"message":{"header":{"status_code":403,"execute_time":0.0056290626525879},"body":{}}}"#;
|
||||||
|
|
||||||
|
let res = serde_json::from_str::<Resp<SubtitleBody>>(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(res.message.header.status_code, 403);
|
||||||
|
assert_eq!(res.message.header.hint, "");
|
||||||
|
assert!(matches!(
|
||||||
|
res.message.body.as_ref().unwrap(),
|
||||||
|
MessageBody::EmptyObj {}
|
||||||
|
));
|
||||||
|
|
||||||
|
let err = res.body_or_err().unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"Error 403 returned by the Musixmatch API. Message: ''"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -12,7 +12,7 @@ impl Musixmatch {
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/album-get>
|
/// <https://developer.musixmatch.com/documentation/api-reference/album-get>
|
||||||
pub async fn album(&self, id: AlbumId<'_>) -> Result<Album> {
|
pub async fn album(&self, id: AlbumId<'_>) -> Result<Album> {
|
||||||
let mut url = self.new_url("album.get");
|
let mut url = self.new_url("album.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ impl Musixmatch {
|
||||||
page_size: u8,
|
page_size: u8,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<Album>> {
|
) -> Result<Vec<Album>> {
|
||||||
let mut url = self.new_url("artist.albums.get");
|
let mut url = self.new_url("artist.albums.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ impl Musixmatch {
|
||||||
page_size: u8,
|
page_size: u8,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<Album>> {
|
) -> Result<Vec<Album>> {
|
||||||
let mut url = self.new_url("chart.albums.get");
|
let mut url = self.new_url("chart.albums.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ impl Musixmatch {
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/artist-get>
|
/// <https://developer.musixmatch.com/documentation/api-reference/artist-get>
|
||||||
pub async fn artist(&self, id: ArtistId<'_>) -> Result<Artist> {
|
pub async fn artist(&self, id: ArtistId<'_>) -> Result<Artist> {
|
||||||
let mut url = self.new_url("artist.get");
|
let mut url = self.new_url("artist.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ impl Musixmatch {
|
||||||
page_size: u8,
|
page_size: u8,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<Artist>> {
|
) -> Result<Vec<Artist>> {
|
||||||
let mut url = self.new_url("artist.related.get");
|
let mut url = self.new_url("artist.related.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ impl Musixmatch {
|
||||||
page_size: u8,
|
page_size: u8,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<Artist>> {
|
) -> Result<Vec<Artist>> {
|
||||||
let mut url = self.new_url("artist.search");
|
let mut url = self.new_url("artist.search")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ impl Musixmatch {
|
||||||
page_size: u8,
|
page_size: u8,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<Artist>> {
|
) -> Result<Vec<Artist>> {
|
||||||
let mut url = self.new_url("chart.artists.get");
|
let mut url = self.new_url("chart.artists.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ impl Musixmatch {
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-lyrics-get>
|
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-lyrics-get>
|
||||||
pub async fn matcher_lyrics(&self, q_track: &str, q_artist: &str) -> Result<Lyrics> {
|
pub async fn matcher_lyrics(&self, q_track: &str, q_artist: &str) -> Result<Lyrics> {
|
||||||
let mut url = self.new_url("matcher.lyrics.get");
|
let mut url = self.new_url("matcher.lyrics.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
if !q_track.is_empty() {
|
if !q_track.is_empty() {
|
||||||
|
@ -39,7 +39,7 @@ impl Musixmatch {
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/track-lyrics-get>
|
/// <https://developer.musixmatch.com/documentation/api-reference/track-lyrics-get>
|
||||||
pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result<Lyrics> {
|
pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result<Lyrics> {
|
||||||
let mut url = self.new_url("track.lyrics.get");
|
let mut url = self.new_url("track.lyrics.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
let id_param = id.to_param();
|
let id_param = id.to_param();
|
||||||
|
@ -66,7 +66,7 @@ impl Musixmatch {
|
||||||
id: TrackId<'_>,
|
id: TrackId<'_>,
|
||||||
selected_language: &str,
|
selected_language: &str,
|
||||||
) -> Result<TranslationList> {
|
) -> Result<TranslationList> {
|
||||||
let mut url = self.new_url("crowd.track.translations.get");
|
let mut url = self.new_url("crowd.track.translations.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
let id_param = id.to_param();
|
let id_param = id.to_param();
|
||||||
|
|
|
@ -16,7 +16,7 @@ impl Musixmatch {
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/track-snippet-get>
|
/// <https://developer.musixmatch.com/documentation/api-reference/track-snippet-get>
|
||||||
pub async fn track_snippet(&self, id: TrackId<'_>) -> Result<Snippet> {
|
pub async fn track_snippet(&self, id: TrackId<'_>) -> Result<Snippet> {
|
||||||
let mut url = self.new_url("track.snippet.get");
|
let mut url = self.new_url("track.snippet.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ impl Musixmatch {
|
||||||
f_subtitle_length: Option<f32>,
|
f_subtitle_length: Option<f32>,
|
||||||
f_subtitle_length_max_deviation: Option<f32>,
|
f_subtitle_length_max_deviation: Option<f32>,
|
||||||
) -> Result<Subtitle> {
|
) -> Result<Subtitle> {
|
||||||
let mut url = self.new_url("matcher.subtitle.get");
|
let mut url = self.new_url("matcher.subtitle.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
if !q_track.is_empty() {
|
if !q_track.is_empty() {
|
||||||
|
@ -73,7 +73,7 @@ impl Musixmatch {
|
||||||
f_subtitle_length: Option<f32>,
|
f_subtitle_length: Option<f32>,
|
||||||
f_subtitle_length_max_deviation: Option<f32>,
|
f_subtitle_length_max_deviation: Option<f32>,
|
||||||
) -> Result<Subtitle> {
|
) -> Result<Subtitle> {
|
||||||
let mut url = self.new_url("track.subtitle.get");
|
let mut url = self.new_url("track.subtitle.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ impl Musixmatch {
|
||||||
translation_status: bool,
|
translation_status: bool,
|
||||||
lang_3c: bool,
|
lang_3c: bool,
|
||||||
) -> Result<Track> {
|
) -> Result<Track> {
|
||||||
let mut url = self.new_url("matcher.track.get");
|
let mut url = self.new_url("matcher.track.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ impl Musixmatch {
|
||||||
translation_status: bool,
|
translation_status: bool,
|
||||||
lang_3c: bool,
|
lang_3c: bool,
|
||||||
) -> Result<Track> {
|
) -> Result<Track> {
|
||||||
let mut url = self.new_url("track.get");
|
let mut url = self.new_url("track.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ impl Musixmatch {
|
||||||
page_size: u8,
|
page_size: u8,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<Track>> {
|
) -> Result<Vec<Track>> {
|
||||||
let mut url = self.new_url("album.tracks.get");
|
let mut url = self.new_url("album.tracks.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ impl Musixmatch {
|
||||||
page_size: u8,
|
page_size: u8,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<Track>> {
|
) -> Result<Vec<Track>> {
|
||||||
let mut url = self.new_url("chart.tracks.get");
|
let mut url = self.new_url("chart.tracks.get")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ impl Musixmatch {
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// <https://developer.musixmatch.com/documentation/api-reference/music-genres-get>
|
/// <https://developer.musixmatch.com/documentation/api-reference/music-genres-get>
|
||||||
pub async fn genres(&self) -> Result<Vec<Genre>> {
|
pub async fn genres(&self) -> Result<Vec<Genre>> {
|
||||||
let url = self.new_url("music.genres.get");
|
let url = self.new_url("music.genres.get")?;
|
||||||
let genres = self.execute_get_request::<Genres>(&url).await?;
|
let genres = self.execute_get_request::<Genres>(&url).await?;
|
||||||
Ok(genres.music_genre_list)
|
Ok(genres.music_genre_list)
|
||||||
}
|
}
|
||||||
|
@ -347,7 +347,7 @@ impl<'a> TrackSearchQuery<'a> {
|
||||||
/// - `page_size`: Define the page size for paginated results. Range is 1 to 100.
|
/// - `page_size`: Define the page size for paginated results. Range is 1 to 100.
|
||||||
/// - `page`: Define the page number for paginated results, starting from 1.
|
/// - `page`: Define the page number for paginated results, starting from 1.
|
||||||
pub async fn send(&self, page_size: u8, page: u32) -> Result<Vec<Track>> {
|
pub async fn send(&self, page_size: u8, page: u32) -> Result<Vec<Track>> {
|
||||||
let mut url = self.mxm.new_url("track.search");
|
let mut url = self.mxm.new_url("track.search")?;
|
||||||
{
|
{
|
||||||
let mut url_query = url.query_pairs_mut();
|
let mut url_query = url.query_pairs_mut();
|
||||||
|
|
||||||
|
|
15
src/error.rs
15
src/error.rs
|
@ -20,6 +20,9 @@ pub enum Error {
|
||||||
/// Error message
|
/// Error message
|
||||||
msg: String,
|
msg: String,
|
||||||
},
|
},
|
||||||
|
/// Musixmatch returned no data or the data that could not be deserialized
|
||||||
|
#[error("Musixmatch returned no data or data that could not be deserialized")]
|
||||||
|
NoData,
|
||||||
/// Client requires credentials, but none were given
|
/// Client requires credentials, but none were given
|
||||||
#[error("You did not input credentials")]
|
#[error("You did not input credentials")]
|
||||||
MissingCredentials,
|
MissingCredentials,
|
||||||
|
@ -32,12 +35,12 @@ pub enum Error {
|
||||||
/// Musixmatch content not available
|
/// Musixmatch content not available
|
||||||
#[error("Unfortunately we're not authorized to show these lyrics")]
|
#[error("Unfortunately we're not authorized to show these lyrics")]
|
||||||
NotAvailable,
|
NotAvailable,
|
||||||
/// Musixmatch returned no data or the data that could not be deserialized
|
|
||||||
#[error("JSON parsing error: {0}")]
|
|
||||||
InvalidData(Cow<'static, str>),
|
|
||||||
/// Error from the HTTP client
|
/// Error from the HTTP client
|
||||||
#[error("http error: {0}")]
|
#[error("http error: {0}")]
|
||||||
Http(reqwest::Error),
|
Http(reqwest::Error),
|
||||||
|
/// Unspecified error
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(Cow<'static, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::Error> for Error {
|
impl From<reqwest::Error> for Error {
|
||||||
|
@ -47,8 +50,8 @@ impl From<reqwest::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<serde_json::Error> for Error {
|
impl From<url::ParseError> for Error {
|
||||||
fn from(value: serde_json::Error) -> Self {
|
fn from(value: url::ParseError) -> Self {
|
||||||
Self::InvalidData(value.to_string().into())
|
Self::Other(format!("url parse error: {value}").into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
210
src/lib.rs
210
src/lib.rs
|
@ -8,6 +8,7 @@ pub mod models;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
@ -28,17 +29,36 @@ use time::macros::format_description;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::api_model::parse_body;
|
use crate::api_model::Resp;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
const YMD_FORMAT: &[time::format_description::FormatItem] =
|
const YMD_FORMAT: &[time::format_description::FormatItem] =
|
||||||
format_description!("[year][month][day]");
|
format_description!("[year][month][day]");
|
||||||
|
|
||||||
const APP_ID: &str = "android-player-v1.0";
|
/// Hardcoded client configuration
|
||||||
const API_URL: &str = "https://apic.musixmatch.com/ws/1.1/";
|
struct ClientCfg {
|
||||||
const SIGNATURE_SECRET: &[u8; 20] = b"967Pn4)N3&R_GBg5$b('";
|
app_id: &'static str,
|
||||||
|
api_url: &'static str,
|
||||||
|
signature_secret: &'static [u8; 20],
|
||||||
|
user_agent: &'static str,
|
||||||
|
login: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DESKTOP_CLIENT: ClientCfg = ClientCfg {
|
||||||
|
app_id: "web-desktop-app-v1.0",
|
||||||
|
api_url: "https://apic-desktop.musixmatch.com/ws/1.1/",
|
||||||
|
signature_secret: b"IEJ5E8XFaHQvIQNfs7IC",
|
||||||
|
user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Musixmatch/0.19.4 Chrome/58.0.3029.110 Electron/1.7.6 Safari/537.36",
|
||||||
|
login: false
|
||||||
|
};
|
||||||
|
const ANDROID_CLIENT: ClientCfg = ClientCfg {
|
||||||
|
app_id: "android-player-v1.0",
|
||||||
|
api_url: "https://apic.musixmatch.com/ws/1.1/",
|
||||||
|
signature_secret: b"967Pn4)N3&R_GBg5$b('",
|
||||||
|
user_agent: "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Build/T3B2.230316.003)",
|
||||||
|
login: true,
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_UA: &str = "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Build/T3B2.230316.003)";
|
|
||||||
const DEFAULT_BRAND: &str = "Google";
|
const DEFAULT_BRAND: &str = "Google";
|
||||||
const DEFAULT_DEVICE: &str = "Pixel 6";
|
const DEFAULT_DEVICE: &str = "Pixel 6";
|
||||||
|
|
||||||
|
@ -56,6 +76,7 @@ pub struct Musixmatch {
|
||||||
/// Used to construct a new [`Musixmatch`] client.#
|
/// Used to construct a new [`Musixmatch`] client.#
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct MusixmatchBuilder {
|
pub struct MusixmatchBuilder {
|
||||||
|
client_type: ClientType,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
brand: Option<String>,
|
brand: Option<String>,
|
||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
|
@ -63,6 +84,29 @@ pub struct MusixmatchBuilder {
|
||||||
credentials: Option<Credentials>,
|
credentials: Option<Credentials>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Musixmatch client type
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ClientType {
|
||||||
|
/// The desktop client is used with Musixmatch's electron-based Desktop application.
|
||||||
|
///
|
||||||
|
/// The client allows anonymous access and is currently the default option.
|
||||||
|
///
|
||||||
|
/// Since Musixmatch's desktop application is discontinued, the client may stop working in the future.
|
||||||
|
#[default]
|
||||||
|
Desktop,
|
||||||
|
/// The Android client requires a (free) Musixmatch account
|
||||||
|
Android,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ClientType> for ClientCfg {
|
||||||
|
fn from(value: ClientType) -> Self {
|
||||||
|
match value {
|
||||||
|
ClientType::Desktop => DESKTOP_CLIENT,
|
||||||
|
ClientType::Android => ANDROID_CLIENT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
enum DefaultOpt<T> {
|
enum DefaultOpt<T> {
|
||||||
Some(T),
|
Some(T),
|
||||||
|
@ -85,6 +129,8 @@ struct MusixmatchRef {
|
||||||
http: Client,
|
http: Client,
|
||||||
storage: Option<Box<dyn SessionStorage>>,
|
storage: Option<Box<dyn SessionStorage>>,
|
||||||
credentials: RwLock<Option<Credentials>>,
|
credentials: RwLock<Option<Credentials>>,
|
||||||
|
client_type: ClientType,
|
||||||
|
client_cfg: ClientCfg,
|
||||||
brand: String,
|
brand: String,
|
||||||
device: String,
|
device: String,
|
||||||
usertoken: Mutex<Option<String>>,
|
usertoken: Mutex<Option<String>>,
|
||||||
|
@ -98,6 +144,7 @@ struct Credentials {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct StoredSession {
|
struct StoredSession {
|
||||||
|
client_type: ClientType,
|
||||||
usertoken: String,
|
usertoken: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,8 +158,8 @@ impl MusixmatchBuilder {
|
||||||
|
|
||||||
/// Set the Musixmatch credentials
|
/// Set the Musixmatch credentials
|
||||||
///
|
///
|
||||||
/// The Musixmatch API required a free account on <https://www.musixmatch.com> to be
|
/// You have to create a free account on <https://www.musixmatch.com> to use
|
||||||
/// used. However, as of 2024, this requirement was removed.
|
/// the API.
|
||||||
///
|
///
|
||||||
/// The Musixmatch client can be constructed without any credentials.
|
/// The Musixmatch client can be constructed without any credentials.
|
||||||
/// In this case you rely on the stored session token to authenticate
|
/// In this case you rely on the stored session token to authenticate
|
||||||
|
@ -166,6 +213,12 @@ impl MusixmatchBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the client type (Desktop, Android) of the Musixmatch client
|
||||||
|
pub fn client_type(mut self, client_type: ClientType) -> Self {
|
||||||
|
self.client_type = client_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the device brand of the Musixmatch client
|
/// Set the device brand of the Musixmatch client
|
||||||
pub fn device_brand<S: Into<String>>(mut self, device_brand: S) -> Self {
|
pub fn device_brand<S: Into<String>>(mut self, device_brand: S) -> Self {
|
||||||
self.brand = Some(device_brand.into());
|
self.brand = Some(device_brand.into());
|
||||||
|
@ -186,13 +239,18 @@ impl MusixmatchBuilder {
|
||||||
/// Returns a new, configured Musixmatch client using a Reqwest client builder
|
/// Returns a new, configured Musixmatch client using a Reqwest client builder
|
||||||
pub fn build_with_client(self, client_builder: ClientBuilder) -> Result<Musixmatch> {
|
pub fn build_with_client(self, client_builder: ClientBuilder) -> Result<Musixmatch> {
|
||||||
let storage = self.storage.or_default(|| Box::<FileStorage>::default());
|
let storage = self.storage.or_default(|| Box::<FileStorage>::default());
|
||||||
let stored_session = Musixmatch::retrieve_session(&storage);
|
let stored_session =
|
||||||
|
Musixmatch::retrieve_session(&storage).filter(|s| s.client_type == self.client_type);
|
||||||
|
let client_cfg = ClientCfg::from(self.client_type);
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(header::COOKIE, "AWSELBCORS=0; AWSELB=0".parse().unwrap());
|
headers.insert(header::COOKIE, "AWSELBCORS=0; AWSELB=0".parse().unwrap());
|
||||||
|
|
||||||
let http = client_builder
|
let http = client_builder
|
||||||
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
|
.user_agent(
|
||||||
|
self.user_agent
|
||||||
|
.unwrap_or_else(|| client_cfg.user_agent.to_owned()),
|
||||||
|
)
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.default_headers(headers)
|
.default_headers(headers)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
@ -202,6 +260,8 @@ impl MusixmatchBuilder {
|
||||||
http,
|
http,
|
||||||
storage,
|
storage,
|
||||||
credentials: RwLock::new(self.credentials),
|
credentials: RwLock::new(self.credentials),
|
||||||
|
client_type: self.client_type,
|
||||||
|
client_cfg,
|
||||||
brand: self.brand.unwrap_or_else(|| DEFAULT_BRAND.to_owned()),
|
brand: self.brand.unwrap_or_else(|| DEFAULT_BRAND.to_owned()),
|
||||||
device: self.device.unwrap_or_else(|| DEFAULT_DEVICE.to_owned()),
|
device: self.device.unwrap_or_else(|| DEFAULT_DEVICE.to_owned()),
|
||||||
usertoken: Mutex::new(stored_session.map(|s| s.usertoken)),
|
usertoken: Mutex::new(stored_session.map(|s| s.usertoken)),
|
||||||
|
@ -235,24 +295,40 @@ impl Musixmatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let credentials = {
|
let credentials = if self.inner.client_cfg.login {
|
||||||
let c = self.inner.credentials.read().unwrap();
|
let c = self.inner.credentials.read().unwrap();
|
||||||
c.clone()
|
match c.deref() {
|
||||||
|
Some(c) => Some(c.clone()),
|
||||||
|
None => return Err(Error::MissingCredentials),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let guid = random_guid();
|
|
||||||
let adv_id = random_uuid();
|
|
||||||
|
|
||||||
// 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 base_url = format!("{}{}", self.inner.client_cfg.api_url, "token.get");
|
||||||
&format!("{}{}", API_URL, "token.get"),
|
let mut url = match self.inner.client_type {
|
||||||
|
ClientType::Desktop => Url::parse_with_params(
|
||||||
|
&base_url,
|
||||||
|
&[
|
||||||
|
("format", "json"),
|
||||||
|
("user_language", "en"),
|
||||||
|
("app_id", self.inner.client_cfg.app_id),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ClientType::Android => {
|
||||||
|
let guid = random_guid();
|
||||||
|
let adv_id = random_uuid();
|
||||||
|
Url::parse_with_params(
|
||||||
|
&base_url,
|
||||||
&[
|
&[
|
||||||
("adv_id", adv_id.as_str()),
|
("adv_id", adv_id.as_str()),
|
||||||
("root", "0"),
|
("root", "0"),
|
||||||
("sideloaded", "0"),
|
("sideloaded", "0"),
|
||||||
("app_id", "android-player-v1.0"),
|
("app_id", self.inner.client_cfg.app_id),
|
||||||
// App version (7.9.5)
|
// App version (7.9.5)
|
||||||
("build_number", "2022090901"),
|
("build_number", "2022090901"),
|
||||||
("guid", guid.as_str()),
|
("guid", guid.as_str()),
|
||||||
|
@ -263,14 +339,16 @@ impl Musixmatch {
|
||||||
now.format(&Rfc3339).unwrap_or_default().as_str(),
|
now.format(&Rfc3339).unwrap_or_default().as_str(),
|
||||||
),
|
),
|
||||||
("format", "json"),
|
("format", "json"),
|
||||||
|
("user_language", "en"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.unwrap();
|
}
|
||||||
sign_url_with_date(&mut url, now);
|
}?;
|
||||||
|
self.sign_url_with_date(&mut url, now);
|
||||||
|
|
||||||
let resp = self.inner.http.get(url).send().await?.error_for_status()?;
|
let resp = self.inner.http.get(url).send().await?.error_for_status()?;
|
||||||
let resp_txt = resp.text().await?;
|
let tdata = resp.json::<Resp<api_model::GetToken>>().await?;
|
||||||
let usertoken = parse_body::<api_model::GetToken>(&resp_txt)?.user_token;
|
let usertoken = tdata.body_or_err()?.user_token;
|
||||||
info!("Received new usertoken: {}****", &usertoken[0..8]);
|
info!("Received new usertoken: {}****", &usertoken[0..8]);
|
||||||
|
|
||||||
if let Some(credentials) = credentials {
|
if let Some(credentials) = credentials {
|
||||||
|
@ -288,8 +366,8 @@ impl Musixmatch {
|
||||||
usertoken: &str,
|
usertoken: &str,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
) -> Result<api_model::Account> {
|
) -> Result<api_model::Account> {
|
||||||
let mut url = new_url_from_token("credential.post", usertoken);
|
let mut url = self.new_url_from_token("credential.post", usertoken)?;
|
||||||
sign_url_with_date(&mut url, OffsetDateTime::now_utc());
|
self.sign_url_with_date(&mut url, OffsetDateTime::now_utc());
|
||||||
|
|
||||||
let api_credentials = api_model::Credentials {
|
let api_credentials = api_model::Credentials {
|
||||||
credential_list: &[api_model::CredentialWrap {
|
credential_list: &[api_model::CredentialWrap {
|
||||||
|
@ -311,14 +389,8 @@ impl Musixmatch {
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
|
|
||||||
let resp_txt = resp.text().await?;
|
let login = resp.json::<Resp<api_model::Login>>().await?.body_or_err()?;
|
||||||
let login = parse_body::<api_model::Login>(&resp_txt)?;
|
let credential = login.0.into_iter().next().ok_or(Error::NoData)?.credential;
|
||||||
let credential = login
|
|
||||||
.0
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::InvalidData("no credentials returned".into()))?
|
|
||||||
.credential;
|
|
||||||
|
|
||||||
match credential {
|
match credential {
|
||||||
api_model::LoginCredential::Account { account } => Ok(account),
|
api_model::LoginCredential::Account { account } => Ok(account),
|
||||||
|
@ -338,6 +410,7 @@ impl Musixmatch {
|
||||||
fn store_session(&self, usertoken: &str) {
|
fn store_session(&self, usertoken: &str) {
|
||||||
if let Some(storage) = &self.inner.storage {
|
if let Some(storage) = &self.inner.storage {
|
||||||
let to_store = StoredSession {
|
let to_store = StoredSession {
|
||||||
|
client_type: self.inner.client_type,
|
||||||
usertoken: usertoken.to_owned(),
|
usertoken: usertoken.to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -372,12 +445,12 @@ impl Musixmatch {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_url(&self, endpoint: &str) -> reqwest::Url {
|
fn new_url(&self, endpoint: &str) -> Result<reqwest::Url> {
|
||||||
Url::parse_with_params(
|
Url::parse_with_params(
|
||||||
&format!("{}{}", API_URL, endpoint),
|
&format!("{}{}", self.inner.client_cfg.api_url, endpoint),
|
||||||
&[("app_id", APP_ID), ("format", "json")],
|
&[("app_id", self.inner.client_cfg.app_id), ("format", "json")],
|
||||||
)
|
)
|
||||||
.unwrap()
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finish_url(&self, url: &mut Url, force_new_session: bool) -> Result<()> {
|
async fn finish_url(&self, url: &mut Url, force_new_session: bool) -> Result<()> {
|
||||||
|
@ -386,7 +459,7 @@ impl Musixmatch {
|
||||||
.append_pair("usertoken", &usertoken)
|
.append_pair("usertoken", &usertoken)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
sign_url_with_date(url, OffsetDateTime::now_utc());
|
self.sign_url_with_date(url, OffsetDateTime::now_utc());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,9 +477,9 @@ impl Musixmatch {
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
let resp_txt = resp.text().await?;
|
let resp_obj = resp.json::<Resp<T>>().await?;
|
||||||
|
|
||||||
match parse_body(&resp_txt) {
|
match resp_obj.body_or_err() {
|
||||||
Ok(body) => Ok(body),
|
Ok(body) => Ok(body),
|
||||||
Err(Error::TokenExpired) => {
|
Err(Error::TokenExpired) => {
|
||||||
info!("Usertoken expired, getting a new one");
|
info!("Usertoken expired, getting a new one");
|
||||||
|
@ -422,8 +495,7 @@ impl Musixmatch {
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
|
|
||||||
let resp_txt = resp.text().await?;
|
resp.json::<Resp<T>>().await?.body_or_err()
|
||||||
parse_body(&resp_txt)
|
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
|
@ -446,6 +518,33 @@ impl Musixmatch {
|
||||||
password: password.into(),
|
password: password.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_url_from_token(&self, endpoint: &str, usertoken: &str) -> Result<reqwest::Url> {
|
||||||
|
Url::parse_with_params(
|
||||||
|
&format!("{}{}", self.inner.client_cfg.api_url, endpoint),
|
||||||
|
&[
|
||||||
|
("app_id", self.inner.client_cfg.app_id),
|
||||||
|
("usertoken", usertoken),
|
||||||
|
("format", "json"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_url_with_date(&self, url: &mut Url, date: OffsetDateTime) {
|
||||||
|
let mut mac = Hmac::<Sha1>::new_from_slice(self.inner.client_cfg.signature_secret).unwrap();
|
||||||
|
|
||||||
|
mac.update(url.as_str().as_bytes());
|
||||||
|
mac.update(date.format(YMD_FORMAT).unwrap_or_default().as_bytes());
|
||||||
|
|
||||||
|
let sig = mac.finalize().into_bytes();
|
||||||
|
let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig) + "\n";
|
||||||
|
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("signature", &sig_b64)
|
||||||
|
.append_pair("signature_protocol", "sha1")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_guid() -> String {
|
fn random_guid() -> String {
|
||||||
|
@ -466,33 +565,6 @@ fn random_uuid() -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_url_from_token(endpoint: &str, usertoken: &str) -> reqwest::Url {
|
|
||||||
Url::parse_with_params(
|
|
||||||
&format!("{}{}", API_URL, endpoint),
|
|
||||||
&[
|
|
||||||
("app_id", APP_ID),
|
|
||||||
("usertoken", usertoken),
|
|
||||||
("format", "json"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign_url_with_date(url: &mut Url, date: OffsetDateTime) {
|
|
||||||
let mut mac = Hmac::<Sha1>::new_from_slice(SIGNATURE_SECRET).unwrap();
|
|
||||||
|
|
||||||
mac.update(url.as_str().as_bytes());
|
|
||||||
mac.update(date.format(YMD_FORMAT).unwrap_or_default().as_bytes());
|
|
||||||
|
|
||||||
let sig = mac.finalize().into_bytes();
|
|
||||||
let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig) + "\n";
|
|
||||||
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("signature", &sig_b64)
|
|
||||||
.append_pair("signature_protocol", "sha1")
|
|
||||||
.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use time::macros::datetime;
|
use time::macros::datetime;
|
||||||
|
@ -501,8 +573,12 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_sign_url() {
|
fn t_sign_url() {
|
||||||
|
let mxm = Musixmatch::builder()
|
||||||
|
.client_type(ClientType::Android)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let mut url = Url::parse("https://apic.musixmatch.com/ws/1.1/track.subtitle.get?app_id=android-player-v1.0&usertoken=22092860c49e8d783b569a7bd847cd5b289bbec306f8a0bb2d3771&format=json&track_spotify_id=7Ga0ByppmSXWuKXdsD8JGL&subtitle_format=mxm").unwrap();
|
let mut url = Url::parse("https://apic.musixmatch.com/ws/1.1/track.subtitle.get?app_id=android-player-v1.0&usertoken=22092860c49e8d783b569a7bd847cd5b289bbec306f8a0bb2d3771&format=json&track_spotify_id=7Ga0ByppmSXWuKXdsD8JGL&subtitle_format=mxm").unwrap();
|
||||||
sign_url_with_date(&mut url, datetime!(2022-09-28 0:00 UTC));
|
mxm.sign_url_with_date(&mut url, datetime!(2022-09-28 0:00 UTC));
|
||||||
assert_eq!(url.as_str(), "https://apic.musixmatch.com/ws/1.1/track.subtitle.get?app_id=android-player-v1.0&usertoken=22092860c49e8d783b569a7bd847cd5b289bbec306f8a0bb2d3771&format=json&track_spotify_id=7Ga0ByppmSXWuKXdsD8JGL&subtitle_format=mxm&signature=cvXbedVvGneT7o4k8QG6jfk9pAM%3D%0A&signature_protocol=sha1")
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,7 +226,7 @@ impl Subtitle {
|
||||||
/// Only works with [SubtitleFormat::Json].
|
/// Only works with [SubtitleFormat::Json].
|
||||||
pub fn to_lines(&self) -> Result<SubtitleLines> {
|
pub fn to_lines(&self) -> Result<SubtitleLines> {
|
||||||
Ok(SubtitleLines {
|
Ok(SubtitleLines {
|
||||||
lines: serde_json::from_str(&self.subtitle_body)?,
|
lines: serde_json::from_str(&self.subtitle_body).map_err(|_| Error::NoData)?,
|
||||||
lang: self.subtitle_language.to_owned(),
|
lang: self.subtitle_language.to_owned(),
|
||||||
length: self.subtitle_length,
|
length: self.subtitle_length,
|
||||||
})
|
})
|
||||||
|
@ -256,7 +256,7 @@ impl TryFrom<Subtitle> for SubtitleLines {
|
||||||
impl SubtitleLines {
|
impl SubtitleLines {
|
||||||
/// Convert subtitles into the [JSON](SubtitleFormat::Json) format
|
/// Convert subtitles into the [JSON](SubtitleFormat::Json) format
|
||||||
pub fn to_json(&self) -> Result<String> {
|
pub fn to_json(&self) -> Result<String> {
|
||||||
serde_json::to_string(&self).map_err(Error::from)
|
serde_json::to_string(&self).map_err(|_| Error::NoData)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert subtitles into the [LRC](SubtitleFormat::Lrc) format
|
/// Convert subtitles into the [LRC](SubtitleFormat::Lrc) format
|
||||||
|
|
428
tests/tests.rs
428
tests/tests.rs
|
@ -1,32 +1,32 @@
|
||||||
use std::{
|
use std::path::{Path, PathBuf};
|
||||||
num::NonZeroU32,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
|
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rstest::{fixture, rstest};
|
use rstest::rstest;
|
||||||
use time::macros::{date, datetime};
|
use time::macros::{date, datetime};
|
||||||
|
|
||||||
use musixmatch_inofficial::{
|
use musixmatch_inofficial::{
|
||||||
models::{AlbumId, ArtistId, TrackId},
|
models::{AlbumId, ArtistId, TrackId},
|
||||||
Error, Musixmatch,
|
ClientType, Error, Musixmatch,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
|
#[ctor::ctor]
|
||||||
path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name)
|
fn init() {
|
||||||
|
let _ = dotenvy::dotenv();
|
||||||
|
env_logger::init();
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(new_mxm().login())
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[fixture]
|
fn new_mxm() -> Musixmatch {
|
||||||
async fn mxm() -> Musixmatch {
|
let client_type = std::env::var("MUSIXMATCH_CLIENT")
|
||||||
static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
|
.map(|ctype| serde_plain::from_str::<ClientType>(&ctype).expect("valid client type"))
|
||||||
static MXM_LIMITER: LazyLock<DefaultDirectRateLimiter> =
|
.unwrap_or_default();
|
||||||
LazyLock::new(|| RateLimiter::direct(Quota::per_second(NonZeroU32::new(1).unwrap())));
|
|
||||||
|
|
||||||
MXM_LIMITER.until_ready().await;
|
let mut mxm = Musixmatch::builder().client_type(client_type);
|
||||||
|
|
||||||
let mut mxm = Musixmatch::builder();
|
|
||||||
|
|
||||||
if let (Ok(email), Ok(password)) = (
|
if let (Ok(email), Ok(password)) = (
|
||||||
std::env::var("MUSIXMATCH_EMAIL"),
|
std::env::var("MUSIXMATCH_EMAIL"),
|
||||||
|
@ -35,10 +35,11 @@ async fn mxm() -> Musixmatch {
|
||||||
mxm = mxm.credentials(email, password);
|
mxm = mxm.credentials(email, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mxm = mxm.build().unwrap();
|
mxm.build().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
LOGIN_LOCK.get_or_try_init(|| mxm.login()).await.unwrap();
|
fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
|
||||||
mxm
|
path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name)
|
||||||
}
|
}
|
||||||
|
|
||||||
mod album {
|
mod album {
|
||||||
|
@ -49,8 +50,8 @@ mod album {
|
||||||
#[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<'_>) {
|
||||||
let album = mxm.await.album(album_id).await.unwrap();
|
let album = new_mxm().album(album_id).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(album.album_id, 14248253);
|
assert_eq!(album.album_id, 14248253);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -59,7 +60,7 @@ mod album {
|
||||||
);
|
);
|
||||||
assert_eq!(album.album_name, "Gangnam Style (강남스타일)");
|
assert_eq!(album.album_name, "Gangnam Style (강남스타일)");
|
||||||
assert!(album.album_rating > 20);
|
assert!(album.album_rating > 20);
|
||||||
assert_eq!(album.album_track_count, 0);
|
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);
|
||||||
assert_eq!(album.artist_id, 410698);
|
assert_eq!(album.artist_id, 410698);
|
||||||
|
@ -92,25 +93,31 @@ mod album {
|
||||||
);
|
);
|
||||||
assert_eq!(album.album_vanity_id, "410698/Gangnam-Style-Single");
|
assert_eq!(album.album_vanity_id, "410698/Gangnam-Style-Single");
|
||||||
assert!(album.updated_time > datetime!(2022-6-3 0:00 UTC));
|
assert!(album.updated_time > datetime!(2022-6-3 0:00 UTC));
|
||||||
assert_imgurl(&album.album_coverart_100x100, "/26544045.jpg");
|
assert_eq!(
|
||||||
assert_imgurl(&album.album_coverart_350x350, "/26544045_350_350.jpg");
|
album.album_coverart_100x100.unwrap(),
|
||||||
assert_imgurl(&album.album_coverart_500x500, "/26544045_500_500.jpg");
|
"https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045.jpg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
album.album_coverart_350x350.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_350_350.jpg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
album.album_coverart_500x500.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums/5/4/0/4/4/5/26544045_500_500.jpg"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn album_ep(#[future] mxm: Musixmatch) {
|
async fn album_ep() {
|
||||||
let album = mxm.await.album(AlbumId::AlbumId(23976123)).await.unwrap();
|
let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap();
|
||||||
assert_eq!(album.album_name, "Waldbrand EP");
|
assert_eq!(album.album_name, "Waldbrand EP");
|
||||||
// assert_eq!(album.album_release_type, AlbumType::Ep);
|
assert_eq!(album.album_release_type, AlbumType::Ep);
|
||||||
assert_eq!(album.album_release_date, Some(date!(2016 - 09 - 30)));
|
assert_eq!(album.album_release_date, Some(date!(2016 - 09 - 30)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn by_id_missing(#[future] mxm: Musixmatch) {
|
async fn by_id_missing() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.album(AlbumId::AlbumId(999999999999))
|
.album(AlbumId::AlbumId(999999999999))
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
@ -118,12 +125,9 @@ mod album {
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore]
|
async fn artist_albums() {
|
||||||
async fn artist_albums(#[future] mxm: Musixmatch) {
|
let albums = new_mxm()
|
||||||
let albums = mxm
|
|
||||||
.await
|
|
||||||
.artist_albums(ArtistId::ArtistId(1039), None, 10, 1)
|
.artist_albums(ArtistId::ArtistId(1039), None, 10, 1)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -131,11 +135,9 @@ mod album {
|
||||||
assert_eq!(albums.len(), 10);
|
assert_eq!(albums.len(), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn artist_albums_missing(#[future] mxm: Musixmatch) {
|
async fn artist_albums_missing() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1)
|
.artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
@ -143,10 +145,9 @@ mod album {
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn charts(#[future] mxm: Musixmatch) {
|
async fn charts() {
|
||||||
let albums = mxm.await.chart_albums("US", 10, 1).await.unwrap();
|
let albums = new_mxm().chart_albums("US", 10, 1).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(albums.len(), 10);
|
assert_eq!(albums.len(), 10);
|
||||||
}
|
}
|
||||||
|
@ -159,8 +160,8 @@ mod artist {
|
||||||
#[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<'_>) {
|
||||||
let artist = mxm.await.artist(artist_id).await.unwrap();
|
let artist = new_mxm().artist(artist_id).await.unwrap();
|
||||||
|
|
||||||
// dbg!(&artist);
|
// dbg!(&artist);
|
||||||
|
|
||||||
|
@ -202,11 +203,9 @@ mod artist {
|
||||||
assert_eq!(artist.end_date, None);
|
assert_eq!(artist.end_date, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn by_id_missing(#[future] mxm: Musixmatch) {
|
async fn by_id_missing() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.artist(ArtistId::ArtistId(999999999999))
|
.artist(ArtistId::ArtistId(999999999999))
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
@ -214,11 +213,9 @@ mod artist {
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn related(#[future] mxm: Musixmatch) {
|
async fn related() {
|
||||||
let artists = mxm
|
let artists = new_mxm()
|
||||||
.await
|
|
||||||
.artist_related(ArtistId::ArtistId(26485840), 10, 1)
|
.artist_related(ArtistId::ArtistId(26485840), 10, 1)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -226,11 +223,9 @@ mod artist {
|
||||||
assert_eq!(artists.len(), 10);
|
assert_eq!(artists.len(), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn related_missing(#[future] mxm: Musixmatch) {
|
async fn related_missing() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.artist_related(ArtistId::ArtistId(999999999999), 10, 1)
|
.artist_related(ArtistId::ArtistId(999999999999), 10, 1)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
@ -238,25 +233,20 @@ mod artist {
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search(#[future] mxm: Musixmatch) {
|
async fn search() {
|
||||||
let artists = mxm
|
let artists = new_mxm().artist_search("psy", 5, 1).await.unwrap();
|
||||||
.await
|
|
||||||
.artist_search("Snollebollekes", 5, 1)
|
assert_eq!(artists.len(), 5);
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let artist = &artists[0];
|
let artist = &artists[0];
|
||||||
assert_eq!(artist.artist_id, 25344078);
|
assert_eq!(artist.artist_id, 410698);
|
||||||
assert_eq!(artist.artist_name, "Snollebollekes");
|
assert_eq!(artist.artist_name, "PSY");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search_empty(#[future] mxm: Musixmatch) {
|
async fn search_empty() {
|
||||||
let artists = mxm
|
let artists = new_mxm()
|
||||||
.await
|
|
||||||
.artist_search(
|
.artist_search(
|
||||||
"Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz",
|
"Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz",
|
||||||
5,
|
5,
|
||||||
|
@ -268,18 +258,16 @@ mod artist {
|
||||||
assert_eq!(artists.len(), 0);
|
assert_eq!(artists.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn charts(#[future] mxm: Musixmatch) {
|
async fn charts() {
|
||||||
let artists = mxm.await.chart_artists("US", 10, 1).await.unwrap();
|
let artists = new_mxm().chart_artists("US", 10, 1).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(artists.len(), 10);
|
assert_eq!(artists.len(), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn charts_no_country(#[future] mxm: Musixmatch) {
|
async fn charts_no_country() {
|
||||||
let artists = mxm.await.chart_artists("XY", 10, 1).await.unwrap();
|
let artists = new_mxm().chart_artists("XY", 10, 1).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(artists.len(), 10);
|
assert_eq!(artists.len(), 10);
|
||||||
}
|
}
|
||||||
|
@ -294,13 +282,8 @@ mod track {
|
||||||
#[case::translation_2c(true, false)]
|
#[case::translation_2c(true, false)]
|
||||||
#[case::translation_3c(true, true)]
|
#[case::translation_3c(true, true)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_match(
|
async fn from_match(#[case] translation_status: bool, #[case] lang_3c: bool) {
|
||||||
#[case] translation_status: bool,
|
let track = new_mxm()
|
||||||
#[case] lang_3c: bool,
|
|
||||||
#[future] mxm: Musixmatch,
|
|
||||||
) {
|
|
||||||
let track = mxm
|
|
||||||
.await
|
|
||||||
.matcher_track(
|
.matcher_track(
|
||||||
"Poker Face",
|
"Poker Face",
|
||||||
"Lady Gaga",
|
"Lady Gaga",
|
||||||
|
@ -313,12 +296,12 @@ mod track {
|
||||||
|
|
||||||
// dbg!(&track);
|
// dbg!(&track);
|
||||||
|
|
||||||
assert_eq!(track.track_id, 85213841);
|
assert_eq!(track.track_id, 15476784);
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// track.track_mbid.unwrap(),
|
track.track_mbid.unwrap(),
|
||||||
// "080975b0-39b1-493c-ae64-5cb3292409bb"
|
"080975b0-39b1-493c-ae64-5cb3292409bb"
|
||||||
// );
|
);
|
||||||
// assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
|
assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
|
||||||
assert!(
|
assert!(
|
||||||
track.commontrack_isrcs[0]
|
track.commontrack_isrcs[0]
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -326,7 +309,7 @@ mod track {
|
||||||
"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(), "5R8dQOPq8haW94K7mgERlO");
|
||||||
assert!(
|
assert!(
|
||||||
track
|
track
|
||||||
.commontrack_spotify_ids
|
.commontrack_spotify_ids
|
||||||
|
@ -346,7 +329,7 @@ mod track {
|
||||||
assert!(track.num_favourite > 50);
|
assert!(track.num_favourite > 50);
|
||||||
assert!(track.lyrics_id.is_some());
|
assert!(track.lyrics_id.is_some());
|
||||||
assert_eq!(track.subtitle_id.unwrap(), 36450705);
|
assert_eq!(track.subtitle_id.unwrap(), 36450705);
|
||||||
assert_eq!(track.album_id, 20960801);
|
assert_eq!(track.album_id, 13810402);
|
||||||
assert_eq!(track.album_name, "The Fame");
|
assert_eq!(track.album_name, "The Fame");
|
||||||
assert_eq!(track.artist_id, 378462);
|
assert_eq!(track.artist_id, 378462);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -354,10 +337,18 @@ mod track {
|
||||||
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
|
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
|
||||||
);
|
);
|
||||||
assert_eq!(track.artist_name, "Lady Gaga");
|
assert_eq!(track.artist_name, "Lady Gaga");
|
||||||
assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg");
|
assert_eq!(
|
||||||
assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg");
|
track.album_coverart_100x100.unwrap(),
|
||||||
assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg");
|
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg"
|
||||||
assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg");
|
);
|
||||||
|
assert_eq!(
|
||||||
|
track.album_coverart_350x350.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
track.album_coverart_500x500.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg"
|
||||||
|
);
|
||||||
assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1");
|
assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1");
|
||||||
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!(2008 - 1 - 1));
|
||||||
|
@ -405,8 +396,8 @@ mod track {
|
||||||
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
|
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
|
||||||
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
|
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
|
async fn from_id(#[case] track_id: TrackId<'_>) {
|
||||||
let track = mxm.await.track(track_id, true, false).await.unwrap();
|
let track = new_mxm().track(track_id, true, false).await.unwrap();
|
||||||
|
|
||||||
// dbg!(&track);
|
// dbg!(&track);
|
||||||
|
|
||||||
|
@ -427,9 +418,18 @@ mod track {
|
||||||
assert_eq!(track.album_name, "Black Mamba");
|
assert_eq!(track.album_name, "Black Mamba");
|
||||||
assert_eq!(track.artist_id, 46970441);
|
assert_eq!(track.artist_id, 46970441);
|
||||||
assert_eq!(track.artist_name, "aespa");
|
assert_eq!(track.artist_name, "aespa");
|
||||||
assert_imgurl(&track.album_coverart_100x100, "/52156772.jpg");
|
assert_eq!(
|
||||||
assert_imgurl(&track.album_coverart_350x350, "/52156772_350_350.jpg");
|
track.album_coverart_100x100.unwrap(),
|
||||||
assert_imgurl(&track.album_coverart_500x500, "/52156772_500_500.jpg");
|
"https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772.jpg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
track.album_coverart_350x350.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_350_350.jpg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
track.album_coverart_500x500.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums5/2/7/7/6/5/1/52156772_500_500.jpg"
|
||||||
|
);
|
||||||
assert_eq!(track.commontrack_vanity_id, "aespa/Black-Mamba");
|
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();
|
||||||
|
@ -446,25 +446,20 @@ mod track {
|
||||||
#[case::translation_2c(true, false)]
|
#[case::translation_2c(true, false)]
|
||||||
#[case::translation_3c(true, true)]
|
#[case::translation_3c(true, true)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_id_translations(
|
async fn from_id_translations(#[case] translation_status: bool, #[case] lang_3c: bool) {
|
||||||
#[case] translation_status: bool,
|
let track = new_mxm()
|
||||||
#[case] lang_3c: bool,
|
.track(TrackId::TrackId(15476784), translation_status, lang_3c)
|
||||||
#[future] mxm: Musixmatch,
|
|
||||||
) {
|
|
||||||
let track = mxm
|
|
||||||
.await
|
|
||||||
.track(TrackId::Commontrack(47672612), translation_status, lang_3c)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// dbg!(&track);
|
// dbg!(&track);
|
||||||
|
|
||||||
assert_eq!(track.track_id, 85213841);
|
assert_eq!(track.track_id, 15476784);
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// track.track_mbid.unwrap(),
|
track.track_mbid.unwrap(),
|
||||||
// "080975b0-39b1-493c-ae64-5cb3292409bb"
|
"080975b0-39b1-493c-ae64-5cb3292409bb"
|
||||||
// );
|
);
|
||||||
// assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
|
assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
|
||||||
assert!(
|
assert!(
|
||||||
track.commontrack_isrcs[0]
|
track.commontrack_isrcs[0]
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -472,7 +467,7 @@ mod track {
|
||||||
"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(), "5R8dQOPq8haW94K7mgERlO");
|
||||||
assert!(
|
assert!(
|
||||||
track
|
track
|
||||||
.commontrack_spotify_ids
|
.commontrack_spotify_ids
|
||||||
|
@ -492,7 +487,7 @@ mod track {
|
||||||
assert!(track.num_favourite > 50);
|
assert!(track.num_favourite > 50);
|
||||||
assert!(track.lyrics_id.is_some());
|
assert!(track.lyrics_id.is_some());
|
||||||
assert_eq!(track.subtitle_id.unwrap(), 36450705);
|
assert_eq!(track.subtitle_id.unwrap(), 36450705);
|
||||||
assert_eq!(track.album_id, 20960801);
|
assert_eq!(track.album_id, 13810402);
|
||||||
assert_eq!(track.album_name, "The Fame");
|
assert_eq!(track.album_name, "The Fame");
|
||||||
assert_eq!(track.artist_id, 378462);
|
assert_eq!(track.artist_id, 378462);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -500,10 +495,18 @@ mod track {
|
||||||
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
|
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
|
||||||
);
|
);
|
||||||
assert_eq!(track.artist_name, "Lady Gaga");
|
assert_eq!(track.artist_name, "Lady Gaga");
|
||||||
assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg");
|
assert_eq!(
|
||||||
assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg");
|
track.album_coverart_100x100.unwrap(),
|
||||||
assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg");
|
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.jpg"
|
||||||
assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg");
|
);
|
||||||
|
assert_eq!(
|
||||||
|
track.album_coverart_350x350.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_350_350.jpg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
track.album_coverart_500x500.unwrap(),
|
||||||
|
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636_500_500.jpg"
|
||||||
|
);
|
||||||
assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1");
|
assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1");
|
||||||
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!(2008 - 1 - 1));
|
||||||
|
@ -544,11 +547,9 @@ mod track {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_id_missing(#[future] mxm: Musixmatch) {
|
async fn from_id_missing() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.track(TrackId::TrackId(999999999999), false, false)
|
.track(TrackId::TrackId(999999999999), false, false)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
@ -556,11 +557,9 @@ mod track {
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn album_tracks(#[future] mxm: Musixmatch) {
|
async fn album_tracks() {
|
||||||
let tracks = mxm
|
let tracks = new_mxm()
|
||||||
.await
|
|
||||||
.album_tracks(AlbumId::AlbumId(17118624), true, 20, 1)
|
.album_tracks(AlbumId::AlbumId(17118624), true, 20, 1)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -597,11 +596,9 @@ mod track {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn album_missing(#[future] mxm: Musixmatch) {
|
async fn album_missing() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1)
|
.album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
@ -613,9 +610,8 @@ mod track {
|
||||||
#[case::top(ChartName::Top)]
|
#[case::top(ChartName::Top)]
|
||||||
#[case::hot(ChartName::Hot)]
|
#[case::hot(ChartName::Hot)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn charts(#[case] chart_name: ChartName, #[future] mxm: Musixmatch) {
|
async fn charts(#[case] chart_name: ChartName) {
|
||||||
let tracks = mxm
|
let tracks = new_mxm()
|
||||||
.await
|
|
||||||
.chart_tracks("US", chart_name, true, 20, 1)
|
.chart_tracks("US", chart_name, true, 20, 1)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -623,11 +619,9 @@ mod track {
|
||||||
assert_eq!(tracks.len(), 20);
|
assert_eq!(tracks.len(), 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search(#[future] mxm: Musixmatch) {
|
async fn search() {
|
||||||
let tracks = mxm
|
let tracks = new_mxm()
|
||||||
.await
|
|
||||||
.track_search()
|
.track_search()
|
||||||
.q_artist("Lena")
|
.q_artist("Lena")
|
||||||
.q_track("Satellite")
|
.q_track("Satellite")
|
||||||
|
@ -646,11 +640,9 @@ mod track {
|
||||||
assert_eq!(track.artist_name, "Lena");
|
assert_eq!(track.artist_name, "Lena");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search_lyrics(#[future] mxm: Musixmatch) {
|
async fn search_lyrics() {
|
||||||
let tracks = mxm
|
let tracks = new_mxm()
|
||||||
.await
|
|
||||||
.track_search()
|
.track_search()
|
||||||
.q_lyrics("the whole world stops and stares for a while")
|
.q_lyrics("the whole world stops and stares for a while")
|
||||||
.s_track_rating(SortOrder::Desc)
|
.s_track_rating(SortOrder::Desc)
|
||||||
|
@ -658,18 +650,16 @@ mod track {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_gte(tracks.len(), 8, "tracks");
|
assert_eq!(tracks.len(), 10);
|
||||||
|
|
||||||
let track = &tracks[0];
|
let track = &tracks[0];
|
||||||
assert_eq!(track.track_name, "Just the Way You Are");
|
assert_eq!(track.track_name, "Just the Way You Are");
|
||||||
assert_eq!(track.artist_name, "Bruno Mars");
|
assert_eq!(track.artist_name, "Bruno Mars");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search_empty(#[future] mxm: Musixmatch) {
|
async fn search_empty() {
|
||||||
let artists = mxm
|
let artists = new_mxm()
|
||||||
.await
|
|
||||||
.track_search()
|
.track_search()
|
||||||
.q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz")
|
.q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz")
|
||||||
.send(10, 1)
|
.send(10, 1)
|
||||||
|
@ -679,19 +669,16 @@ mod track {
|
||||||
assert_eq!(artists.len(), 0);
|
assert_eq!(artists.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn genres(#[future] mxm: Musixmatch) {
|
async fn genres() {
|
||||||
let genres = mxm.await.genres().await.unwrap();
|
let genres = new_mxm().genres().await.unwrap();
|
||||||
assert!(genres.len() > 360);
|
assert!(genres.len() > 360);
|
||||||
dbg!(&genres);
|
// dbg!(&genres);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn snippet(#[future] mxm: Musixmatch) {
|
async fn snippet() {
|
||||||
let snippet = mxm
|
let snippet = new_mxm()
|
||||||
.await
|
|
||||||
.track_snippet(TrackId::Commontrack(8874280))
|
.track_snippet(TrackId::Commontrack(8874280))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -713,27 +700,22 @@ mod lyrics {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_match(#[future] mxm: Musixmatch) {
|
async fn from_match() {
|
||||||
let lyrics = mxm.await.matcher_lyrics("Shine", "Spektrem").await.unwrap();
|
let lyrics = new_mxm().matcher_lyrics("Shine", "Spektrem").await.unwrap();
|
||||||
|
|
||||||
// dbg!(&lyrics);
|
// dbg!(&lyrics);
|
||||||
|
|
||||||
assert_eq!(lyrics.lyrics_id, 34583240);
|
assert_eq!(lyrics.lyrics_id, 25947036);
|
||||||
assert!(!lyrics.instrumental);
|
assert!(!lyrics.instrumental);
|
||||||
assert!(!lyrics.explicit);
|
assert!(!lyrics.explicit);
|
||||||
assert!(
|
assert!(lyrics
|
||||||
lyrics
|
|
||||||
.lyrics_body
|
.lyrics_body
|
||||||
.starts_with("Eyes in the sky gazing far into the night\n"),
|
.starts_with("Eyes, in the sky, gazing far into the night\n"));
|
||||||
"got: {}",
|
|
||||||
lyrics.lyrics_body
|
|
||||||
);
|
|
||||||
assert_eq!(lyrics.lyrics_language.unwrap(), "en");
|
assert_eq!(lyrics.lyrics_language.unwrap(), "en");
|
||||||
assert_eq!(lyrics.lyrics_language_description.unwrap(), "English");
|
assert_eq!(lyrics.lyrics_language_description.unwrap(), "English");
|
||||||
let copyright = lyrics.lyrics_copyright.unwrap();
|
let copyright = lyrics.lyrics_copyright.unwrap();
|
||||||
assert!(copyright.contains("Jesse Warren"), "copyright: {copyright}",);
|
assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",);
|
||||||
assert!(lyrics.updated_time > datetime!(2021-6-3 0:00 UTC));
|
assert!(lyrics.updated_time > datetime!(2021-6-3 0:00 UTC));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -744,8 +726,8 @@ mod lyrics {
|
||||||
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
|
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
|
||||||
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
|
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
|
async fn from_id(#[case] track_id: TrackId<'_>) {
|
||||||
let lyrics = mxm.await.track_lyrics(track_id).await.unwrap();
|
let lyrics = new_mxm().track_lyrics(track_id).await.unwrap();
|
||||||
|
|
||||||
// dbg!(&lyrics);
|
// dbg!(&lyrics);
|
||||||
|
|
||||||
|
@ -761,11 +743,9 @@ mod lyrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This track has no lyrics
|
/// This track has no lyrics
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn instrumental(#[future] mxm: Musixmatch) {
|
async fn instrumental() {
|
||||||
let lyrics = mxm
|
let lyrics = new_mxm()
|
||||||
.await
|
|
||||||
.matcher_lyrics("drivers license", "Bobby G")
|
.matcher_lyrics("drivers license", "Bobby G")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -781,28 +761,24 @@ mod lyrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This track does not exist
|
/// This track does not exist
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn missing(#[future] mxm: Musixmatch) {
|
async fn missing() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.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));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn download_testdata(#[future] mxm: Musixmatch) {
|
async fn download_testdata() {
|
||||||
let mxm = mxm.await;
|
|
||||||
let json_path = testfile("lyrics.json");
|
let json_path = testfile("lyrics.json");
|
||||||
if json_path.exists() {
|
if json_path.exists() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lyrics = mxm
|
let lyrics = new_mxm()
|
||||||
.track_lyrics(TrackId::Commontrack(18576954))
|
.track_lyrics(TrackId::Commontrack(18576954))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -811,16 +787,14 @@ mod lyrics {
|
||||||
serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap();
|
serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn download_testdata_translation(#[future] mxm: Musixmatch) {
|
async fn download_testdata_translation() {
|
||||||
let mxm = mxm.await;
|
|
||||||
let json_path = testfile("translation.json");
|
let json_path = testfile("translation.json");
|
||||||
if json_path.exists() {
|
if json_path.exists() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let translations = mxm
|
let translations = new_mxm()
|
||||||
.track_lyrics_translation(TrackId::Commontrack(18576954), "de")
|
.track_lyrics_translation(TrackId::Commontrack(18576954), "de")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -829,10 +803,9 @@ mod lyrics {
|
||||||
serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap();
|
serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn concurrency(#[future] mxm: Musixmatch) {
|
async fn concurrency() {
|
||||||
let mxm = mxm.await;
|
let mxm = new_mxm();
|
||||||
|
|
||||||
let album = mxm
|
let album = mxm
|
||||||
.album_tracks(
|
.album_tracks(
|
||||||
|
@ -867,11 +840,9 @@ mod subtitles {
|
||||||
use super::*;
|
use super::*;
|
||||||
use musixmatch_inofficial::models::SubtitleFormat;
|
use musixmatch_inofficial::models::SubtitleFormat;
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_match(#[future] mxm: Musixmatch) {
|
async fn from_match() {
|
||||||
let subtitle = mxm
|
let subtitle = new_mxm()
|
||||||
.await
|
|
||||||
.matcher_subtitle(
|
.matcher_subtitle(
|
||||||
"Shine",
|
"Shine",
|
||||||
"Spektrem",
|
"Spektrem",
|
||||||
|
@ -884,12 +855,12 @@ mod subtitles {
|
||||||
|
|
||||||
// dbg!(&subtitle);
|
// dbg!(&subtitle);
|
||||||
|
|
||||||
assert_eq!(subtitle.subtitle_id, 35340319);
|
assert_eq!(subtitle.subtitle_id, 36913312);
|
||||||
assert_eq!(subtitle.subtitle_language.unwrap(), "en");
|
assert_eq!(subtitle.subtitle_language.unwrap(), "en");
|
||||||
assert_eq!(subtitle.subtitle_language_description.unwrap(), "English");
|
assert_eq!(subtitle.subtitle_language_description.unwrap(), "English");
|
||||||
let copyright = subtitle.lyrics_copyright.unwrap();
|
let copyright = subtitle.lyrics_copyright.unwrap();
|
||||||
assert!(copyright.contains("Jesse Warren"), "copyright: {copyright}",);
|
assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",);
|
||||||
assert_eq!(subtitle.subtitle_length, 316);
|
assert_eq!(subtitle.subtitle_length, 315);
|
||||||
assert!(subtitle.updated_time > datetime!(2021-6-30 0:00 UTC));
|
assert!(subtitle.updated_time > datetime!(2021-6-30 0:00 UTC));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -900,9 +871,8 @@ mod subtitles {
|
||||||
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
|
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
|
||||||
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
|
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
|
async fn from_id(#[case] track_id: TrackId<'_>) {
|
||||||
let subtitle = mxm
|
let subtitle = new_mxm()
|
||||||
.await
|
|
||||||
.track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0))
|
.track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -922,11 +892,9 @@ mod subtitles {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This track has no lyrics
|
/// This track has no lyrics
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn instrumental(#[future] mxm: Musixmatch) {
|
async fn instrumental() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.matcher_subtitle(
|
.matcher_subtitle(
|
||||||
"drivers license",
|
"drivers license",
|
||||||
"Bobby G",
|
"Bobby G",
|
||||||
|
@ -941,11 +909,9 @@ mod subtitles {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This track has not been synced
|
/// This track has not been synced
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn unsynced(#[future] mxm: Musixmatch) {
|
async fn unsynced() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.track_subtitle(
|
.track_subtitle(
|
||||||
TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()),
|
TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()),
|
||||||
SubtitleFormat::Json,
|
SubtitleFormat::Json,
|
||||||
|
@ -959,11 +925,9 @@ mod subtitles {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to get subtitles with wrong length parameter
|
/// Try to get subtitles with wrong length parameter
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn wrong_length(#[future] mxm: Musixmatch) {
|
async fn wrong_length() {
|
||||||
let err = mxm
|
let err = new_mxm()
|
||||||
.await
|
|
||||||
.track_subtitle(
|
.track_subtitle(
|
||||||
TrackId::Commontrack(118480583),
|
TrackId::Commontrack(118480583),
|
||||||
SubtitleFormat::Json,
|
SubtitleFormat::Json,
|
||||||
|
@ -976,16 +940,14 @@ mod subtitles {
|
||||||
assert!(matches!(err, Error::NotFound));
|
assert!(matches!(err, Error::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn download_testdata(#[future] mxm: Musixmatch) {
|
async fn download_testdata() {
|
||||||
let json_path = testfile("subtitles.json");
|
let json_path = testfile("subtitles.json");
|
||||||
if json_path.exists() {
|
if json_path.exists() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitle = mxm
|
let subtitle = new_mxm()
|
||||||
.await
|
|
||||||
.track_subtitle(
|
.track_subtitle(
|
||||||
TrackId::Commontrack(18576954),
|
TrackId::Commontrack(18576954),
|
||||||
SubtitleFormat::Json,
|
SubtitleFormat::Json,
|
||||||
|
@ -1036,20 +998,16 @@ mod translation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[tokio::test]
|
||||||
fn assert_imgurl(url: &Option<String>, ends_with: &str) {
|
async fn no_credentials() {
|
||||||
assert!(
|
let mxm = Musixmatch::builder()
|
||||||
url.as_deref().is_some_and(
|
.client_type(ClientType::Android)
|
||||||
|url| url.starts_with("https://s.mxmcdn.net/images-storage/")
|
.no_storage()
|
||||||
&& url.ends_with(ends_with)
|
.build()
|
||||||
),
|
.unwrap();
|
||||||
"expected url ending with {ends_with}\ngot {:?}",
|
let err = mxm
|
||||||
url
|
.track_lyrics(TrackId::TrackId(205688271))
|
||||||
);
|
.await
|
||||||
}
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, Error::MissingCredentials), "error: {err}");
|
||||||
/// Assert that number A is greater than or equal to number B
|
|
||||||
#[track_caller]
|
|
||||||
fn assert_gte<T: PartialOrd + std::fmt::Display>(a: T, b: T, msg: &str) {
|
|
||||||
assert!(a >= b, "expected >= {b} {msg}, got {a}");
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue