Files
qbz/src-tauri/src/musicbrainz/models.rs
T
vicrodh fd08dc8f2c feat(TagEditor): add track count and descriptive result status
Backend:
- Add media/track-count fields to MusicBrainz ReleaseResult model
- Extract track_count and format from search results

Frontend:
- Show "{n} results" or "No results, try different search"
- Auto-expand results panel when search returns data
- Track hasSearched state for proper status display
2026-01-26 20:14:33 -06:00

542 lines
14 KiB
Rust

//! MusicBrainz API response models
//!
//! Types for deserializing MusicBrainz JSON responses
use serde::{Deserialize, Serialize};
/// Match confidence levels for MusicBrainz lookups
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MatchConfidence {
Exact, // ISRC/UPC exact match
High, // Score >= 95
Medium, // Score >= 80
Low, // Score >= 60
None, // No match found
}
impl MatchConfidence {
pub fn from_score(score: Option<i32>) -> Self {
match score {
Some(s) if s >= 100 => Self::Exact,
Some(s) if s >= 95 => Self::High,
Some(s) if s >= 80 => Self::Medium,
Some(s) if s >= 60 => Self::Low,
_ => Self::None,
}
}
}
/// Artist type classification
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ArtistType {
Person,
Group,
Orchestra,
Choir,
Character,
Other,
}
impl Default for ArtistType {
fn default() -> Self {
Self::Other
}
}
impl From<Option<&str>> for ArtistType {
fn from(s: Option<&str>) -> Self {
match s.map(|s| s.to_lowercase()).as_deref() {
Some("person") => Self::Person,
Some("group") => Self::Group,
Some("orchestra") => Self::Orchestra,
Some("choir") => Self::Choir,
Some("character") => Self::Character,
_ => Self::Other,
}
}
}
// ============ API Response Types ============
/// Recording search response
#[derive(Debug, Deserialize)]
pub struct RecordingSearchResponse {
pub created: Option<String>,
pub count: i32,
pub offset: i32,
pub recordings: Vec<RecordingResult>,
}
/// Single recording in search results
#[derive(Debug, Deserialize)]
pub struct RecordingResult {
pub id: String,
pub score: Option<i32>,
pub title: Option<String>,
pub length: Option<i64>,
#[serde(rename = "artist-credit")]
pub artist_credit: Option<Vec<ArtistCredit>>,
pub isrcs: Option<Vec<String>>,
pub releases: Option<Vec<ReleaseRef>>,
}
/// Artist credit entry
#[derive(Debug, Deserialize)]
pub struct ArtistCredit {
pub name: Option<String>,
pub joinphrase: Option<String>,
pub artist: ArtistRef,
}
/// Reference to an artist
#[derive(Debug, Deserialize)]
pub struct ArtistRef {
pub id: String,
pub name: String,
#[serde(rename = "sort-name")]
pub sort_name: Option<String>,
pub disambiguation: Option<String>,
}
/// Reference to a release (album)
#[derive(Debug, Deserialize)]
pub struct ReleaseRef {
pub id: String,
pub title: Option<String>,
pub status: Option<String>,
pub date: Option<String>,
pub country: Option<String>,
#[serde(rename = "release-group")]
pub release_group: Option<ReleaseGroupRef>,
}
/// Reference to a release group
#[derive(Debug, Deserialize)]
pub struct ReleaseGroupRef {
pub id: String,
#[serde(rename = "primary-type")]
pub primary_type: Option<String>,
}
/// Artist search response
#[derive(Debug, Deserialize)]
pub struct ArtistSearchResponse {
pub created: Option<String>,
pub count: i32,
pub offset: i32,
pub artists: Vec<ArtistResult>,
}
/// Single artist in search results
#[derive(Debug, Deserialize)]
pub struct ArtistResult {
pub id: String,
pub score: Option<i32>,
pub name: String,
#[serde(rename = "sort-name")]
pub sort_name: Option<String>,
#[serde(rename = "type")]
pub artist_type: Option<String>,
pub country: Option<String>,
pub disambiguation: Option<String>,
pub aliases: Option<Vec<Alias>>,
#[serde(rename = "life-span")]
pub life_span: Option<LifeSpan>,
}
/// Artist alias
#[derive(Debug, Deserialize)]
pub struct Alias {
pub name: String,
#[serde(rename = "sort-name")]
pub sort_name: Option<String>,
#[serde(rename = "type")]
pub alias_type: Option<String>,
pub locale: Option<String>,
pub primary: Option<bool>,
}
/// Life span for an artist
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LifeSpan {
pub begin: Option<String>,
pub end: Option<String>,
pub ended: Option<bool>,
}
/// Full artist response (with includes)
#[derive(Debug, Deserialize)]
pub struct ArtistFullResponse {
pub id: String,
pub name: String,
#[serde(rename = "sort-name")]
pub sort_name: Option<String>,
#[serde(rename = "type")]
pub artist_type: Option<String>,
pub country: Option<String>,
pub disambiguation: Option<String>,
#[serde(rename = "life-span")]
pub life_span: Option<LifeSpan>,
pub relations: Option<Vec<Relation>>,
}
/// Relation between entities
#[derive(Debug, Deserialize)]
pub struct Relation {
#[serde(rename = "type")]
pub relation_type: String,
#[serde(rename = "type-id")]
pub type_id: Option<String>,
pub direction: Option<String>,
pub begin: Option<String>,
pub end: Option<String>,
pub ended: Option<bool>,
pub attributes: Option<Vec<String>>,
pub artist: Option<ArtistRef>,
}
/// Release search response
#[derive(Debug, Deserialize)]
pub struct ReleaseSearchResponse {
pub created: Option<String>,
pub count: i32,
pub offset: i32,
pub releases: Vec<ReleaseResult>,
}
/// Single release in search results
#[derive(Debug, Deserialize)]
pub struct ReleaseResult {
pub id: String,
pub score: Option<i32>,
pub title: String,
pub status: Option<String>,
pub date: Option<String>,
pub country: Option<String>,
pub barcode: Option<String>,
#[serde(rename = "label-info")]
pub label_info: Option<Vec<LabelInfo>>,
#[serde(rename = "release-group")]
pub release_group: Option<ReleaseGroupRef>,
#[serde(rename = "artist-credit")]
pub artist_credit: Option<Vec<ArtistCredit>>,
/// Media info (contains track counts per disc)
pub media: Option<Vec<ReleaseSearchMedium>>,
/// Total track count across all media
#[serde(rename = "track-count")]
pub track_count: Option<u16>,
}
/// Simplified medium info for search results
#[derive(Debug, Deserialize)]
pub struct ReleaseSearchMedium {
pub format: Option<String>,
#[serde(rename = "track-count")]
pub track_count: Option<u16>,
}
/// Label information
#[derive(Debug, Deserialize)]
pub struct LabelInfo {
#[serde(rename = "catalog-number")]
pub catalog_number: Option<String>,
pub label: Option<LabelRef>,
}
/// Reference to a label
#[derive(Debug, Deserialize)]
pub struct LabelRef {
pub id: 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 track with MusicBrainz data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedTrack {
pub mbid: Option<String>,
pub title: Option<String>,
pub artist_credit: Option<String>,
pub artist_mbids: Option<Vec<String>>,
pub release_mbid: Option<String>,
pub release_title: Option<String>,
pub confidence: MatchConfidence,
}
impl ResolvedTrack {
pub fn empty() -> Self {
Self {
mbid: None,
title: None,
artist_credit: None,
artist_mbids: None,
release_mbid: None,
release_title: None,
confidence: MatchConfidence::None,
}
}
}
/// Resolved artist with MusicBrainz data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedArtist {
pub mbid: Option<String>,
pub name: Option<String>,
pub sort_name: Option<String>,
pub artist_type: Option<ArtistType>,
pub country: Option<String>,
pub disambiguation: Option<String>,
pub confidence: MatchConfidence,
}
impl ResolvedArtist {
pub fn empty() -> Self {
Self {
mbid: None,
name: None,
sort_name: None,
artist_type: None,
country: None,
disambiguation: None,
confidence: MatchConfidence::None,
}
}
}
/// Resolved release with MusicBrainz data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedRelease {
pub mbid: Option<String>,
pub title: Option<String>,
pub release_group_mbid: Option<String>,
pub label: Option<String>,
pub catalog_number: Option<String>,
pub country: Option<String>,
pub date: Option<String>,
pub confidence: MatchConfidence,
}
impl ResolvedRelease {
pub fn empty() -> Self {
Self {
mbid: None,
title: None,
release_group_mbid: None,
label: None,
catalog_number: None,
country: None,
date: None,
confidence: MatchConfidence::None,
}
}
}
/// Related artist (for relationships)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelatedArtist {
pub mbid: String,
pub name: String,
pub role: Option<String>,
pub period: Option<Period>,
pub ended: bool,
}
/// Time period for a relationship
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Period {
pub begin: Option<String>,
pub end: Option<String>,
}
/// Artist relationships
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtistRelationships {
pub members: Vec<RelatedArtist>,
pub past_members: Vec<RelatedArtist>,
pub groups: Vec<RelatedArtist>,
pub collaborators: Vec<RelatedArtist>,
}
impl ArtistRelationships {
pub fn empty() -> Self {
Self {
members: Vec::new(),
past_members: Vec::new(),
groups: Vec::new(),
collaborators: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.members.is_empty()
&& self.past_members.is_empty()
&& self.groups.is_empty()
&& self.collaborators.is_empty()
}
}
// ============ Musician Types ============
/// Musician confidence level for MusicBrainz ↔ Qobuz matching
///
/// This determines what UI is shown when a musician is clicked:
/// - Confirmed (3): Navigate to Qobuz Artist Page
/// - Contextual (2): Navigate to Musician Page
/// - Weak (1): Show Informational Modal only
/// - None (0): Show Informational Modal only
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MusicianConfidence {
/// Level 3: Dedicated Qobuz artist page exists
Confirmed,
/// Level 2: Appears in Qobuz credits, no standalone artist page
Contextual,
/// Level 1: MusicBrainz entity exists, sparse Qobuz results
Weak,
/// Level 0: No safe match, high risk of incorrect association
None,
}
impl MusicianConfidence {
pub fn level(&self) -> u8 {
match self {
Self::Confirmed => 3,
Self::Contextual => 2,
Self::Weak => 1,
Self::None => 0,
}
}
}
impl Default for MusicianConfidence {
fn default() -> Self {
Self::None
}
}
/// Resolved musician with confidence assessment
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedMusician {
/// Musician name (from credits or MusicBrainz)
pub name: String,
/// Primary role (e.g., "drums", "piano")
pub role: String,
/// MusicBrainz ID (if resolved)
pub mbid: Option<String>,
/// Qobuz artist ID (if exists)
pub qobuz_artist_id: Option<i64>,
/// Confidence level
pub confidence: MusicianConfidence,
/// Known bands/projects (from MusicBrainz)
pub bands: Vec<String>,
/// Number of albums the musician appears on (Qobuz)
pub appears_on_count: usize,
}
impl ResolvedMusician {
pub fn empty(name: String, role: String) -> Self {
Self {
name,
role,
mbid: None,
qobuz_artist_id: None,
confidence: MusicianConfidence::None,
bands: Vec::new(),
appears_on_count: 0,
}
}
}
/// Album appearance for a musician
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlbumAppearance {
pub album_id: String,
pub album_title: String,
pub album_artwork: String,
pub artist_name: String,
pub year: Option<String>,
pub role_on_album: String,
}
/// Musician appearances response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MusicianAppearances {
pub albums: Vec<AlbumAppearance>,
pub total: usize,
}