diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index f1d9970..feec70b 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -27,30 +27,26 @@ jobs: run: cargo clippy --all -- -D warnings - name: ๐Ÿงช Test - # run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 -j 1 --workspace - run: cargo test --workspace -- --nocapture --test-threads 1 + run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --workspace --nocapture env: - RUST_LOG: debug + RUST_LOG: info ALL_PROXY: "http://warpproxy:8124" - MUSIXMATCH_EMAIL: ${{ secrets.MUSIXMATCH_EMAIL }} - MUSIXMATCH_PASSWORD: ${{ secrets.MUSIXMATCH_PASSWORD }} - CAPMONSTER_KEY: ${{ secrets.CAPMONSTER_KEY }} - # - name: Move test report - # if: always() - # run: mv target/nextest/ci/junit.xml junit.xml || true + - name: Move test report + if: always() + run: mv target/nextest/ci/junit.xml junit.xml || true - # - name: ๐Ÿ’Œ Upload test report - # if: always() - # uses: https://code.forgejo.org/forgejo/upload-artifact@v4 - # with: - # name: test - # path: | - # junit.xml + - name: ๐Ÿ’Œ Upload test report + if: always() + uses: https://code.forgejo.org/forgejo/upload-artifact@v4 + with: + name: test + path: | + junit.xml - # - name: ๐Ÿ”— Artifactview PR comment - # if: ${{ always() && github.event_name == 'pull_request' }} - # run: | - # if [[ "$GITEA_ACTIONS" == "true" ]]; then RUN_NUMBER="$GITHUB_RUN_NUMBER"; else RUN_NUMBER="$GITHUB_RUN_ID"; fi - # curl -SsL --fail-with-body -w "\n" -X POST https://av.thetadev.de/.well-known/api/prComment -H "Content-Type: application/json" \ - # --data '{"url": "'"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_NUMBER"'", "pr": ${{ github.event.number }}, "artifact_titles": {"test":"๐Ÿงช Test report"}, "artifact_paths": {"test":"/junit.xml?viewer=1"}}' + - name: ๐Ÿ”— Artifactview PR comment + if: ${{ always() && github.event_name == 'pull_request' }} + run: | + if [[ "$GITEA_ACTIONS" == "true" ]]; then RUN_NUMBER="$GITHUB_RUN_NUMBER"; else RUN_NUMBER="$GITHUB_RUN_ID"; fi + curl -SsL --fail-with-body -w "\n" -X POST https://av.thetadev.de/.well-known/api/prComment -H "Content-Type: application/json" \ + --data '{"url": "'"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_NUMBER"'", "pr": ${{ github.event.number }}, "artifact_titles": {"test":"๐Ÿงช Test report"}, "artifact_paths": {"test":"/junit.xml?viewer=1"}}' diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index dd48fde..8d99fbf 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -24,7 +24,9 @@ jobs: } >> "$GITHUB_ENV" - name: ๐Ÿ“ค Publish crate on crates.io - run: cargo publish --token ${{ secrets.CARGO_TOKEN }} --package "${{ env.CRATE }}" + run: cargo publish --package "${{ env.CRATE }}" + env: + CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_TOKEN }}" - name: ๐ŸŽ‰ Publish release uses: https://gitea.com/actions/release-action@main diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f857c4..617c5aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. +## [v0.5.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.4.0..musixmatch-inofficial/v0.5.0) - 2026-06-11 + +### ๐Ÿš€ Features + +- Lock cache files before reading/writing - ([e188175](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/e188175f2ea849d4663235aa3f510759e2486ec4)) + +### ๐Ÿ› Bug Fixes + +- Creation of session file - ([a5810b9](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/a5810b91c1094b7fac3ac1abdf65dac2e696f47e)) +- Fix clippy lints - ([be82476](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/be8247666c27b6b6e841fd0e08355cd497f5d42a)) + +### โš™๏ธ Miscellaneous Tasks + +- Update dependencies - ([7383f15](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/7383f156626f7acc71e10ceb88e6a21ec43962a0)) +- Update edition to 2024 - ([266b282](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/266b282e04f5813564ad07b87bd965be73fc3f7f)) + + ## [v0.4.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-inofficial/v0.3.0..musixmatch-inofficial/v0.4.0) - 2025-12-19 ### ๐Ÿš€ Features diff --git a/Cargo.toml b/Cargo.toml index c2696a8..80dc7c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-inofficial" -version = "0.4.0" +version = "0.5.0" rust-version = "1.89.0" edition.workspace = true authors.workspace = true @@ -15,7 +15,7 @@ include = ["/src", "README.md", "CHANGELOG.md", "LICENSE"] members = [".", "cli"] [workspace.package] -edition = "2021" +edition = "2024" authors = ["ThetaDev "] license = "MIT" repository = "https://codeberg.org/ThetaDev/musixmatch-inofficial" @@ -23,7 +23,7 @@ keywords = ["music", "lyrics"] categories = ["api-bindings", "multimedia"] [workspace.dependencies] -musixmatch-inofficial = { version = "0.4.0", path = ".", default-features = false } +musixmatch-inofficial = { version = "0.5.0", path = ".", default-features = false } [features] default = ["default-tls"] diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 28bdae4..892af76 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. +## [v0.5.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.4.0..musixmatch-cli/v0.5.0) - 2026-06-12 + +### โš™๏ธ Miscellaneous Tasks + +- Update musixmatch to v0.5.0 +- Update reqwest to 0.13.0 (new TLS feature flags) +- Update edition to 2024 - ([266b282](https://codeberg.org/ThetaDev/musixmatch-inofficial/commit/266b282e04f5813564ad07b87bd965be73fc3f7f)) + + ## [v0.4.0](https://codeberg.org/ThetaDev/musixmatch-inofficial/compare/musixmatch-cli/v0.3.1..musixmatch-cli/v0.4.0) - 2025-12-19 ### ๐Ÿš€ Features diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e6cd6e1..2390b29 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "musixmatch-cli" -version = "0.4.0" +version = "0.5.0" rust-version = "1.89.0" edition.workspace = true authors.workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs index da4a38c..09361c8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,15 +2,15 @@ #![warn(missing_docs, clippy::todo)] use std::{ - io::{stdin, stdout, Write}, + io::{Write, stdin, stdout}, path::PathBuf, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{Result, anyhow, bail}; use clap::{Args, Parser, Subcommand}; use musixmatch_inofficial::{ - models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap}, Musixmatch, + models::{AlbumId, ArtistId, SubtitleFormat, Track, TrackId, TranslationMap}, }; #[derive(Parser)] @@ -233,31 +233,31 @@ async fn run(cli: Cli) -> Result<()> { let mut lyrics_body = lyrics.lyrics_body; - if let Some(lang) = lang { - if Some(&lang) != lyrics.lyrics_language.as_ref() { - let tl = mxm.track_lyrics_translation(track_id, &lang).await?; - if tl.is_empty() { - eprintln!("Translation not found. Returning lyrics in original language."); + if let Some(lang) = lang + && Some(&lang) != lyrics.lyrics_language.as_ref() + { + let tl = mxm.track_lyrics_translation(track_id, &lang).await?; + if tl.is_empty() { + eprintln!("Translation not found. Returning lyrics in original language."); + } else { + eprintln!("Translated to: {}", tl.lang); + let tm = TranslationMap::from(tl); + let translated = tm.translate_lyrics(&lyrics_body); + lyrics_body = if bi { + lyrics_body + .lines() + .zip(translated.lines()) + .map(|(a, b)| { + if a == b { + a.to_string() + "\n" + } else { + format!("{a}\n> {b}\n") + } + }) + .collect() } else { - eprintln!("Translated to: {}", tl.lang); - let tm = TranslationMap::from(tl); - let translated = tm.translate_lyrics(&lyrics_body); - lyrics_body = if bi { - lyrics_body - .lines() - .zip(translated.lines()) - .map(|(a, b)| { - if a == b { - a.to_string() + "\n" - } else { - format!("{a}\n> {b}\n") - } - }) - .collect() - } else { - translated - }; - } + translated + }; } } diff --git a/src/api_model.rs b/src/api_model.rs index d69aaa8..4c9fd5b 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -1,8 +1,8 @@ use std::{marker::PhantomData, str::FromStr}; use serde::{ - de::{DeserializeOwned, Visitor}, Deserialize, Deserializer, Serialize, + de::{DeserializeOwned, Visitor}, }; use time::OffsetDateTime; @@ -416,9 +416,9 @@ where pub mod optional_date { use super::*; - use serde::ser::Error as _; use serde::Serializer; - use time::{macros::format_description, Date}; + use serde::ser::Error as _; + use time::{Date, macros::format_description}; const DATE_FORMAT: &[time::format_description::FormatItem] = format_description!("[year]-[month]-[day]"); diff --git a/src/apis/album_api.rs b/src/apis/album_api.rs index 88c175a..1ee4d8b 100644 --- a/src/apis/album_api.rs +++ b/src/apis/album_api.rs @@ -1,7 +1,7 @@ +use crate::Musixmatch; use crate::error::Result; use crate::models::album::{Album, AlbumBody, AlbumListBody}; use crate::models::{AlbumId, ArtistId, SortOrder}; -use crate::Musixmatch; impl Musixmatch { /// Get the metadata for an album specified by its ID. diff --git a/src/apis/artist_api.rs b/src/apis/artist_api.rs index 664c603..7792d7d 100644 --- a/src/apis/artist_api.rs +++ b/src/apis/artist_api.rs @@ -1,7 +1,7 @@ -use crate::error::Result; -use crate::models::artist::{Artist, ArtistBody, ArtistListBody}; -use crate::models::ArtistId; use crate::Musixmatch; +use crate::error::Result; +use crate::models::ArtistId; +use crate::models::artist::{Artist, ArtistBody, ArtistListBody}; impl Musixmatch { /// Get the metadata for an artist specified by its ID. diff --git a/src/apis/lyrics_api.rs b/src/apis/lyrics_api.rs index f1e0cc2..4200a7e 100644 --- a/src/apis/lyrics_api.rs +++ b/src/apis/lyrics_api.rs @@ -1,8 +1,8 @@ +use crate::Musixmatch; use crate::error::Result; +use crate::models::TrackId; use crate::models::lyrics::{Lyrics, LyricsBody}; use crate::models::translation::{TranslationList, TranslationListBody}; -use crate::models::TrackId; -use crate::Musixmatch; impl Musixmatch { /// Get the lyrics for a track specified by its name and artist. diff --git a/src/apis/richsync_api.rs b/src/apis/richsync_api.rs index 2d726e3..6492336 100644 --- a/src/apis/richsync_api.rs +++ b/src/apis/richsync_api.rs @@ -1,7 +1,7 @@ -use crate::error::Result; -use crate::models::richsync::{RichsyncBody, RichsyncLyrics}; -use crate::models::TrackId; use crate::Musixmatch; +use crate::error::Result; +use crate::models::TrackId; +use crate::models::richsync::{RichsyncBody, RichsyncLyrics}; impl Musixmatch { /// Get the richsync (word-by-word synchronized lyrics) for a track specified by its ID. diff --git a/src/apis/snippet_api.rs b/src/apis/snippet_api.rs index cf0fdfd..dbacd4e 100644 --- a/src/apis/snippet_api.rs +++ b/src/apis/snippet_api.rs @@ -1,7 +1,7 @@ -use crate::error::Result; -use crate::models::snippet::{Snippet, SnippetBody}; -use crate::models::TrackId; use crate::Musixmatch; +use crate::error::Result; +use crate::models::TrackId; +use crate::models::snippet::{Snippet, SnippetBody}; impl Musixmatch { /// Get a lyrics snippet for a track specified by its ID. diff --git a/src/apis/subtitle_api.rs b/src/apis/subtitle_api.rs index 656e0c2..a6b9ffb 100644 --- a/src/apis/subtitle_api.rs +++ b/src/apis/subtitle_api.rs @@ -1,7 +1,7 @@ -use crate::error::Result; -use crate::models::subtitle::{Subtitle, SubtitleBody, SubtitleFormat}; -use crate::models::TrackId; use crate::Musixmatch; +use crate::error::Result; +use crate::models::TrackId; +use crate::models::subtitle::{Subtitle, SubtitleBody, SubtitleFormat}; impl Musixmatch { /// Get the subtitles (synchronized lyrics) for a track specified by its name and artist. diff --git a/src/apis/track_api.rs b/src/apis/track_api.rs index 7ab24b3..6bb8acd 100644 --- a/src/apis/track_api.rs +++ b/src/apis/track_api.rs @@ -1,9 +1,9 @@ use time::Date; +use crate::Musixmatch; use crate::error::Result; use crate::models::track::{Track, TrackBody, TrackListBody}; use crate::models::{AlbumId, ChartName, Genre, Genres, SortOrder, TrackId}; -use crate::Musixmatch; impl Musixmatch { /// Get the metadata for a track specified by its name, artist and album. @@ -403,21 +403,17 @@ impl<'a> TrackSearchQuery<'a> { } if let Some(f_track_release_group_first_release_date_min) = self.f_track_release_group_first_release_date_min - { - if let Ok(date) = + && let Ok(date) = f_track_release_group_first_release_date_min.format(crate::YMD_FORMAT) - { - url_query.append_pair("f_track_release_group_first_release_date_min", &date); - } + { + url_query.append_pair("f_track_release_group_first_release_date_min", &date); } if let Some(f_track_release_group_first_release_date_max) = self.f_track_release_group_first_release_date_max - { - if let Ok(date) = + && let Ok(date) = f_track_release_group_first_release_date_max.format(crate::YMD_FORMAT) - { - url_query.append_pair("f_track_release_group_first_release_date_max", &date); - } + { + url_query.append_pair("f_track_release_group_first_release_date_max", &date); } if let Some(s_artist_rating) = &self.s_artist_rating { url_query.append_pair("s_artist_rating", s_artist_rating.as_str()); diff --git a/src/captcha.rs b/src/captcha.rs deleted file mode 100644 index 6e098e4..0000000 --- a/src/captcha.rs +++ /dev/null @@ -1,213 +0,0 @@ -use std::time::Duration; - -use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; - -use crate::api_model::{BodyMsg, Resp}; -use crate::error::Result; -use crate::{Captcha, Error, Musixmatch}; - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct CreateTaskRequest<'a> { - client_key: &'a str, - task: CaptchaTask<'a>, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CreateTaskResponse { - error_id: u8, - task_id: u32, - error_code: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct CaptchaTask<'a> { - #[serde(rename = "type")] - ttype: &'a str, - website_url: &'a str, - website_key: &'a str, - min_score: f32, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct TaskResultRequest<'a> { - client_key: &'a str, - task_id: u32, -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -enum TaskStatus { - Processing, - Ready, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TaskResultResponse { - error_id: u8, - status: TaskStatus, - error_code: Option, - solution: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CaptchaSolution { - g_recaptcha_response: String, -} - -#[derive(Serialize)] -struct PostCaptchaRequest<'a> { - response: &'a str, -} - -#[derive(Debug, Deserialize)] -struct PostCaptchaResponse { - success: bool, - #[serde(default, rename = "error-codes")] - error_codes: Vec, - captcha_id: Option, -} - -impl Musixmatch { - async fn solve_captcha(&self) -> Result { - let ck = self - .inner - .capmonster_key - .as_deref() - .ok_or(Error::Capmonster("no API key set".into()))?; - - let req = CreateTaskRequest { - client_key: ck, - task: CaptchaTask { - ttype: "RecaptchaV2Task", - website_url: "https://apic.musixmatch.com/captcha.html?callback_url=mxm://captcha", - website_key: "6LcJjQoTAAAAAKd6014UF3baDSR0rkTthD1Dev1j", - min_score: 0.8, - }, - }; - let resp = self - .inner - .http - .post("https://api.capmonster.cloud/createTask") - .json(&req) - .send() - .await?; - let created_task = resp.json::().await?; - if created_task.error_id != 0 { - return Err(Error::Capmonster( - created_task.error_code.unwrap_or_default().into(), - )); - } - let task_id = created_task.task_id; - - for i in 1..=60 { - tokio::time::sleep(Duration::from_secs(2)).await; - - let get_req = TaskResultRequest { - client_key: ck, - task_id, - }; - let resp = self - .inner - .http - .post("https://api.capmonster.cloud/getTaskResult") - .json(&get_req) - .send() - .await?; - let task_res = resp.json::().await?; - - if task_res.error_id == 0 { - if task_res.status == TaskStatus::Ready { - if let Some(solution) = task_res.solution { - log::debug!("got captcha solution; task {task_id}"); - return Ok(solution.g_recaptcha_response); - } - } else { - log::debug!("waiting for captcha solution; task {task_id}; #{i}"); - } - } else { - return Err(Error::Capmonster( - task_res.error_code.unwrap_or_default().into(), - )); - } - } - - Err(Error::Capmonster("timeout".into())) - } - - async fn post_captcha_solution(&self, solution: &str) -> Result { - let req = PostCaptchaRequest { response: solution }; - let mut url = self.new_url("captcha.post"); - - if let Ok(usertoken) = self.inner.usertoken.try_read() { - if let Some(usertoken) = usertoken.as_ref() { - url.query_pairs_mut() - .append_pair("usertoken", usertoken) - .finish(); - } - } - Self::sign_url(&mut url); - let resp = self.inner.http.post(url).json(&req).send().await?; - let data = resp - .json::>>() - .await? - .message - .body; - if data.success { - if let Some(c) = data.captcha_id { - Ok(c) - } else { - Err(Error::InvalidCaptcha("no captcha_id".into())) - } - } else { - Err(Error::InvalidCaptcha(data.error_codes.join(";").into())) - } - } - - async fn _obtain_captcha_id_attempt(&self) -> Result { - let solution = self.solve_captcha().await?; - self.post_captcha_solution(&solution).await - } - - pub(crate) async fn obtain_captcha_id(&self) -> Result<()> { - // Lock the captcha ID here to prevent concurrent tasks from obtaining captchas - let mut captcha = self.inner.captcha.write().await; - - if captcha - .as_ref() - .is_some_and(|c| c.solved_at > OffsetDateTime::now_utc() - time::Duration::minutes(15)) - { - log::warn!("captcha already solved; still got ratelimit error"); - return Err(Error::Ratelimit); - } - - for att in 1..=3 { - log::info!("Getting recaptcha token; attempt {att}"); - - match self._obtain_captcha_id_attempt().await { - Ok(captcha_id) => { - *captcha = Some(Captcha { - captcha_id, - solved_at: OffsetDateTime::now_utc(), - }); - return Ok(()); - } - Err(Error::InvalidCaptcha(e)) => { - if e != "incorrect-captcha-sol" { - return Err(Error::InvalidCaptcha(e)); - } - } - Err(e) => return Err(e), - } - } - Err(Error::InvalidCaptcha( - "incorrect-captcha-sol; retried 3 times".into(), - )) - } -} diff --git a/src/error.rs b/src/error.rs index 34cb853..f58312b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,12 +35,6 @@ pub enum Error { /// Musixmatch returned no data or the data that could not be deserialized #[error("JSON parsing error: {0}")] InvalidData(Cow<'static, str>), - /// Error solving captcha - #[error("Capmonster error: {0}]")] - Capmonster(Cow<'static, str>), - /// Error submitting captcha solution - #[error("Invalid captcha solution: {0}")] - InvalidCaptcha(Cow<'static, str>), /// Error from the HTTP client #[error("http error: {0}")] Http(reqwest::Error), diff --git a/src/lib.rs b/src/lib.rs index 119b1c4..55aa489 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ mod api_model; mod apis; -mod captcha; mod error; pub mod models; pub mod storage; @@ -24,10 +23,10 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use sha1::Sha1; use storage::{FileStorage, SessionStorage}; +use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; use time::macros::format_description; -use time::OffsetDateTime; -use tokio::sync::RwLock as AsyncRwLock; +use tokio::sync::Mutex; use crate::api_model::parse_body; use crate::error::Result; @@ -62,7 +61,6 @@ pub struct MusixmatchBuilder { device: Option, storage: DefaultOpt>, credentials: Option, - capmonster_key: Option, } #[derive(Default)] @@ -89,9 +87,7 @@ struct MusixmatchRef { credentials: RwLock>, brand: String, device: String, - usertoken: AsyncRwLock>, - capmonster_key: Option, - captcha: AsyncRwLock>, + usertoken: Mutex>, } #[derive(Default, Clone)] @@ -100,17 +96,9 @@ struct Credentials { password: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Captcha { - captcha_id: String, - solved_at: OffsetDateTime, -} - #[derive(Debug, Serialize, Deserialize)] struct StoredSession { usertoken: String, - #[serde(skip_serializing_if = "Option::is_none")] - captcha: Option, } impl MusixmatchBuilder { @@ -190,14 +178,6 @@ impl MusixmatchBuilder { self } - /// Set the CapMonster API key for solving captchas - /// - /// - pub fn capmonster_key>(mut self, capmonster_key: S) -> Self { - self.capmonster_key = Some(capmonster_key.into()); - self - } - /// Returns a new, configured Musixmatch client pub fn build(self) -> Result { self.build_with_client(ClientBuilder::new()) @@ -217,10 +197,6 @@ impl MusixmatchBuilder { .default_headers(headers) .build()?; - let (usertoken, captcha) = stored_session - .map(|s| (Some(s.usertoken), s.captcha)) - .unwrap_or_default(); - Ok(Musixmatch { inner: MusixmatchRef { http, @@ -228,9 +204,7 @@ impl MusixmatchBuilder { credentials: RwLock::new(self.credentials), brand: self.brand.unwrap_or_else(|| DEFAULT_BRAND.to_owned()), device: self.device.unwrap_or_else(|| DEFAULT_DEVICE.to_owned()), - usertoken: AsyncRwLock::new(usertoken), - capmonster_key: self.capmonster_key, - captcha: AsyncRwLock::new(captcha), + usertoken: Mutex::new(stored_session.map(|s| s.usertoken)), } .into(), }) @@ -253,12 +227,10 @@ impl Musixmatch { async fn get_usertoken(&self, force_new: bool) -> Result { // Lock the session here to prevent concurrent tasks from obtaining sessions - let mut stored_usertoken = self.inner.usertoken.write().await; + let mut stored_usertoken = self.inner.usertoken.lock().await; - if !force_new { - if let Some(usertoken) = &mut *stored_usertoken { - return Ok(usertoken.clone()); - } + if !force_new && let Some(usertoken) = &mut *stored_usertoken { + return Ok(usertoken.clone()); } let credentials = { @@ -305,36 +277,17 @@ impl Musixmatch { } *stored_usertoken = Some(usertoken.to_owned()); - drop(stored_usertoken); - self.store_session().await; + self.store_session(&usertoken); Ok(usertoken) } - async fn _post_credentials_request_attempt( - &self, - url: &Url, - credentials: &api_model::Credentials<'_>, - ) -> Result { - let resp = self - .inner - .http - .post(url.clone()) - .json(credentials) - .send() - .await? - .error_for_status()?; - - let resp_txt = resp.text().await?; - parse_body::(&resp_txt) - } - async fn post_credentials( &self, usertoken: &str, credentials: &Credentials, ) -> Result { let mut url = new_url_from_token("credential.post", usertoken); - Self::sign_url(&mut url); + sign_url_with_date(&mut url, OffsetDateTime::now_utc()); let api_credentials = api_model::Credentials { credential_list: &[api_model::CredentialWrap { @@ -347,19 +300,17 @@ impl Musixmatch { }], }; - let login = match self - ._post_credentials_request_attempt(&url, &api_credentials) - .await - { - Ok(login) => login, - Err(Error::Ratelimit) => { - info!("ratelimit, attempting to solve captcha"); - self.obtain_captcha_id().await?; - self._post_credentials_request_attempt(&url, &api_credentials) - .await? - } - Err(e) => return Err(e), - }; + let resp = self + .inner + .http + .post(url) + .json(&api_credentials) + .send() + .await? + .error_for_status()?; + + let resp_txt = resp.text().await?; + let login = parse_body::(&resp_txt)?; let credential = login .0 .into_iter() @@ -382,18 +333,15 @@ impl Musixmatch { } } - async fn store_session(&self) { + fn store_session(&self, usertoken: &str) { if let Some(storage) = &self.inner.storage { - let usertoken = { self.inner.usertoken.read().await.to_owned() }; - if let Some(usertoken) = usertoken { - let captcha = { self.inner.captcha.read().await.to_owned() }; + let to_store = StoredSession { + usertoken: usertoken.to_owned(), + }; - let to_store = StoredSession { usertoken, captcha }; - - match serde_json::to_string(&to_store) { - Ok(json) => storage.write(&json), - Err(e) => error!("Could not serialize session. Error: {e}"), - } + match serde_json::to_string(&to_store) { + Ok(json) => storage.write(&json), + Err(e) => error!("Could not serialize session. Error: {e}"), } } } @@ -430,35 +378,22 @@ impl Musixmatch { .unwrap() } - fn sign_url(url: &mut Url) { - sign_url_with_date(url, OffsetDateTime::now_utc()); - } - async fn finish_url(&self, url: &mut Url, force_new_session: bool) -> Result<()> { - { - let mut query = url.query_pairs_mut(); - let usertoken = self.get_usertoken(force_new_session).await?; - query.append_pair("usertoken", &usertoken); + let usertoken = self.get_usertoken(force_new_session).await?; + url.query_pairs_mut() + .append_pair("usertoken", &usertoken) + .finish(); - let captcha = self.inner.captcha.read().await; - if let Some(captcha) = captcha.as_ref() { - if captcha.solved_at > OffsetDateTime::now_utc() - time::Duration::days(1) { - query.append_pair("captcha_id", &captcha.captcha_id); - } - } - query.finish(); - } - - Self::sign_url(url); + sign_url_with_date(url, OffsetDateTime::now_utc()); Ok(()) } - async fn _get_request_attempt(&self, url: &Url, force_new_session: bool) -> Result + async fn execute_get_request(&self, url: &Url) -> Result where T: DeserializeOwned, { let mut req_url = url.clone(); - self.finish_url(&mut req_url, force_new_session).await?; + self.finish_url(&mut req_url, false).await?; let resp = self .inner @@ -469,25 +404,27 @@ impl Musixmatch { .error_for_status()?; let resp_txt = resp.text().await?; - parse_body(&resp_txt) - } - - async fn execute_get_request(&self, url: &Url) -> Result - where - T: DeserializeOwned, - { - match self._get_request_attempt(url, false).await { - Ok(resp) => return Ok(resp), + match parse_body(&resp_txt) { + Ok(body) => Ok(body), Err(Error::TokenExpired) => { info!("Usertoken expired, getting a new one"); + + let mut req_url = url.clone(); + self.finish_url(&mut req_url, true).await?; + + let resp = self + .inner + .http + .get(req_url) + .send() + .await? + .error_for_status()?; + + let resp_txt = resp.text().await?; + parse_body(&resp_txt) } - Err(Error::Ratelimit) => { - info!("ratelimit, attempting to solve captcha"); - self.obtain_captcha_id().await?; - } - Err(e) => return Err(e), + Err(e) => Err(e), } - self._get_request_attempt(url, true).await } /// Ensure the client has a login token @@ -564,6 +501,9 @@ mod tests { fn t_sign_url() { 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)); - 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=78ywxkeXlazpevI%2BbD8E3YluLPc%3D%0A&signature_protocol=sha1") + 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=78ywxkeXlazpevI%2BbD8E3YluLPc%3D%0A&signature_protocol=sha1" + ) } } diff --git a/src/models/id.rs b/src/models/id.rs index c91fe5c..1503a6d 100644 --- a/src/models/id.rs +++ b/src/models/id.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, convert::Infallible, fmt::Write, str::FromStr}; -use serde::{de::Visitor, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::Visitor}; use crate::IdError; diff --git a/src/models/lyrics.rs b/src/models/lyrics.rs index 0070249..33a0652 100644 --- a/src/models/lyrics.rs +++ b/src/models/lyrics.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use crate::{error::Result, Error}; +use crate::{Error, error::Result}; #[derive(Debug, Deserialize)] pub(crate) struct LyricsBody { diff --git a/src/models/subtitle.rs b/src/models/subtitle.rs index a79fee3..479bc11 100644 --- a/src/models/subtitle.rs +++ b/src/models/subtitle.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Write; use time::OffsetDateTime; -use crate::{error::Result, Error}; +use crate::{Error, error::Result}; #[derive(Debug, Deserialize)] pub(crate) struct SubtitleBody { diff --git a/src/models/translation.rs b/src/models/translation.rs index 8b249e9..46d2d27 100644 --- a/src/models/translation.rs +++ b/src/models/translation.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use super::{subtitle::SubtitleLines, SubtitleLine}; +use super::{SubtitleLine, subtitle::SubtitleLines}; #[derive(Debug, Deserialize)] pub(crate) struct TranslationListBody { diff --git a/src/storage.rs b/src/storage.rs index b4c5815..2541add 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -55,12 +55,14 @@ fn read_file(path: &Path) -> Result { } fn write_file(path: &Path, data: &str) -> Result<(), std::io::Error> { + // Open file in append mode to not immediately truncate it let mut f = File::options() .create(true) .read(true) .append(true) .open(path)?; f.lock()?; + // Truncate file after making sure no one is reading/writing to it f.set_len(0)?; f.write_all(data.as_bytes())?; Ok(()) diff --git a/tests/tests.rs b/tests/tests.rs index b4049ff..a145fd1 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,15 +1,12 @@ -use std::{ - path::{Path, PathBuf}, - time::Duration, -}; +use std::path::{Path, PathBuf}; use path_macro::path; use rstest::{fixture, rstest}; use time::macros::{date, datetime}; use musixmatch_inofficial::{ - models::{AlbumId, ArtistId, TrackId}, Error, Musixmatch, + models::{AlbumId, ArtistId, TrackId}, }; fn testfile>(name: P) -> PathBuf { @@ -18,20 +15,8 @@ fn testfile>(name: P) -> PathBuf { #[fixture] async fn mxm() -> Musixmatch { - static SETUP_LOCK: std::sync::OnceLock<()> = std::sync::OnceLock::new(); static LOGIN_LOCK: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new(); - SETUP_LOCK.get_or_init(|| { - _ = dotenvy::dotenv(); - }); - - // let d = if std::env::var("CI").is_ok() { - // 2000 - // } else { - // 500 - // }; - // tokio::time::sleep(Duration::from_millis(d)).await; - let mut mxm = Musixmatch::builder(); if let (Ok(email), Ok(password)) = ( @@ -41,11 +26,8 @@ async fn mxm() -> Musixmatch { mxm = mxm.credentials(email, password); } - if let Ok(capmonster_key) = std::env::var("CAPMONSTER_KEY") { - mxm = mxm.capmonster_key(capmonster_key); - } - let mxm = mxm.build().unwrap(); + LOGIN_LOCK.get_or_try_init(|| mxm.login()).await.unwrap(); mxm } @@ -751,7 +733,7 @@ mod track { async fn genres(#[future] mxm: Musixmatch) { let genres = mxm.await.genres().await.unwrap(); assert!(genres.len() > 360); - dbg!(&genres); + // dbg!(&genres); } #[rstest] @@ -918,8 +900,9 @@ mod lyrics { ) .await .unwrap(); + assert_eq!(album.len(), 13); - let x = stream::iter(album) + let lyrics = stream::iter(album) .map(|track| { mxm.track_lyrics(musixmatch_inofficial::models::TrackId::TrackId( track.track_id, @@ -931,8 +914,7 @@ mod lyrics { .into_iter() .map(Result::unwrap) .collect::>(); - - dbg!(x); + assert_eq!(lyrics.len(), 13); } }