Compare commits

..

5 commits

23 changed files with 759 additions and 24 deletions

View file

@ -3,6 +3,17 @@
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.4.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.3.0..musixmatch-inofficial/v0.4.0) - 2025-12-19
### 🚀 Features
- Add richsync API (word-by-word lyrics) - ([16e4440](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/16e4440a5d31cf8078c1327d8168975576d4db1f))
### 🐛 Bug Fixes
- Mark structs non-exhaustive, add weekly charts, update URLs to new MXM documentation - ([e044cfc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/e044cfcbd231380e11c14cab31e46b651a942819))
## [v0.3.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.2.1..musixmatch-inofficial/v0.3.0) - 2025-12-08 ## [v0.3.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.2.1..musixmatch-inofficial/v0.3.0) - 2025-12-08
### 🚀 Features ### 🚀 Features

View file

@ -1,6 +1,6 @@
[package] [package]
name = "musixmatch-inofficial" name = "musixmatch-inofficial"
version = "0.3.0" version = "0.4.0"
rust-version = "1.70.0" rust-version = "1.70.0"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
@ -23,7 +23,7 @@ keywords = ["music", "lyrics"]
categories = ["api-bindings", "multimedia"] categories = ["api-bindings", "multimedia"]
[workspace.dependencies] [workspace.dependencies]
musixmatch-inofficial = { version = "0.3.0", path = ".", default-features = false } musixmatch-inofficial = { version = "0.4.0", path = ".", default-features = false }
[features] [features]
default = ["default-tls"] default = ["default-tls"]

View file

