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:
vicrodh committed 2026-01-26 18:39:47 -06:00
1 parent 70df504af3
commit b8340ad6c7
3 files changed
+155 -11

No files matched your search

+80 -8
View File
@@ -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('\\', "\\\\")
+3 -3
View File
@@ -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;
+72
View File
@@ -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