Compare commits

...

33 commits

Author SHA1 Message Date
ThetaBot
5ef76f5a6b chore(deps): update rust crate rstest to 0.23.0 (#2) 2024-09-30 00:07:35 +00:00
424a522708
chore(release): release musixmatch-cli v0.2.0 2024-08-19 00:21:36 +02:00
38386c0132
chore(release): release musixmatch-inofficial v0.1.1 2024-08-19 00:20:39 +02:00
a95f3fcf47
feat: add msrv 2024-08-19 00:13:29 +02:00
3b69b36ae6
test: add rate limiter 2024-08-19 00:05:19 +02:00
df150d9ffd
ci: remove musixmatch credentials 2024-08-18 23:45:47 +02:00
54235e6fb6
feat!: remove MP3 feature, refactor cmd structure 2024-08-18 23:44:27 +02:00
c4bfbe563a
feat: add get album, get artist, search artist 2024-08-18 23:44:24 +02:00
dc1bea13cc
fix: use native TLS for CLI 2024-08-18 22:58:52 +02:00
bc0dd99f7d
ci: enable retries 2024-08-18 18:38:21 +02:00
c9fea762ec
test: fix tests 2024-08-18 18:35:00 +02:00
c120583bf8
test: fix tests 2024-08-18 18:32:21 +02:00
05978665de
ci: use credentials 2024-08-18 18:30:42 +02:00
f45ad3cefb
ci: enable warpproxy 2024-08-18 18:28:00 +02:00
348e9c5427
doc: update readme 2024-08-18 18:25:03 +02:00
19e209e34f
feat: add format option to mp3 subtitles cmd 2024-08-18 18:19:00 +02:00
30e2afd367
chore: change repo to codeberg 2024-08-18 18:16:07 +02:00
c7d40a75ee
ci: add renovate
Some checks failed
renovate / renovate (push) Successful in 41s
CI / Test (push) Failing after 3m27s
2024-08-18 17:39:34 +02:00
dcc25bff20
chore: update dependencies 2024-08-18 17:26:42 +02:00
1bc5ae4083
chore: update justfile 2024-08-18 16:16:26 +02:00
d2a7aed917
test: fix tests 2024-08-18 16:14:05 +02:00
dc01542515
ci: fix changelog tag pattern 2024-04-12 03:28:34 +02:00
e72d2b4363
chore: fix changelogs
All checks were successful
CI / Test (push) Successful in 1m49s
2024-04-11 13:49:36 +02:00
8afc43a097
chore(release): release musixmatch-cli v0.1.0
All checks were successful
Release / Release (push) Successful in 1m5s
CI / Test (push) Successful in 35s
2024-03-23 02:51:41 +01:00
9faa2cd6b8
chore(release): release musixmatch-inofficial v0.1.0
All checks were successful
CI / Test (push) Successful in 50s
Release / Release (push) Successful in 53s
2024-03-23 02:48:48 +01:00
053702c2f5
fix: move package attributes to workspace 2024-03-23 02:48:27 +01:00
8168566498
ci: add workflows
All checks were successful
CI / Test (push) Successful in 1m50s
2024-03-23 02:43:18 +01:00
b30b9d7b9f
chore: add git-cliff 2024-03-23 02:36:33 +01:00
7d153499ed
chore: update dependencies 2024-03-23 02:35:46 +01:00
4282889ebe
feat: add search function 2024-03-23 02:35:25 +01:00
9958c57c73
test: update tests 2024-03-23 02:33:31 +01:00
6bece62893
fix: improved response parsing and errors
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-06 13:16:30 +01:00
ad865e1a26
feat: allow usage without credentials
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-03 17:20:24 +01:00
19 changed files with 1043 additions and 527 deletions

29
.gitea/workflows/ci.yaml Normal file
View file

@ -0,0 +1,29 @@
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"

View file

@ -0,0 +1,33 @@
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 }}"

View file

@ -0,0 +1,63 @@
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 }}

View file

@ -1,13 +0,0 @@
pipeline:
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
- cargo test --workspace

33
CHANGELOG.md Normal file
View file

@ -0,0 +1,33 @@
# Changelog
All notable changes to this project will be documented in this file.
## [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 -->

View file

