Compare commits

..

No commits in common. "main" and "musixmatch-inofficial/v0.1.0" have entirely different histories.

15 changed files with 362 additions and 677 deletions

View file

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

View file

@ -23,8 +23,13 @@ jobs:
echo END_OF_FILE echo END_OF_FILE
} >> "$GITHUB_ENV" } >> "$GITHUB_ENV"
- name: 📤 Publish crate on crates.io - name: 📤 Publish crate on code.thetadev.de
run: cargo publish --token ${{ secrets.CARGO_TOKEN }} --package "${{ env.CRATE }}" 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 release - name: 🎉 Publish release
uses: https://gitea.com/actions/release-action@main uses: https://gitea.com/actions/release-action@main

View file

@ -1,63 +0,0 @@
name: renovate
on:
push:
branches: ["main"]
paths:
- ".forgejo/workflows/renovate.yaml"
- "renovate.json"
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
env:
RENOVATE_REPOSITORIES: ${{ github.repository }}
jobs:
renovate:
runs-on: docker
container:
image: renovate/renovate:latest
steps:
- name: Load renovate repo cache
uses: actions/cache/restore@v4
with:
path: |
.tmp/cache/renovate/repository
.tmp/cache/renovate/renovate-cache-sqlite
.tmp/osv
key: repo-cache-${{ github.run_id }}
restore-keys: |
repo-cache-
- name: Run renovate
run: renovate
env:
LOG_LEVEL: debug
RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp
RENOVATE_ENDPOINT: ${{ github.server_url }}
RENOVATE_PLATFORM: gitea
RENOVATE_REPOSITORY_CACHE: 'enabled'
RENOVATE_TOKEN: ${{ secrets.FORGEJO_CI_BOT_TOKEN }}
GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_TOKEN }}
RENOVATE_GIT_AUTHOR: 'Renovate Bot <forgejo-renovate-action@forgejo.org>'
RENOVATE_X_SQLITE_PACKAGE_CACHE: true
GIT_AUTHOR_NAME: 'Renovate Bot'
GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org'
GIT_COMMITTER_NAME: 'Renovate Bot'
GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org'
OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv
- name: Save renovate repo cache
if: always() && env.RENOVATE_DRY_RUN != 'full'
uses: actions/cache/save@v4
with:
path: |
.tmp/cache/renovate/repository
.tmp/cache/renovate/renovate-cache-sqlite
.tmp/osv
key: repo-cache-${{ github.run_id }}

View file

