Compare commits

...

26 commits

Author SHA1 Message Date
e4cffa53ca
test: skip artist_albums test 2024-10-23 22:51:41 +02:00
ThetaBot
4bfcb79173 chore(deps): update rust crate governor to 0.7.0 (#3) 2024-10-23 20:08:45 +00:00
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
15 changed files with 682 additions and 366 deletions

View file

@ -4,6 +4,14 @@ 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
@ -17,3 +25,5 @@ jobs:
- name: 🧪 Test
run: cargo test --workspace
env:
ALL_PROXY: "http://warpproxy:8124"

View file

@ -23,13 +23,8 @@ jobs:
echo END_OF_FILE
} >> "$GITHUB_ENV"
- name: 📤 Publish crate on code.thetadev.de
run: |
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 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

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.
## 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

View file

@ -1,6 +1,7 @@
[package]
name = "musixmatch-inofficial"
version = "0.1.0"
version = "0.1.1"
rust-version = "1.70.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
@ -8,22 +9,21 @@ repository.workspace = true
keywords.workspace = true
description = "Inofficial client for the Musixmatch API"
include = ["/src", "README.md", "LICENSE"]
include = ["/src", "README.md", "CHANGELOG.md", "LICENSE"]
[workspace]
members = [".", "cli"]
[workspace.package]
edition = "2021"
authors = ["ThetaDev <t.testboy@gmail.com>"]
authors = ["ThetaDev <thetadev@magenta.de>"]
license = "MIT"
repository = "https://code.thetadev.de/ThetaDev/musixmatch-inofficial"
repository = "https://codeberg.org/ThetaDev/musixmatch-inofficial"
keywords = ["music", "lyrics"]
categories = ["api-bindings", "multimedia"]
[workspace.dependencies]
musixmatch-inofficial = { version = "0.1.0", path = ".", default-features = false }
musixmatch-inofficial = { version = "0.1.1", path = ".", default-features = false }
[features]
default = ["default-tls"]
@ -41,30 +41,26 @@ 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"
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.7.0"

View file