@ -1,17 +1,30 @@
[package]
name = "musixmatch-inofficial"
version = "0.1.0"
edition = "2021"
authors = ["ThetaDev <t.testboy@gmail.com>"]
license = "MIT"
version = "0.1.1"
rust-version = "1.70.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
description = "Inofficial client for the Musixmatch API"
keywords = ["music", "lyrics"]
include = ["/src", "README.md", "LICENSE"]
include = ["/src", "README.md", "CHANGELOG.md", "LICENSE"]
[workspace]
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]
default = ["default-tls"]
@ -24,34 +37,30 @@ rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
[dependencies]
reqwest = { version = "0.11.11", default-features = false, features = [
reqwest = { version = "0.12.0", default-features = false, features = [
"json",
"gzip",
] }
tokio = { version = "1.20.0" }
tokio = { version = "1.20.4" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
thiserror = "1.0.36"
thiserror = "1.0.0"
log = "0.4.17"
time = { version = "0.3.15", features = [
time = { version = "0.3.10", features = [
"macros",
"formatting",
"serde",
"serde-well-known",
] }
hmac = "0.12.1"
sha1 = "0.10.5"
rand = "0.8.5"
base64 = "0.21.0"
hmac = "0.12.0"
sha1 = "0.10.0"
rand = "0.8.0"
base64 = "0.22.0"
[dev-dependencies]
ctor = "0.2.0"
rstest = { version = "0.18.0", default-features = false }
env_logger = "0.11.0"
rstest = { version = "0.23.0", default-features = false }
dotenvy = "0.15.5"
tokio = { version = "1.20.0", features = ["macros"] }
tokio = { version = "1.20.4", features = ["macros"] }
futures = "0.3.21"
path_macro = "1.0.0"
[profile.release]
strip = true
governor = "0.6.3"

44
Justfile Normal file
View file

@ -0,0 +1,44 @@
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"

View file

@ -1,35 +1,39 @@
# Musixmatch-Inofficial
# musixmatch-inofficial
This is an inofficial client for the Musixmatch API that uses the
key embedded in the Musixmatch Android app.
[![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
Musixmatch Android app.
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.
A free Musixmatch account is required for operation
([you can sign up here](https://www.musixmatch.com/de/sign-up)).
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 client still allows you to supply credentials if Musixmatch decides to
close the API down again.
## ⚠️ Copyright disclaimer
Song lyrics are copyrighted works (just like books, poems and the songs
themselves).
Song lyrics are copyrighted works (just like books, poems and the songs themselves).
Musixmatch does allow its users to obtains song lyrics for private
use (e.g. to enrich their music collection). But it does not allow you
to publish their lyrics or use them commercially.
Musixmatch does allow its users to obtains song lyrics for private use (e.g. to enrich
their music collection). But it does not allow you to publish their lyrics or use them
commercially.
You will get in trouble if you use this client to create a public
lyrics site/app. If you want to use Musixmatch data for this purpose,
you will have to give them money (see their
[commercial plans](https://developer.musixmatch.com/plans))
and use their [official API](https://developer.musixmatch.com/documentation).
You will get in trouble if you use this client to create a public lyrics site/app. If
you want to use Musixmatch data for this purpose, you will have to give them money (see
their [commercial plans](https://developer.musixmatch.com/plans)) and use their
[official API](https://developer.musixmatch.com/documentation).
## Development info
Running the tests requires Musixmatch credentials. The credentials are read
from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment variables.
The test suite reads Musixmatch credentials from the `MUSIXMATCH_EMAIL` and
`MUSIXMATCH_PASSWORD` environment variables.
To make local development easier, I have included `dotenvy` to read the
credentials from an `.env` file. Copy the `.env.example` file
in the root directory, rename it to `.env` and fill in your credentials.
To make local development easier, I have included `dotenvy` to read the credentials from
an `.env` file. Copy the `.env.example` file in the root directory, rename it to `.env`
and fill in your credentials.

39
cli/CHANGELOG.md Normal file
View file

@ -0,0 +1,39 @@
# 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 -->

View file

@ -1,14 +1,16 @@
[package]
name = "musixmatch-cli"
version = "0.1.0"
edition = "2021"
authors = ["ThetaDev"]
license = "MIT"
version = "0.2.0"
rust-version = "1.70.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
description = "Inofficial command line interface for the Musixmatch API"
keywords = ["music", "lyrics", "cli"]
[features]
default = ["rustls-tls-native-roots"]
default = ["native-tls"]
# Reqwest TLS options
native-tls = ["musixmatch-inofficial/native-tls"]
@ -18,12 +20,10 @@ rustls-tls-webpki-roots = ["musixmatch-inofficial/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"]
[dependencies]
musixmatch-inofficial = { path = "../" }
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
id3 = "1.3.0"
mp3-duration = "0.1.10"
clap = { version = "4.0.10", features = ["derive"] }
anyhow = "1.0.65"
musixmatch-inofficial.workspace = true
tokio = { version = "1.20.4", features = ["macros", "rt-multi-thread"] }
clap = { version = "4.0.0", features = ["derive"] }
anyhow = "1.0.0"
rpassword = "7.0.0"
dirs = "5.0.0"
serde_json = "1.0.91"
serde_json = "1.0.85"

77
cli/README.md Normal file
View file

@ -0,0 +1,77 @@
# 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
...
```

View file

@ -1,3 +1,6 @@
#![doc = include_str!("../README.md")]
#![warn(missing_docs, clippy::todo)]
use std::{
io::{stdin, stdout, Write},
path::PathBuf,
@ -5,9 +8,8 @@ use std::{
use anyhow::{anyhow, bail, Result};
use clap::{Args, Parser, Subcommand};
use id3::{Tag, TagLike};
use musixmatch_inofficial::{
models::{SubtitleFormat, Track, TrackId, TranslationMap},
models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap},
Musixmatch,
};
@ -20,18 +22,7 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
Get {
#[command(subcommand)]
command: GetCommands,
},
Mp3 {
#[command(subcommand)]
command: FileCommands,
},
}
#[derive(Subcommand)]
enum GetCommands {
/// Get lyrics text
Lyrics {
#[clap(flatten)]
ident: TrackIdentifiers,
@ -42,26 +33,55 @@ enum GetCommands {
#[clap(long)]
bi: bool,
},
/// Get subtitles (time-synced lyrics)
Subtitles {
#[clap(flatten)]
ident: TrackIdentifiers,
/// Track length
#[clap(long, short)]
#[clap(short, long)]
length: Option<f32>,
/// Maximum deviation from track length (Default: 1s)
#[clap(long)]
max_deviation: Option<f32>,
/// Subtitle format
#[clap(long, default_value = "lrc")]
#[clap(short, long, default_value = "lrc")]
format: SubtitleFormatClap,
/// Language
#[clap(long)]
lang: Option<String>,
},
/// Get track metadata
Track {
#[clap(flatten)]
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)]
@ -92,22 +112,38 @@ struct TrackIdentifiers {
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)]
enum FileCommands {
/// Get lyrics text
Lyrics {
/// Music file
#[clap(value_parser)]
file: PathBuf,
},
/// Get subtitles (time-synced lyrics)
Subtitles {
/// Music file
#[clap(value_parser)]
file: PathBuf,
/// Subtitle format
#[clap(short, long, default_value = "lrc")]
format: SubtitleFormatClap,
},
}
#[derive(clap::ValueEnum, Debug, Copy, Clone)]
pub enum SubtitleFormatClap {
enum SubtitleFormatClap {
Lrc,
Ttml,
TtmlStructured,
@ -161,8 +197,7 @@ async fn run(cli: Cli) -> Result<()> {
};
match cli.command {
Commands::Get { command } => match command {
GetCommands::Lyrics { ident, lang, bi } => {
Commands::Lyrics { ident, lang, bi } => {
let track_id = get_track_id(ident, &mxm).await?;
let lyrics = mxm.track_lyrics(track_id.clone()).await?;
@ -186,9 +221,7 @@ async fn run(cli: Cli) -> Result<()> {
if Some(&lang) != lyrics.lyrics_language.as_ref() {
let tl = mxm.track_lyrics_translation(track_id, &lang).await?;
if tl.is_empty() {
eprintln!(
"Translation not found. Returning lyrics in original language."
);
eprintln!("Translation not found. Returning lyrics in original language.");
} else {
eprintln!("Translated to: {}", tl.lang);
let tm = TranslationMap::from(tl);
@ -215,7 +248,7 @@ async fn run(cli: Cli) -> Result<()> {
eprintln!();
println!("{}", lyrics_body);
}
GetCommands::Subtitles {
Commands::Subtitles {
ident,
length,
max_deviation,
@ -280,45 +313,77 @@ async fn run(cli: Cli) -> Result<()> {
println!("{}", subtitles.subtitle_body);
}
}
GetCommands::Track { ident } => {
Commands::Track { ident } => {
let track = get_track(ident, &mxm).await?;
println!("{}", serde_json::to_string_pretty(&track)?)
}
},
Commands::Mp3 { command } => match command {
FileCommands::Lyrics { file } => {
let tag = Tag::read_from_path(&file)?;
let title = tag.title().ok_or(anyhow!("no title"))?;
let artist = tag.artist().ok_or(anyhow!("no artist"))?;
let lyrics = mxm.matcher_lyrics(title, artist).await?;
Commands::Album { ident } => {
let id = if let Some(id) = ident.mxm_id {
AlbumId::AlbumId(id)
} else if let Some(mb) = &ident.musicbrainz {
AlbumId::Musicbrainz(mb)
} else {
bail!("no album ID specified")
};
let album = mxm.album(id).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!(
"Lyrics for {} by {}:\n\n{}",
title, artist, lyrics.lyrics_body
"{} - {} ({}) ISRC'{}' <https://musixmatch.com/lyrics/{}>",
t.track_name,
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(())
}

100
cliff.toml Normal file
View file

@ -0,0 +1,100 @@
# 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", group = "<!-- 6 -->🧪 Testing" },
{ 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

13
renovate.json Normal file
View file

@ -0,0 +1,13 @@
{
"$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
}

View file

@ -1,6 +1,9 @@
use std::{marker::PhantomData, str::FromStr};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
use serde::{
de::{DeserializeOwned, Visitor},
Deserialize, Deserializer, Serialize,
};
use time::OffsetDateTime;
use crate::error::{Error, Result as MxmResult};
@ -9,24 +12,17 @@ use crate::error::{Error, Result as MxmResult};
#[derive(Debug, Deserialize)]
pub struct Resp<T> {
pub message: Message<T>,
pub message: T,
}
#[derive(Debug, Deserialize)]
pub struct Message<T> {
pub struct HeaderMsg {
pub header: Header,
pub body: Option<MessageBody<T>>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum MessageBody<T> {
Some(T),
// "body": []
EmptyArr(Vec<()>),
// "body": {}
EmptyObj {},
EmptyStr(String),
pub struct BodyMsg<T> {
pub body: T,
}
#[derive(Debug, Deserialize)]
@ -37,31 +33,25 @@ pub struct Header {
pub hint: String,
}
impl<T> Resp<T> {
pub fn body_or_err(self) -> MxmResult<T> {
match (self.message.body, self.message.header.status_code < 400) {
(Some(MessageBody::Some(body)), true) => Ok(body),
(_, true) => Err(Error::NoData),
(_, false) => {
if self.message.header.status_code == 404 {
pub fn parse_body<T: DeserializeOwned>(response: &str) -> MxmResult<T> {
let header = serde_json::from_str::<Resp<HeaderMsg>>(response)?
.message
.header;
if header.status_code < 400 {
let body = serde_json::from_str::<Resp<BodyMsg<T>>>(response)?;
Ok(body.message.body)
} else if header.status_code == 404 {
Err(Error::NotFound)
} else if self.message.header.status_code == 401
&& self.message.header.hint == "renew"
{
} else if header.status_code == 401 && header.hint == "renew" {
Err(Error::TokenExpired)
} else if self.message.header.status_code == 401
&& self.message.header.hint == "captcha"
{
} else if header.status_code == 401 && header.hint == "captcha" {
Err(Error::Ratelimit)
} else {
Err(Error::MusixmatchError {
status_code: self.message.header.status_code,
msg: self.message.header.hint,
status_code: header.status_code,
msg: header.hint,
})
}
}
}
}
}
//#SESSION
@ -109,8 +99,8 @@ pub enum LoginCredential {
#[derive(Debug, Deserialize)]
pub struct Account {
pub id: String,
pub email: String,
// pub id: String,
// pub email: String,
pub name: String,
}
@ -565,57 +555,22 @@ mod tests {
let json =
r#"{"message":{"header":{"status_code":401,"execute_time":0.002,"hint":"fsck"}}}"#;
let res = serde_json::from_str::<Resp<SubtitleBody>>(json).unwrap();
let err = parse_body::<SubtitleBody>(json).unwrap_err();
assert_eq!(res.message.header.status_code, 401);
assert_eq!(res.message.header.hint, "fsck");
assert!(res.message.body.is_none());
let err = res.body_or_err().unwrap_err();
assert_eq!(
err.to_string(),
"Error 401 returned by the Musixmatch API. Message: 'fsck'"
);
if let Error::MusixmatchError { status_code, msg } = err {
assert_eq!(status_code, 401);
assert_eq!(msg, "fsck");
} else {
panic!("invalid error: {err}");
}
}
#[test]
fn deserialize_emptyarr_body() {
let json = r#"{"message":{"header":{"status_code":403,"execute_time":0.0056290626525879},"body":[]}}"#;
fn deserialize_body() {
let json = r#"{"message":{"header":{"status_code":200,"execute_time":0.002},"body":"Hello World"}}"#;
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::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: ''"
);
let res = parse_body::<String>(json).unwrap();
assert_eq!(res, "Hello World");
}
#[test]

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
pub(crate) type Result<T> = core::result::Result<T, Error>;
/// Custom error type for the Musixmatch client
@ -18,9 +20,6 @@ pub enum Error {
/// Error message
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
#[error("You did not input credentials")]
MissingCredentials,
@ -33,6 +32,9 @@ pub enum Error {
/// Musixmatch content not available
#[error("Unfortunately we're not authorized to show these lyrics")]
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("http error: {0}")]
Http(reqwest::Error),
@ -44,3 +46,9 @@ impl From<reqwest::Error> for Error {
Self::Http(value.without_url())
}
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Self::InvalidData(value.to_string().into())
}
}

View file

@ -8,7 +8,6 @@ pub mod models;
pub mod storage;
use std::fmt::Debug;
use std::ops::Deref;
use std::path::Path;
use std::sync::{Arc, RwLock};
@ -29,7 +28,7 @@ use time::macros::format_description;
use time::OffsetDateTime;
use tokio::sync::Mutex;
use crate::api_model::Resp;
use crate::api_model::parse_body;
use crate::error::Result;
const YMD_FORMAT: &[time::format_description::FormatItem] =
@ -112,8 +111,8 @@ impl MusixmatchBuilder {
/// Set the Musixmatch credentials
///
/// You have to create a free account on <https://www.musixmatch.com> to use
/// the API.
/// The Musixmatch API required a free account on <https://www.musixmatch.com> to be
/// used. However, as of 2024, this requirement was removed.
///
/// The Musixmatch client can be constructed without any credentials.
/// In this case you rely on the stored session token to authenticate
@ -238,10 +237,7 @@ impl Musixmatch {
let credentials = {
let c = self.inner.credentials.read().unwrap();
match c.deref() {
Some(c) => c.clone(),
None => return Err(Error::MissingCredentials),
}
c.clone()
};
let now = OffsetDateTime::now_utc();
@ -273,12 +269,14 @@ impl Musixmatch {
sign_url_with_date(&mut url, now);
let resp = self.inner.http.get(url).send().await?.error_for_status()?;
let tdata = resp.json::<Resp<api_model::GetToken>>().await?;
let usertoken = tdata.body_or_err()?.user_token;
let resp_txt = resp.text().await?;
let usertoken = parse_body::<api_model::GetToken>(&resp_txt)?.user_token;
info!("Received new usertoken: {}****", &usertoken[0..8]);
if let Some(credentials) = credentials {
let account = self.post_credentials(&usertoken, &credentials).await?;
info!("Logged in as {} ", account.name);
}
*stored_usertoken = Some(usertoken.to_owned());
self.store_session(&usertoken);
@ -313,8 +311,14 @@ impl Musixmatch {
.await?
.error_for_status()?;
let login = resp.json::<Resp<api_model::Login>>().await?.body_or_err()?;
let credential = login.0.into_iter().next().ok_or(Error::NoData)?.credential;
let resp_txt = resp.text().await?;
let login = parse_body::<api_model::Login>(&resp_txt)?;
let credential = login
.0
.into_iter()
.next()
.ok_or(Error::InvalidData("no credentials returned".into()))?
.credential;
match credential {
api_model::LoginCredential::Account { account } => Ok(account),
@ -400,9 +404,9 @@ impl Musixmatch {
.send()
.await?
.error_for_status()?;
let resp_obj = resp.json::<Resp<T>>().await?;
let resp_txt = resp.text().await?;
match resp_obj.body_or_err() {
match parse_body(&resp_txt) {
Ok(body) => Ok(body),
Err(Error::TokenExpired) => {
info!("Usertoken expired, getting a new one");
@ -418,7 +422,8 @@ impl Musixmatch {
.await?
.error_for_status()?;
resp.json::<Resp<T>>().await?.body_or_err()
let resp_txt = resp.text().await?;
parse_body(&resp_txt)
}
Err(e) => Err(e),
}

View file

@ -226,7 +226,7 @@ impl Subtitle {
/// Only works with [SubtitleFormat::Json].
pub fn to_lines(&self) -> Result<SubtitleLines> {
Ok(SubtitleLines {
lines: serde_json::from_str(&self.subtitle_body).map_err(|_| Error::NoData)?,
lines: serde_json::from_str(&self.subtitle_body)?,
lang: self.subtitle_language.to_owned(),
length: self.subtitle_length,
})
@ -256,7 +256,7 @@ impl TryFrom<Subtitle> for SubtitleLines {
impl SubtitleLines {
/// Convert subtitles into the [JSON](SubtitleFormat::Json) format
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(&self).map_err(|_| Error::NoData)
serde_json::to_string(&self).map_err(Error::from)
}
/// Convert subtitles into the [LRC](SubtitleFormat::Lrc) format

View file

@ -1,7 +1,12 @@
use std::path::{Path, PathBuf};
use std::{
num::NonZeroU32,
path::{Path, PathBuf},
sync::LazyLock,
};
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use path_macro::path;
use rstest::rstest;
use rstest::{fixture, rstest};
use time::macros::{date, datetime};
use musixmatch_inofficial::{
@ -9,32 +14,33 @@ use musixmatch_inofficial::{
Error, Musixmatch,
};
#[ctor::ctor]
fn init() {
let _ = dotenvy::dotenv();
env_logger::init();
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(new_mxm().login())
.unwrap();
}
fn new_mxm() -> Musixmatch {
Musixmatch::builder()
.credentials(
std::env::var("MUSIXMATCH_EMAIL").unwrap(),
std::env::var("MUSIXMATCH_PASSWORD").unwrap(),
)
.build()
.unwrap()
}
fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name)
}
#[fixture]
async fn mxm() -> Musixmatch {
static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
static MXM_LIMITER: LazyLock<DefaultDirectRateLimiter> =
LazyLock::new(|| RateLimiter::direct(Quota::per_second(NonZeroU32::new(1).unwrap())));
MXM_LIMITER.until_ready().await;
let mut mxm = Musixmatch::builder();
if let (Ok(email), Ok(password)) = (
std::env::var("MUSIXMATCH_EMAIL"),
std::env::var("MUSIXMATCH_PASSWORD"),
) {
mxm = mxm.credentials(email, password);
}
let mxm = mxm.build().unwrap();
LOGIN_LOCK.get_or_try_init(|| mxm.login()).await.unwrap();
mxm
}
mod album {
use super::*;
use musixmatch_inofficial::models::AlbumType;
@ -43,8 +49,8 @@ mod album {
#[case::id(AlbumId::AlbumId(14248253))]
#[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))]
#[tokio::test]
async fn by_id(#[case] album_id: AlbumId<'_>) {
let album = new_mxm().album(album_id).await.unwrap();
async fn by_id(#[case] album_id: AlbumId<'_>, #[future] mxm: Musixmatch) {
let album = mxm.await.album(album_id).await.unwrap();
assert_eq!(album.album_id, 14248253);
assert_eq!(
@ -86,31 +92,25 @@ mod album {
);
assert_eq!(album.album_vanity_id, "410698/Gangnam-Style-Single");
assert!(album.updated_time > datetime!(2022-6-3 0:00 UTC));
assert_eq!(
album.album_coverart_100x100.unwrap(),
"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"
);
assert_imgurl(&album.album_coverart_100x100, "/26544045.jpg");
assert_imgurl(&album.album_coverart_350x350, "/26544045_350_350.jpg");
assert_imgurl(&album.album_coverart_500x500, "/26544045_500_500.jpg");
}
#[rstest]
#[tokio::test]
async fn album_ep() {
let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap();
async fn album_ep(#[future] mxm: Musixmatch) {
let album = mxm.await.album(AlbumId::AlbumId(23976123)).await.unwrap();
assert_eq!(album.album_name, "Waldbrand EP");
assert_eq!(album.album_release_type, AlbumType::Ep);
// assert_eq!(album.album_release_type, AlbumType::Ep);
assert_eq!(album.album_release_date, Some(date!(2016 - 09 - 30)));
}
#[rstest]
#[tokio::test]
async fn by_id_missing() {
let err = new_mxm()
async fn by_id_missing(#[future] mxm: Musixmatch) {
let err = mxm
.await
.album(AlbumId::AlbumId(999999999999))
.await
.unwrap_err();
@ -118,9 +118,11 @@ mod album {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn artist_albums() {
let albums = new_mxm()
async fn artist_albums(#[future] mxm: Musixmatch) {
let albums = mxm
.await
.artist_albums(ArtistId::ArtistId(1039), None, 10, 1)
.await
.unwrap();
@ -128,9 +130,11 @@ mod album {
assert_eq!(albums.len(), 10);
}
#[rstest]
#[tokio::test]
async fn artist_albums_missing() {
let err = new_mxm()
async fn artist_albums_missing(#[future] mxm: Musixmatch) {
let err = mxm
.await
.artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1)
.await
.unwrap_err();
@ -138,9 +142,10 @@ mod album {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn charts() {
let albums = new_mxm().chart_albums("US", 10, 1).await.unwrap();
async fn charts(#[future] mxm: Musixmatch) {
let albums = mxm.await.chart_albums("US", 10, 1).await.unwrap();
assert_eq!(albums.len(), 10);
}
@ -153,8 +158,8 @@ mod artist {
#[case::id(ArtistId::ArtistId(410698))]
#[case::musicbrainz(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))]
#[tokio::test]
async fn by_id(#[case] artist_id: ArtistId<'_>) {
let artist = new_mxm().artist(artist_id).await.unwrap();
async fn by_id(#[case] artist_id: ArtistId<'_>, #[future] mxm: Musixmatch) {
let artist = mxm.await.artist(artist_id).await.unwrap();
// dbg!(&artist);
@ -196,9 +201,11 @@ mod artist {
assert_eq!(artist.end_date, None);
}
#[rstest]
#[tokio::test]
async fn by_id_missing() {
let err = new_mxm()
async fn by_id_missing(#[future] mxm: Musixmatch) {
let err = mxm
.await
.artist(ArtistId::ArtistId(999999999999))
.await
.unwrap_err();
@ -206,9 +213,11 @@ mod artist {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn related() {
let artists = new_mxm()
async fn related(#[future] mxm: Musixmatch) {
let artists = mxm
.await
.artist_related(ArtistId::ArtistId(26485840), 10, 1)
.await
.unwrap();
@ -216,9 +225,11 @@ mod artist {
assert_eq!(artists.len(), 10);
}
#[rstest]
#[tokio::test]
async fn related_missing() {
let err = new_mxm()
async fn related_missing(#[future] mxm: Musixmatch) {
let err = mxm
.await
.artist_related(ArtistId::ArtistId(999999999999), 10, 1)
.await
.unwrap_err();
@ -226,20 +237,25 @@ mod artist {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn search() {
let artists = new_mxm().artist_search("psy", 5, 1).await.unwrap();
assert_eq!(artists.len(), 5);
async fn search(#[future] mxm: Musixmatch) {
let artists = mxm
.await
.artist_search("Snollebollekes", 5, 1)
.await
.unwrap();
let artist = &artists[0];
assert_eq!(artist.artist_id, 410698);
assert_eq!(artist.artist_name, "PSY");
assert_eq!(artist.artist_id, 25344078);
assert_eq!(artist.artist_name, "Snollebollekes");
}
#[rstest]
#[tokio::test]
async fn search_empty() {
let artists = new_mxm()
async fn search_empty(#[future] mxm: Musixmatch) {
let artists = mxm
.await
.artist_search(
"Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz",
5,
@ -251,16 +267,18 @@ mod artist {
assert_eq!(artists.len(), 0);
}
#[rstest]
#[tokio::test]
async fn charts() {
let artists = new_mxm().chart_artists("US", 10, 1).await.unwrap();
async fn charts(#[future] mxm: Musixmatch) {
let artists = mxm.await.chart_artists("US", 10, 1).await.unwrap();
assert_eq!(artists.len(), 10);
}
#[rstest]
#[tokio::test]
async fn charts_no_country() {
let artists = new_mxm().chart_artists("XY", 10, 1).await.unwrap();
async fn charts_no_country(#[future] mxm: Musixmatch) {
let artists = mxm.await.chart_artists("XY", 10, 1).await.unwrap();
assert_eq!(artists.len(), 10);
}
@ -275,8 +293,13 @@ mod track {
#[case::translation_2c(true, false)]
#[case::translation_3c(true, true)]
#[tokio::test]
async fn from_match(#[case] translation_status: bool, #[case] lang_3c: bool) {
let track = new_mxm()
async fn from_match(
#[case] translation_status: bool,
#[case] lang_3c: bool,
#[future] mxm: Musixmatch,
) {
let track = mxm
.await
.matcher_track(
"Poker Face",
"Lady Gaga",
@ -289,12 +312,12 @@ mod track {
// dbg!(&track);
assert_eq!(track.track_id, 15476784);
assert_eq!(
track.track_mbid.unwrap(),
"080975b0-39b1-493c-ae64-5cb3292409bb"
);
assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
assert_eq!(track.track_id, 85213841);
// assert_eq!(
// track.track_mbid.unwrap(),
// "080975b0-39b1-493c-ae64-5cb3292409bb"
// );
// assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
assert!(
track.commontrack_isrcs[0]
.iter()
@ -302,7 +325,7 @@ mod track {
"commontrack_isrcs: {:?}",
&track.commontrack_isrcs[0],
);
assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO");
assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg");
assert!(
track
.commontrack_spotify_ids
@ -322,7 +345,7 @@ mod track {
assert!(track.num_favourite > 50);
assert!(track.lyrics_id.is_some());
assert_eq!(track.subtitle_id.unwrap(), 36450705);
assert_eq!(track.album_id, 13810402);
assert_eq!(track.album_id, 20960801);
assert_eq!(track.album_name, "The Fame");
assert_eq!(track.artist_id, 378462);
assert_eq!(
@ -330,18 +353,10 @@ mod track {
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
);
assert_eq!(track.artist_name, "Lady Gaga");
assert_eq!(
track.album_coverart_100x100.unwrap(),
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.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_imgurl(&track.album_coverart_100x100, "/32133892.jpg");
assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg");
assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg");
assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1");
let first_release = track.first_release_date.unwrap();
assert_eq!(first_release.date(), date!(2008 - 1 - 1));
@ -389,8 +404,8 @@ mod track {
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
#[tokio::test]
async fn from_id(#[case] track_id: TrackId<'_>) {
let track = new_mxm().track(track_id, true, false).await.unwrap();
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
let track = mxm.await.track(track_id, true, false).await.unwrap();
// dbg!(&track);
@ -411,18 +426,9 @@ mod track {
assert_eq!(track.album_name, "Black Mamba");
assert_eq!(track.artist_id, 46970441);
assert_eq!(track.artist_name, "aespa");
assert_eq!(
track.album_coverart_100x100.unwrap(),
"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_imgurl(&track.album_coverart_100x100, "/52156772.jpg");
assert_imgurl(&track.album_coverart_350x350, "/52156772_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/52156772_500_500.jpg");
assert_eq!(track.commontrack_vanity_id, "aespa/Black-Mamba");
let release_date = track.first_release_date.unwrap();
@ -439,20 +445,25 @@ mod track {
#[case::translation_2c(true, false)]
#[case::translation_3c(true, true)]
#[tokio::test]
async fn from_id_translations(#[case] translation_status: bool, #[case] lang_3c: bool) {
let track = new_mxm()
.track(TrackId::TrackId(15476784), translation_status, lang_3c)
async fn from_id_translations(
#[case] translation_status: bool,
#[case] lang_3c: bool,
#[future] mxm: Musixmatch,
) {
let track = mxm
.await
.track(TrackId::Commontrack(47672612), translation_status, lang_3c)
.await
.unwrap();
// dbg!(&track);
assert_eq!(track.track_id, 15476784);
assert_eq!(
track.track_mbid.unwrap(),
"080975b0-39b1-493c-ae64-5cb3292409bb"
);
assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
assert_eq!(track.track_id, 85213841);
// assert_eq!(
// track.track_mbid.unwrap(),
// "080975b0-39b1-493c-ae64-5cb3292409bb"
// );
// assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
assert!(
track.commontrack_isrcs[0]
.iter()
@ -460,7 +471,7 @@ mod track {
"commontrack_isrcs: {:?}",
&track.commontrack_isrcs[0],
);
assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO");
assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg");
assert!(
track
.commontrack_spotify_ids
@ -480,7 +491,7 @@ mod track {
assert!(track.num_favourite > 50);
assert!(track.lyrics_id.is_some());
assert_eq!(track.subtitle_id.unwrap(), 36450705);
assert_eq!(track.album_id, 13810402);
assert_eq!(track.album_id, 20960801);
assert_eq!(track.album_name, "The Fame");
assert_eq!(track.artist_id, 378462);
assert_eq!(
@ -488,18 +499,10 @@ mod track {
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
);
assert_eq!(track.artist_name, "Lady Gaga");
assert_eq!(
track.album_coverart_100x100.unwrap(),
"https://s.mxmcdn.net/images-storage/albums/6/3/6/9/1/3/26319636.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_imgurl(&track.album_coverart_100x100, "/32133892.jpg");
assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg");
assert_imgurl(&track.album_coverart_800x800, "/32133892_800_800.jpg");
assert_eq!(track.commontrack_vanity_id, "Lady-Gaga/poker-face-1");
let first_release = track.first_release_date.unwrap();
assert_eq!(first_release.date(), date!(2008 - 1 - 1));
@ -540,9 +543,11 @@ mod track {
}
}
#[rstest]
#[tokio::test]
async fn from_id_missing() {
let err = new_mxm()
async fn from_id_missing(#[future] mxm: Musixmatch) {
let err = mxm
.await
.track(TrackId::TrackId(999999999999), false, false)
.await
.unwrap_err();
@ -550,9 +555,11 @@ mod track {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn album_tracks() {
let tracks = new_mxm()
async fn album_tracks(#[future] mxm: Musixmatch) {
let tracks = mxm
.await
.album_tracks(AlbumId::AlbumId(17118624), true, 20, 1)
.await
.unwrap();
@ -589,9 +596,11 @@ mod track {
});
}
#[rstest]
#[tokio::test]
async fn album_missing() {
let err = new_mxm()
async fn album_missing(#[future] mxm: Musixmatch) {
let err = mxm
.await
.album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1)
.await
.unwrap_err();
@ -603,8 +612,9 @@ mod track {
#[case::top(ChartName::Top)]
#[case::hot(ChartName::Hot)]
#[tokio::test]
async fn charts(#[case] chart_name: ChartName) {
let tracks = new_mxm()
async fn charts(#[case] chart_name: ChartName, #[future] mxm: Musixmatch) {
let tracks = mxm
.await
.chart_tracks("US", chart_name, true, 20, 1)
.await
.unwrap();
@ -612,9 +622,11 @@ mod track {
assert_eq!(tracks.len(), 20);
}
#[rstest]
#[tokio::test]
async fn search() {
let tracks = new_mxm()
async fn search(#[future] mxm: Musixmatch) {
let tracks = mxm
.await
.track_search()
.q_artist("Lena")
.q_track("Satellite")
@ -633,9 +645,11 @@ mod track {
assert_eq!(track.artist_name, "Lena");
}
#[rstest]
#[tokio::test]
async fn search_lyrics() {
let tracks = new_mxm()
async fn search_lyrics(#[future] mxm: Musixmatch) {
let tracks = mxm
.await
.track_search()
.q_lyrics("the whole world stops and stares for a while")
.s_track_rating(SortOrder::Desc)
@ -643,16 +657,18 @@ mod track {
.await
.unwrap();
assert_eq!(tracks.len(), 10);
assert_gte(tracks.len(), 8, "tracks");
let track = &tracks[0];
assert_eq!(track.track_name, "Just the Way You Are");
assert_eq!(track.artist_name, "Bruno Mars");
}
#[rstest]
#[tokio::test]
async fn search_empty() {
let artists = new_mxm()
async fn search_empty(#[future] mxm: Musixmatch) {
let artists = mxm
.await
.track_search()
.q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz")
.send(10, 1)
@ -662,16 +678,19 @@ mod track {
assert_eq!(artists.len(), 0);
}
#[rstest]
#[tokio::test]
async fn genres() {
let genres = new_mxm().genres().await.unwrap();
async fn genres(#[future] mxm: Musixmatch) {
let genres = mxm.await.genres().await.unwrap();
assert!(genres.len() > 360);
dbg!(&genres);
}
#[rstest]
#[tokio::test]
async fn snippet() {
let snippet = new_mxm()
async fn snippet(#[future] mxm: Musixmatch) {
let snippet = mxm
.await
.track_snippet(TrackId::Commontrack(8874280))
.await
.unwrap();
@ -693,22 +712,27 @@ mod lyrics {
use super::*;
#[rstest]
#[tokio::test]
async fn from_match() {
let lyrics = new_mxm().matcher_lyrics("Shine", "Spektrem").await.unwrap();
async fn from_match(#[future] mxm: Musixmatch) {
let lyrics = mxm.await.matcher_lyrics("Shine", "Spektrem").await.unwrap();
// dbg!(&lyrics);
assert_eq!(lyrics.lyrics_id, 25947036);
assert_eq!(lyrics.lyrics_id, 34583240);
assert!(!lyrics.instrumental);
assert!(!lyrics.explicit);
assert!(lyrics
assert!(
lyrics
.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_description.unwrap(), "English");
let copyright = lyrics.lyrics_copyright.unwrap();
assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",);
assert!(copyright.contains("Jesse Warren"), "copyright: {copyright}",);
assert!(lyrics.updated_time > datetime!(2021-6-3 0:00 UTC));
}
@ -719,8 +743,8 @@ mod lyrics {
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
#[tokio::test]
async fn from_id(#[case] track_id: TrackId<'_>) {
let lyrics = new_mxm().track_lyrics(track_id).await.unwrap();
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
let lyrics = mxm.await.track_lyrics(track_id).await.unwrap();
// dbg!(&lyrics);
@ -736,9 +760,11 @@ mod lyrics {
}
/// This track has no lyrics
#[rstest]
#[tokio::test]
async fn instrumental() {
let lyrics = new_mxm()
async fn instrumental(#[future] mxm: Musixmatch) {
let lyrics = mxm
.await
.matcher_lyrics("drivers license", "Bobby G")
.await
.unwrap();
@ -754,9 +780,11 @@ mod lyrics {
}
/// This track does not exist
#[rstest]
#[tokio::test]
async fn missing() {
let err = new_mxm()
async fn missing(#[future] mxm: Musixmatch) {
let err = mxm
.await
.track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into()))
.await
.unwrap_err();
@ -764,14 +792,16 @@ mod lyrics {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn download_testdata() {
async fn download_testdata(#[future] mxm: Musixmatch) {
let mxm = mxm.await;
let json_path = testfile("lyrics.json");
if json_path.exists() {
return;
}
let lyrics = new_mxm()
let lyrics = mxm
.track_lyrics(TrackId::Commontrack(18576954))
.await
.unwrap();
@ -780,14 +810,16 @@ mod lyrics {
serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap();
}
#[rstest]
#[tokio::test]
async fn download_testdata_translation() {
async fn download_testdata_translation(#[future] mxm: Musixmatch) {
let mxm = mxm.await;
let json_path = testfile("translation.json");
if json_path.exists() {
return;
}
let translations = new_mxm()
let translations = mxm
.track_lyrics_translation(TrackId::Commontrack(18576954), "de")
.await
.unwrap();
@ -796,9 +828,10 @@ mod lyrics {
serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap();
}
#[rstest]
#[tokio::test]
async fn concurrency() {
let mxm = new_mxm();
async fn concurrency(#[future] mxm: Musixmatch) {
let mxm = mxm.await;
let album = mxm
.album_tracks(
@ -833,9 +866,11 @@ mod subtitles {
use super::*;
use musixmatch_inofficial::models::SubtitleFormat;
#[rstest]
#[tokio::test]
async fn from_match() {
let subtitle = new_mxm()
async fn from_match(#[future] mxm: Musixmatch) {
let subtitle = mxm
.await
.matcher_subtitle(
"Shine",
"Spektrem",
@ -848,12 +883,12 @@ mod subtitles {
// dbg!(&subtitle);
assert_eq!(subtitle.subtitle_id, 36913312);
assert_eq!(subtitle.subtitle_id, 35340319);
assert_eq!(subtitle.subtitle_language.unwrap(), "en");
assert_eq!(subtitle.subtitle_language_description.unwrap(), "English");
let copyright = subtitle.lyrics_copyright.unwrap();
assert!(copyright.contains("Kim Jeffeson"), "copyright: {copyright}",);
assert_eq!(subtitle.subtitle_length, 315);
assert!(copyright.contains("Jesse Warren"), "copyright: {copyright}",);
assert_eq!(subtitle.subtitle_length, 316);
assert!(subtitle.updated_time > datetime!(2021-6-30 0:00 UTC));
}
@ -864,8 +899,9 @@ mod subtitles {
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
#[tokio::test]
async fn from_id(#[case] track_id: TrackId<'_>) {
let subtitle = new_mxm()
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
let subtitle = mxm
.await
.track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0))
.await
.unwrap();
@ -885,9 +921,11 @@ mod subtitles {
}
/// This track has no lyrics
#[rstest]
#[tokio::test]
async fn instrumental() {
let err = new_mxm()
async fn instrumental(#[future] mxm: Musixmatch) {
let err = mxm
.await
.matcher_subtitle(
"drivers license",
"Bobby G",
@ -902,9 +940,11 @@ mod subtitles {
}
/// This track has not been synced
#[rstest]
#[tokio::test]
async fn unsynced() {
let err = new_mxm()
async fn unsynced(#[future] mxm: Musixmatch) {
let err = mxm
.await
.track_subtitle(
TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()),
SubtitleFormat::Json,
@ -918,9 +958,11 @@ mod subtitles {
}
/// Try to get subtitles with wrong length parameter
#[rstest]
#[tokio::test]
async fn wrong_length() {
let err = new_mxm()
async fn wrong_length(#[future] mxm: Musixmatch) {
let err = mxm
.await
.track_subtitle(
TrackId::Commontrack(118480583),
SubtitleFormat::Json,
@ -933,14 +975,16 @@ mod subtitles {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn download_testdata() {
async fn download_testdata(#[future] mxm: Musixmatch) {
let json_path = testfile("subtitles.json");
if json_path.exists() {
return;
}
let subtitle = new_mxm()
let subtitle = mxm
.await
.track_subtitle(
TrackId::Commontrack(18576954),
SubtitleFormat::Json,
@ -991,12 +1035,20 @@ mod translation {
}
}
#[tokio::test]
async fn no_credentials() {
let mxm = Musixmatch::builder().no_storage().build().unwrap();
let err = mxm
.track_lyrics(TrackId::TrackId(205688271))
.await
.unwrap_err();
assert!(matches!(err, Error::MissingCredentials), "error: {err}");
#[track_caller]
fn assert_imgurl(url: &Option<String>, ends_with: &str) {
assert!(
url.as_deref().is_some_and(
|url| url.starts_with("https://s.mxmcdn.net/images-storage/")
&& url.ends_with(ends_with)
),
"expected url ending with {ends_with}\ngot {:?}",
url
);
}
/// 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}");
}