musicbrainz: add get_release_with_tracks with media/tracks support
- Add ReleaseFullResponse model with media, tracks, and tags - Add Medium, MediumTrack, TrackRecording, Tag models - Add get_release_with_tracks() for fetching full release details - Add search_releases_extended() with configurable limit and catalog number - Export new types from module
This commit is contained in:
1 parent
70df504af3
commit
b8340ad6c7
3 files changed
+155
-11
No files matched your search
@@ -314,6 +314,21 @@ impl MusicBrainzClient {
|
||||
&self,
|
||||
title: &str,
|
||||
artist: &str,
|
||||
) -> Result<ReleaseSearchResponse, String> {
|
||||
self.search_releases_extended(title, artist, None, 5).await
|
||||
}
|
||||
|
||||
/// Search releases with extended options
|
||||
/// - `title`: Album title to search
|
||||
/// - `artist`: Artist name to search
|
||||
/// - `catalog_number`: Optional catalog number for more precise matching
|
||||
/// - `limit`: Maximum results to return (1-25)
|
||||
pub async fn search_releases_extended(
|
||||
&self,
|
||||
title: &str,
|
||||
artist: &str,
|
||||
catalog_number: Option<&str>,
|
||||
limit: usize,
|
||||
) -> Result<ReleaseSearchResponse, String> {
|
||||
if !self.is_enabled().await {
|
||||
return Err("MusicBrainz integration is disabled".to_string());
|
||||
@@ -322,18 +337,37 @@ impl MusicBrainzClient {
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
let base_url = self.base_url().await;
|
||||
let query = format!(
|
||||
"release:\"{}\" AND artist:\"{}\"",
|
||||
Self::escape_query(title),
|
||||
Self::escape_query(artist)
|
||||
);
|
||||
|
||||
// Build query - if catalog number provided, prioritize it
|
||||
let query = if let Some(catno) = catalog_number.filter(|s| !s.trim().is_empty()) {
|
||||
format!(
|
||||
"catno:\"{}\" AND artist:\"{}\"",
|
||||
Self::escape_query(catno),
|
||||
Self::escape_query(artist)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"release:\"{}\" AND artist:\"{}\"",
|
||||
Self::escape_query(title),
|
||||
Self::escape_query(artist)
|
||||
)
|
||||
};
|
||||
|
||||
let limit = limit.min(25).max(1);
|
||||
let url = format!(
|
||||
"{}/release?query={}&fmt=json&limit=5",
|
||||
"{}/release?query={}&fmt=json&limit={}",
|
||||
base_url,
|
||||
urlencoding::encode(&query)
|
||||
urlencoding::encode(&query),
|
||||
limit
|
||||
);
|
||||
|
||||
log::debug!("MusicBrainz release search: {} - {}", artist, title);
|
||||
log::debug!(
|
||||
"MusicBrainz release search: {} - {} (catalog: {:?}, limit: {})",
|
||||
artist,
|
||||
title,
|
||||
catalog_number,
|
||||
limit
|
||||
);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
@@ -354,6 +388,44 @@ impl MusicBrainzClient {
|
||||
.map_err(|e| format!("Failed to parse MusicBrainz response: {}", e))
|
||||
}
|
||||
|
||||
/// Get full release details including tracks
|
||||
/// Fetches media, recordings, artist credits, labels, and tags
|
||||
pub async fn get_release_with_tracks(&self, release_id: &str) -> Result<ReleaseFullResponse, String> {
|
||||
if !self.is_enabled().await {
|
||||
return Err("MusicBrainz integration is disabled".to_string());
|
||||
}
|
||||
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
let base_url = self.base_url().await;
|
||||
// inc=recordings gets track info, artist-credits for artist info,
|
||||
// labels for label/catalog, tags for genres
|
||||
let url = format!(
|
||||
"{}/release/{}?inc=recordings+artist-credits+labels+tags&fmt=json",
|
||||
base_url, release_id
|
||||
);
|
||||
|
||||
log::debug!("MusicBrainz release lookup with tracks: {}", release_id);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("MusicBrainz request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
return Err(format!("MusicBrainz API error {}: {}", status, text));
|
||||
}
|
||||
|
||||
response
|
||||
.json::<ReleaseFullResponse>()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse MusicBrainz response: {}", e))
|
||||
}
|
||||
|
||||
/// Escape special characters in Lucene queries
|
||||
fn escape_query(s: &str) -> String {
|
||||
s.replace('\\', "\\\\")
|
||||
|
||||
@@ -22,9 +22,9 @@ pub mod smart_playlists;
|
||||
pub use cache::{CacheStats, MusicBrainzCache, MusicBrainzCacheState};
|
||||
pub use client::{MusicBrainzClient, MusicBrainzConfig};
|
||||
pub use models::{
|
||||
AlbumAppearance, ArtistRelationships, ArtistType, MatchConfidence, MusicianAppearances,
|
||||
MusicianConfidence, Period, RelatedArtist, ResolvedArtist, ResolvedMusician, ResolvedRelease,
|
||||
ResolvedTrack,
|
||||
AlbumAppearance, ArtistRelationships, ArtistType, MatchConfidence, Medium, MediumTrack,
|
||||
MusicianAppearances, MusicianConfidence, Period, RelatedArtist, ReleaseFullResponse,
|
||||
ReleaseSearchResponse, ResolvedArtist, ResolvedMusician, ResolvedRelease, ResolvedTrack, Tag,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -239,6 +239,78 @@ pub struct LabelRef {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
// ============ Full Release Response (with tracks) ============
|
||||
|
||||
/// Full release response from lookup endpoint
|
||||
/// Includes media, tracks, and tags when requested via inc parameter
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReleaseFullResponse {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub status: Option<String>,
|
||||
pub date: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub barcode: Option<String>,
|
||||
#[serde(rename = "artist-credit")]
|
||||
pub artist_credit: Option<Vec<ArtistCredit>>,
|
||||
#[serde(rename = "label-info")]
|
||||
pub label_info: Option<Vec<LabelInfo>>,
|
||||
#[serde(rename = "release-group")]
|
||||
pub release_group: Option<ReleaseGroupRef>,
|
||||
/// Media (CDs, vinyl sides, etc.) containing tracks
|
||||
pub media: Option<Vec<Medium>>,
|
||||
/// Community tags (genres)
|
||||
pub tags: Option<Vec<Tag>>,
|
||||
}
|
||||
|
||||
/// A medium (disc) containing tracks
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Medium {
|
||||
/// Position of this medium in the release (1-based)
|
||||
pub position: Option<u8>,
|
||||
/// Format (CD, Vinyl, Digital Media, etc.)
|
||||
pub format: Option<String>,
|
||||
/// Track count on this medium
|
||||
#[serde(rename = "track-count")]
|
||||
pub track_count: Option<u16>,
|
||||
/// Tracks on this medium
|
||||
pub tracks: Option<Vec<MediumTrack>>,
|
||||
}
|
||||
|
||||
/// A track on a medium
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MediumTrack {
|
||||
/// Track position on the medium (1-based)
|
||||
pub position: Option<u8>,
|
||||
/// Track number as string (may differ from position for vinyl etc.)
|
||||
pub number: Option<String>,
|
||||
/// Track title (may differ from recording title)
|
||||
pub title: Option<String>,
|
||||
/// Track length in milliseconds
|
||||
pub length: Option<i64>,
|
||||
/// The recording this track is linked to
|
||||
pub recording: Option<TrackRecording>,
|
||||
}
|
||||
|
||||
/// Recording reference within a track
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TrackRecording {
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
pub length: Option<i64>,
|
||||
#[serde(rename = "artist-credit")]
|
||||
pub artist_credit: Option<Vec<ArtistCredit>>,
|
||||
}
|
||||
|
||||
/// Community tag (used for genres)
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Tag {
|
||||
/// Tag name (e.g., "rock", "electronic")
|
||||
pub name: String,
|
||||
/// Number of times this tag was applied
|
||||
pub count: Option<i32>,
|
||||
}
|
||||
|
||||
// ============ Resolved Types (Output) ============
|
||||
|
||||
/// Resolved track with MusicBrainz data
|
||||
|
||||
Reference in new issue
Block a user