diff --git a/src/musicbrainz.rs b/src/musicbrainz.rs index ef3b9a9..1dc3419 100644 --- a/src/musicbrainz.rs +++ b/src/musicbrainz.rs @@ -4,7 +4,10 @@ use tokio::time::{Duration, Instant}; use crate::cleaning::escape_lucene; use crate::error::{TagError, TagResult}; -use crate::provider::{MetadataProvider, RecordingDetails, RecordingMatch, ReleaseMatch, ReleaseRef}; +use crate::provider::{ + ArtistSearchResult, DiscographyEntry, MetadataProvider, RecordingDetails, RecordingMatch, + ReleaseMatch, ReleaseRef, +}; const BASE_URL: &str = "https://musicbrainz.org/ws/2"; const USER_AGENT: &str = "Shanty/0.1.0 (shanty-music-app)"; @@ -161,6 +164,53 @@ impl MetadataProvider for MusicBrainzClient { .collect(), }) } + async fn search_artist( + &self, + query: &str, + limit: u32, + ) -> TagResult> { + let url = format!( + "{BASE_URL}/artist/?query={}&fmt=json&limit={limit}", + urlencoded(&escape_lucene(query)) + ); + let resp: MbArtistSearchResponse = self.get_json(&url).await?; + + Ok(resp + .artists + .into_iter() + .map(|a| ArtistSearchResult { + mbid: a.id, + name: a.name, + disambiguation: a.disambiguation.filter(|s| !s.is_empty()), + country: a.country, + artist_type: a.artist_type, + score: a.score.unwrap_or(0), + }) + .collect()) + } + + async fn get_artist_releases( + &self, + artist_mbid: &str, + limit: u32, + ) -> TagResult> { + let url = format!( + "{BASE_URL}/release/?artist={artist_mbid}&fmt=json&limit={limit}" + ); + let resp: MbReleaseSearchResponse = self.get_json(&url).await?; + + Ok(resp + .releases + .into_iter() + .map(|r| DiscographyEntry { + mbid: r.id, + title: r.title, + date: r.date, + release_type: None, // release-group type not in this response + track_count: r.track_count, + }) + .collect()) + } } fn extract_artist_credit(credits: &Option>) -> (String, Option) { @@ -192,6 +242,22 @@ fn urlencoded(s: &str) -> String { // --- MusicBrainz API response types --- +#[derive(Deserialize)] +struct MbArtistSearchResponse { + artists: Vec, +} + +#[derive(Deserialize)] +struct MbArtistResult { + id: String, + name: String, + score: Option, + disambiguation: Option, + country: Option, + #[serde(rename = "type")] + artist_type: Option, +} + #[derive(Deserialize)] struct MbRecordingSearchResponse { recordings: Vec, diff --git a/src/provider.rs b/src/provider.rs index 21150a5..4ba5b2b 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -47,6 +47,27 @@ pub struct RecordingDetails { pub genres: Vec, } +/// An artist match from a search query. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ArtistSearchResult { + pub mbid: String, + pub name: String, + pub disambiguation: Option, + pub country: Option, + pub artist_type: Option, + pub score: u8, +} + +/// A release entry in an artist's discography. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscographyEntry { + pub mbid: String, + pub title: String, + pub date: Option, + pub release_type: Option, + pub track_count: Option, +} + /// Trait for metadata lookup backends. MusicBrainz is the default implementation; /// others (Last.fm, Discogs, etc.) can be added later. pub trait MetadataProvider: Send + Sync { @@ -66,4 +87,16 @@ pub trait MetadataProvider: Send + Sync { &self, mbid: &str, ) -> impl std::future::Future> + Send; + + fn search_artist( + &self, + query: &str, + limit: u32, + ) -> impl std::future::Future>> + Send; + + fn get_artist_releases( + &self, + artist_mbid: &str, + limit: u32, + ) -> impl std::future::Future>> + Send; } diff --git a/tests/integration.rs b/tests/integration.rs index ae325d7..aa64913 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2,7 +2,10 @@ use chrono::Utc; use sea_orm::ActiveValue::Set; use shanty_db::{Database, queries}; -use shanty_tag::provider::{MetadataProvider, RecordingDetails, RecordingMatch, ReleaseMatch, ReleaseRef}; +use shanty_tag::provider::{ + ArtistSearchResult, DiscographyEntry, MetadataProvider, RecordingDetails, RecordingMatch, + ReleaseMatch, ReleaseRef, +}; use shanty_tag::error::TagResult; use shanty_tag::{TagConfig, run_tagging}; @@ -55,6 +58,14 @@ impl MetadataProvider for MockProvider { Err(shanty_tag::TagError::Other("not found".into())) } } + + async fn search_artist(&self, _query: &str, _limit: u32) -> TagResult> { + Ok(vec![]) + } + + async fn get_artist_releases(&self, _artist_mbid: &str, _limit: u32) -> TagResult> { + Ok(vec![]) + } } async fn test_db() -> Database {