@ -27,7 +27,7 @@ commercially.
You will get in trouble if you use this client to create a public lyrics site/app. If You will get in trouble if you use this client to create a public lyrics site/app. If
you want to use Musixmatch data for this purpose, you will have to give them money (see you want to use Musixmatch data for this purpose, you will have to give them money (see
their [commercial plans](https://developer.musixmatch.com/plans)) and use their their [commercial plans](https://developer.musixmatch.com/plans)) and use their
[official API](https://developer.musixmatch.com/documentation). [official API](https://docs.musixmatch.com/lyrics-api).
## Development info ## Development info

View file

@ -3,6 +3,17 @@
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.4.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.3.1..musixmatch-cli/v0.4.0) - 2025-12-19
### 🚀 Features
- Add richsync support to CLI - ([4d1fbab](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/4d1fbabaa8ff3268c83bd3725412e48d81b3f7c1))
### 🐛 Bug Fixes
- Mark structs non-exhaustive, add weekly charts, update URLs to new MXM documentation - ([e044cfc](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/e044cfcbd231380e11c14cab31e46b651a942819))
## [v0.3.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.3.0..musixmatch-cli/v0.3.1) - 2025-12-08 ## [v0.3.1](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.3.0..musixmatch-cli/v0.3.1) - 2025-12-08
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View file

@ -1,6 +1,6 @@
[package] [package]
name = "musixmatch-cli" name = "musixmatch-cli"
version = "0.3.1" version = "0.4.0"
rust-version = "1.70.0" rust-version = "1.70.0"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true

View file

@ -50,6 +50,17 @@ enum Commands {
#[clap(long)] #[clap(long)]
lang: Option<String>, lang: Option<String>,
}, },
/// Get richsync (word-by-word synced lyrics)
Richsync {
#[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>,
},
/// Get track metadata /// Get track metadata
Track { Track {
#[clap(flatten)] #[clap(flatten)]
@ -318,6 +329,34 @@ async fn run(cli: Cli) -> Result<()> {
println!("{}", subtitles.subtitle_body); println!("{}", subtitles.subtitle_body);
} }
} }
Commands::Richsync {
ident,
length,
max_deviation,
} => {
let track_id = get_track_id(ident, &mxm).await?;
let richsync = mxm
.track_richsync(track_id.clone(), length, max_deviation.or(Some(1.0)))
.await?;
eprintln!("Richsync ID: {}", richsync.richsync_id);
eprintln!(
"Language: {}",
richsync.richsync_language.as_deref().unwrap_or(NA_STR)
);
eprintln!("Length: {}", richsync.richsync_length);
eprintln!(
"Copyright: {}",
richsync
.lyrics_copyright
.as_deref()
.map(|s| s.trim())
.unwrap_or(NA_STR)
);
eprintln!();
println!("{}", richsync.richsync_body);
}
Commands::Track { ident } => { Commands::Track { ident } => {
let track = get_track(ident, &mxm, false).await?; let track = get_track(ident, &mxm, false).await?;
println!("{}", serde_json::to_string_pretty(&track)?) println!("{}", serde_json::to_string_pretty(&track)?)

View file

@ -10,7 +10,7 @@ impl Musixmatch {
/// - `id`: [Album ID](crate::models::AlbumId) /// - `id`: [Album ID](crate::models::AlbumId)
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/album-get> /// <https://docs.musixmatch.com/lyrics-api/album/album-get>
pub async fn album(&self, id: AlbumId<'_>) -> Result<Album> { pub async fn album(&self, id: AlbumId<'_>) -> Result<Album> {
let mut url = self.new_url("album.get"); let mut url = self.new_url("album.get");
{ {
@ -35,7 +35,7 @@ impl Musixmatch {
/// - `page`: Define the page number for paginated results, starting from 1. /// - `page`: Define the page number for paginated results, starting from 1.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/artist-albums-get> /// <https://docs.musixmatch.com/lyrics-api/artist/artist-albums-get>
pub async fn artist_albums( pub async fn artist_albums(
&self, &self,
artist_id: ArtistId<'_>, artist_id: ArtistId<'_>,
@ -65,7 +65,7 @@ impl Musixmatch {
.collect()) .collect())
} }
/// This api provides you the list of the top albums of a given country. /// This api provides you the list of the newly released albums of a given country.
/// ///
/// # Parameters /// # Parameters
/// - `country`: A valid country code (default: "US") /// - `country`: A valid country code (default: "US")

View file

@ -10,7 +10,7 @@ impl Musixmatch {
/// - `id`: [Artist ID](crate::models::ArtistId) /// - `id`: [Artist ID](crate::models::ArtistId)
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/artist-get> /// <https://docs.musixmatch.com/lyrics-api/artist/artist-get>
pub async fn artist(&self, id: ArtistId<'_>) -> Result<Artist> { pub async fn artist(&self, id: ArtistId<'_>) -> Result<Artist> {
let mut url = self.new_url("artist.get"); let mut url = self.new_url("artist.get");
{ {
@ -34,7 +34,7 @@ impl Musixmatch {
/// - `page`: Define the page number for paginated results, starting from 1. /// - `page`: Define the page number for paginated results, starting from 1.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/artist-search> /// <https://docs.musixmatch.com/lyrics-api/artist/artist-search>
pub async fn artist_search( pub async fn artist_search(
&self, &self,
q_artist: &str, q_artist: &str,
@ -67,7 +67,7 @@ impl Musixmatch {
/// - `page`: Define the page number for paginated results, starting from 1. /// - `page`: Define the page number for paginated results, starting from 1.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/artist-chart-get> /// <https://docs.musixmatch.com/lyrics-api/charts/chart-artists-get>
pub async fn chart_artists( pub async fn chart_artists(
&self, &self,
country: &str, country: &str,

View file

@ -12,7 +12,7 @@ impl Musixmatch {
/// - `q_artist`: Artist of the track /// - `q_artist`: Artist of the track
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-lyrics-get> /// <https://docs.musixmatch.com/lyrics-api/matcher/matcher-lyrics-get>
pub async fn matcher_lyrics(&self, q_track: &str, q_artist: &str) -> Result<Lyrics> { pub async fn matcher_lyrics(&self, q_track: &str, q_artist: &str) -> Result<Lyrics> {
let mut url = self.new_url("matcher.lyrics.get"); let mut url = self.new_url("matcher.lyrics.get");
{ {
@ -37,7 +37,7 @@ impl Musixmatch {
/// - `id`: [Track ID](crate::models::TrackId) /// - `id`: [Track ID](crate::models::TrackId)
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-lyrics-get> /// <https://docs.musixmatch.com/lyrics-api/track/track-lyrics-get>
pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result<Lyrics> { pub async fn track_lyrics(&self, id: TrackId<'_>) -> Result<Lyrics> {
let mut url = self.new_url("track.lyrics.get"); let mut url = self.new_url("track.lyrics.get");
{ {
@ -58,9 +58,9 @@ impl Musixmatch {
/// - `id`: [Track ID](crate::models::TrackId) /// - `id`: [Track ID](crate::models::TrackId)
/// - `selected_language`: The language of the translated lyrics /// - `selected_language`: The language of the translated lyrics
/// [(ISO 6391)](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) /// [(ISO 6391)](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
///
/// # Reference /// # Reference
/// None, the [public translation API](https://developer.musixmatch.com/documentation/api-reference/track-lyrics-translation-get) /// <https://docs.musixmatch.com/lyrics-api/track/track-lyrics-translation-get>
/// is only available on commercial plans
pub async fn track_lyrics_translation( pub async fn track_lyrics_translation(
&self, &self,
id: TrackId<'_>, id: TrackId<'_>,

View file

@ -1,6 +1,7 @@
mod album_api; mod album_api;
mod artist_api; mod artist_api;
mod lyrics_api; mod lyrics_api;
mod richsync_api;
mod snippet_api; mod snippet_api;
mod subtitle_api; mod subtitle_api;
mod track_api; mod track_api;

45
src/apis/richsync_api.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::error::Result;
use crate::models::richsync::{RichsyncBody, RichsyncLyrics};
use crate::models::TrackId;
use crate::Musixmatch;
impl Musixmatch {
/// Get the richsync (word-by-word synchronized lyrics) for a track specified by its ID.
///
/// # Parameters
/// - `id`: [Track ID](crate::models::TrackId)
/// - `f_richsync_length`: Optional richsync length (track duration) in seconds
/// - `f_richsync_length_max_deviation`: Optional maximum amount of seconds the richsync length
/// is allowed to deviate from the given value. The Musixmatch app sets this value to 1,
/// so this should be the recommended value.
///
/// # Reference
/// <https://docs.musixmatch.com/lyrics-api/track/track-richsync-get>
pub async fn track_richsync(
&self,
id: TrackId<'_>,
f_richsync_length: Option<f32>,
f_richsync_length_max_deviation: Option<f32>,
) -> Result<RichsyncLyrics> {
let mut url = self.new_url("track.richsync.get");
{
let mut url_query = url.query_pairs_mut();
let id_param = id.to_param();
url_query.append_pair(id_param.0, &id_param.1);
if let Some(f_richsync_length) = f_richsync_length {
url_query.append_pair("f_richsync_length", &f_richsync_length.to_string());
}
if let Some(f_richsync_length_max_deviation) = f_richsync_length_max_deviation {
url_query.append_pair(
"f_richsync_length_max_deviation",
&f_richsync_length_max_deviation.to_string(),
);
}
url_query.finish();
}
let richsync_body = self.execute_get_request::<RichsyncBody>(&url).await?;
Ok(richsync_body.richsync)
}
}

View file

@ -14,7 +14,7 @@ impl Musixmatch {
/// - `id`: [Track ID](crate::models::TrackId) /// - `id`: [Track ID](crate::models::TrackId)
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-snippet-get> /// <https://docs.musixmatch.com/lyrics-api/track/track-snippet-get>
pub async fn track_snippet(&self, id: TrackId<'_>) -> Result<Snippet> { pub async fn track_snippet(&self, id: TrackId<'_>) -> Result<Snippet> {
let mut url = self.new_url("track.snippet.get"); let mut url = self.new_url("track.snippet.get");
{ {

View file

@ -16,7 +16,7 @@ impl Musixmatch {
/// so this should be the recommended value. /// so this should be the recommended value.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-subtitle-get> /// <https://docs.musixmatch.com/lyrics-api/matcher/matcher-subtitle-get>
pub async fn matcher_subtitle( pub async fn matcher_subtitle(
&self, &self,
q_track: &str, q_track: &str,
@ -65,7 +65,7 @@ impl Musixmatch {
/// so this should be the recommended value. /// so this should be the recommended value.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-subtitle-get> /// <https://docs.musixmatch.com/lyrics-api/track/track-subtitle-get>
pub async fn track_subtitle( pub async fn track_subtitle(
&self, &self,
id: TrackId<'_>, id: TrackId<'_>,

View file

@ -18,7 +18,7 @@ impl Musixmatch {
/// instead of [ISO 6391](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes. /// instead of [ISO 6391](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-track-get> /// <https://docs.musixmatch.com/lyrics-api/matcher/matcher-track-get>
pub async fn matcher_track( pub async fn matcher_track(
&self, &self,
q_track: &str, q_track: &str,
@ -77,7 +77,7 @@ impl Musixmatch {
/// instead of [ISO 6391](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes. /// instead of [ISO 6391](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-get> /// <https://docs.musixmatch.com/lyrics-api/track/track-get>
pub async fn track( pub async fn track(
&self, &self,
id: TrackId<'_>, id: TrackId<'_>,
@ -126,7 +126,7 @@ impl Musixmatch {
/// - `page`: Define the page number for paginated results, starting from 1. /// - `page`: Define the page number for paginated results, starting from 1.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/album-tracks-get> /// <https://docs.musixmatch.com/lyrics-api/album/album-tracks-get>
pub async fn album_tracks( pub async fn album_tracks(
&self, &self,
id: AlbumId<'_>, id: AlbumId<'_>,
@ -166,7 +166,7 @@ impl Musixmatch {
/// - `page`: Define the page number for paginated results, starting from 1. /// - `page`: Define the page number for paginated results, starting from 1.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-chart-get> /// <https://docs.musixmatch.com/lyrics-api/charts/chart-tracks-get>
pub async fn chart_tracks( pub async fn chart_tracks(
&self, &self,
country: &str, country: &str,
@ -202,7 +202,7 @@ impl Musixmatch {
/// Get the list of the music genres the Musixmatch catalogue. /// Get the list of the music genres the Musixmatch catalogue.
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/music-genres-get> /// <https://docs.musixmatch.com/lyrics-api/charts/music-genres-get>
pub async fn genres(&self) -> Result<Vec<Genre>> { pub async fn genres(&self) -> Result<Vec<Genre>> {
let url = self.new_url("music.genres.get"); let url = self.new_url("music.genres.get");
let genres = self.execute_get_request::<Genres>(&url).await?; let genres = self.execute_get_request::<Genres>(&url).await?;
@ -225,7 +225,7 @@ impl Musixmatch {
/// ``` /// ```
/// ///
/// # Reference /// # Reference
/// <https://developer.musixmatch.com/documentation/api-reference/track-search> /// <https://docs.musixmatch.com/lyrics-api/track/track-search>
pub fn track_search(&self) -> TrackSearchQuery<'_> { pub fn track_search(&self) -> TrackSearchQuery<'_> {
TrackSearchQuery { TrackSearchQuery {
mxm: self.clone(), mxm: self.clone(),

View file

@ -83,7 +83,7 @@ pub struct Album {
/// Album type /// Album type
/// ///
/// Source: <https://developer.musixmatch.com/documentation/music-meta-data> /// Source: <https://docs.musixmatch.com/lyrics-api/musixmatch-metadata#album-type-and-release-date>
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlbumType { pub enum AlbumType {

View file

@ -103,6 +103,7 @@ impl SortOrder {
/// Musixmatch fully qualified ID /// Musixmatch fully qualified ID
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)]
pub struct Fqid { pub struct Fqid {
/// Numeric Musixmatch ID /// Numeric Musixmatch ID
pub id: u64, pub id: u64,

View file

@ -1,4 +1,5 @@
//! Musixmatch API models //! Musixmatch API models
#![warn(clippy::exhaustive_structs)]
pub(crate) mod subtitle; pub(crate) mod subtitle;
pub use subtitle::Subtitle; pub use subtitle::Subtitle;
@ -18,6 +19,11 @@ pub use id::TrackId;
pub(crate) mod lyrics; pub(crate) mod lyrics;
pub use lyrics::Lyrics; pub use lyrics::Lyrics;
pub(crate) mod richsync;
pub use richsync::RichsyncLine;
pub use richsync::RichsyncLyrics;
pub use richsync::RichsyncWord;
pub(crate) mod translation; pub(crate) mod translation;
pub use translation::Translation; pub use translation::Translation;
pub use translation::TranslationList; pub use translation::TranslationList;

83
src/models/richsync.rs Normal file
View file

@ -0,0 +1,83 @@
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::error::Error;
#[derive(Debug, Deserialize)]
pub(crate) struct RichsyncBody {
pub richsync: RichsyncLyrics,
}
/// Lyrics synced word-by-word
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RichsyncLyrics {
/// Unique Musixmatch richsync ID
pub richsync_id: u64,
/// Richsync JSON data
///
/// List of [`RichsyncLine`]
pub richsync_body: String,
/// Language code (e.g. "en")
///
/// Note that this field has a typo and is called `richssync_language` in the Musixmatch implementation.
#[serde(
default,
deserialize_with = "crate::api_model::null_if_empty",
alias = "richssync_language"
)]
pub richsync_language: Option<String>,
/// Language name (e.g. "English")
#[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub richsync_language_description: Option<String>,
/// Duration of the synced track in seconds
pub richsync_length: u32,
/// Copyright text of the lyrics
///
/// Ends with a newline.
///
/// Example:
/// ```text
/// Writer(s): David Hodges
/// Copyright: Emi Blackwood Music Inc., 12.06 Publishing, Hipgnosis Sfh I Limited, Hifi Music Ip Issuer L.p.
/// ```
#[serde(default, deserialize_with = "crate::api_model::null_if_empty")]
pub lyrics_copyright: Option<String>,
/// Date and time when the lyrics were last updated
#[serde(with = "time::serde::rfc3339")]
pub updated_time: OffsetDateTime,
}
/// Line of word-by-word synchronized lyrics
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RichsyncLine {
/// Line start timestamp in seconds
pub ts: f32,
/// Line end timestamp in seconds
pub te: f32,
/// Words in the line
#[serde(default)]
pub l: Vec<RichsyncWord>,
/// Text content of the line
pub x: String,
}
/// Single word of word-by-word synchronized lyrics
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RichsyncWord {
/// Word content
pub c: String,
/// Time offset from the line timestamp in seconds
///
/// Position of the word in the track is line.ts + o.
pub o: f32,
}
impl RichsyncLyrics {
/// Get the synchronized lyrics lines
pub fn get_lines(&self) -> Result<Vec<RichsyncLine>, Error> {
serde_json::from_str(&self.richsync_body).map_err(Error::from)
}
}

View file

@ -15,6 +15,7 @@ pub(crate) struct SnippetBody {
/// Example: "There's not a thing that I would change" /// Example: "There's not a thing that I would change"
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)] #[allow(missing_docs)]
#[non_exhaustive]
pub struct Snippet { pub struct Snippet {
/// Unique Musixmatch Snippet ID /// Unique Musixmatch Snippet ID
pub snippet_id: u64, pub snippet_id: u64,

View file

@ -323,6 +323,7 @@ fn escape_xml(input: &str) -> String {
/// Single subtitle line /// Single subtitle line
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::exhaustive_structs)]
pub struct SubtitleLine { pub struct SubtitleLine {
/// Subtitle line text /// Subtitle line text
pub text: String, pub text: String,
@ -332,6 +333,7 @@ pub struct SubtitleLine {
/// Position of a subtitle line in the track /// Position of a subtitle line in the track
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::exhaustive_structs)]
pub struct SubtitleTime { pub struct SubtitleTime {
/// Minute component of the timestamp /// Minute component of the timestamp
pub minutes: u32, pub minutes: u32,

View file

@ -160,6 +160,7 @@ pub struct TrackLyricsTranslationStatus {
/// Lyrics parts marked with the performer who is singing them /// Lyrics parts marked with the performer who is singing them
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TrackPerformerTagging { pub struct TrackPerformerTagging {
/// Musixmatch user ID of the user who added the performer tags /// Musixmatch user ID of the user who added the performer tags
/// ///
@ -185,6 +186,7 @@ pub struct TrackPerformerTagging {
/// Performer-tagged lyrics part /// Performer-tagged lyrics part
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PerformerTaggingPart { pub struct PerformerTaggingPart {
/// Part of the lyrics text /// Part of the lyrics text
/// ///
@ -200,6 +202,7 @@ pub struct PerformerTaggingPart {
/// Lyrics performer /// Lyrics performer
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Performer { pub struct Performer {
/// artist / unknown /// artist / unknown
#[serde(rename = "type")] #[serde(rename = "type")]
@ -219,6 +222,7 @@ pub struct Performer {
/// Artists (and possibly other objects) that are referenced by the tagged parts /// Artists (and possibly other objects) that are referenced by the tagged parts
#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
#[non_exhaustive]
pub struct PerformerTaggingResources { pub struct PerformerTaggingResources {
/// List of artists tagged as performers /// List of artists tagged as performers
pub artists: Vec<Artist>, pub artists: Vec<Artist>,
@ -226,11 +230,16 @@ pub struct PerformerTaggingResources {
/// Available track charts /// Available track charts
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ChartName { pub enum ChartName {
/// Editorial chart /// Editorial chart
Top, Top,
/// Most viewed lyrics in the last 2 hours /// Most viewed lyrics in the last 2 hours
Hot, Hot,
/// Most viewed lyrics in the last 7 days
MxmWeekly,
/// Most viewed lyrics in the last 7 days, limited to new releases only.
MxmWeeklyNew,
} }
impl ChartName { impl ChartName {
@ -238,6 +247,8 @@ impl ChartName {
match self { match self {
ChartName::Top => "top", ChartName::Top => "top",
ChartName::Hot => "hot", ChartName::Hot => "hot",
ChartName::MxmWeekly => "mxmweekly",
ChartName::MxmWeeklyNew => "mxmweekly_new",
} }
} }
} }

422
testfiles/richsync.json Normal file
View file

@ -0,0 +1,422 @@
[
{
"ts": 3.94,
"te": 7.24,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.33
},
{
"c": "birthday",
"o": 0.66
},
{
"c": " ",
"o": 1.8
},
{
"c": "to",
"o": 1.92
},
{
"c": " ",
"o": 2.235
},
{
"c": "you",
"o": 2.55
}
],
"x": "Happy birthday to you"
},
{
"ts": 8.0399,
"te": 11.34,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.33
},
{
"c": "birthday",
"o": 0.66
},
{
"c": " ",
"o": 1.26
},
{
"c": "to",
"o": 1.86
},
{
"c": " ",
"o": 2.205
},
{
"c": "you",
"o": 2.55
}
],
"x": "Happy birthday to you"
},
{
"ts": 11.88,
"te": 15.57,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.315
},
{
"c": "birthday,",
"o": 0.63
},
{
"c": " ",
"o": 1.275
},
{
"c": "happy",
"o": 1.92
},
{
"c": " ",
"o": 2.25
},
{
"c": "birthday",
"o": 2.58
}
],
"x": "Happy birthday, happy birthday"
},
{
"ts": 15.74,
"te": 19.01,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.315
},
{
"c": "birthday",
"o": 0.63
},
{
"c": " ",
"o": 1.185
},
{
"c": "to",
"o": 1.74
},
{
"c": " ",
"o": 2.145
},
{
"c": "you",
"o": 2.55
}
],
"x": "Happy birthday to you"
},
{
"ts": 19.71,
"te": 23.07,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.344
},
{
"c": "birthday",
"o": 0.689
},
{
"c": " ",
"o": 1.274
},
{
"c": "to",
"o": 1.86
},
{
"c": " ",
"o": 2.22
},
{
"c": "you",
"o": 2.58
}
],
"x": "Happy birthday to you"
},
{
"ts": 23.57,
"te": 26.96,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.33
},
{
"c": "birthday",
"o": 0.66
},
{
"c": " ",
"o": 1.26
},
{
"c": "to",
"o": 1.86
},
{
"c": " ",
"o": 2.235
},
{
"c": "you",
"o": 2.61
}
],
"x": "Happy birthday to you"
},
{
"ts": 27.52,
"te": 31.21,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.315
},
{
"c": "birthday,",
"o": 0.63
},
{
"c": " ",
"o": 1.29
},
{
"c": "happy",
"o": 1.95
},
{
"c": " ",
"o": 2.28
},
{
"c": "birthday",
"o": 2.61
}
],
"x": "Happy birthday, happy birthday"
},
{
"ts": 31.42,
"te": 34.42,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.315
},
{
"c": "birthday",
"o": 0.63
},
{
"c": " ",
"o": 1.2
},
{
"c": "to",
"o": 1.77
},
{
"c": " ",
"o": 2.175
},
{
"c": "you",
"o": 2.58
}
],
"x": "Happy birthday to you"
},
{
"ts": 37.44,
"te": 40.59,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.315
},
{
"c": "birthday",
"o": 0.63
},
{
"c": " ",
"o": 1.74
},
{
"c": "to",
"o": 1.86
},
{
"c": " ",
"o": 2.22
},
{
"c": "you",
"o": 2.58
}
],
"x": "Happy birthday to you"
},
{
"ts": 41.26,
"te": 44.38,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.3
},
{
"c": "birthday",
"o": 0.6
},
{
"c": " ",
"o": 1.2
},
{
"c": "to",
"o": 1.8
},
{
"c": " ",
"o": 2.145
},
{
"c": "you",
"o": 2.49
}
],
"x": "Happy birthday to you"
},
{
"ts": 45.17,
"te": 50.24,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.315
},
{
"c": "birthday,",
"o": 0.63
},
{
"c": " ",
"o": 1.275
},
{
"c": "happy",
"o": 1.92
},
{
"c": " ",
"o": 2.264
},
{
"c": "birthday",
"o": 2.61
}
],
"x": "Happy birthday, happy birthday"
},
{
"ts": 50.99,
"te": 54.11,
"l": [
{
"c": "Happy",
"o": 0.0
},
{
"c": " ",
"o": 0.315
},
{
"c": "birthday",
"o": 0.63
},
{
"c": " ",
"o": 1.2
},
{
"c": "to",
"o": 1.77
},
{
"c": " ",
"o": 2.13
},
{
"c": "you",
"o": 2.49
}
],
"x": "Happy birthday to you"
}
]

View file

@ -647,6 +647,8 @@ mod track {
#[rstest] #[rstest]
#[case::top(ChartName::Top)] #[case::top(ChartName::Top)]
#[case::hot(ChartName::Hot)] #[case::hot(ChartName::Hot)]
#[case::weekly(ChartName::MxmWeekly)]
#[case::weekly_new(ChartName::MxmWeeklyNew)]
#[tokio::test] #[tokio::test]
async fn charts(#[case] chart_name: ChartName, #[future] mxm: Musixmatch) { async fn charts(#[case] chart_name: ChartName, #[future] mxm: Musixmatch) {
let tracks = mxm let tracks = mxm
@ -1071,6 +1073,106 @@ mod translation {
} }
} }
mod richsync {
use std::{fs::File, io::BufWriter};
use super::*;
#[rstest]
#[case::trackid(TrackId::TrackId(205688271))]
#[case::commontrack(TrackId::Commontrack(118480583))]
#[case::vanity(TrackId::CommontrackVanity("aespa/Black-Mamba".into()))]
#[case::isrc(TrackId::Isrc("KRA302000590".into()))]
#[case::spotify(TrackId::Spotify("1t2qYCAjUAoGfeFeoBlK51".into()))]
#[tokio::test]
async fn from_id(#[case] track_id: TrackId<'_>, #[future] mxm: Musixmatch) {
let richsync = mxm
.await
.track_richsync(track_id, Some(175.0), Some(1.0))
.await
.unwrap();
let lines = richsync.get_lines().unwrap();
assert_eq!(richsync.richsync_id, 8298962);
assert_eq!(richsync.richsync_language.unwrap(), "ko");
assert_eq!(richsync.richsync_language_description.unwrap(), "korean");
let copyright = richsync.lyrics_copyright.unwrap();
assert!(
copyright.contains("Kenneth Scott Chesak"),
"copyright: {copyright}",
);
assert_eq!(richsync.richsync_length, 175);
assert!(richsync.updated_time > datetime!(2022-8-27 0:00 UTC));
assert_eq!(lines.len(), 67);
for line in &lines {
assert!(!line.l.is_empty());
}
}
/// This track has no lyrics
#[rstest]
#[tokio::test]
async fn instrumental(#[future] mxm: Musixmatch) {
let err = mxm
.await
.track_richsync(TrackId::Commontrack(126454494), Some(246.0), Some(1.0))
.await
.unwrap_err();
assert!(matches!(err, Error::NotFound), "got: {err:?}");
}
/// This track has not been synced
#[rstest]
#[tokio::test]
async fn unsynced(#[future] mxm: Musixmatch) {
let err = mxm
.await
.track_richsync(
TrackId::Spotify("6oaWIABGL7eeiMILEDyGX1".into()),
Some(213.0),
Some(1.0),
)
.await
.unwrap_err();
assert!(matches!(err, Error::NotFound), "got: {err:?}");
}
/// Try to get subtitles with wrong length parameter
#[rstest]
#[tokio::test]
async fn wrong_length(#[future] mxm: Musixmatch) {
let err = mxm
.await
.track_richsync(TrackId::Commontrack(118480583), Some(200.0), Some(1.0))
.await
.unwrap_err();
assert!(matches!(err, Error::NotFound), "got: {err:?}");
}
#[rstest]
#[tokio::test]
async fn download_testdata(#[future] mxm: Musixmatch) {
let json_path = testfile("richsync.json");
if json_path.exists() {
return;
}
let richsync = mxm
.await
.track_richsync(TrackId::Commontrack(26134491), None, None)
.await
.unwrap();
let lines = richsync.get_lines().unwrap();
let json_file = File::create(json_path).unwrap();
serde_json::to_writer_pretty(BufWriter::new(json_file), &lines).unwrap();
}
}
#[track_caller] #[track_caller]
fn assert_imgurl(url: &Option<String>, ends_with: &str) { fn assert_imgurl(url: &Option<String>, ends_with: &str) {
assert!( assert!(