Compare commits
3 commits
ea0f606f75
...
6b4312a6a5
Author | SHA1 | Date | |
---|---|---|---|
6b4312a6a5 | |||
d4eb894648 | |||
2a03bb6d77 |
15 changed files with 243 additions and 130 deletions
|
@ -1,2 +1,3 @@
|
|||
MUSIXMATCH_CLIENT=Android
|
||||
MUSIXMATCH_EMAIL=mail@example.com
|
||||
MUSIXMATCH_PASSWORD=super-secret
|
||||
|
|
|
@ -10,4 +10,5 @@ pipeline:
|
|||
- rustup component add rustfmt clippy
|
||||
- cargo fmt --all --check
|
||||
- cargo clippy --all -- -D warnings
|
||||
- cargo test --workspace
|
||||
- MUSIXMATCH_CLIENT=Desktop cargo test --workspace
|
||||
- MUSIXMATCH_CLIENT=Android cargo test --workspace
|
||||
|
|
|
@ -28,6 +28,7 @@ reqwest = { version = "0.11.11", default-features = false, features = [
|
|||
"json",
|
||||
"gzip",
|
||||
] }
|
||||
url = "2.0.0"
|
||||
tokio = { version = "1.20.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.85"
|
||||
|
@ -46,12 +47,13 @@ base64 = "0.21.0"
|
|||
|
||||
[dev-dependencies]
|
||||
ctor = "0.2.0"
|
||||
rstest = { version = "0.17.0", default-features = false }
|
||||
env_logger = "0.10.0"
|
||||
rstest = { version = "0.18.0", default-features = false }
|
||||
env_logger = "0.11.0"
|
||||
dotenvy = "0.15.5"
|
||||
tokio = { version = "1.20.0", features = ["macros"] }
|
||||
futures = "0.3.21"
|
||||
path_macro = "1.0.0"
|
||||
serde_plain = "1.0.2"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
|
44
README.md
44
README.md
|
@ -1,35 +1,39 @@
|
|||
# Musixmatch-Inofficial
|
||||
|
||||
This is an inofficial client for the Musixmatch API that uses the
|
||||
key embedded in the Musixmatch Android app.
|
||||
This is an inofficial client for the Musixmatch API that uses the key embedded in the
|
||||
Musixmatch Android app or desktop client.
|
||||
|
||||
It allows you to obtain synchronized lyrics in different formats
|
||||
([LRC](https://en.wikipedia.org/wiki/LRC_(file_format)),
|
||||
([LRC](<https://en.wikipedia.org/wiki/LRC_(file_format)>),
|
||||
[DFXP](https://www.w3.org/TR/ttml1/), JSON) for almost any song.
|
||||
|
||||
A free Musixmatch account is required for operation
|
||||
([you can sign up here](https://www.musixmatch.com/de/sign-up)).
|
||||
If you use the Android client, you need a free Musixmatch account
|
||||
([you can sign up here](https://www.musixmatch.com/de/sign-up)). The desktop client can
|
||||
be used anonymously and is currently the default option, but since Musixmatch
|
||||
discontinued that application, they may shut it down.
|
||||
|
||||
## ⚠️ Copyright disclaimer
|
||||
|
||||
Song lyrics are copyrighted works (just like books, poems and the songs
|
||||
themselves).
|
||||
Song lyrics are copyrighted works (just like books, poems and the songs themselves).
|
||||
|
||||
Musixmatch does allow its users to obtains song lyrics for private
|
||||
use (e.g. to enrich their music collection). But it does not allow you
|
||||
to publish their lyrics or use them commercially.
|
||||
Musixmatch does allow its users to obtains song lyrics for private use (e.g. to enrich
|
||||
their music collection). But it does not allow you to publish their lyrics or use them
|
||||
commercially.
|
||||
|
||||
You will get in trouble if you use this client to create a public
|
||||
lyrics site/app. If you want to use Musixmatch data for this purpose,
|
||||
you will have to give them money (see their
|
||||
[commercial plans](https://developer.musixmatch.com/plans))
|
||||
and use their [official API](https://developer.musixmatch.com/documentation).
|
||||
You will get in trouble if you use this client to create a public lyrics site/app. If
|
||||
you want to use Musixmatch data for this purpose, you will have to give them money (see
|
||||
their [commercial plans](https://developer.musixmatch.com/plans)) and use their
|
||||
[official API](https://developer.musixmatch.com/documentation).
|
||||
|
||||
## Development info
|
||||
|
||||
Running the tests requires Musixmatch credentials. The credentials are read
|
||||
from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment variables.
|
||||
You can choose which client to test by setting the `MUSIXMATCH_CLIENT` environment
|
||||
variable to either `Desktop` or `Android` (it defaults to Desktop).
|
||||
|
||||
To make local development easier, I have included `dotenvy` to read the
|
||||
credentials from an `.env` file. Copy the `.env.example` file
|
||||
in the root directory, rename it to `.env` and fill in your credentials.
|
||||
Running the tests for the Android client requires Musixmatch credentials. The
|
||||
credentials are read from the `MUSIXMATCH_EMAIL` and `MUSIXMATCH_PASSWORD` environment
|
||||
variables.
|
||||
|
||||
To make local development easier, I have included `dotenvy` to read the credentials from
|
||||
an `.env` file. Copy the `.env.example` file in the root directory, rename it to `.env`
|
||||
and fill in your credentials.
|
||||
|
|
|
@ -12,7 +12,7 @@ impl Musixmatch {
|
|||
/// # Reference
|
||||
/// <https://developer.musixmatch.com/documentation/api-reference/album-get>
|
||||
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")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -43,7 +43,7 @@ impl Musixmatch {
|
|||
page_size: u8,
|
||||
page: u32,
|
||||
) -> Result<Vec<Album>> {
|
||||
let mut url = self.new_url("artist.albums.get");
|
||||
let mut url = self.new_url("artist.albums.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -80,7 +80,7 @@ impl Musixmatch {
|
|||
page_size: u8,
|
||||
page: u32,
|
||||
) -> Result<Vec<Album>> {
|
||||
let mut url = self.new_url("chart.albums.get");
|
||||
let mut url = self.new_url("chart.albums.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ impl Musixmatch {
|
|||
/// # Reference
|
||||
/// <https://developer.musixmatch.com/documentation/api-reference/artist-get>
|
||||
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")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -40,7 +40,7 @@ impl Musixmatch {
|
|||
page_size: u8,
|
||||
page: u32,
|
||||
) -> Result<Vec<Artist>> {
|
||||
let mut url = self.new_url("artist.related.get");
|
||||
let mut url = self.new_url("artist.related.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -74,7 +74,7 @@ impl Musixmatch {
|
|||
page_size: u8,
|
||||
page: u32,
|
||||
) -> Result<Vec<Artist>> {
|
||||
let mut url = self.new_url("artist.search");
|
||||
let mut url = self.new_url("artist.search")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -107,7 +107,7 @@ impl Musixmatch {
|
|||
page_size: u8,
|
||||
page: u32,
|
||||
) -> Result<Vec<Artist>> {
|
||||
let mut url = self.new_url("chart.artists.get");
|
||||
let mut url = self.new_url("chart.artists.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ impl Musixmatch {
|
|||
/// # Reference
|
||||
/// <https://developer.musixmatch.com/documentation/api-reference/matcher-lyrics-get>
|
||||
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")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
if !q_track.is_empty() {
|
||||
|
@ -39,7 +39,7 @@ impl Musixmatch {
|
|||
/// # Reference
|
||||
/// <https://developer.musixmatch.com/documentation/api-reference/track-lyrics-get>
|
||||
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")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
let id_param = id.to_param();
|
||||
|
@ -66,7 +66,7 @@ impl Musixmatch {
|
|||
id: TrackId<'_>,
|
||||
selected_language: &str,
|
||||
) -> Result<TranslationList> {
|
||||
let mut url = self.new_url("crowd.track.translations.get");
|
||||
let mut url = self.new_url("crowd.track.translations.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
let id_param = id.to_param();
|
||||
|
|
|
@ -16,7 +16,7 @@ impl Musixmatch {
|
|||
/// # Reference
|
||||
/// <https://developer.musixmatch.com/documentation/api-reference/track-snippet-get>
|
||||
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")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ impl Musixmatch {
|
|||
f_subtitle_length: Option<f32>,
|
||||
f_subtitle_length_max_deviation: Option<f32>,
|
||||
) -> Result<Subtitle> {
|
||||
let mut url = self.new_url("matcher.subtitle.get");
|
||||
let mut url = self.new_url("matcher.subtitle.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
if !q_track.is_empty() {
|
||||
|
@ -73,7 +73,7 @@ impl Musixmatch {
|
|||
f_subtitle_length: Option<f32>,
|
||||
f_subtitle_length_max_deviation: Option<f32>,
|
||||
) -> Result<Subtitle> {
|
||||
let mut url = self.new_url("track.subtitle.get");
|
||||
let mut url = self.new_url("track.subtitle.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ impl Musixmatch {
|
|||
translation_status: bool,
|
||||
lang_3c: bool,
|
||||
) -> Result<Track> {
|
||||
let mut url = self.new_url("matcher.track.get");
|
||||
let mut url = self.new_url("matcher.track.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -74,7 +74,7 @@ impl Musixmatch {
|
|||
translation_status: bool,
|
||||
lang_3c: bool,
|
||||
) -> Result<Track> {
|
||||
let mut url = self.new_url("track.get");
|
||||
let mut url = self.new_url("track.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -114,7 +114,7 @@ impl Musixmatch {
|
|||
page_size: u8,
|
||||
page: u32,
|
||||
) -> Result<Vec<Track>> {
|
||||
let mut url = self.new_url("album.tracks.get");
|
||||
let mut url = self.new_url("album.tracks.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -155,7 +155,7 @@ impl Musixmatch {
|
|||
page_size: u8,
|
||||
page: u32,
|
||||
) -> Result<Vec<Track>> {
|
||||
let mut url = self.new_url("chart.tracks.get");
|
||||
let mut url = self.new_url("chart.tracks.get")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
@ -184,7 +184,7 @@ impl Musixmatch {
|
|||
/// # Reference
|
||||
/// <https://developer.musixmatch.com/documentation/api-reference/music-genres-get>
|
||||
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?;
|
||||
Ok(genres.music_genre_list)
|
||||
}
|
||||
|
@ -347,7 +347,7 @@ impl<'a> TrackSearchQuery<'a> {
|
|||
/// - `page_size`: Define the page size for paginated results. Range is 1 to 100.
|
||||
/// - `page`: Define the page number for paginated results, starting from 1.
|
||||
pub async fn send(&self, page_size: u8, page: u32) -> Result<Vec<Track>> {
|
||||
let mut url = self.mxm.new_url("track.search");
|
||||
let mut url = self.mxm.new_url("track.search")?;
|
||||
{
|
||||
let mut url_query = url.query_pairs_mut();
|
||||
|
||||
|
|
11
src/error.rs
11
src/error.rs
|
@ -1,3 +1,5 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
pub(crate) type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
/// Custom error type for the Musixmatch client
|
||||
|
@ -36,6 +38,9 @@ pub enum Error {
|
|||
/// Error from the HTTP client
|
||||
#[error("http error: {0}")]
|
||||
Http(reqwest::Error),
|
||||
/// Unspecified error
|
||||
#[error("{0}")]
|
||||
Other(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
|
@ -44,3 +49,9 @@ impl From<reqwest::Error> for Error {
|
|||
Self::Http(value.without_url())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(value: url::ParseError) -> Self {
|
||||
Self::Other(format!("url parse error: {value}").into())
|
||||
}
|
||||
}
|
||||
|
|
181
src/lib.rs
181
src/lib.rs
|
@ -35,11 +35,30 @@ use crate::error::Result;
|
|||
const YMD_FORMAT: &[time::format_description::FormatItem] =
|
||||
format_description!("[year][month][day]");
|
||||
|
||||
const APP_ID: &str = "android-player-v1.0";
|
||||
const API_URL: &str = "https://apic.musixmatch.com/ws/1.1/";
|
||||
const SIGNATURE_SECRET: &[u8; 20] = b"967Pn4)N3&R_GBg5$b('";
|
||||
/// Hardcoded client configuration
|
||||
struct ClientCfg {
|
||||
app_id: &'static str,
|
||||
api_url: &'static str,
|
||||
signature_secret: &'static [u8; 20],
|
||||
user_agent: &'static str,
|
||||
login: bool,
|
||||
}
|
||||
|
||||
const DESKTOP_CLIENT: ClientCfg = ClientCfg {
|
||||
app_id: "web-desktop-app-v1.0",
|
||||
api_url: "https://apic-desktop.musixmatch.com/ws/1.1/",
|
||||
signature_secret: b"IEJ5E8XFaHQvIQNfs7IC",
|
||||
user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Musixmatch/0.19.4 Chrome/58.0.3029.110 Electron/1.7.6 Safari/537.36",
|
||||
login: false
|
||||
};
|
||||
const ANDROID_CLIENT: ClientCfg = ClientCfg {
|
||||
app_id: "android-player-v1.0",
|
||||
api_url: "https://apic.musixmatch.com/ws/1.1/",
|
||||
signature_secret: b"967Pn4)N3&R_GBg5$b('",
|
||||
user_agent: "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Build/T3B2.230316.003)",
|
||||
login: true,
|
||||
};
|
||||
|
||||
const DEFAULT_UA: &str = "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Build/T3B2.230316.003)";
|
||||
const DEFAULT_BRAND: &str = "Google";
|
||||
const DEFAULT_DEVICE: &str = "Pixel 6";
|
||||
|
||||
|
@ -57,6 +76,7 @@ pub struct Musixmatch {
|
|||
/// Used to construct a new [`Musixmatch`] client.#
|
||||
#[derive(Default)]
|
||||
pub struct MusixmatchBuilder {
|
||||
client_type: ClientType,
|
||||
user_agent: Option<String>,
|
||||
brand: Option<String>,
|
||||
device: Option<String>,
|
||||
|
@ -64,6 +84,29 @@ pub struct MusixmatchBuilder {
|
|||
credentials: Option<Credentials>,
|
||||
}
|
||||
|
||||
/// Musixmatch client type
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ClientType {
|
||||
/// The desktop client is used with Musixmatch's electron-based Desktop application.
|
||||
///
|
||||
/// The client allows anonymous access and is currently the default option.
|
||||
///
|
||||
/// Since Musixmatch's desktop application is discontinued, the client may stop working in the future.
|
||||
#[default]
|
||||
Desktop,
|
||||
/// The Android client requires a (free) Musixmatch account
|
||||
Android,
|
||||
}
|
||||
|
||||
impl From<ClientType> for ClientCfg {
|
||||
fn from(value: ClientType) -> Self {
|
||||
match value {
|
||||
ClientType::Desktop => DESKTOP_CLIENT,
|
||||
ClientType::Android => ANDROID_CLIENT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum DefaultOpt<T> {
|
||||
Some(T),
|
||||
|
@ -86,6 +129,8 @@ struct MusixmatchRef {
|
|||
http: Client,
|
||||
storage: Option<Box<dyn SessionStorage>>,
|
||||
credentials: RwLock<Option<Credentials>>,
|
||||
client_type: ClientType,
|
||||
client_cfg: ClientCfg,
|
||||
brand: String,
|
||||
device: String,
|
||||
usertoken: Mutex<Option<String>>,
|
||||
|
@ -99,6 +144,7 @@ struct Credentials {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StoredSession {
|
||||
client_type: ClientType,
|
||||
usertoken: String,
|
||||
}
|
||||
|
||||
|
@ -167,6 +213,12 @@ impl MusixmatchBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the client type (Desktop, Android) of the Musixmatch client
|
||||
pub fn client_type(mut self, client_type: ClientType) -> Self {
|
||||
self.client_type = client_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the device brand of the Musixmatch client
|
||||
pub fn device_brand<S: Into<String>>(mut self, device_brand: S) -> Self {
|
||||
self.brand = Some(device_brand.into());
|
||||
|
@ -187,13 +239,18 @@ impl MusixmatchBuilder {
|
|||
/// Returns a new, configured Musixmatch client using a Reqwest client builder
|
||||
pub fn build_with_client(self, client_builder: ClientBuilder) -> Result<Musixmatch> {
|
||||
let storage = self.storage.or_default(|| Box::<FileStorage>::default());
|
||||
let stored_session = Musixmatch::retrieve_session(&storage);
|
||||
let stored_session =
|
||||
Musixmatch::retrieve_session(&storage).filter(|s| s.client_type == self.client_type);
|
||||
let client_cfg = ClientCfg::from(self.client_type);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::COOKIE, "AWSELBCORS=0; AWSELB=0".parse().unwrap());
|
||||
|
||||
let http = client_builder
|
||||
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
|
||||
.user_agent(
|
||||
self.user_agent
|
||||
.unwrap_or_else(|| client_cfg.user_agent.to_owned()),
|
||||
)
|
||||
.gzip(true)
|
||||
.default_headers(headers)
|
||||
.build()?;
|
||||
|
@ -203,6 +260,8 @@ impl MusixmatchBuilder {
|
|||
http,
|
||||
storage,
|
||||
credentials: RwLock::new(self.credentials),
|
||||
client_type: self.client_type,
|
||||
client_cfg,
|
||||
brand: self.brand.unwrap_or_else(|| DEFAULT_BRAND.to_owned()),
|
||||
device: self.device.unwrap_or_else(|| DEFAULT_DEVICE.to_owned()),
|
||||
usertoken: Mutex::new(stored_session.map(|s| s.usertoken)),
|
||||
|
@ -236,27 +295,40 @@ impl Musixmatch {
|
|||
}
|
||||
}
|
||||
|
||||
let credentials = {
|
||||
let credentials = if self.inner.client_cfg.login {
|
||||
let c = self.inner.credentials.read().unwrap();
|
||||
match c.deref() {
|
||||
Some(c) => c.clone(),
|
||||
Some(c) => Some(c.clone()),
|
||||
None => return Err(Error::MissingCredentials),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let guid = random_guid();
|
||||
let adv_id = random_uuid();
|
||||
|
||||
// Get user token
|
||||
// The get_token endpoint seems to be rate limited for 2 requests per minute
|
||||
let mut url = Url::parse_with_params(
|
||||
&format!("{}{}", API_URL, "token.get"),
|
||||
let base_url = format!("{}{}", self.inner.client_cfg.api_url, "token.get");
|
||||
let mut url = match self.inner.client_type {
|
||||
ClientType::Desktop => Url::parse_with_params(
|
||||
&base_url,
|
||||
&[
|
||||
("format", "json"),
|
||||
("user_language", "en"),
|
||||
("app_id", self.inner.client_cfg.app_id),
|
||||
],
|
||||
),
|
||||
ClientType::Android => {
|
||||
let guid = random_guid();
|
||||
let adv_id = random_uuid();
|
||||
Url::parse_with_params(
|
||||
&base_url,
|
||||
&[
|
||||
("adv_id", adv_id.as_str()),
|
||||
("root", "0"),
|
||||
("sideloaded", "0"),
|
||||
("app_id", "android-player-v1.0"),
|
||||
("app_id", self.inner.client_cfg.app_id),
|
||||
// App version (7.9.5)
|
||||
("build_number", "2022090901"),
|
||||
("guid", guid.as_str()),
|
||||
|
@ -267,18 +339,22 @@ impl Musixmatch {
|
|||
now.format(&Rfc3339).unwrap_or_default().as_str(),
|
||||
),
|
||||
("format", "json"),
|
||||
("user_language", "en"),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
sign_url_with_date(&mut url, now);
|
||||
}
|
||||
}?;
|
||||
self.sign_url_with_date(&mut url, now);
|
||||
|
||||
let resp = self.inner.http.get(url).send().await?.error_for_status()?;
|
||||
let tdata = resp.json::<Resp<api_model::GetToken>>().await?;
|
||||
let usertoken = tdata.body_or_err()?.user_token;
|
||||
info!("Received new usertoken: {}****", &usertoken[0..8]);
|
||||
|
||||
if let Some(credentials) = credentials {
|
||||
let account = self.post_credentials(&usertoken, &credentials).await?;
|
||||
info!("Logged in as {} ", account.name);
|
||||
}
|
||||
|
||||
*stored_usertoken = Some(usertoken.to_owned());
|
||||
self.store_session(&usertoken);
|
||||
|
@ -290,8 +366,8 @@ impl Musixmatch {
|
|||
usertoken: &str,
|
||||
credentials: &Credentials,
|
||||
) -> Result<api_model::Account> {
|
||||
let mut url = new_url_from_token("credential.post", usertoken);
|
||||
sign_url_with_date(&mut url, OffsetDateTime::now_utc());
|
||||
let mut url = self.new_url_from_token("credential.post", usertoken)?;
|
||||
self.sign_url_with_date(&mut url, OffsetDateTime::now_utc());
|
||||
|
||||
let api_credentials = api_model::Credentials {
|
||||
credential_list: &[api_model::CredentialWrap {
|
||||
|
@ -334,6 +410,7 @@ impl Musixmatch {
|
|||
fn store_session(&self, usertoken: &str) {
|
||||
if let Some(storage) = &self.inner.storage {
|
||||
let to_store = StoredSession {
|
||||
client_type: self.inner.client_type,
|
||||
usertoken: usertoken.to_owned(),
|
||||
};
|
||||
|
||||
|
@ -368,12 +445,12 @@ impl Musixmatch {
|
|||
)
|
||||
}
|
||||
|
||||
fn new_url(&self, endpoint: &str) -> reqwest::Url {
|
||||
fn new_url(&self, endpoint: &str) -> Result<reqwest::Url> {
|
||||
Url::parse_with_params(
|
||||
&format!("{}{}", API_URL, endpoint),
|
||||
&[("app_id", APP_ID), ("format", "json")],
|
||||
&format!("{}{}", self.inner.client_cfg.api_url, endpoint),
|
||||
&[("app_id", self.inner.client_cfg.app_id), ("format", "json")],
|
||||
)
|
||||
.unwrap()
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
async fn finish_url(&self, url: &mut Url, force_new_session: bool) -> Result<()> {
|
||||
|
@ -382,7 +459,7 @@ impl Musixmatch {
|
|||
.append_pair("usertoken", &usertoken)
|
||||
.finish();
|
||||
|
||||
sign_url_with_date(url, OffsetDateTime::now_utc());
|
||||
self.sign_url_with_date(url, OffsetDateTime::now_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -441,6 +518,33 @@ impl Musixmatch {
|
|||
password: password.into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn new_url_from_token(&self, endpoint: &str, usertoken: &str) -> Result<reqwest::Url> {
|
||||
Url::parse_with_params(
|
||||
&format!("{}{}", self.inner.client_cfg.api_url, endpoint),
|
||||
&[
|
||||
("app_id", self.inner.client_cfg.app_id),
|
||||
("usertoken", usertoken),
|
||||
("format", "json"),
|
||||
],
|
||||
)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
fn sign_url_with_date(&self, url: &mut Url, date: OffsetDateTime) {
|
||||
let mut mac = Hmac::<Sha1>::new_from_slice(self.inner.client_cfg.signature_secret).unwrap();
|
||||
|
||||
mac.update(url.as_str().as_bytes());
|
||||
mac.update(date.format(YMD_FORMAT).unwrap_or_default().as_bytes());
|
||||
|
||||
let sig = mac.finalize().into_bytes();
|
||||
let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig) + "\n";
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("signature", &sig_b64)
|
||||
.append_pair("signature_protocol", "sha1")
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
fn random_guid() -> String {
|
||||
|
@ -461,33 +565,6 @@ fn random_uuid() -> String {
|
|||
)
|
||||
}
|
||||
|
||||
fn new_url_from_token(endpoint: &str, usertoken: &str) -> reqwest::Url {
|
||||
Url::parse_with_params(
|
||||
&format!("{}{}", API_URL, endpoint),
|
||||
&[
|
||||
("app_id", APP_ID),
|
||||
("usertoken", usertoken),
|
||||
("format", "json"),
|
||||
],
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn sign_url_with_date(url: &mut Url, date: OffsetDateTime) {
|
||||
let mut mac = Hmac::<Sha1>::new_from_slice(SIGNATURE_SECRET).unwrap();
|
||||
|
||||
mac.update(url.as_str().as_bytes());
|
||||
mac.update(date.format(YMD_FORMAT).unwrap_or_default().as_bytes());
|
||||
|
||||
let sig = mac.finalize().into_bytes();
|
||||
let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig) + "\n";
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("signature", &sig_b64)
|
||||
.append_pair("signature_protocol", "sha1")
|
||||
.finish();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use time::macros::datetime;
|
||||
|
@ -496,8 +573,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn t_sign_url() {
|
||||
let mxm = Musixmatch::builder()
|
||||
.client_type(ClientType::Android)
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut url = Url::parse("https://apic.musixmatch.com/ws/1.1/track.subtitle.get?app_id=android-player-v1.0&usertoken=22092860c49e8d783b569a7bd847cd5b289bbec306f8a0bb2d3771&format=json&track_spotify_id=7Ga0ByppmSXWuKXdsD8JGL&subtitle_format=mxm").unwrap();
|
||||
sign_url_with_date(&mut url, datetime!(2022-09-28 0:00 UTC));
|
||||
mxm.sign_url_with_date(&mut url, datetime!(2022-09-28 0:00 UTC));
|
||||
assert_eq!(url.as_str(), "https://apic.musixmatch.com/ws/1.1/track.subtitle.get?app_id=android-player-v1.0&usertoken=22092860c49e8d783b569a7bd847cd5b289bbec306f8a0bb2d3771&format=json&track_spotify_id=7Ga0ByppmSXWuKXdsD8JGL&subtitle_format=mxm&signature=cvXbedVvGneT7o4k8QG6jfk9pAM%3D%0A&signature_protocol=sha1")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ pub struct Album {
|
|||
pub enum AlbumType {
|
||||
#[default]
|
||||
Album,
|
||||
Ep,
|
||||
Single,
|
||||
Compilation,
|
||||
Remix,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{error::Result, Error};
|
||||
|
@ -260,12 +261,11 @@ impl SubtitleLines {
|
|||
|
||||
/// Convert subtitles into the [LRC](SubtitleFormat::Lrc) format
|
||||
pub fn to_lrc(&self) -> String {
|
||||
let mut t = self
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| format!("[{}] {}\n", line.time.format_lrc(), line.text))
|
||||
.collect::<String>();
|
||||
t.pop();
|
||||
let mut t = self.lines.iter().fold(String::new(), |mut acc, line| {
|
||||
_ = writeln!(acc, "[{}] {}", line.time.format_lrc(), line.text);
|
||||
acc
|
||||
});
|
||||
t.pop(); // trailing newline
|
||||
t
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use time::macros::{date, datetime};
|
|||
|
||||
use musixmatch_inofficial::{
|
||||
models::{AlbumId, ArtistId, TrackId},
|
||||
Error, Musixmatch,
|
||||
ClientType, Error, Musixmatch,
|
||||
};
|
||||
|
||||
#[ctor::ctor]
|
||||
|
@ -22,13 +22,20 @@ fn init() {
|
|||
}
|
||||
|
||||
fn new_mxm() -> Musixmatch {
|
||||
Musixmatch::builder()
|
||||
.credentials(
|
||||
std::env::var("MUSIXMATCH_EMAIL").unwrap(),
|
||||
std::env::var("MUSIXMATCH_PASSWORD").unwrap(),
|
||||
)
|
||||
.build()
|
||||
.unwrap()
|
||||
let client_type = std::env::var("MUSIXMATCH_CLIENT")
|
||||
.map(|ctype| serde_plain::from_str::<ClientType>(&ctype).expect("valid client type"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut mxm = Musixmatch::builder().client_type(client_type);
|
||||
|
||||
if let (Ok(email), Ok(password)) = (
|
||||
std::env::var("MUSIXMATCH_EMAIL"),
|
||||
std::env::var("MUSIXMATCH_PASSWORD"),
|
||||
) {
|
||||
mxm = mxm.credentials(email, password);
|
||||
}
|
||||
|
||||
mxm.build().unwrap()
|
||||
}
|
||||
|
||||
fn testfile<P: AsRef<Path>>(name: P) -> PathBuf {
|
||||
|
@ -101,10 +108,11 @@ mod album {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn album_tst() {
|
||||
async fn album_ep() {
|
||||
let album = new_mxm().album(AlbumId::AlbumId(23976123)).await.unwrap();
|
||||
println!("type: {:?}", album.album_release_type);
|
||||
println!("date: {:?}", album.album_release_date);
|
||||
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)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -407,7 +415,7 @@ mod track {
|
|||
assert!(track.lyrics_id.is_some());
|
||||
assert_eq!(track.subtitle_id.unwrap(), 36476905);
|
||||
assert_eq!(track.album_id, 41035954);
|
||||
assert_eq!(track.album_name, "Black Mamba - Single");
|
||||
assert_eq!(track.album_name, "Black Mamba");
|
||||
assert_eq!(track.artist_id, 46970441);
|
||||
assert_eq!(track.artist_name, "aespa");
|
||||
assert_eq!(
|
||||
|
@ -665,7 +673,7 @@ mod track {
|
|||
async fn genres() {
|
||||
let genres = new_mxm().genres().await.unwrap();
|
||||
assert!(genres.len() > 360);
|
||||
dbg!(&genres);
|
||||
// dbg!(&genres);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -992,7 +1000,11 @@ mod translation {
|
|||
|
||||
#[tokio::test]
|
||||
async fn no_credentials() {
|
||||
let mxm = Musixmatch::builder().no_storage().build().unwrap();
|
||||
let mxm = Musixmatch::builder()
|
||||
.client_type(ClientType::Android)
|
||||
.no_storage()
|
||||
.build()
|
||||
.unwrap();
|
||||
let err = mxm
|
||||
.track_lyrics(TrackId::TrackId(205688271))
|
||||
.await
|
||||
|
|
Loading…
Reference in a new issue