diff --git a/src/musicbrainz.rs b/src/musicbrainz.rs index aa91998..38eaa44 100644 --- a/src/musicbrainz.rs +++ b/src/musicbrainz.rs @@ -6,7 +6,7 @@ use crate::cleaning::escape_lucene; use crate::error::{TagError, TagResult}; use crate::provider::{ ArtistSearchResult, DiscographyEntry, MetadataProvider, RecordingDetails, RecordingMatch, - ReleaseMatch, ReleaseRef, ReleaseTrack, + ReleaseGroupEntry, ReleaseMatch, ReleaseRef, ReleaseTrack, }; const BASE_URL: &str = "https://musicbrainz.org/ws/2"; @@ -54,6 +54,13 @@ impl MusicBrainzClient { } Ok(resp.json().await?) } + + /// Look up an artist directly by MBID. Returns (name, disambiguation). + pub async fn get_artist_by_mbid(&self, mbid: &str) -> TagResult<(String, Option)> { + let url = format!("{BASE_URL}/artist/{mbid}?fmt=json"); + let resp: MbArtistLookup = self.get_json(&url).await?; + Ok((resp.name, resp.disambiguation.filter(|s| !s.is_empty()))) + } } impl MetadataProvider for MusicBrainzClient { @@ -240,6 +247,32 @@ impl MetadataProvider for MusicBrainzClient { Ok(tracks) } + + async fn get_artist_release_groups( + &self, + artist_mbid: &str, + ) -> TagResult> { + // Fetch album, single, and EP release groups + let url = format!( + "{BASE_URL}/release-group?artist={artist_mbid}&type=album|single|ep&fmt=json&limit=100" + ); + let resp: MbReleaseGroupResponse = self.get_json(&url).await?; + + Ok(resp + .release_groups + .unwrap_or_default() + .into_iter() + .map(|rg| ReleaseGroupEntry { + mbid: rg.id, + title: rg.title, + primary_type: rg.primary_type, + secondary_types: rg.secondary_types.unwrap_or_default(), + first_release_date: rg.first_release_date, + first_release_mbid: rg.releases + .and_then(|r| r.into_iter().next().map(|rel| rel.id)), + }) + .collect()) + } } fn extract_artist_credit(credits: &Option>) -> (String, Option) { @@ -287,6 +320,12 @@ struct MbArtistResult { artist_type: Option, } +#[derive(Deserialize)] +struct MbArtistLookup { + name: String, + disambiguation: Option, +} + #[derive(Deserialize)] struct MbRecordingSearchResponse { recordings: Vec, @@ -369,3 +408,27 @@ struct MbTrackEntry { struct MbTrackRecording { id: String, } + +#[derive(Deserialize)] +struct MbReleaseGroupResponse { + #[serde(rename = "release-groups")] + release_groups: Option>, +} + +#[derive(Deserialize)] +struct MbReleaseGroup { + id: String, + title: String, + #[serde(rename = "primary-type")] + primary_type: Option, + #[serde(rename = "secondary-types", default)] + secondary_types: Option>, + #[serde(rename = "first-release-date")] + first_release_date: Option, + releases: Option>, +} + +#[derive(Deserialize)] +struct MbReleaseGroupRelease { + id: String, +} diff --git a/src/provider.rs b/src/provider.rs index 9870439..a5bd0a8 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -104,6 +104,24 @@ pub trait MetadataProvider: Send + Sync { &self, release_mbid: &str, ) -> impl std::future::Future>> + Send; + + /// Get deduplicated release groups (albums, EPs, singles) for an artist. + fn get_artist_release_groups( + &self, + artist_mbid: &str, + ) -> impl std::future::Future>> + Send; +} + +/// A release group (deduplicated album/EP/single concept). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReleaseGroupEntry { + pub mbid: String, + pub title: String, + pub primary_type: Option, + pub secondary_types: Vec, + pub first_release_date: Option, + /// MBID of the first release in this group (for fetching tracks). + pub first_release_mbid: Option, } /// A track within a release. diff --git a/tests/integration.rs b/tests/integration.rs index b03fbb3..10b5f54 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -70,6 +70,10 @@ impl MetadataProvider for MockProvider { async fn get_release_tracks(&self, _release_mbid: &str) -> TagResult> { Ok(vec![]) } + + async fn get_artist_release_groups(&self, _artist_mbid: &str) -> TagResult> { + Ok(vec![]) + } } async fn test_db() -> Database {