@ -2,31 +2,7 @@
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,7 +1,6 @@
[package] [package]
name = "musixmatch-inofficial" name = "musixmatch-inofficial"
version = "0.1.1" version = "0.1.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,21 +8,22 @@ 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 <thetadev@magenta.de>"] authors = ["ThetaDev <t.testboy@gmail.com>"]
license = "MIT" license = "MIT"
repository = "https://codeberg.org/ThetaDev/musixmatch-inofficial" repository = "https://code.thetadev.de/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.1", path = ".", default-features = false } musixmatch-inofficial = { version = "0.1.0", path = ".", default-features = false }
[features] [features]
default = ["default-tls"] default = ["default-tls"]
@ -41,26 +41,30 @@ reqwest = { version = "0.12.0", default-features = false, features = [
"json", "json",
"gzip", "gzip",
] } ] }
tokio = { version = "1.20.4" } tokio = { version = "1.20.0" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85" serde_json = "1.0.85"
thiserror = "1.0.0" thiserror = "1.0.36"
log = "0.4.17" log = "0.4.17"
time = { version = "0.3.10", features = [ time = { version = "0.3.15", features = [
"macros", "macros",
"formatting", "formatting",
"serde", "serde",
"serde-well-known", "serde-well-known",
] } ] }
hmac = "0.12.0" hmac = "0.12.1"
sha1 = "0.10.0" sha1 = "0.10.5"
rand = "0.8.0" rand = "0.8.5"
base64 = "0.22.0" base64 = "0.22.0"
[dev-dependencies] [dev-dependencies]
rstest = { version = "0.22.0", default-features = false } ctor = "0.2.0"
rstest = { version = "0.18.0", default-features = false }
env_logger = "0.11.0"
dotenvy = "0.15.5" dotenvy = "0.15.5"
tokio = { version = "1.20.4", features = ["macros"] } tokio = { version = "1.20.0", features = ["macros"] }
futures = "0.3.21" futures = "0.3.21"
path_macro = "1.0.0" path_macro = "1.0.0"
governor = "0.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 '${TAG}' --tag-pattern '${CRATE}/v*' --unreleased $INCLUDES" CLIFF_ARGS="--tag v${VERSION} --tag-pattern ${CRATE}/* --unreleased $INCLUDES"
echo "git-cliff $CLIFF_ARGS" echo "git-cliff $CLIFF_ARGS"
if [ -f "$CHANGELOG" ]; then if [ -f "$CHANGELOG" ]; then
eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'" git-cliff $CLIFF_ARGS --prepend "$CHANGELOG"
else else
eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'" git-cliff $CLIFF_ARGS --output "$CHANGELOG"
fi fi
editor "$CHANGELOG" editor "$CHANGELOG"
git add . git add "$CHANGELOG"
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,8 +1,4 @@
# 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.
@ -11,10 +7,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 used to require a free account on <https://www.musixmatch.com> to be The Musixmatch API required a free account on <https://www.musixmatch.com> to be used.
used. However, as of 2024, this requirement was removed and the API can be used However, as of 2024, this requirement was removed and the API can be used anonymously.
anonymously. The client still allows you to supply credentials if Musixmatch decides to The client still allows you to supply credentials if Musixmatch decided to close the API
close the API down again. down again.
## ⚠️ Copyright disclaimer ## ⚠️ Copyright disclaimer

View file

@ -1,39 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [v0.2.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.1.0..musixmatch-cli/v0.2.0) - 2024-08-18
### 🚀 Features
- Add format option to mp3 subtitles cmd - ([19e209e](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/19e209e34f4d129a4223930bfd41e1ccf117f231))
- Add get album, get artist, search artist - ([c4bfbe5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/c4bfbe563a00d399b3645dd68f03c1215ee51fdb))
- [**breaking**] Remove MP3 feature, refactor cmd structure - ([54235e6](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/54235e6fb61084823a6583aaa7d59b1799deb07f))
- Add msrv - ([a95f3fc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/a95f3fcf478f1acda9fad12741604b6793e128c1))
### 🐛 Bug Fixes
- Use native TLS for CLI - ([dc1bea1](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dc1bea13cc2a37eae7f3727dc72f865a01430a2e))
### 📚 Documentation
- Update readme - ([348e9c5](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/348e9c5427e59c488d7e2f7cef9e7006a12864f2))
### 🧪 Testing
- Fix tests - ([d2a7aed](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/d2a7aed917bfcec75ce00bb49d380fbc31c47384))
- Add rate limiter - ([3b69b36](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/3b69b36ae6c945d786534e0eaa353fb737b1fb54))
### ⚙️ Miscellaneous Tasks
- Fix changelogs - ([e72d2b4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/e72d2b4363a3a9a48dec8f2be9389f6cc239035c))
- Update justfile - ([1bc5ae4](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/1bc5ae408343e6755e390909e7017647efcf59a1))
- Update dependencies - ([dcc25bf](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/dcc25bff202becdec7101c5ce1825cd75e445f99))
- Change repo to codeberg - ([30e2afd](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/30e2afd3679d2c17a49afd523c8b8bad70f291e5))
## [v0.1.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/commits/tag/musixmatch-cli/v0.1.0) - 2024-03-23
Initial release
<!-- generated by git-cliff -->

View file

@ -1,7 +1,6 @@
[package] [package]
name = "musixmatch-cli" name = "musixmatch-cli"
version = "0.2.0" version = "0.1.0"
rust-version = "1.70.0"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
@ -10,7 +9,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 = ["native-tls"] default = ["rustls-tls-native-roots"]
# Reqwest TLS options # Reqwest TLS options
native-tls = ["musixmatch-inofficial/native-tls"] native-tls = ["musixmatch-inofficial/native-tls"]
@ -21,9 +20,11 @@ rustls-tls-native-roots = ["musixmatch-inofficial/rustls-tls-native-roots"]
[dependencies] [dependencies]
musixmatch-inofficial.workspace = true musixmatch-inofficial.workspace = true
tokio = { version = "1.20.4", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
clap = { version = "4.0.0", features = ["derive"] } id3 = "1.3.0"
anyhow = "1.0.0" mp3-duration = "0.1.10"
clap = { version = "4.0.10", features = ["derive"] }
anyhow = "1.0.65"
rpassword = "7.0.0" rpassword = "7.0.0"
dirs = "5.0.0" dirs = "5.0.0"
serde_json = "1.0.85" serde_json = "1.0.91"

View file

@ -1,77 +0,0 @@
# musixmatch-cli
[![Current crates.io version](https://img.shields.io/crates/v/musixmatch-cli.svg)](https://crates.io/crates/musixmatch-cli)
[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT)
[![CI status](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/musixmatch-inofficial/actions/?workflow=ci.yaml)
The Musixmatch CLI allows you to fetch lyrics, subtitles and track metadata from the
command line using the Musixmatch API.
The Musixmatch API used to require a free account on <https://www.musixmatch.com> to be
used. However, as of 2024, this requirement was removed and the API can be used
anonymously. The CLI still allows you to supply credentials if Musixmatch decides to
close the API down again.
### Get lyrics
```txt
musixmatch-cli lyrics -n shine -a spektrem
Lyrics ID: 34583240
Language: en
Copyright: Writer(s): Jesse Warren
Copyright: Ncs Music
Eyes in the sky gazing far into the night
I raise my hand to the fire, but it's no use
'Cause you can't stop it from shining through
It's true
...
```
### Get translated lyrics
Musixmatch also offers translated lyrics. You have to select a language using the
`--lang` flag. You can also set the `--bi` flag to output both the original and
translated lines.
```txt
musixmatch-cli lyrics -n shine -a spektrem --lang de --bi
Lyrics ID: 34583240
Language: en
Copyright: Writer(s): Jesse Warren
Copyright: Ncs Music
Translated to: de
Eyes in the sky gazing far into the night
> Augen starren in die weite Nacht
I raise my hand to the fire, but it's no use
> Ich hebe meine Hand in das Feuer, doch ihr geschieht nichts
'Cause you can't stop it from shining through
> Denn du kannst es nicht daran hindern, hindurch zu scheinen
It's true
> Es ist wahr
...
```
### Get subtitles (synchronized lyrics)
For most lyrics Musixmatch provides timestamps for the individual lines so you can
display them in sync during playback.
Musixmatch offers multiple subtitle formats you can select using the `--format` flag.
The available formats are: `lrc`, `ttml`, `ttml-structured`, `json`, `ebu-stl`
```txt
musixmatch-cli subtitles -n shine -a spektrem
Subtitle ID: 35340319
Language: en
Length: 316
Copyright: Writer(s): Jesse Warren
Copyright: Ncs Music
[00:59.84] Eyes in the sky gazing far into the night
[01:06.55] I raise my hand to the fire, but it's no use
[01:11.97] 'Cause you can't stop it from shining through
[01:16.07] It's true
...
```

View file

@ -1,6 +1,3 @@
#![doc = include_str!("../README.md")]
#![warn(missing_docs, clippy::todo)]
use std::{ use std::{
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
path::PathBuf, path::PathBuf,
@ -8,8 +5,9 @@ use std::{
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use id3::{Tag, TagLike};
use musixmatch_inofficial::{ use musixmatch_inofficial::{
models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap}, models::{SubtitleFormat, Track, TrackId, TranslationMap},
Musixmatch, Musixmatch,
}; };
@ -22,50 +20,14 @@ struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
/// Get lyrics text Get {
Lyrics { #[command(subcommand)]
#[clap(flatten)] command: GetCommands,
ident: TrackIdentifiers,
/// Language
#[clap(long)]
lang: Option<String>,
/// Bilingual
#[clap(long)]
bi: bool,
}, },
/// Get subtitles (time-synced lyrics) Mp3 {
Subtitles { #[command(subcommand)]
#[clap(flatten)] command: FileCommands,
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
@ -80,8 +42,40 @@ 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)]
@ -112,38 +106,22 @@ struct TrackIdentifiers {
isrc: Option<String>, isrc: Option<String>,
} }
#[derive(Args)]
#[group(multiple = false)]
struct AlbumArtistIdentifiers {
/// Musixmatch-ID
#[clap(long)]
mxm_id: Option<u64>,
/// Musicbrainz-ID
#[clap(long)]
musicbrainz: Option<String>,
}
#[derive(Subcommand)] #[derive(Subcommand)]
enum FileCommands { enum FileCommands {
/// Get lyrics text
Lyrics { Lyrics {
/// Music file /// Music file
#[clap(value_parser)] #[clap(value_parser)]
file: PathBuf, file: PathBuf,
}, },
/// Get subtitles (time-synced lyrics)
Subtitles { Subtitles {
/// Music file /// Music file
#[clap(value_parser)] #[clap(value_parser)]
file: PathBuf, file: PathBuf,
/// Subtitle format
#[clap(short, long, default_value = "lrc")]
format: SubtitleFormatClap,
}, },
} }
#[derive(clap::ValueEnum, Debug, Copy, Clone)] #[derive(clap::ValueEnum, Debug, Copy, Clone)]
enum SubtitleFormatClap { pub enum SubtitleFormatClap {
Lrc, Lrc,
Ttml, Ttml,
TtmlStructured, TtmlStructured,
@ -197,148 +175,164 @@ async fn run(cli: Cli) -> Result<()> {
}; };
match cli.command { match cli.command {
Commands::Lyrics { ident, lang, bi } => { Commands::Get { command } => match command {
let track_id = get_track_id(ident, &mxm).await?; GetCommands::Lyrics { ident, lang, bi } => {
let lyrics = mxm.track_lyrics(track_id.clone()).await?; 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!("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!("Translation not found. Returning lyrics in original language."); eprintln!(
} else { "Translation not found. Returning lyrics in original language."
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 {
translated 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
};
}
} }
} }
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);
}
}
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)?;
eprintln!(); let title = tag.title().ok_or(anyhow!("no title"))?;
println!("{}", lyrics_body); let artist = tag.artist().ok_or(anyhow!("no artist"))?;
}
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); let lyrics = mxm.matcher_lyrics(title, artist).await?;
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 { println!(
let mut lines = subtitles.to_lines()?; "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)?;
if Some(&lang) != subtitles.subtitle_language.as_ref() { let title = tag.title().ok_or(anyhow!("no title"))?;
let tl = mxm.track_lyrics_translation(track_id, &lang).await?; let artist = tag.artist().ok_or(anyhow!("no artist"))?;
if tl.is_empty() {
bail!("Translation not found") let subtitles = mxm
} else { .matcher_subtitle(
eprintln!("Translated to: {}", tl.lang); title,
let tm = TranslationMap::from(tl); artist,
lines = tm.translate_subtitles(&lines); SubtitleFormat::Lrc,
} Some(duration.as_secs_f32()),
} Some(1.0),
)
.await?;
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); 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,
@ -375,15 +369,6 @@ 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,13 +14,12 @@ 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://codeberg.org/ThetaDev/musixmatch-inofficial" %}\ {% set repo_url = "https://code.thetadev.de/ThetaDev/rustypipe" %}\
{% if version %}\ {% if version %}\
{%set vname = version | split(pat="/") | last %}
{%if previous.version %}\ {%if previous.version %}\
## [{{ vname }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\ ## [{{ version }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\
{% else %}\ {% else %}\
## [{{ vname }}]({{ repo_url }}/commits/tag/{{ version }})\ ## {{ version }}\
{% endif %} - {{ timestamp | date(format="%Y-%m-%d") }} {% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\ {% else %}\
## [unreleased] ## [unreleased]
@ -74,7 +73,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\\)", skip = true }, { message = "^chore\\(release\\): prepare for", 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" },

