From 2debaa5363f9ed8e6ba83724d8f430dfaff18ef5 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:19:16 +0700 Subject: [PATCH] refactor(providers): remove the unused Deezer catalogue search DeezerClient::search_all was reachable only through Backend::search_deezer, which had no production caller: no UniFFI export, platform channel or extension API wraps it, and no built-in Deezer catalogue search is planned. Remove the chain together with the items that existed only for it: the core search result types, the Deezer search page types and their API error wrappers, the cached Search value, and the latency test that exercised the search fan-out. The Search cache bucket stays because the coalesced Deezer lookups use it. The shared error context helper moves into the deezer module now that search.rs is gone. searchDeezerByISRC is a separate, live method and is untouched. --- .../crates/core/src/metadata/catalog.rs | 35 +--- .../core/src/metadata/deezer/catalog.rs | 93 +-------- .../src/backend/download_latency_tests.rs | 104 ---------- .../crates/extensions/src/backend/metadata.rs | 17 -- rust_backend/crates/providers/src/deezer.rs | 8 +- .../crates/providers/src/deezer/cache.rs | 3 +- .../crates/providers/src/deezer/extended.rs | 2 +- .../crates/providers/src/deezer/search.rs | 182 ------------------ 8 files changed, 12 insertions(+), 432 deletions(-) delete mode 100644 rust_backend/crates/providers/src/deezer/search.rs diff --git a/rust_backend/crates/core/src/metadata/catalog.rs b/rust_backend/crates/core/src/metadata/catalog.rs index 7afec51d..b7209852 100644 --- a/rust_backend/crates/core/src/metadata/catalog.rs +++ b/rust_backend/crates/core/src/metadata/catalog.rs @@ -1,4 +1,4 @@ -use super::{TrackMetadata, zero}; +use super::zero; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -121,39 +121,6 @@ pub struct ArtistResponsePayload { pub albums: Vec, } -pub type SearchArtistResult = ArtistInfoMetadata; - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct SearchAlbumResult { - pub id: String, - pub name: String, - pub artists: String, - pub images: String, - pub release_date: String, - pub total_tracks: isize, - pub album_type: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct SearchPlaylistResult { - pub id: String, - pub name: String, - pub owner: String, - pub images: String, - pub total_tracks: isize, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct SearchAllResult { - pub tracks: Vec, - pub artists: Vec, - pub albums: Vec, - pub playlists: Vec, -} - // Go's internal extended-metadata response has exported, untagged fields. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "PascalCase")] diff --git a/rust_backend/crates/core/src/metadata/deezer/catalog.rs b/rust_backend/crates/core/src/metadata/deezer/catalog.rs index 7ee0de52..6213986e 100644 --- a/rust_backend/crates/core/src/metadata/deezer/catalog.rs +++ b/rust_backend/crates/core/src/metadata/deezer/catalog.rs @@ -1,48 +1,5 @@ use super::{Artist, Track, Tracks, best_image}; -use crate::lyrics::json::{Update, go_deserialize}; -use serde::Deserialize; - -#[derive(Clone, Debug, Default)] -pub struct ApiError { - pub error_type: String, - pub message: String, - pub code: isize, -} -go_deserialize!(ApiError { "type" => error_type, "message" => message, "code" => code, }); - -/// A repeated non-null Go pointer field merges into the existing object. -#[derive(Clone, Debug, Default)] -pub struct OptionalApiError(pub Option); - -impl<'de> Update<'de> for OptionalApiError { - fn update>(&mut self, decoder: D) -> Result<(), D::Error> { - match Option::<&serde_json::value::RawValue>::deserialize(decoder)? { - None => self.0 = None, - Some(raw) => { - self.0 - .get_or_insert_default() - .update(&mut serde_json::Deserializer::from_str(raw.get())) - .map_err(serde::de::Error::custom)?; - } - } - Ok(()) - } -} - -macro_rules! search_page { - ($name:ident, $item:ty) => { - #[derive(Clone, Debug, Default)] - pub struct $name { - pub data: Option>, - pub error: OptionalApiError, - } - go_deserialize!($name { "data" => data, "error" => error, }); - }; -} -search_page!(TrackSearch, Track); -search_page!(ArtistSearch, Artist); -search_page!(AlbumSearch, SearchAlbum); -search_page!(PlaylistSearch, SearchPlaylist); +use crate::lyrics::json::go_deserialize; #[derive(Clone, Debug, Default)] pub struct TrackPage { @@ -142,25 +99,6 @@ go_deserialize!(FullPlaylist { "creator" => creator, "tracks" => tracks, }); -#[derive(Clone, Debug, Default)] -pub struct SearchAlbum { - pub id: i64, - pub title: String, - pub cover: String, - pub cover_medium: String, - pub cover_big: String, - pub cover_xl: String, - pub nb_tracks: isize, - pub release_date: String, - pub record_type: String, - pub artist: Artist, -} -go_deserialize!(SearchAlbum { - "id" => id, "title" => title, "cover" => cover, "cover_medium" => cover_medium, - "cover_big" => cover_big, "cover_xl" => cover_xl, "nb_tracks" => nb_tracks, - "release_date" => release_date, "record_type" => record_type, "artist" => artist, -}); - #[derive(Clone, Debug, Default)] pub struct ArtistAlbum { pub id: i64, @@ -191,22 +129,6 @@ pub struct AlbumTrackCount { } go_deserialize!(AlbumTrackCount { "nb_tracks" => nb_tracks, }); -#[derive(Clone, Debug, Default)] -pub struct SearchPlaylist { - pub id: i64, - pub title: String, - pub picture: String, - pub picture_medium: String, - pub picture_big: String, - pub picture_xl: String, - pub nb_tracks: isize, - pub user: Name, -} -go_deserialize!(SearchPlaylist { - "id" => id, "title" => title, "picture" => picture, "picture_medium" => picture_medium, - "picture_big" => picture_big, "picture_xl" => picture_xl, "nb_tracks" => nb_tracks, "user" => user, -}); - macro_rules! cover_image { ($($name:ty),*) => {$( impl $name { @@ -216,7 +138,7 @@ macro_rules! cover_image { } )*}; } -cover_image!(FullAlbum, SearchAlbum, ArtistAlbum); +cover_image!(FullAlbum, ArtistAlbum); impl FullArtist { pub fn image(&self) -> String { @@ -228,14 +150,3 @@ impl FullArtist { ]) } } - -impl SearchPlaylist { - pub fn image(&self) -> String { - best_image([ - self.picture_xl.as_str(), - &self.picture_big, - &self.picture_medium, - &self.picture, - ]) - } -} diff --git a/rust_backend/crates/extensions/src/backend/download_latency_tests.rs b/rust_backend/crates/extensions/src/backend/download_latency_tests.rs index c9fc74b1..893f428a 100644 --- a/rust_backend/crates/extensions/src/backend/download_latency_tests.rs +++ b/rust_backend/crates/extensions/src/backend/download_latency_tests.rs @@ -391,110 +391,6 @@ impl Drop for GatedServer { } } -#[test] -fn catalog_search_gates_required_tracks_then_overlaps_optional_categories() { - for mode in ["all", "artist", "track_failure"] { - let (network, config) = network(); - let track = if mode == "track_failure" { - r#"{"error":{"code":100,"message":"required track failure"}}"# - } else { - r#"{"data":[{"id":1,"title":"Track"}]}"# - }; - let artist = if mode == "artist" { - r#"{"data":[{"id":2,"name":"Artist"}]}"# - } else { - r#"{"error":{"code":100,"message":"optional artist failure"}}"# - }; - let mut server = GatedServer::new( - config, - &[ - ("/2.0/search/track", 200, track), - ("/2.0/search/artist", 200, artist), - ( - "/2.0/search/album", - 200, - r#"{"data":[{"id":3,"title":"Album"}]}"#, - ), - ( - "/2.0/search/playlist", - 200, - r#"{"data":[{"id":4,"title":"Playlist"}]}"#, - ), - ], - ); - let directory = tempfile::tempdir().unwrap(); - let backend = backend( - directory.path(), - server.url.clone(), - server.url.clone(), - network, - ); - let active = backend.clone(); - let worker = thread::spawn(move || { - active.search_deezer( - "needle", - 2, - 2, - if mode == "artist" { "artist" } else { "" }, - &|| Ok(()), - ) - }); - let (first, release) = server.next(); - assert_eq!( - first, - if mode == "artist" { - "/2.0/search/artist" - } else { - "/2.0/search/track" - } - ); - assert!(matches!( - server.started.recv_timeout(Duration::from_millis(100)), - Err(mpsc::RecvTimeoutError::Timeout) - )); - release.send(()).unwrap(); - if mode == "all" { - let mut pending = Vec::new(); - for _ in 0..3 { - pending.push(server.next()); - } - let mut categories: Vec<_> = pending.iter().map(|(path, _)| path.as_str()).collect(); - categories.sort(); - assert_eq!( - categories, - [ - "/2.0/search/album", - "/2.0/search/artist", - "/2.0/search/playlist" - ] - ); - for (_, release) in pending { - release.send(()).unwrap(); - } - } - let result = worker.join().unwrap(); - if mode == "track_failure" { - assert!(result.unwrap_err().contains("required track failure")); - } else { - let value: Value = serde_json::from_str(&result.unwrap()).unwrap(); - if mode == "all" { - assert_eq!(value["tracks"][0]["spotify_id"], "deezer:1"); - assert_eq!(value["artists"], json!([])); - assert_eq!(value["albums"][0]["id"], "deezer:3"); - assert_eq!(value["playlists"][0]["id"], "deezer:4"); - } else { - assert_eq!(value["artists"][0]["id"], "deezer:2"); - assert_eq!(value["tracks"], json!([])); - assert_eq!(value["albums"], json!([])); - assert_eq!(value["playlists"], json!([])); - } - } - let paths = server.finish(); - assert_eq!(paths.len(), if mode == "all" { 4 } else { 1 }); - backend.shutdown(); - } -} - #[test] fn reenrich_cover_and_lyrics_overlap_preserve_best_effort_results_and_cancel() { for mode in [ diff --git a/rust_backend/crates/extensions/src/backend/metadata.rs b/rust_backend/crates/extensions/src/backend/metadata.rs index d1f97a1a..fc5349d7 100644 --- a/rust_backend/crates/extensions/src/backend/metadata.rs +++ b/rust_backend/crates/extensions/src/backend/metadata.rs @@ -108,23 +108,6 @@ impl Backend { Ok(()) } - pub fn search_deezer( - &self, - query: &str, - track_limit: isize, - artist_limit: isize, - filter: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - encode( - self.deezer - .search_all(query, track_limit, artist_limit, filter, check)? - .as_ref(), - ) - }) - } - pub(super) fn deezer_metadata( &self, kind: &str, diff --git a/rust_backend/crates/providers/src/deezer.rs b/rust_backend/crates/providers/src/deezer.rs index bdf32ebb..7c28ab1a 100644 --- a/rust_backend/crates/providers/src/deezer.rs +++ b/rust_backend/crates/providers/src/deezer.rs @@ -3,7 +3,6 @@ mod browse; mod cache; mod extended; -mod search; use crate::resolver::{Check, ResolverError, http::ResolverHttp}; use serde::de::DeserializeOwned; @@ -179,6 +178,13 @@ pub fn retry_delay(value: &str, now: SystemTime) -> Duration { .min(Duration::from_secs(120)) } +fn context(error: ResolverError, message: &str) -> ResolverError { + match error { + ResolverError::Cancelled(_) | ResolverError::Closed | ResolverError::Busy => error, + _ => ResolverError::Failed(format!("{message}: {error}")), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust_backend/crates/providers/src/deezer/cache.rs b/rust_backend/crates/providers/src/deezer/cache.rs index 8b1d9553..ad3269e5 100644 --- a/rust_backend/crates/providers/src/deezer/cache.rs +++ b/rust_backend/crates/providers/src/deezer/cache.rs @@ -1,6 +1,6 @@ use super::{Check, DeezerClient, ResolverError}; use spotiflac_core::metadata::{ - AlbumExtendedMetadata, AlbumResponsePayload, ArtistResponsePayload, SearchAllResult, + AlbumExtendedMetadata, AlbumResponsePayload, ArtistResponsePayload, }; use std::collections::BTreeMap; use std::panic::{AssertUnwindSafe, catch_unwind}; @@ -9,7 +9,6 @@ use std::time::{Duration, Instant}; #[derive(Clone)] pub(super) enum Value { - Search(Arc), Album(Arc), Artist(Arc), Extended(Arc), diff --git a/rust_backend/crates/providers/src/deezer/extended.rs b/rust_backend/crates/providers/src/deezer/extended.rs index 8b226dc3..2fafe6d3 100644 --- a/rust_backend/crates/providers/src/deezer/extended.rs +++ b/rust_backend/crates/providers/src/deezer/extended.rs @@ -1,4 +1,4 @@ -use super::{Check, DeezerClient, MetadataLookup, ResolverError, cache::Value, search::context}; +use super::{Check, DeezerClient, MetadataLookup, ResolverError, cache::Value, context}; use spotiflac_core::metadata::{ AlbumExtendedMetadata, TrackMetadata, deezer::{FullAlbum, Track}, diff --git a/rust_backend/crates/providers/src/deezer/search.rs b/rust_backend/crates/providers/src/deezer/search.rs deleted file mode 100644 index 19a6bc0c..00000000 --- a/rust_backend/crates/providers/src/deezer/search.rs +++ /dev/null @@ -1,182 +0,0 @@ -use super::{ - Check, DeezerClient, ResolverError, - cache::{Bucket, Value}, -}; -use spotiflac_core::metadata::{ - SearchAlbumResult, SearchAllResult, SearchArtistResult, SearchPlaylistResult, - deezer::{self, AlbumSearch, ArtistSearch, PlaylistSearch, TrackSearch}, -}; -use spotiflac_network::query; -use std::sync::Arc; - -impl DeezerClient { - pub fn search_all( - &self, - query: &str, - track_limit: isize, - artist_limit: isize, - filter: &str, - check: &Check<'_>, - ) -> Result, ResolverError> { - check().map_err(ResolverError::Cancelled)?; - let (tracks, artists, albums, playlists) = match filter { - "track" => (50, 0, 0, 0), - "artist" => (0, 20, 0, 0), - "album" => (0, 0, 20, 0), - "playlist" => (0, 0, 0, 20), - _ => (track_limit, artist_limit, 5, 5), - }; - if tracks < 0 || artists < 0 { - return Err(ResolverError::Failed("negative search limit".into())); - } - let key = format!("deezer:all:{query}:{tracks}:{artists}:{albums}:{playlists}:{filter}"); - if let Some(Value::Search(result)) = self.cached(Bucket::Search, &key) { - return Ok(result); - } - let mut parameters = query::Query::new(); - query::set(&mut parameters, "q", query); - let encoded = query::encode(¶meters); - let endpoint = |kind: &str, limit: isize| { - format!("https://api.deezer.com/2.0/search/{kind}?{encoded}&limit={limit}") - }; - let mut result = SearchAllResult::default(); - if tracks > 0 { - let response: TrackSearch = self - .get_json(&endpoint("track", tracks), check) - .map_err(|error| context(error, "deezer track search failed"))?; - if let Some(error) = response.error.0 { - return Err(ResolverError::Failed(format!( - "deezer API error: {} (code {})", - error.message, error.code - ))); - } - result.tracks = response - .data - .unwrap_or_default() - .iter() - .map(|track| track.metadata()) - .collect(); - } - let fetch_artists = || { - (artists > 0) - .then(|| self.get_json::(&endpoint("artist", artists), check)) - }; - let fetch_albums = || { - (albums > 0).then(|| self.get_json::(&endpoint("album", albums), check)) - }; - let fetch_playlists = || { - (playlists > 0) - .then(|| self.get_json::(&endpoint("playlist", playlists), check)) - }; - let parallel = [artists, albums, playlists] - .into_iter() - .filter(|n| *n > 0) - .count() - > 1; - let (artist_response, album_response, playlist_response) = std::thread::scope(|scope| { - let artist = (parallel && artists > 0).then(|| { - std::thread::Builder::new() - .name("search-artists".into()) - .spawn_scoped(scope, fetch_artists) - }); - let album = (parallel && albums > 0).then(|| { - std::thread::Builder::new() - .name("search-albums".into()) - .spawn_scoped(scope, fetch_albums) - }); - let playlist = fetch_playlists(); - let artist = match artist { - Some(Ok(worker)) => worker - .join() - .map_err(|_| ResolverError::Failed("artist search panicked".into()))?, - _ => fetch_artists(), - }; - let album = match album { - Some(Ok(worker)) => worker - .join() - .map_err(|_| ResolverError::Failed("album search panicked".into()))?, - _ => fetch_albums(), - }; - Ok::<_, ResolverError>((artist, album, playlist)) - })?; - if let Some(response) = artist_response { - match response { - Ok(response) if response.error.0.is_none() => { - result.artists = response - .data - .unwrap_or_default() - .iter() - .map(|artist| SearchArtistResult { - id: format!("deezer:{}", artist.id), - name: artist.name.clone(), - images: artist.image(), - followers: artist.nb_fan, - popularity: 0, - }) - .collect(); - } - Err(error @ (ResolverError::Cancelled(_) | ResolverError::Closed)) => { - return Err(error); - } - _ => {} - } - } - if let Some(response) = album_response { - match response { - Ok(response) if response.error.0.is_none() => { - result.albums = response - .data - .unwrap_or_default() - .iter() - .map(|album| SearchAlbumResult { - id: format!("deezer:{}", album.id), - name: album.title.clone(), - artists: album.artist.name.clone(), - images: album.image(), - release_date: album.release_date.clone(), - total_tracks: album.nb_tracks, - album_type: deezer::album_type(&album.record_type), - }) - .collect(); - } - Err(error @ (ResolverError::Cancelled(_) | ResolverError::Closed)) => { - return Err(error); - } - _ => {} - } - } - if let Some(response) = playlist_response { - match response { - Ok(response) if response.error.0.is_none() => { - result.playlists = response - .data - .unwrap_or_default() - .iter() - .map(|playlist| SearchPlaylistResult { - id: format!("deezer:{}", playlist.id), - name: playlist.title.clone(), - owner: playlist.user.name.clone(), - images: playlist.image(), - total_tracks: playlist.nb_tracks, - }) - .collect(); - } - Err(error @ (ResolverError::Cancelled(_) | ResolverError::Closed)) => { - return Err(error); - } - _ => {} - } - } - check().map_err(ResolverError::Cancelled)?; - let result = Arc::new(result); - self.store(Bucket::Search, key, Value::Search(result.clone())); - Ok(result) - } -} - -pub(super) fn context(error: ResolverError, message: &str) -> ResolverError { - match error { - ResolverError::Cancelled(_) | ResolverError::Closed | ResolverError::Busy => error, - _ => ResolverError::Failed(format!("{message}: {error}")), - } -}