Compare commits

..

22 commits

Author SHA1 Message Date
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
15 changed files with 672 additions and 366 deletions

View file

@ -4,6 +4,14 @@ on: [push, pull_request]
jobs: jobs:
Test: Test:
runs-on: cimaster-latest 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: steps:
- name: 📦 Checkout repository - name: 📦 Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -17,3 +25,5 @@ jobs:
- name: 🧪 Test - name: 🧪 Test
run: cargo test --workspace run: cargo test --workspace
env:
ALL_PROXY: "http://warpproxy:8124"

View file

@ -23,13 +23,8 @@ jobs:
echo END_OF_FILE echo END_OF_FILE
} >> "$GITHUB_ENV" } >> "$GITHUB_ENV"
- name: 📤 Publish crate on code.thetadev.de - name: 📤 Publish crate on crates.io
run: | run: cargo publish --token ${{ secrets.CARGO_TOKEN }} --package "${{ env.CRATE }}"
mkdir -p ~/.cargo
printf '[registries.thetadev]\nindex = "https://code.thetadev.de/ThetaDev/_cargo-index.git"\ntoken = "Bearer ${{ secrets.TOKEN_GITEA }}"\n' >> ~/.cargo/config.toml
sed -i "s/^musixmatch-.*=\s*{/\0 registry = \"thetadev\",/g" Cargo.toml
cargo publish --registry thetadev --package "${{ env.CRATE }}" --allow-dirty
git restore Cargo.toml
- name: 🎉 Publish release - name: 🎉 Publish release
uses: https://gitea.com/actions/release-action@main uses: https://gitea.com/actions/release-action@main

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

@ -2,7 +2,31 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## v0.1.0 - 2024-03-23
## [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 Initial release

View file

