feat(models): Track.streamable becomes Option<bool>, and absence stops meaning dead

`#[serde(default)]` on a bare bool means an absent key deserializes to FALSE,
so a catalog Track from any endpoint that omits `streamable` came back marked
unavailable. The models' own tests document this as a hazard rather than a bug
(the "streamable split": PurchaseTrack defaults true, catalog Track defaults
false, and the comment warns that a wrong default "inverts that whole screen's
behaviour"). The inferred OpenAPI disagrees with the code and documents
`default: true`.

That was survivable only because nothing read the field. It stops being
survivable the moment a frontend greys rows out and keeps them out of the
queue: one terse endpoint would blank an entire view, silently.

`None` now means the endpoint made no claim, and only `Some(false)` marks a
track pulled. `is_streamable()` is the ONE place absence is interpreted, and it
resolves to available: refusing to play a track because a response was terse is
strictly worse than playing one that turns out to be dead, which the reactive
skip path already handles. Album.streamable was already Option<bool> with a doc
comment naming the same causes, so this makes Track consistent rather than
novel.

Every consumer in the workspace moves to the helper: both matchers, the ISRC
validator, the daemon driver, the purchases service (which maps catalog Track
into PurchaseTrack and therefore feeds the very screen the split protects), the
radio pool, the mixtape resolver and the QConnect renderer. The replaced tests
pin both directions of the new contract.

Also here, because it lives in the same file as the matcher's streamable skip:
`rank_candidates`, an additive public entry point over the existing weighted
matcher (ISRC short-circuit, title/artist/album weights, duration bonus,
stop-word normalisation, quality tie-break). `match_tracks` is untouched.

cc
This commit is contained in:
vicrodh committed 2026-08-17 14:40:57 -06:00
1 parent 3f5f27bba5
commit 371be04be8
13 files changed
+445 -24

No files matched your search

+8 -2
View File
@@ -502,7 +502,9 @@ pub(crate) fn track_to_queue_track(track: &Track) -> QueueTrack {
is_local: false,
album_id,
artist_id,
streamable: track.streamable,
// `QueueTrack.streamable` is a resolved `bool`; absence is interpreted
// once, here, by `Track::is_streamable` (absent = available).
streamable: track.is_streamable(),
source: Some("qobuz".to_string()),
parental_warning: track.parental_warning,
source_item_id_hint: None,
@@ -707,7 +709,11 @@ mod tests {
hires: true,
maximum_bit_depth: Some(24),
maximum_sampling_rate: Some(96.0),
streamable: true,
// The catalog shape reports availability explicitly here — that is
// what `/album/get` sends. `Some(true)`, not a bare `true`, since
// the model distinguishes "the server said yes" from "the server
// said nothing".
streamable: Some(true),
parental_warning: false,
..Default::default()
};