View file

@ -1,13 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices"
],
"semanticCommits": "enabled",
"automerge": true,
"automergeStrategy": "squash",
"osvVulnerabilityAlerts": true,
"labels": ["dependency-upgrade"],
"enabledManagers": ["cargo"],
"prHourlyLimit": 5
}

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,12 +1,7 @@
use std::{ use std::path::{Path, PathBuf};
num::NonZeroU32,
path::{Path, PathBuf},
sync::LazyLock,
};
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use path_macro::path; use path_macro::path;
use rstest::{fixture, rstest}; use rstest::rstest;
use time::macros::{date, datetime}; use time::macros::{date, datetime};
use musixmatch_inofficial::{ use musixmatch_inofficial::{
@ -14,18 +9,19 @@ use musixmatch_inofficial::{
Error, Musixmatch, Error, Musixmatch,
}; };
fn testfile<P: AsRef<Path>>(name: P) -> PathBuf { #[ctor::ctor]
path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name) fn init() {
let _ = dotenvy::dotenv();
env_logger::init();
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(new_mxm().login())
.unwrap();
} }
#[fixture] fn new_mxm() -> Musixmatch {
async fn mxm() -> Musixmatch {
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)) = (
@ -35,10 +31,11 @@ async fn mxm() -> Musixmatch {
mxm = mxm.credentials(email, password); mxm = mxm.credentials(email, password);
} }
let mxm = mxm.build().unwrap(); mxm.build().unwrap()
}
LOGIN_LOCK.get_or_try_init(|| mxm.login()).await.unwrap(); fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
mxm path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / name)
} }
mod album { mod album {
@ -49,8 +46,8 @@ mod album {
#[case::id(AlbumId::AlbumId(14248253))] #[case::id(AlbumId::AlbumId(14248253))]
#[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))] #[case::musicbrainz(AlbumId::Musicbrainz("6c3cf9d8-88a8-43ed-850b-55813f01e451"))]
#[tokio::test] #[tokio::test]
async fn by_id(#[case] album_id: AlbumId<'_>, #[future] mxm: Musixmatch) { async fn by_id(#[case] album_id: AlbumId<'_>) {
let album = mxm.await.album(album_id).await.unwrap(); let album = new_mxm().album(album_id).await.unwrap();
assert_eq!(album.album_id, 14248253); assert_eq!(album.album_id, 14248253);
assert_eq!( assert_eq!(
@ -97,20 +94,17 @@ 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(#[future] mxm: Musixmatch) { async fn album_ep() {
let album = mxm.await.album(AlbumId::AlbumId(23976123)).await.unwrap(); let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap();
assert_eq!(album.album_name, "Waldbrand EP"); assert_eq!(album.album_name, "Waldbrand EP");
// assert_eq!(album.album_release_type, AlbumType::Ep); // assert_eq!(album.album_release_type, AlbumType::Ep);
assert_eq!(album.album_release_date, Some(date!(2016 - 09 - 30))); assert_eq!(album.album_release_date, Some(date!(2016 - 09 - 30)));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn by_id_missing(#[future] mxm: Musixmatch) { async fn by_id_missing() {
let err = mxm let err = new_mxm()
.await
.album(AlbumId::AlbumId(999999999999)) .album(AlbumId::AlbumId(999999999999))
.await .await
.unwrap_err(); .unwrap_err();
@ -118,11 +112,9 @@ mod album {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn artist_albums(#[future] mxm: Musixmatch) { async fn artist_albums() {
let albums = mxm let albums = new_mxm()
.await
.artist_albums(ArtistId::ArtistId(1039), None, 10, 1) .artist_albums(ArtistId::ArtistId(1039), None, 10, 1)
.await .await
.unwrap(); .unwrap();
@ -130,11 +122,9 @@ mod album {
assert_eq!(albums.len(), 10); assert_eq!(albums.len(), 10);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn artist_albums_missing(#[future] mxm: Musixmatch) { async fn artist_albums_missing() {
let err = mxm let err = new_mxm()
.await
.artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1) .artist_albums(ArtistId::ArtistId(999999999999), None, 10, 1)
.await .await
.unwrap_err(); .unwrap_err();
@ -142,10 +132,9 @@ mod album {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn charts(#[future] mxm: Musixmatch) { async fn charts() {
let albums = mxm.await.chart_albums("US", 10, 1).await.unwrap(); let albums = new_mxm().chart_albums("US", 10, 1).await.unwrap();
assert_eq!(albums.len(), 10); assert_eq!(albums.len(), 10);
} }
@ -158,8 +147,8 @@ mod artist {
#[case::id(ArtistId::ArtistId(410698))] #[case::id(ArtistId::ArtistId(410698))]
#[case::musicbrainz(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))] #[case::musicbrainz(ArtistId::Musicbrainz("f99b7d67-4e63-4678-aa66-4c6ac0f7d24a"))]
#[tokio::test] #[tokio::test]
async fn by_id(#[case] artist_id: ArtistId<'_>, #[future] mxm: Musixmatch) { async fn by_id(#[case] artist_id: ArtistId<'_>) {
let artist = mxm.await.artist(artist_id).await.unwrap(); let artist = new_mxm().artist(artist_id).await.unwrap();
// dbg!(&artist); // dbg!(&artist);
@ -201,11 +190,9 @@ mod artist {
assert_eq!(artist.end_date, None); assert_eq!(artist.end_date, None);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn by_id_missing(#[future] mxm: Musixmatch) { async fn by_id_missing() {
let err = mxm let err = new_mxm()
.await
.artist(ArtistId::ArtistId(999999999999)) .artist(ArtistId::ArtistId(999999999999))
.await .await
.unwrap_err(); .unwrap_err();
@ -213,11 +200,9 @@ mod artist {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn related(#[future] mxm: Musixmatch) { async fn related() {
let artists = mxm let artists = new_mxm()
.await
.artist_related(ArtistId::ArtistId(26485840), 10, 1) .artist_related(ArtistId::ArtistId(26485840), 10, 1)
.await .await
.unwrap(); .unwrap();
@ -225,11 +210,9 @@ mod artist {
assert_eq!(artists.len(), 10); assert_eq!(artists.len(), 10);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn related_missing(#[future] mxm: Musixmatch) { async fn related_missing() {
let err = mxm let err = new_mxm()
.await
.artist_related(ArtistId::ArtistId(999999999999), 10, 1) .artist_related(ArtistId::ArtistId(999999999999), 10, 1)
.await .await
.unwrap_err(); .unwrap_err();
@ -237,25 +220,20 @@ mod artist {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search(#[future] mxm: Musixmatch) { async fn search() {
let artists = mxm let artists = new_mxm().artist_search("psy", 5, 1).await.unwrap();
.await
.artist_search("Snollebollekes", 5, 1) assert_eq!(artists.len(), 5);
.await
.unwrap();
let artist = &artists[0]; let artist = &artists[0];
assert_eq!(artist.artist_id, 25344078); assert_eq!(artist.artist_id, 410698);
assert_eq!(artist.artist_name, "Snollebollekes"); assert_eq!(artist.artist_name, "PSY");
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search_empty(#[future] mxm: Musixmatch) { async fn search_empty() {
let artists = mxm let artists = new_mxm()
.await
.artist_search( .artist_search(
"Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz", "Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz",
5, 5,
@ -267,18 +245,16 @@ mod artist {
assert_eq!(artists.len(), 0); assert_eq!(artists.len(), 0);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn charts(#[future] mxm: Musixmatch) { async fn charts() {
let artists = mxm.await.chart_artists("US", 10, 1).await.unwrap(); let artists = new_mxm().chart_artists("US", 10, 1).await.unwrap();
assert_eq!(artists.len(), 10); assert_eq!(artists.len(), 10);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn charts_no_country(#[future] mxm: Musixmatch) { async fn charts_no_country() {
let artists = mxm.await.chart_artists("XY", 10, 1).await.unwrap(); let artists = new_mxm().chart_artists("XY", 10, 1).await.unwrap();
assert_eq!(artists.len(), 10); assert_eq!(artists.len(), 10);
} }
@ -293,13 +269,8 @@ mod track {
#[case::translation_2c(true, false)] #[case::translation_2c(true, false)]
#[case::translation_3c(true, true)] #[case::translation_3c(true, true)]
#[tokio::test] #[tokio::test]
async fn from_match( async fn from_match(#[case] translation_status: bool, #[case] lang_3c: bool) {
#[case] translation_status: bool, let track = new_mxm()
#[case] lang_3c: bool,
#[future] mxm: Musixmatch,
) {
let track = mxm
.await
.matcher_track( .matcher_track(
"Poker Face", "Poker Face",
"Lady Gaga", "Lady Gaga",
@ -312,12 +283,12 @@ mod track {
// dbg!(&track); // dbg!(&track);
assert_eq!(track.track_id, 85213841); assert_eq!(track.track_id, 15476784);
// assert_eq!( assert_eq!(
// track.track_mbid.unwrap(), track.track_mbid.unwrap(),
// "080975b0-39b1-493c-ae64-5cb3292409bb" "080975b0-39b1-493c-ae64-5cb3292409bb"
// ); );
// assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
assert!( assert!(
track.commontrack_isrcs[0] track.commontrack_isrcs[0]
.iter() .iter()
@ -325,7 +296,7 @@ mod track {
"commontrack_isrcs: {:?}", "commontrack_isrcs: {:?}",
&track.commontrack_isrcs[0], &track.commontrack_isrcs[0],
); );
assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg"); assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO");
assert!( assert!(
track track
.commontrack_spotify_ids .commontrack_spotify_ids
@ -345,7 +316,7 @@ mod track {
assert!(track.num_favourite > 50); assert!(track.num_favourite > 50);
assert!(track.lyrics_id.is_some()); assert!(track.lyrics_id.is_some());
assert_eq!(track.subtitle_id.unwrap(), 36450705); assert_eq!(track.subtitle_id.unwrap(), 36450705);
assert_eq!(track.album_id, 20960801); assert_eq!(track.album_id, 13810402);
assert_eq!(track.album_name, "The Fame"); assert_eq!(track.album_name, "The Fame");
assert_eq!(track.artist_id, 378462); assert_eq!(track.artist_id, 378462);
assert_eq!( assert_eq!(
@ -353,10 +324,9 @@ mod track {
"650e7db6-b795-4eb5-a702-5ea2fc46c848" "650e7db6-b795-4eb5-a702-5ea2fc46c848"
); );
assert_eq!(track.artist_name, "Lady Gaga"); assert_eq!(track.artist_name, "Lady Gaga");
assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg"); assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg");
assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg"); assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg"); assert_imgurl(&track.album_coverart_500x500, "/26319636_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));
@ -404,8 +374,8 @@ mod track {
#[case::isrc(TrackId::Isrc("KRA302000590".into()))] #[case::isrc(TrackId::Isrc("KRA302000590".into()))]
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
#[tokio::test] #[tokio::test]
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { async fn from_id(#[case] track_id: TrackId<'_>) {
let track = mxm.await.track(track_id, true, false).await.unwrap(); let track = new_mxm().track(track_id, true, false).await.unwrap();
// dbg!(&track); // dbg!(&track);
@ -445,25 +415,20 @@ mod track {
#[case::translation_2c(true, false)] #[case::translation_2c(true, false)]
#[case::translation_3c(true, true)] #[case::translation_3c(true, true)]
#[tokio::test] #[tokio::test]
async fn from_id_translations( async fn from_id_translations(#[case] translation_status: bool, #[case] lang_3c: bool) {
#[case] translation_status: bool, let track = new_mxm()
#[case] lang_3c: bool, .track(TrackId::TrackId(15476784), translation_status, lang_3c)
#[future] mxm: Musixmatch,
) {
let track = mxm
.await
.track(TrackId::Commontrack(47672612), translation_status, lang_3c)
.await .await
.unwrap(); .unwrap();
// dbg!(&track); // dbg!(&track);
assert_eq!(track.track_id, 85213841); assert_eq!(track.track_id, 15476784);
// assert_eq!( assert_eq!(
// track.track_mbid.unwrap(), track.track_mbid.unwrap(),
// "080975b0-39b1-493c-ae64-5cb3292409bb" "080975b0-39b1-493c-ae64-5cb3292409bb"
// ); );
// assert_eq!(track.track_isrc.unwrap(), "USUM70824409"); assert_eq!(track.track_isrc.unwrap(), "USUM70824409");
assert!( assert!(
track.commontrack_isrcs[0] track.commontrack_isrcs[0]
.iter() .iter()
@ -471,7 +436,7 @@ mod track {
"commontrack_isrcs: {:?}", "commontrack_isrcs: {:?}",
&track.commontrack_isrcs[0], &track.commontrack_isrcs[0],
); );
assert_eq!(track.track_spotify_id.unwrap(), "1QV6tiMFM6fSOKOGLMHYYg"); assert_eq!(track.track_spotify_id.unwrap(), "5R8dQOPq8haW94K7mgERlO");
assert!( assert!(
track track
.commontrack_spotify_ids .commontrack_spotify_ids
@ -491,7 +456,7 @@ mod track {
assert!(track.num_favourite > 50); assert!(track.num_favourite > 50);
assert!(track.lyrics_id.is_some()); assert!(track.lyrics_id.is_some());
assert_eq!(track.subtitle_id.unwrap(), 36450705); assert_eq!(track.subtitle_id.unwrap(), 36450705);
assert_eq!(track.album_id, 20960801); assert_eq!(track.album_id, 13810402);
assert_eq!(track.album_name, "The Fame"); assert_eq!(track.album_name, "The Fame");
assert_eq!(track.artist_id, 378462); assert_eq!(track.artist_id, 378462);
assert_eq!( assert_eq!(
@ -499,10 +464,9 @@ mod track {
"650e7db6-b795-4eb5-a702-5ea2fc46c848" "650e7db6-b795-4eb5-a702-5ea2fc46c848"
); );
assert_eq!(track.artist_name, "Lady Gaga"); assert_eq!(track.artist_name, "Lady Gaga");
assert_imgurl(&track.album_coverart_100x100, "/32133892.jpg"); assert_imgurl(&track.album_coverart_100x100, "/26319636.jpg");
assert_imgurl(&track.album_coverart_350x350, "/32133892_350_350.jpg"); assert_imgurl(&track.album_coverart_350x350, "/26319636_350_350.jpg");
assert_imgurl(&track.album_coverart_500x500, "/32133892_500_500.jpg"); assert_imgurl(&track.album_coverart_500x500, "/26319636_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));
@ -543,11 +507,9 @@ mod track {
} }
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn from_id_missing(#[future] mxm: Musixmatch) { async fn from_id_missing() {
let err = mxm let err = new_mxm()
.await
.track(TrackId::TrackId(999999999999), false, false) .track(TrackId::TrackId(999999999999), false, false)
.await .await
.unwrap_err(); .unwrap_err();
@ -555,11 +517,9 @@ mod track {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn album_tracks(#[future] mxm: Musixmatch) { async fn album_tracks() {
let tracks = mxm let tracks = new_mxm()
.await
.album_tracks(AlbumId::AlbumId(17118624), true, 20, 1) .album_tracks(AlbumId::AlbumId(17118624), true, 20, 1)
.await .await
.unwrap(); .unwrap();
@ -596,11 +556,9 @@ mod track {
}); });
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn album_missing(#[future] mxm: Musixmatch) { async fn album_missing() {
let err = mxm let err = new_mxm()
.await
.album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1) .album_tracks(AlbumId::AlbumId(999999999999), false, 20, 1)
.await .await
.unwrap_err(); .unwrap_err();
@ -612,9 +570,8 @@ mod track {
#[case::top(ChartName::Top)] #[case::top(ChartName::Top)]
#[case::hot(ChartName::Hot)] #[case::hot(ChartName::Hot)]
#[tokio::test] #[tokio::test]
async fn charts(#[case] chart_name: ChartName, #[future] mxm: Musixmatch) { async fn charts(#[case] chart_name: ChartName) {
let tracks = mxm let tracks = new_mxm()
.await
.chart_tracks("US", chart_name, true, 20, 1) .chart_tracks("US", chart_name, true, 20, 1)
.await .await
.unwrap(); .unwrap();
@ -622,11 +579,9 @@ mod track {
assert_eq!(tracks.len(), 20); assert_eq!(tracks.len(), 20);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search(#[future] mxm: Musixmatch) { async fn search() {
let tracks = mxm let tracks = new_mxm()
.await
.track_search() .track_search()
.q_artist("Lena") .q_artist("Lena")
.q_track("Satellite") .q_track("Satellite")
@ -645,11 +600,9 @@ mod track {
assert_eq!(track.artist_name, "Lena"); assert_eq!(track.artist_name, "Lena");
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search_lyrics(#[future] mxm: Musixmatch) { async fn search_lyrics() {
let tracks = mxm let tracks = new_mxm()
.await
.track_search() .track_search()
.q_lyrics("the whole world stops and stares for a while") .q_lyrics("the whole world stops and stares for a while")
.s_track_rating(SortOrder::Desc) .s_track_rating(SortOrder::Desc)
@ -657,18 +610,16 @@ mod track {
.await .await
.unwrap(); .unwrap();
assert_gte(tracks.len(), 8, "tracks"); assert_eq!(tracks.len(), 10);
let track = &tracks[0]; let track = &tracks[0];
assert_eq!(track.track_name, "Just the Way You Are"); assert_eq!(track.track_name, "Just the Way You Are");
assert_eq!(track.artist_name, "Bruno Mars"); assert_eq!(track.artist_name, "Bruno Mars");
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn search_empty(#[future] mxm: Musixmatch) { async fn search_empty() {
let artists = mxm let artists = new_mxm()
.await
.track_search() .track_search()
.q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz") .q("Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz")
.send(10, 1) .send(10, 1)
@ -678,19 +629,16 @@ mod track {
assert_eq!(artists.len(), 0); assert_eq!(artists.len(), 0);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn genres(#[future] mxm: Musixmatch) { async fn genres() {
let genres = mxm.await.genres().await.unwrap(); let genres = new_mxm().genres().await.unwrap();
assert!(genres.len() > 360); assert!(genres.len() > 360);
dbg!(&genres); dbg!(&genres);
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn snippet(#[future] mxm: Musixmatch) { async fn snippet() {
let snippet = mxm let snippet = new_mxm()
.await
.track_snippet(TrackId::Commontrack(8874280)) .track_snippet(TrackId::Commontrack(8874280))
.await .await
.unwrap(); .unwrap();
@ -712,10 +660,9 @@ mod lyrics {
use super::*; use super::*;
#[rstest]
#[tokio::test] #[tokio::test]
async fn from_match(#[future] mxm: Musixmatch) { async fn from_match() {
let lyrics = mxm.await.matcher_lyrics("Shine", "Spektrem").await.unwrap(); let lyrics = new_mxm().matcher_lyrics("Shine", "Spektrem").await.unwrap();
// dbg!(&lyrics); // dbg!(&lyrics);
@ -743,8 +690,8 @@ mod lyrics {
#[case::isrc(TrackId::Isrc("KRA302000590".into()))] #[case::isrc(TrackId::Isrc("KRA302000590".into()))]
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
#[tokio::test] #[tokio::test]
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { async fn from_id(#[case] track_id: TrackId<'_>) {
let lyrics = mxm.await.track_lyrics(track_id).await.unwrap(); let lyrics = new_mxm().track_lyrics(track_id).await.unwrap();
// dbg!(&lyrics); // dbg!(&lyrics);
@ -760,11 +707,9 @@ mod lyrics {
} }
/// This track has no lyrics /// This track has no lyrics
#[rstest]
#[tokio::test] #[tokio::test]
async fn instrumental(#[future] mxm: Musixmatch) { async fn instrumental() {
let lyrics = mxm let lyrics = new_mxm()
.await
.matcher_lyrics("drivers license", "Bobby G") .matcher_lyrics("drivers license", "Bobby G")
.await .await
.unwrap(); .unwrap();
@ -780,11 +725,9 @@ mod lyrics {
} }
/// This track does not exist /// This track does not exist
#[rstest]
#[tokio::test] #[tokio::test]
async fn missing(#[future] mxm: Musixmatch) { async fn missing() {
let err = mxm let err = new_mxm()
.await
.track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into())) .track_lyrics(TrackId::Spotify("674JwwTP7xCje81T0DRrLn".into()))
.await .await
.unwrap_err(); .unwrap_err();
@ -792,16 +735,14 @@ mod lyrics {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn download_testdata(#[future] mxm: Musixmatch) { async fn download_testdata() {
let mxm = mxm.await;
let json_path = testfile("lyrics.json"); let json_path = testfile("lyrics.json");
if json_path.exists() { if json_path.exists() {
return; return;
} }
let lyrics = mxm let lyrics = new_mxm()
.track_lyrics(TrackId::Commontrack(18576954)) .track_lyrics(TrackId::Commontrack(18576954))
.await .await
.unwrap(); .unwrap();
@ -810,16 +751,14 @@ mod lyrics {
serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap(); serde_json::to_writer_pretty(BufWriter::new(json_file), &lyrics).unwrap();
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn download_testdata_translation(#[future] mxm: Musixmatch) { async fn download_testdata_translation() {
let mxm = mxm.await;
let json_path = testfile("translation.json"); let json_path = testfile("translation.json");
if json_path.exists() { if json_path.exists() {
return; return;
} }
let translations = mxm let translations = new_mxm()
.track_lyrics_translation(TrackId::Commontrack(18576954), "de") .track_lyrics_translation(TrackId::Commontrack(18576954), "de")
.await .await
.unwrap(); .unwrap();
@ -828,10 +767,9 @@ mod lyrics {
serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap(); serde_json::to_writer_pretty(BufWriter::new(json_file), &translations).unwrap();
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn concurrency(#[future] mxm: Musixmatch) { async fn concurrency() {
let mxm = mxm.await; let mxm = new_mxm();
let album = mxm let album = mxm
.album_tracks( .album_tracks(
@ -866,11 +804,9 @@ mod subtitles {
use super::*; use super::*;
use musixmatch_inofficial::models::SubtitleFormat; use musixmatch_inofficial::models::SubtitleFormat;
#[rstest]
#[tokio::test] #[tokio::test]
async fn from_match(#[future] mxm: Musixmatch) { async fn from_match() {
let subtitle = mxm let subtitle = new_mxm()
.await
.matcher_subtitle( .matcher_subtitle(
"Shine", "Shine",
"Spektrem", "Spektrem",
@ -899,9 +835,8 @@ mod subtitles {
#[case::isrc(TrackId::Isrc("KRA302000590".into()))] #[case::isrc(TrackId::Isrc("KRA302000590".into()))]
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))] #[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
#[tokio::test] #[tokio::test]
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) { async fn from_id(#[case] track_id: TrackId<'_>) {
let subtitle = mxm let subtitle = new_mxm()
.await
.track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0)) .track_subtitle(track_id, SubtitleFormat::Json, Some(175.0), Some(1.0))
.await .await
.unwrap(); .unwrap();
@ -921,11 +856,9 @@ mod subtitles {
} }
/// This track has no lyrics /// This track has no lyrics
#[rstest]
#[tokio::test] #[tokio::test]
async fn instrumental(#[future] mxm: Musixmatch) { async fn instrumental() {
let err = mxm let err = new_mxm()
.await
.matcher_subtitle( .matcher_subtitle(
"drivers license", "drivers license",
"Bobby G", "Bobby G",
@ -940,11 +873,9 @@ mod subtitles {
} }
/// This track has not been synced /// This track has not been synced
#[rstest]
#[tokio::test] #[tokio::test]
async fn unsynced(#[future] mxm: Musixmatch) { async fn unsynced() {
let err = mxm let err = new_mxm()
.await
.track_subtitle( .track_subtitle(
TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()), TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()),
SubtitleFormat::Json, SubtitleFormat::Json,
@ -958,11 +889,9 @@ mod subtitles {
} }
/// Try to get subtitles with wrong length parameter /// Try to get subtitles with wrong length parameter
#[rstest]
#[tokio::test] #[tokio::test]
async fn wrong_length(#[future] mxm: Musixmatch) { async fn wrong_length() {
let err = mxm let err = new_mxm()
.await
.track_subtitle( .track_subtitle(
TrackId::Commontrack(118480583), TrackId::Commontrack(118480583),
SubtitleFormat::Json, SubtitleFormat::Json,
@ -975,16 +904,14 @@ mod subtitles {
assert!(matches!(err, Error::NotFound)); assert!(matches!(err, Error::NotFound));
} }
#[rstest]
#[tokio::test] #[tokio::test]
async fn download_testdata(#[future] mxm: Musixmatch) { async fn download_testdata() {
let json_path = testfile("subtitles.json"); let json_path = testfile("subtitles.json");
if json_path.exists() { if json_path.exists() {
return; return;
} }
let subtitle = mxm let subtitle = new_mxm()
.await
.track_subtitle( .track_subtitle(
TrackId::Commontrack(18576954), TrackId::Commontrack(18576954),
SubtitleFormat::Json, SubtitleFormat::Json,
@ -1046,9 +973,3 @@ 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}");
}