@ -1,6 +1,7 @@
[package] [package]
name = "musixmatch-inofficial" name = "musixmatch-inofficial"
version = "0.1.0" version = "0.1.1"
rust-version = "1.70.0"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
@ -8,22 +9,21 @@ repository.workspace = true
keywords.workspace = true keywords.workspace = true
description = "Inofficial client for the Musixmatch API" description = "Inofficial client for the Musixmatch API"
include = ["/src", "README.md", "CHANGELOG.md", "LICENSE"]
include = ["/src", "README.md", "LICENSE"]
[workspace] [workspace]
members = [".", "cli"] members = [".", "cli"]
[workspace.package] [workspace.package]
edition = "2021" edition = "2021"
authors = ["ThetaDev <t.testboy@gmail.com>"] authors = ["ThetaDev <thetadev@magenta.de>"]
license = "MIT" license = "MIT"
repository = "https://code.thetadev.de/ThetaDev/musixmatch-inofficial" repository = "https://codeberg.org/ThetaDev/musixmatch-inofficial"
keywords = ["music", "lyrics"] keywords = ["music", "lyrics"]
categories = ["api-bindings", "multimedia"] categories = ["api-bindings", "multimedia"]
[workspace.dependencies] [workspace.dependencies]
musixmatch-inofficial = { version = "0.1.0", path = ".", default-features = false } musixmatch-inofficial = { version = "0.1.1", path = ".", default-features = false }
[features] [features]
default = ["default-tls"] default = ["default-tls"]
@ -41,30 +41,26 @@ reqwest = { version = "0.12.0", default-features = false, features = [
"json", "json",
"gzip", "gzip",
] } ] }
tokio = { version = "1.20.0" } tokio = { version = "1.20.4" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85" serde_json = "1.0.85"
thiserror = "1.0.36" thiserror = "1.0.0"
log = "0.4.17" log = "0.4.17"
time = { version = "0.3.15", features = [ time = { version = "0.3.10", features = [
"macros", "macros",
"formatting", "formatting",
"serde", "serde",
"serde-well-known", "serde-well-known",
] } ] }
hmac = "0.12.1" hmac = "0.12.0"
sha1 = "0.10.5" sha1 = "0.10.0"
rand = "0.8.5" rand = "0.8.0"
base64 = "0.22.0" base64 = "0.22.0"
[dev-dependencies] [dev-dependencies]
ctor = "0.2.0" rstest = { version = "0.22.0", default-features = false }
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.0", features = ["macros"] } tokio = { version = "1.20.4", features = ["macros"] }
futures = "0.3.21" futures = "0.3.21"
path_macro = "1.0.0" path_macro = "1.0.0"
governor = "0.6.3"
[profile.release]
strip = true

View file

@ -10,12 +10,12 @@ release crate="musixmatch-inofficial":
CHANGELOG="CHANGELOG.md" CHANGELOG="CHANGELOG.md"
if [ "$CRATE" = "musixmatch-inofficial" ]; then if [ "$CRATE" = "musixmatch-inofficial" ]; then
INCLUDES="$INCLUDES --include-path src/** --include-path tests/** --include-path testfiles/**" INCLUDES="$INCLUDES --include-path 'src/**' --include-path 'tests/**' --include-path 'testfiles/**'"
else else
if [ ! -d "$CRATE" ]; then if [ ! -d "$CRATE" ]; then
echo "$CRATE does not exist."; exit 1 echo "$CRATE does not exist."; exit 1
fi fi
INCLUDES="$INCLUDES --include-path $CRATE/**" INCLUDES="$INCLUDES --include-path '$CRATE/**'"
CHANGELOG="$CRATE/$CHANGELOG" CHANGELOG="$CRATE/$CHANGELOG"
CRATE="musixmatch-$CRATE" # Add crate name prefix CRATE="musixmatch-$CRATE" # Add crate name prefix
fi fi
@ -26,17 +26,17 @@ release crate="musixmatch-inofficial":
if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi
CLIFF_ARGS="--tag v${VERSION} --tag-pattern ${CRATE}/* --unreleased $INCLUDES" CLIFF_ARGS="--tag '${TAG}' --tag-pattern '${CRATE}/v*' --unreleased $INCLUDES"
echo "git-cliff $CLIFF_ARGS" echo "git-cliff $CLIFF_ARGS"
if [ -f "$CHANGELOG" ]; then if [ -f "$CHANGELOG" ]; then
git-cliff $CLIFF_ARGS --prepend "$CHANGELOG" eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'"
else else
git-cliff $CLIFF_ARGS --output "$CHANGELOG" eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'"
fi fi
editor "$CHANGELOG" editor "$CHANGELOG"
git add "$CHANGELOG" git add .
git commit -m "chore(release): release $CRATE v$VERSION" 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" awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG"

View file

@ -1,4 +1,8 @@
# 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.
@ -7,10 +11,10 @@ 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 required a free account on <https://www.musixmatch.com> to be used. The Musixmatch API used to require a free account on <https://www.musixmatch.com> to be
However, as of 2024, this requirement was removed and the API can be used anonymously. used. However, as of 2024, this requirement was removed and the API can be used
The client still allows you to supply credentials if Musixmatch decided to close the API anonymously. The client still allows you to supply credentials if Musixmatch decides to
down again. close the API down again.
## ⚠️ Copyright disclaimer ## ⚠️ Copyright disclaimer

View file

@ -2,7 +2,37 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## v0.1.0 - 2024-03-23
## [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 Initial release

View file

@ -1,6 +1,7 @@
[package] [package]
name = "musixmatch-cli" name = "musixmatch-cli"
version = "0.1.0" version = "0.2.0"
rust-version = "1.70.0"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
@ -9,7 +10,7 @@ keywords.workspace = true
description = "Inofficial command line interface for the Musixmatch API" description = "Inofficial command line interface for the Musixmatch API"
[features] [features]
default = ["rustls-tls-native-roots"] default = ["native-tls"]
# Reqwest TLS options # Reqwest TLS options
native-tls = ["musixmatch-inofficial/native-tls"] native-tls = ["musixmatch-inofficial/native-tls"]
@ -20,11 +21,9 @@ rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"]
[dependencies] [dependencies]
musixmatch-inofficial.workspace = true musixmatch-inofficial.workspace = true
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.20.4", features = ["macros", "rt-multi-thread"] }
id3 = "1.3.0" clap = { version = "4.0.0", features = ["derive"] }
mp3-duration = "0.1.10" anyhow = "1.0.0"
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.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::{ use std::{
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
path::PathBuf, path::PathBuf,
@ -5,9 +8,8 @@ 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::{SubtitleFormat, Track, TrackId, TranslationMap}, models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap},
Musixmatch, Musixmatch,
}; };
@ -20,14 +22,50 @@ struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
Get { /// Get lyrics text
#[command(subcommand)] Lyrics {
command: GetCommands, #[clap(flatten)]
ident: TrackIdentifiers,
/// Language
#[clap(long)]
lang: Option<String>,
/// Bilingual
#[clap(long)]
bi: bool,
}, },
Mp3 { /// Get subtitles (time-synced lyrics)
#[command(subcommand)] Subtitles {
command: FileCommands, #[clap(flatten)]
ident: TrackIdentifiers,
/// Track length
#[clap(short, long)]
length: Option<f32>,
/// Maximum deviation from track length (Default: 1s)
#[clap(long)]
max_deviation: Option<f32>,
/// Subtitle format
#[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)] #[group(required = true)]
Search { Search {
/// Track name /// Track name
@ -42,40 +80,8 @@ enum Commands {
/// Search query /// Search query
query: Option<Vec<String>>, query: Option<Vec<String>>,
}, },
} /// Search for Musixmatch artists
SearchArtist { query: Vec<String> },
#[derive(Subcommand)]
enum GetCommands {
Lyrics {
#[clap(flatten)]
ident: TrackIdentifiers,
/// Language
#[clap(long)]
lang: Option<String>,
/// Bilingual
#[clap(long)]
bi: bool,
},
Subtitles {
#[clap(flatten)]
ident: TrackIdentifiers,
/// Track length
#[clap(long, short)]
length: Option<f32>,
/// Maximum deviation from track length (Default: 1s)
#[clap(long)]
max_deviation: Option<f32>,
/// Subtitle format
#[clap(long, default_value = "lrc")]
format: SubtitleFormatClap,
/// Language
#[clap(long)]
lang: Option<String>,
},
Track {
#[clap(flatten)]
ident: TrackIdentifiers,
},
} }
#[derive(Args)] #[derive(Args)]
@ -106,22 +112,38 @@ 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)]
pub enum SubtitleFormatClap { enum SubtitleFormatClap {
Lrc, Lrc,
Ttml, Ttml,
TtmlStructured, TtmlStructured,
@ -175,164 +197,148 @@ async fn run(cli: Cli) -> Result<()> {
}; };
match cli.command { match cli.command {
Commands::Get { command } => match command { Commands::Lyrics { ident, lang, bi } => {
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?;
eprintln!("Lyrics ID: {}", lyrics.lyrics_id); eprintln!("Lyrics ID: {}", lyrics.lyrics_id);
eprintln!( eprintln!(
"Language: {}", "Language: {}",
lyrics.lyrics_language.as_deref().unwrap_or(NA_STR) lyrics.lyrics_language.as_deref().unwrap_or(NA_STR)
); );
eprintln!( eprintln!(
"Copyright: {}", "Copyright: {}",
lyrics lyrics
.lyrics_copyright .lyrics_copyright
.as_deref() .as_deref()
.map(|c| c.trim()) .map(|c| c.trim())
.unwrap_or(NA_STR) .unwrap_or(NA_STR)
); );
let mut lyrics_body = lyrics.lyrics_body; let mut lyrics_body = lyrics.lyrics_body;
if let Some(lang) = lang { if let Some(lang) = lang {
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!( eprintln!("Translation not found. Returning lyrics in original language.");
"Translation not found. Returning lyrics in original language." } else {
); eprintln!("Translated to: {}", tl.lang);
let tm = TranslationMap::from(tl);
let translated = tm.translate_lyrics(&lyrics_body);
lyrics_body = if bi {
lyrics_body
.lines()
.zip(translated.lines())
.map(|(a, b)| {
if a == b {
a.to_string() + "\n"
} else {
format!("{a}\n> {b}\n")
}
})
.collect()
} else { } else {
eprintln!("Translated to: {}", tl.lang); translated
let tm = TranslationMap::from(tl); };
let translated = tm.translate_lyrics(&lyrics_body); }
lyrics_body = if bi { }
lyrics_body }
.lines()
.zip(translated.lines()) eprintln!();
.map(|(a, b)| { println!("{}", lyrics_body);
if a == b { }
a.to_string() + "\n" Commands::Subtitles {
} else { ident,
format!("{a}\n> {b}\n") length,
} max_deviation,
}) format,
.collect() lang,
} else { } => {
translated let track_id = get_track_id(ident, &mxm).await?;
}; let subtitles = mxm
} .track_subtitle(
track_id.clone(),
if lang.is_some() {
SubtitleFormat::Json
} else {
format.into()
},
length,
max_deviation.or(Some(1.0)),
)
.await?;
eprintln!("Subtitle ID: {}", subtitles.subtitle_id);
eprintln!(
"Language: {}",
subtitles.subtitle_language.as_deref().unwrap_or(NA_STR)
);
eprintln!("Length: {}", subtitles.subtitle_length);
eprintln!(
"Copyright: {}",
subtitles
.lyrics_copyright
.as_deref()
.map(|s| s.trim())
.unwrap_or(NA_STR)
);
if let Some(lang) = lang {
let mut lines = subtitles.to_lines()?;
if Some(&lang) != subtitles.subtitle_language.as_ref() {
let tl = mxm.track_lyrics_translation(track_id, &lang).await?;
if tl.is_empty() {
bail!("Translation not found")
} else {
eprintln!("Translated to: {}", tl.lang);
let tm = TranslationMap::from(tl);
lines = tm.translate_subtitles(&lines);
} }
} }
eprintln!(); eprintln!();
println!("{}", lyrics_body); let res = match format {
} SubtitleFormatClap::Lrc => lines.to_lrc(),
GetCommands::Subtitles { SubtitleFormatClap::Ttml => lines.to_ttml(),
ident, SubtitleFormatClap::Json => lines.to_json()?,
length, SubtitleFormatClap::TtmlStructured | SubtitleFormatClap::EbuStl => {
max_deviation, bail!("subtitle format {format:?} cant be translated")
format,
lang,
} => {
let track_id = get_track_id(ident, &mxm).await?;
let subtitles = mxm
.track_subtitle(
track_id.clone(),
if lang.is_some() {
SubtitleFormat::Json
} else {
format.into()
},
length,
max_deviation.or(Some(1.0)),
)
.await?;
eprintln!("Subtitle ID: {}", subtitles.subtitle_id);
eprintln!(
"Language: {}",
subtitles.subtitle_language.as_deref().unwrap_or(NA_STR)
);
eprintln!("Length: {}", subtitles.subtitle_length);
eprintln!(
"Copyright: {}",
subtitles
.lyrics_copyright
.as_deref()
.map(|s| s.trim())
.unwrap_or(NA_STR)
);
if let Some(lang) = lang {
let mut lines = subtitles.to_lines()?;
if Some(&lang) != subtitles.subtitle_language.as_ref() {
let tl = mxm.track_lyrics_translation(track_id, &lang).await?;
if tl.is_empty() {
bail!("Translation not found")
} else {
eprintln!("Translated to: {}", tl.lang);
let tm = TranslationMap::from(tl);
lines = tm.translate_subtitles(&lines);
}
} }
};
eprintln!(); println!("{}", res);
let res = match format { } else {
SubtitleFormatClap::Lrc => lines.to_lrc(), eprintln!();
SubtitleFormatClap::Ttml => lines.to_ttml(),
SubtitleFormatClap::Json => lines.to_json()?,
SubtitleFormatClap::TtmlStructured | SubtitleFormatClap::EbuStl => {
bail!("subtitle format {format:?} cant be translated")
}
};
println!("{}", res);
} else {
eprintln!();
println!("{}", subtitles.subtitle_body);
}
}
GetCommands::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?;
println!(
"Lyrics for {} by {}:\n\n{}",
title, artist, lyrics.lyrics_body
);
}
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); println!("{}", subtitles.subtitle_body);
} }
}, }
Commands::Track { ident } => {
let track = get_track(ident, &mxm).await?;
println!("{}", serde_json::to_string_pretty(&track)?)
}
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 { Commands::Search {
query, query,
name, name,
@ -369,6 +375,15 @@ async fn run(cli: Cli) -> Result<()> {
); );
} }
} }
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
);
}
}
}; };
Ok(()) Ok(())
} }

