mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
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.
This commit is contained in:
1 parent
d60baee4a7
commit
2debaa5363
8 files changed
+12
-432
No files matched your search
@@ -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<ArtistAlbumMetadata>,
|
||||
}
|
||||
|
||||
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<TrackMetadata>,
|
||||
pub artists: Vec<SearchArtistResult>,
|
||||
pub albums: Vec<SearchAlbumResult>,
|
||||
pub playlists: Vec<SearchPlaylistResult>,
|
||||
}
|
||||
|
||||
// Go's internal extended-metadata response has exported, untagged fields.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "PascalCase")]
|
||||
|
||||
@@ -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<ApiError>);
|
||||
|
||||
impl<'de> Update<'de> for OptionalApiError {
|
||||
fn update<D: serde::Deserializer<'de>>(&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<Vec<$item>>,
|
||||
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,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -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 [
|
||||
|
||||
@@ -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<String, String> {
|
||||
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,
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<SearchAllResult>),
|
||||
Album(Arc<AlbumResponsePayload>),
|
||||
Artist(Arc<ArtistResponsePayload>),
|
||||
Extended(Arc<AlbumExtendedMetadata>),
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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<Arc<SearchAllResult>, 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::<ArtistSearch>(&endpoint("artist", artists), check))
|
||||
};
|
||||
let fetch_albums = || {
|
||||
(albums > 0).then(|| self.get_json::<AlbumSearch>(&endpoint("album", albums), check))
|
||||
};
|
||||
let fetch_playlists = || {
|
||||
(playlists > 0)
|
||||
.then(|| self.get_json::<PlaylistSearch>(&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}")),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user