Add artist search and discography to MetadataProvider
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user