View file

@ -14,12 +14,13 @@ All notable changes to this project will be documented in this file.\n
# template for the changelog body # template for the changelog body
# https://keats.github.io/tera/docs/#introduction # https://keats.github.io/tera/docs/#introduction
body = """ body = """
{% set repo_url = "https://code.thetadev.de/ThetaDev/rustypipe" %}\ {% set repo_url = "https://codeberg.org/ThetaDev/musixmatch-inofficial" %}\
{% if version %}\ {% if version %}\
{%set vname = version | split(pat="/") | last %}
{%if previous.version %}\ {%if previous.version %}\
## [{{ version }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\ ## [{{ vname }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\
{% else %}\ {% else %}\
## {{ version }}\ ## [{{ vname }}]({{ repo_url }}/commits/tag/{{ version }})\
{% endif %} - {{ timestamp | date(format="%Y-%m-%d") }} {% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\ {% else %}\
## [unreleased] ## [unreleased]
@ -73,7 +74,7 @@ commit_parsers = [
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" }, { message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" }, { message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" }, { message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true }, { message = "^chore\\(release\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" }, { message = "^chore", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },

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

@ -99,8 +99,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,
} }

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 path_macro::path;
use rstest::rstest; use rstest::{fixture, rstest};
use time::macros::{date, datetime}; use time::macros::{date, datetime};
use musixmatch_inofficial::{ use musixmatch_inofficial::{
@ -9,19 +14,18 @@ use musixmatch_inofficial::{
Error, Musixmatch, Error, Musixmatch,
}; };
#[ctor::ctor] fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
fn init() { path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name)
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 { #[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(); let mut mxm = Musixmatch::builder();
if let (Ok(email), Ok(password)) = ( if let (Ok(email), Ok(password)) = (
@ -31,11 +35,10 @@ fn new_mxm() -> Musixmatch {
mxm = mxm.credentials(email, password); mxm = mxm.credentials(email, password);
} }
mxm.build().unwrap() let mxm = mxm.build().unwrap();
}
fn testfile<P: AsRef<Path>>(name: P) -> PathBuf { LOGIN_LOCK.get_or_try_init(|| mxm.login()).await.unwrap();
path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name) mxm
} }
mod album { mod album {
@ -46,8 +49,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<'_>) { async fn by_id(#[case] album_id: AlbumId<'_>, #[future] mxm: Musixmatch) {
let album = new_mxm().album(album_id).await.unwrap(); let album = mxm.await.album(album_id).await.unwrap();
assert_eq!(album.album_id, 14248253); assert_eq!(album.album_id, 14248253);
assert_eq!( assert_eq!(
@ -94,17 +97,20 @@ mod album {
assert_imgurl(&album.album_coverart_500x500, "/26544045_500_500.jpg"); assert_imgurl(&album.album_coverart_500x500, "/26544045_500_500.jpg");
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn album_ep() { async fn album_ep(#[future] mxm: Musixmatch) {
let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap(); let album = mxm.await.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() { async fn by_id_missing(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.album(AlbumId::AlbumId(999999999999)) .album(AlbumId::AlbumId(999999999999))
.await .await
.unwrap_err(); .unwrap_err();
@ -112,9 +118,11 @@ mod album {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
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();
@ -122,9 +130,11 @@ mod album {
assert_eq!(albums.len(), 10); assert_eq!(albums.len(), 10);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn artist_albums_missing() { async fn artist_albums_missing(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1) .artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1)
.await .await
.unwrap_err(); .unwrap_err();
@ -132,9 +142,10 @@ mod album {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn charts() { async fn charts(#[future] mxm: Musixmatch) {
let albums = new_mxm().chart_albums("US", 10, 1).await.unwrap(); let albums = mxm.await.chart_albums("US", 10, 1).await.unwrap();
assert_eq!(albums.len(), 10); assert_eq!(albums.len(), 10);
} }
@ -147,8 +158,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<'_>) { async fn by_id(#[case] artist_id: ArtistId<'_>, #[future] mxm: Musixmatch) {
let artist = new_mxm().artist(artist_id).await.unwrap(); let artist = mxm.await.artist(artist_id).await.unwrap();
// dbg!(&artist); // dbg!(&artist);
@ -190,9 +201,11 @@ mod artist {
assert_eq!(artist.end_date, None); assert_eq!(artist.end_date, None);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn by_id_missing() { async fn by_id_missing(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.artist(ArtistId::ArtistId(999999999999)) .artist(ArtistId::ArtistId(999999999999))
.await .await
.unwrap_err(); .unwrap_err();
@ -200,9 +213,11 @@ mod artist {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn related() { async fn related(#[future] mxm: Musixmatch) {
let artists = new_mxm() let artists = mxm
.await
.artist_related(ArtistId::ArtistId(26485840), 10, 1) .artist_related(ArtistId::ArtistId(26485840), 10, 1)
.await .await
.unwrap(); .unwrap();
@ -210,9 +225,11 @@ mod artist {
assert_eq!(artists.len(), 10); assert_eq!(artists.len(), 10);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn related_missing() { async fn related_missing(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.artist_related(ArtistId::ArtistId(999999999999), 10, 1) .artist_related(ArtistId::ArtistId(999999999999), 10, 1)
.await .await
.unwrap_err(); .unwrap_err();
@ -220,20 +237,25 @@ mod artist {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search() { async fn search(#[future] mxm: Musixmatch) {
let artists = new_mxm().artist_search("psy", 5, 1).await.unwrap(); let artists = mxm
.await
assert_eq!(artists.len(), 5); .artist_search("Snollebollekes", 5, 1)
.await
.unwrap();
let artist = &artists[0]; let artist = &artists[0];
assert_eq!(artist.artist_id, 410698); assert_eq!(artist.artist_id, 25344078);
assert_eq!(artist.artist_name, "PSY"); assert_eq!(artist.artist_name, "Snollebollekes");
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search_empty() { async fn search_empty(#[future] mxm: Musixmatch) {
let artists = new_mxm() let artists = mxm
.await
.artist_search( .artist_search(
"Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz", "Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz",
5, 5,
@ -245,16 +267,18 @@ mod artist {
assert_eq!(artists.len(), 0); assert_eq!(artists.len(), 0);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn charts() { async fn charts(#[future] mxm: Musixmatch) {
let artists = new_mxm().chart_artists("US", 10, 1).await.unwrap(); let artists = mxm.await.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() { async fn charts_no_country(#[future] mxm: Musixmatch) {
let artists = new_mxm().chart_artists("XY", 10, 1).await.unwrap(); let artists = mxm.await.chart_artists("XY", 10, 1).await.unwrap();
assert_eq!(artists.len(), 10); assert_eq!(artists.len(), 10);
} }
@ -269,8 +293,13 @@ 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(#[case] translation_status: bool, #[case] lang_3c: bool) { async fn from_match(
let track = new_mxm() #[case] translation_status: bool,
#[case] lang_3c: bool,
#[future] mxm: Musixmatch,
) {
let track = mxm
.await
.matcher_track( .matcher_track(
"Poker Face", "Poker Face",
"Lady Gaga", "Lady Gaga",
@ -283,12 +312,12 @@ mod track {
// dbg!(&track); // dbg!(&track);
assert_eq!(track.track_id, 15476784); assert_eq!(track.track_id, 85213841);
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()
@ -296,7 +325,7 @@ mod track {
"commontrack_isrcs: {:?}", "commontrack_isrcs: {:?}",
&track.commontrack_isrcs[0], &track.commontrack_isrcs[0],
); );
assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO"); assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg");
assert!( assert!(
track track
.commontrack_spotify_ids .commontrack_spotify_ids
@ -316,7 +345,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, 13810402); assert_eq!(track.album_id, 20960801);
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!(
@ -324,9 +353,10 @@ 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, "/26319636.jpg"); assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg");
assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg"); assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/26319636_500_500.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"); 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));
@ -374,8 +404,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<'_>) { async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
let track = new_mxm().track(track_id, true, false).await.unwrap(); let track = mxm.await.track(track_id, true, false).await.unwrap();
// dbg!(&track); // dbg!(&track);
@ -415,20 +445,25 @@ 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(#[case] translation_status: bool, #[case] lang_3c: bool) { async fn from_id_translations(
let track = new_mxm() #[case] translation_status: bool,
.track(TrackId::TrackId(15476784), translation_status, lang_3c) #[case] lang_3c: bool,
#[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, 15476784); assert_eq!(track.track_id, 85213841);
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()
@ -436,7 +471,7 @@ mod track {
"commontrack_isrcs: {:?}", "commontrack_isrcs: {:?}",
&track.commontrack_isrcs[0], &track.commontrack_isrcs[0],
); );
assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO"); assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg");
assert!( assert!(
track track
.commontrack_spotify_ids .commontrack_spotify_ids
@ -456,7 +491,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, 13810402); assert_eq!(track.album_id, 20960801);
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!(
@ -464,9 +499,10 @@ 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, "/26319636.jpg"); assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg");
assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg"); assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/26319636_500_500.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"); 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));
@ -507,9 +543,11 @@ mod track {
} }
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn from_id_missing() { async fn from_id_missing(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.track(TrackId::TrackId(999999999999), false, false) .track(TrackId::TrackId(999999999999), false, false)
.await .await
.unwrap_err(); .unwrap_err();
@ -517,9 +555,11 @@ mod track {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn album_tracks() { async fn album_tracks(#[future] mxm: Musixmatch) {
let tracks = new_mxm() let tracks = mxm
.await
.album_tracks(AlbumId::AlbumId(17118624), true, 20, 1) .album_tracks(AlbumId::AlbumId(17118624), true, 20, 1)
.await .await
.unwrap(); .unwrap();
@ -556,9 +596,11 @@ mod track {
}); });
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn album_missing() { async fn album_missing(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1) .album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1)
.await .await
.unwrap_err(); .unwrap_err();
@ -570,8 +612,9 @@ 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) { async fn charts(#[case] chart_name: ChartName, #[future] mxm: Musixmatch) {
let tracks = new_mxm() let tracks = mxm
.await
.chart_tracks("US", chart_name, true, 20, 1) .chart_tracks("US", chart_name, true, 20, 1)
.await .await
.unwrap(); .unwrap();
@ -579,9 +622,11 @@ mod track {
assert_eq!(tracks.len(), 20); assert_eq!(tracks.len(), 20);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search() { async fn search(#[future] mxm: Musixmatch) {
let tracks = new_mxm() let tracks = mxm
.await
.track_search() .track_search()
.q_artist("Lena") .q_artist("Lena")
.q_track("Satellite") .q_track("Satellite")
@ -600,9 +645,11 @@ mod track {
assert_eq!(track.artist_name, "Lena"); assert_eq!(track.artist_name, "Lena");
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search_lyrics() { async fn search_lyrics(#[future] mxm: Musixmatch) {
let tracks = new_mxm() let tracks = 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)
@ -610,16 +657,18 @@ mod track {
.await .await
.unwrap(); .unwrap();
assert_eq!(tracks.len(), 10); assert_gte(tracks.len(), 8, "tracks");
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() { async fn search_empty(#[future] mxm: Musixmatch) {
let artists = new_mxm() let artists = mxm
.await
.track_search() .track_search()
.q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz") .q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz")
.send(10, 1) .send(10, 1)
@ -629,16 +678,19 @@ mod track {
assert_eq!(artists.len(), 0); assert_eq!(artists.len(), 0);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn genres() { async fn genres(#[future] mxm: Musixmatch) {
let genres = new_mxm().genres().await.unwrap(); let genres = mxm.await.genres().await.unwrap();
assert!(genres.len() > 360); assert!(genres.len() > 360);
dbg!(&genres); dbg!(&genres);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn snippet() { async fn snippet(#[future] mxm: Musixmatch) {
let snippet = new_mxm() let snippet = mxm
.await
.track_snippet(TrackId::Commontrack(8874280)) .track_snippet(TrackId::Commontrack(8874280))
.await .await
.unwrap(); .unwrap();
@ -660,9 +712,10 @@ mod lyrics {
use super::*; use super::*;
#[rstest]
#[tokio::test] #[tokio::test]
async fn from_match() { async fn from_match(#[future] mxm: Musixmatch) {
let lyrics = new_mxm().matcher_lyrics("Shine", "Spektrem").await.unwrap(); let lyrics = mxm.await.matcher_lyrics("Shine", "Spektrem").await.unwrap();
// dbg!(&lyrics); // dbg!(&lyrics);
@ -690,8 +743,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<'_>) { async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
let lyrics = new_mxm().track_lyrics(track_id).await.unwrap(); let lyrics = mxm.await.track_lyrics(track_id).await.unwrap();
// dbg!(&lyrics); // dbg!(&lyrics);
@ -707,9 +760,11 @@ mod lyrics {
} }
/// This track has no lyrics /// This track has no lyrics
#[rstest]
#[tokio::test] #[tokio::test]
async fn instrumental() { async fn instrumental(#[future] mxm: Musixmatch) {
let lyrics = new_mxm() let lyrics = mxm
.await
.matcher_lyrics("drivers license", "Bobby G") .matcher_lyrics("drivers license", "Bobby G")
.await .await
.unwrap(); .unwrap();
@ -725,9 +780,11 @@ mod lyrics {
} }
/// This track does not exist /// This track does not exist
#[rstest]
#[tokio::test] #[tokio::test]
async fn missing() { async fn missing(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into())) .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into()))
.await .await
.unwrap_err(); .unwrap_err();
@ -735,14 +792,16 @@ mod lyrics {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn download_testdata() { async fn download_testdata(#[future] mxm: Musixmatch) {
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 = new_mxm() let lyrics = mxm
.track_lyrics(TrackId::Commontrack(18576954)) .track_lyrics(TrackId::Commontrack(18576954))
.await .await
.unwrap(); .unwrap();
@ -751,14 +810,16 @@ 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() { async fn download_testdata_translation(#[future] mxm: Musixmatch) {
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 = new_mxm() let translations = mxm
.track_lyrics_translation(TrackId::Commontrack(18576954), "de") .track_lyrics_translation(TrackId::Commontrack(18576954), "de")
.await .await
.unwrap(); .unwrap();
@ -767,9 +828,10 @@ 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() { async fn concurrency(#[future] mxm: Musixmatch) {
let mxm = new_mxm(); let mxm = mxm.await;
let album = mxm let album = mxm
.album_tracks( .album_tracks(
@ -804,9 +866,11 @@ 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() { async fn from_match(#[future] mxm: Musixmatch) {
let subtitle = new_mxm() let subtitle = mxm
.await
.matcher_subtitle( .matcher_subtitle(
"Shine", "Shine",
"Spektrem", "Spektrem",
@ -835,8 +899,9 @@ 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<'_>) { async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
let subtitle = new_mxm() let subtitle = 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();
@ -856,9 +921,11 @@ mod subtitles {
} }
/// This track has no lyrics /// This track has no lyrics
#[rstest]
#[tokio::test] #[tokio::test]
async fn instrumental() { async fn instrumental(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.matcher_subtitle( .matcher_subtitle(
"drivers license", "drivers license",
"Bobby G", "Bobby G",
@ -873,9 +940,11 @@ mod subtitles {
} }
/// This track has not been synced /// This track has not been synced
#[rstest]
#[tokio::test] #[tokio::test]
async fn unsynced() { async fn unsynced(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.track_subtitle( .track_subtitle(
TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()), TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()),
SubtitleFormat::Json, SubtitleFormat::Json,
@ -889,9 +958,11 @@ 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() { async fn wrong_length(#[future] mxm: Musixmatch) {
let err = new_mxm() let err = mxm
.await
.track_subtitle( .track_subtitle(
TrackId::Commontrack(118480583), TrackId::Commontrack(118480583),
SubtitleFormat::Json, SubtitleFormat::Json,
@ -904,14 +975,16 @@ mod subtitles {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn download_testdata() { async fn download_testdata(#[future] mxm: Musixmatch) {
let json_path = testfile("subtitles.json"); let json_path = testfile("subtitles.json");
if json_path.exists() { if json_path.exists() {
return; return;
} }
let subtitle = new_mxm() let subtitle = mxm
.await
.track_subtitle( .track_subtitle(
TrackId::Commontrack(18576954), TrackId::Commontrack(18576954),
SubtitleFormat::Json, SubtitleFormat::Json,
@ -973,3 +1046,9 @@ fn assert_imgurl(url: &Option<String>, ends_with: &str) {
url 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}");
}