Compare commits

...

1 Commits

Author SHA1 Message Date
Connor Johnstone
9e93c5e6d2 Add artist search and discography to MetadataProvider 2026-03-17 19:02:37 -04:00
3 changed files with 112 additions and 2 deletions

View File

@@ -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<Vec<ArtistSearchResult>> {
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<Vec<DiscographyEntry>> {
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<Vec<MbArtistCredit>>) -> (String, Option<String>) {
@@ -192,6 +242,22 @@ fn urlencoded(s: &str) -> String {
// --- MusicBrainz API response types ---
#[derive(Deserialize)]
struct MbArtistSearchResponse {
artists: Vec<MbArtistResult>,
}
#[derive(Deserialize)]
struct MbArtistResult {
id: String,
name: String,
score: Option<u8>,
disambiguation: Option<String>,
country: Option<String>,
#[serde(rename = "type")]
artist_type: Option<String>,
}
#[derive(Deserialize)]
struct MbRecordingSearchResponse {
recordings: Vec<MbRecordingResult>,

View File

@@ -47,6 +47,27 @@ pub struct RecordingDetails {
pub genres: Vec<String>,
}
/// An artist match from a search query.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtistSearchResult {
pub mbid: String,
pub name: String,
pub disambiguation: Option<String>,
pub country: Option<String>,
pub artist_type: Option<String>,
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<String>,
pub release_type: Option<String>,
pub track_count: Option<i32>,
}
/// 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<Output = TagResult<RecordingDetails>> + Send;
fn search_artist(
&self,
query: &str,
limit: u32,
) -> impl std::future::Future<Output = TagResult<Vec<ArtistSearchResult>>> + Send;
fn get_artist_releases(
&self,
artist_mbid: &str,
limit: u32,
) -> impl std::future::Future<Output = TagResult<Vec<DiscographyEntry>>> + Send;
}

View File

@@ -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<Vec<ArtistSearchResult>> {
Ok(vec![])
}
async fn get_artist_releases(&self, _artist_mbid: &str, _limit: u32) -> TagResult<Vec<DiscographyEntry>> {
Ok(vec![])
}
}
async fn test_db() -> Database {