@ -10,12 +10,12 @@ release crate="musixmatch-inofficial":
CHANGELOG="CHANGELOG.md"
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
if [ ! -d "$CRATE" ]; then
echo "$CRATE does not exist."; exit 1
fi
INCLUDES="$INCLUDES --include-path $CRATE/**"
INCLUDES="$INCLUDES --include-path '$CRATE/**'"
CHANGELOG="$CRATE/$CHANGELOG"
CRATE="musixmatch-$CRATE" # Add crate name prefix
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
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"
if [ -f "$CHANGELOG" ]; then
git-cliff $CLIFF_ARGS --prepend "$CHANGELOG"
eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'"
else
git-cliff $CLIFF_ARGS --output "$CHANGELOG"
eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'"
fi
editor "$CHANGELOG"
git add "$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"

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
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)>),
[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.
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 decided to close the API
down again.
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

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,6 +1,7 @@
[package]
name = "musixmatch-cli"
version = "0.1.0"
version = "0.2.0"
rust-version = "1.70.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
@ -9,7 +10,7 @@ keywords.workspace = true
description = "Inofficial command line interface for the Musixmatch API"
[features]
default = ["rustls-tls-native-roots"]
default = ["native-tls"]
# Reqwest TLS options
native-tls = ["musixmatch-inofficial/native-tls"]
@ -20,11 +21,9 @@ rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"]
[dependencies]
musixmatch-inofficial.workspace = true
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"
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,14 +22,50 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
Get {
#[command(subcommand)]
command: GetCommands,
/// Get lyrics text
Lyrics {
#[clap(flatten)]
ident: TrackIdentifiers,
/// Language
#[clap(long)]
lang: Option<String>,
/// Bilingual
#[clap(long)]
bi: bool,
},
Mp3 {
#[command(subcommand)]
command: FileCommands,
/// Get subtitles (time-synced lyrics)
Subtitles {
#[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)]
Search {
/// Track name
@ -42,40 +80,8 @@ enum Commands {
/// Search query
query: Option<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,
},
/// Search for Musixmatch artists
SearchArtist { query: Vec<String> },
}
#[derive(Args)]
@ -106,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,
@ -175,164 +197,148 @@ async fn run(cli: Cli) -> Result<()> {
};
match cli.command {
Commands::Get { command } => match command {
GetCommands::Lyrics { ident, lang, bi } => {
let track_id = get_track_id(ident, &mxm).await?;
let lyrics = mxm.track_lyrics(track_id.clone()).await?;
Commands::Lyrics { ident, lang, bi } => {
let track_id = get_track_id(ident, &mxm).await?;
let lyrics = mxm.track_lyrics(track_id.clone()).await?;
eprintln!("Lyrics ID: {}", lyrics.lyrics_id);
eprintln!(
"Language: {}",
lyrics.lyrics_language.as_deref().unwrap_or(NA_STR)
);
eprintln!(
"Copyright: {}",
lyrics
.lyrics_copyright
.as_deref()
.map(|c| c.trim())
.unwrap_or(NA_STR)
);
eprintln!("Lyrics ID: {}", lyrics.lyrics_id);
eprintln!(
"Language: {}",
lyrics.lyrics_language.as_deref().unwrap_or(NA_STR)
);
eprintln!(
"Copyright: {}",
lyrics
.lyrics_copyright
.as_deref()
.map(|c| c.trim())
.unwrap_or(NA_STR)
);
let mut lyrics_body = lyrics.lyrics_body;
let mut lyrics_body = lyrics.lyrics_body;
if let Some(lang) = lang {
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."
);
if let Some(lang) = lang {
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.");
} 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 {
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 {
translated
};
}
translated
};
}
}
}
eprintln!();
println!("{}", lyrics_body);
}
Commands::Subtitles {
ident,
length,
max_deviation,
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!("{}", lyrics_body);
}
GetCommands::Subtitles {
ident,
length,
max_deviation,
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);
}
let res = match format {
SubtitleFormatClap::Lrc => lines.to_lrc(),
SubtitleFormatClap::Ttml => lines.to_ttml(),
SubtitleFormatClap::Json => lines.to_json()?,
SubtitleFormatClap::TtmlStructured | SubtitleFormatClap::EbuStl => {
bail!("subtitle format {format:?} cant be translated")
}
eprintln!();
let res = match format {
SubtitleFormatClap::Lrc => lines.to_lrc(),
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!("{}", res);
} else {
eprintln!();
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 {
query,
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(())
}

View file

@ -14,12 +14,13 @@ 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://code.thetadev.de/ThetaDev/rustypipe" %}\
{% set repo_url = "https://codeberg.org/ThetaDev/musixmatch-inofficial" %}\
{% if version %}\
{%set vname = version | split(pat="/") | last %}
{%if previous.version %}\
## [{{ version }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\
## [{{ vname }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\
{% else %}\
## {{ version }}\
## [{{ vname }}]({{ repo_url }}/commits/tag/{{ version }})\
{% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
@ -73,7 +74,7 @@ commit_parsers = [
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(release\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ 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)]
pub struct Account {
pub id: String,
pub email: String,
// pub id: String,
// pub email: 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 rstest::rstest;
use rstest::{fixture, rstest};
use time::macros::{date, datetime};
use musixmatch_inofficial::{
@ -9,19 +14,18 @@ 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 testfile<P: AsRef<Path>>(name: P) -> PathBuf {
path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name)
}
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();
if let (Ok(email), Ok(password)) = (
@ -31,11 +35,10 @@ fn new_mxm() -> Musixmatch {
mxm = mxm.credentials(email, password);
}
mxm.build().unwrap()
}
let mxm = mxm.build().unwrap();
fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name)
LOGIN_LOCK.get_or_try_init(|| mxm.login()).await.unwrap();
mxm
}
mod album {
@ -46,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!(
@ -56,7 +59,7 @@ mod album {
);
assert_eq!(album.album_name, "Gangnam Style (강남스타일)");
assert!(album.album_rating > 20);
assert_eq!(album.album_track_count, 1);
assert_eq!(album.album_track_count, 0);
assert_eq!(album.album_release_date.unwrap(), date!(2012 - 01 - 01));
assert_eq!(album.album_release_type, AlbumType::Single);
assert_eq!(album.artist_id, 410698);
@ -94,17 +97,20 @@ mod album {
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_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();
@ -112,9 +118,12 @@ mod album {
assert!(matches!(err, Error::NotFound));
}
#[rstest]
#[tokio::test]
async fn artist_albums() {
let albums = new_mxm()
#[ignore]
async fn artist_albums(#[future] mxm: Musixmatch) {
let albums = mxm
.await
.artist_albums(ArtistId::ArtistId(1039), None, 10, 1)
.await
.unwrap();
@ -122,9 +131,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();
@ -132,9 +143,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);
}
@ -147,8 +159,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);
@ -190,9 +202,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();
@ -200,9 +214,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();
@ -210,9 +226,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();
@ -220,20 +238,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,
@ -245,16 +268,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);
}
@ -269,8 +294,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",
@ -283,12 +313,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()
@ -296,7 +326,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
@ -316,7 +346,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!(
@ -324,9 +354,10 @@ mod track {
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
);
assert_eq!(track.artist_name, "Lady Gaga");
assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg");
assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/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));
@ -374,8 +405,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);
@ -415,20 +446,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()
@ -436,7 +472,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
@ -456,7 +492,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!(
@ -464,9 +500,10 @@ mod track {
"650e7db6-b795-4eb5-a702-5ea2fc46c848"
);
assert_eq!(track.artist_name, "Lady Gaga");
assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg");
assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/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));
@ -507,9 +544,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();
@ -517,9 +556,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();
@ -556,9 +597,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();
@ -570,8 +613,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();
@ -579,9 +623,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")
@ -600,9 +646,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)
@ -610,16 +658,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)
@ -629,16 +679,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();
@ -660,9 +713,10 @@ 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);
@ -690,8 +744,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);
@ -707,9 +761,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();
@ -725,9 +781,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();
@ -735,14 +793,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();
@ -751,14 +811,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();
@ -767,9 +829,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(
@ -804,9 +867,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",
@ -835,8 +900,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();
@ -856,9 +922,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",
@ -873,9 +941,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,
@ -889,9 +959,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,
@ -904,14 +976,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,
@ -973,3 +1047,9 @@ fn assert_imgurl(url: &Option<String>, ends_with: &str) {
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}");
}