feat(models): QueueTrack carries isrc/recording_mbid; Track models audio_info

QueueTrack gains two additive, serde-default identity keys (the ISRC and
the MusicBrainz recording id) that the listen log stamps; every
constructor in the workspace sets them (Qobuz-backed ones from
Track.isrc, local ones from the scanned tags, the rest None). They are
JOIN keys, not the row identity: numeric ids collide across sources, so
(source, native item id) stays the identity.

Track.audio_info (replaygain_track_gain / peak on DiscoverAudioInfo) is
modelled from the captured search envelopes; no reader yet and no
popularity field (0 in every sample).

cc
This commit is contained in:
vicrodh committed 2026-08-28 20:30:42 -06:00
1 parent 5bd1009ca1
commit ef665dcc5c
23 files changed
+73 -1

No files matched your search

+2
View File
@@ -893,6 +893,8 @@ fn from_persisted(t: PersistedQueueTrack) -> QueueTrack {
source_item_id_hint: t.source_item_id_hint,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -247,6 +247,8 @@ fn from_persisted(t: PersistedQueueTrack) -> QueueTrack {
// track's own album until the next container play re-stamps the queue.
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+1
View File
@@ -3614,6 +3614,7 @@ mod tests {
title: String::new(),
version: None,
isrc: None,
audio_info: None,
duration: 0,
track_number: 0,
media_number: None,
+8
View File
@@ -276,6 +276,8 @@ pub async fn resolve_qobuz_album(
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
})
.collect();
@@ -370,6 +372,8 @@ pub fn track_to_queue_track_from_api(track: &ApiTrack) -> CoreQueueTrack {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
@@ -416,6 +420,8 @@ mod tests {
source_item_id_hint: None, // stamped by resolve_collection_tracks
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
})
.collect())
}
@@ -512,6 +518,8 @@ mod tests {
source_item_id_hint: item.map(String::from),
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -365,6 +365,8 @@ mod tests {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+13
View File
@@ -69,6 +69,19 @@ pub struct QueueTrack {
pub context_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
/// Cross-source identity keys, when the source knows them: the ISRC (the
/// one key Qobuz always has) and the MusicBrainz recording id (Picard-
/// tagged local files, media servers that expose provider ids). Purely
/// additive — `None` everywhere that has not been taught them — and
/// stamped into the listen log, which is the only consumer today.
///
/// NOT an identity on their own: numeric `id`s collide across sources,
/// so `(source, native item id)` stays the row identity and these are
/// the JOIN keys between sources.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub isrc: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_mbid: Option<String>,
}
fn default_streamable() -> bool {
+2
View File
@@ -356,6 +356,8 @@ mod tests {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+13 -1
View File
@@ -477,6 +477,10 @@ pub struct Track {
/// headers on the album view, mirroring the official Qobuz player (PR #536).
pub work: Option<String>,
pub isrc: Option<String>,
/// `audio_info` on the wire: replaygain gain/peak (always present in the
/// captured samples). Absent on older cached payloads → `None`.
#[serde(default)]
pub audio_info: Option<DiscoverAudioInfo>,
#[serde(default)]
pub duration: u32,
#[serde(default)]
@@ -1426,12 +1430,20 @@ pub struct DiscoverAlbumDates {
pub stream: Option<String>,
}
/// Audio info from discover album
/// Audio info from discover album (and, since 2026-08-28, from tracks)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverAudioInfo {
pub maximum_sampling_rate: Option<f64>,
pub maximum_bit_depth: Option<u32>,
pub maximum_channel_count: Option<u32>,
/// ReplayGain track gain (dB) and peak, as Qobuz ships them on every
/// track in search/album envelopes
/// (`qbz-nix-docs/qobuz-api/search-results-response.json`, 15 samples).
/// Modelled only — no reader yet; the loudness analyser stays as is.
#[serde(default)]
pub replaygain_track_gain: Option<f64>,
#[serde(default)]
pub replaygain_track_peak: Option<f64>,
}
// ============ Artist Page Types (/artist/page) ============
+2
View File
@@ -1772,6 +1772,8 @@ mod tests {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
@@ -397,6 +397,7 @@ mod tests {
version: None,
work: None,
isrc: None,
audio_info: None,
duration: 0,
track_number: 0,
media_number: None,
+2
View File
@@ -938,6 +938,8 @@ fn track_row_to_queue(row: &TrackRow) -> QueueTrack {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -88,6 +88,8 @@ fn queue_track(track: &Track) -> qbz_models::QueueTrack {
source_item_id_hint: album_key,
context_kind: None,
context_id: None,
isrc: track.isrc.clone(),
recording_mbid: None,
}
}
+2
View File
@@ -1157,6 +1157,8 @@ pub(crate) fn to_queue_track(track: &Track) -> QueueTrack {
source_item_id_hint: album_key,
context_kind: None,
context_id: None,
isrc: track.isrc.clone(),
recording_mbid: None,
}
}
+2
View File
@@ -2084,6 +2084,8 @@ mod tests {
maximum_sampling_rate: rate,
maximum_bit_depth: depth,
maximum_channel_count: None,
replaygain_track_gain: None,
replaygain_track_peak: None,
}
}
+2
View File
@@ -1464,6 +1464,8 @@ fn track_row_to_queue(row: &TrackRow) -> QueueTrack {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -2040,6 +2040,8 @@ pub(crate) fn feed_track_to_queue(item: &FeedItem) -> Option<QueueTrack> {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
})
}
+3
View File
@@ -329,6 +329,9 @@ pub fn local_queue_track(t: &LocalTrack) -> QueueTrack {
// nothing and falls back per track to the same key.
context_kind: None,
context_id: None,
// Identity straight from the scanned tags (None on untagged files).
isrc: t.isrc.clone(),
recording_mbid: t.musicbrainz_recording_id.clone(),
}
}
+2
View File
@@ -734,6 +734,8 @@ pub(crate) fn row_to_display(item: &RowItem) -> (PlaylistTrackRow, Option<QueueT
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
};
(row, Some(queue))
}
+2
View File
@@ -293,6 +293,8 @@ mod tests {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -875,6 +875,8 @@ fn row_to_queue(row: &PlaylistTrackRow) -> QueueTrack {
// the playlist id is in scope instead (`open_context` / `queue_for`).
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -3318,6 +3318,8 @@ mod tests {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -1538,6 +1538,8 @@ mod tests {
source_item_id_hint: None,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}
+2
View File
@@ -103,6 +103,8 @@ pub fn model_track_to_core_queue_track(track: &Track) -> QueueTrack {
source_item_id_hint: album_id,
context_kind: None,
context_id: None,
isrc: None,
recording_mbid: None,
}
}