diff --git a/crates/qbz-core/src/core.rs b/crates/qbz-core/src/core.rs index 826e8967e..d1e02c4e9 100644 --- a/crates/qbz-core/src/core.rs +++ b/crates/qbz-core/src/core.rs @@ -16,8 +16,9 @@ use qbz_models::{ }; use qbz_integrations::musicbrainz::cache::MusicBrainzCache; use qbz_integrations::musicbrainz::{ - location, ArtistMetadata, ArtistRelationships, DiscoveryArtist, DiscoveryResponse, - MusicBrainzClient, Period, RelatedArtist, ResolvedArtist, + location, AlbumAppearance, ArtistMetadata, ArtistRelationships, DiscoveryArtist, + DiscoveryResponse, MusicBrainzClient, MusicianAppearances, MusicianConfidence, Period, + RelatedArtist, ResolvedArtist, ResolvedMusician, }; use qbz_player::{PlaybackState, Player, QueueManager}; use qbz_qobuz::QobuzClient; @@ -1734,6 +1735,99 @@ impl QbzCore { } } +// ----- Musician resolution + appearances ----------------------------------- + +impl QbzCore { + /// Resolve a musician (band member / collaborator) to the + /// strongest available identity: MBID via MusicBrainz, exact- + /// name Qobuz artist match, or just an appears-on count. Ports + /// v2_resolve_musician end to end so the artist-network sidebar + /// click can route the user to the right view (artist page when + /// confident, MusicianPageView otherwise). + pub async fn musicbrainz_resolve_musician( + &self, + name: &str, + role: &str, + ) -> Result { + let resolved_artist = self + .musicbrainz + .resolve_artist(name) + .await + .map_err(|e| CoreError::Internal(e.to_string()))?; + + let normalized_target = name.trim().to_lowercase(); + let artist_results = self.search_artists(name, 10, 0, None).await?; + let exact = artist_results + .items + .iter() + .find(|artist| artist.name.trim().to_lowercase() == normalized_target); + + if let Some(artist) = exact { + let qobuz_artist_id = i64::try_from(artist.id).ok(); + return Ok(ResolvedMusician { + name: name.to_string(), + role: role.to_string(), + mbid: None, + qobuz_artist_id, + confidence: MusicianConfidence::Confirmed, + bands: Vec::new(), + appears_on_count: 0, + }); + } + + let album_results = self.search_albums(name, 20, 0, None).await?; + let appears_on_count = album_results.total as usize; + + let confidence = if appears_on_count > 0 { + MusicianConfidence::Contextual + } else if resolved_artist.is_some() { + MusicianConfidence::Weak + } else { + MusicianConfidence::None + }; + + Ok(ResolvedMusician { + name: name.to_string(), + role: role.to_string(), + mbid: resolved_artist.as_ref().map(|a| a.mbid.clone()), + qobuz_artist_id: None, + confidence, + bands: Vec::new(), + appears_on_count, + }) + } + + /// Fetch the paginated list of albums on which `name` appears, + /// for the Appears On grid in MusicianPageView. Ports + /// v2_get_musician_appearances — same Qobuz album search + + /// per-row mapping with `role_on_album = role`. + pub async fn musicbrainz_get_musician_appearances( + &self, + name: &str, + role: &str, + limit: u32, + offset: u32, + ) -> Result { + let results = self.search_albums(name, limit, offset, None).await?; + let albums = results + .items + .into_iter() + .map(|album| AlbumAppearance { + album_id: album.id, + album_title: album.title, + album_artwork: album.image.large.or(album.image.small).unwrap_or_default(), + artist_name: album.artist.name, + year: album.release_date_original, + role_on_album: role.to_string(), + }) + .collect::>(); + Ok(MusicianAppearances { + albums, + total: results.total as usize, + }) + } +} + /// Normalize an artist name for dedupe: trim, lowercase, collapse /// whitespace. Used by the discovery pipeline so "Iron Maiden" and /// "iron maiden" hash to the same key in the dismiss store.