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
+152
-8
No files matched your search
@@ -314,6 +314,21 @@ impl MusicBrainzClient {
|
|||||||
&self,
|
&self,
|
||||||
title: &str,
|
title: &str,
|
||||||
artist: &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> {
|
) -> Result<ReleaseSearchResponse, String> {
|
||||||
if !self.is_enabled().await {
|
if !self.is_enabled().await {
|
||||||
return Err("MusicBrainz integration is disabled".to_string());
|
return Err("MusicBrainz integration is disabled".to_string());
|
||||||
@@ -322,18 +337,37 @@ impl MusicBrainzClient {
|
|||||||
self.rate_limiter.wait().await;
|
self.rate_limiter.wait().await;
|
||||||
|
|
||||||
let base_url = self.base_url().await;
|
let base_url = self.base_url().await;
|
||||||
let query = format!(
|
|
||||||
|
// 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:\"{}\"",
|
"release:\"{}\" AND artist:\"{}\"",
|
||||||
Self::escape_query(title),
|
Self::escape_query(title),
|
||||||
Self::escape_query(artist)
|
Self::escape_query(artist)
|
||||||
);
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let limit = limit.min(25).max(1);
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/release?query={}&fmt=json&limit=5",
|
"{}/release?query={}&fmt=json&limit={}",
|
||||||
base_url,
|
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
|
let response = self
|
||||||
.client
|
.client
|
||||||
@@ -354,6 +388,44 @@ impl MusicBrainzClient {
|
|||||||
.map_err(|e| format!("Failed to parse MusicBrainz response: {}", e))
|
.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
|
/// Escape special characters in Lucene queries
|
||||||
fn escape_query(s: &str) -> String {
|
fn escape_query(s: &str) -> String {
|
||||||
s.replace('\\', "\\\\")
|
s.replace('\\', "\\\\")
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ pub mod smart_playlists;
|
|||||||
pub use cache::{CacheStats, MusicBrainzCache, MusicBrainzCacheState};
|
pub use cache::{CacheStats, MusicBrainzCache, MusicBrainzCacheState};
|
||||||
pub use client::{MusicBrainzClient, MusicBrainzConfig};
|
pub use client::{MusicBrainzClient, MusicBrainzConfig};
|
||||||
pub use models::{
|
pub use models::{
|
||||||
AlbumAppearance, ArtistRelationships, ArtistType, MatchConfidence, MusicianAppearances,
|
AlbumAppearance, ArtistRelationships, ArtistType, MatchConfidence, Medium, MediumTrack,
|
||||||
MusicianConfidence, Period, RelatedArtist, ResolvedArtist, ResolvedMusician, ResolvedRelease,
|
MusicianAppearances, MusicianConfidence, Period, RelatedArtist, ReleaseFullResponse,
|
||||||
ResolvedTrack,
|
ReleaseSearchResponse, ResolvedArtist, ResolvedMusician, ResolvedRelease, ResolvedTrack, Tag,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@@ -239,6 +239,78 @@ pub struct LabelRef {
|
|||||||
pub name: String,
|
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 Types (Output) ============
|
||||||
|
|
||||||
/// Resolved track with MusicBrainz data
|
/// Resolved track with MusicBrainz data
|
||||||
|
|||||||
Reference in new issue
Block a user