fix(qt): offline-cache rows, Jellyfin playback, and media art on the bar

Three defects the owner found by using it, all mine, all from the same
root: a source word that stops at Plex.

1. AN OFFLINE-CACHE ROW PLAYED NOTHING.

     audible: file not available at …/audio/tracks-cmaf/426056576

   `SourceId::from_word` folds `qobuz_download` into LOCAL because the row
   IS a file — but for a CMAF-cached download that "file" is the offline
   cache's own container and only `qbz-core`'s offline tier can read it.
   IC-12's first pass declared those rows ours and handed them to the
   local audible step. They now stay on the Qobuz funnel, which is where
   they were before stage 3 and where their `QueueTrack.id` — the real
   catalog id — is meant to be resolved. The symptom was the bar, the
   queue and MPRIS all changing while the previous track kept playing.

2. A JELLYFIN ROW CHANGED THE BAR AND PLAYED NOTHING.

   `local_queue_track` folded everything that was not Plex or a download
   into "local", so the row entered the queue wearing a source word that
   disagreed with its namespaced id and carrying no server id at all.
   Both media words are now arms of their own, and the hint carries the
   SERVER's item id — which `claim` prefers, because it survives a cache
   rebuild that renumbers rowids.

3. MEDIA ART NEVER REACHED THE BAR, THE QUEUE OR MPRIS.

   Those surfaces resolve an `artwork_url` string with no row in hand, so
   `classify` has only the SHAPE to go on — and a Jellyfin image tag or a
   Subsonic coverArt id has no shape worth reading. `local_queue_track`
   now stamps the token SOURCE-TAGGED (`jellyfin:<albumId>/<tag>`) and
   `classify` grows one arm that DELEGATES it to
   `registry().artwork_token` instead of guessing. One arm, not 111 call
   sites, and it shares its cache entry with the grid because it is the
   same call.

Verified in the running window: a Jellyfin album streams and plays —
`[JELLYFIN/STREAMING] 331 MB @ 12 MB/s`, decoder up, 44.1 kHz to the DAC.

429/429.

cc
This commit is contained in:
vicrodh committed 2026-08-20 12:57:36 -06:00
1 parent a3aff5f1b3
commit d5ce8bcaeb
6 files changed
+252 -8

No files matched your search

@@ -67,8 +67,21 @@ Item {
readonly property int cellH: 266
readonly property int headerH: 34
readonly property int listRowH: 56
/// Columns, or 0 while the width is still UNKNOWN.
///
/// Zero is not a degenerate case to be clamped away — it is the honest
/// answer, and clamping it to 1 is what put a full-length ONE-COLUMN grid
/// on screen at every mount. `Component.onCompleted` builds synchronously
/// (deliberately, so the grid is not empty for a frame), and at that moment
/// `width` is still 0, so `Math.max(1, …)` said "one column" and the whole
/// album list was chunked one-per-row. The real width arrives a moment
/// later, `onColsChanged` fires, and the entire model is thrown away and
/// rebuilt — the reflow the owner sees, and it is visible for exactly as
/// long as chunking every album twice takes.
///
/// A list view really is one column, so that arm keeps its 1.
readonly property int cols: viewMode === "grid"
? Math.max(1, Math.floor(width / cellW)) : 1
? (width > 0 ? Math.max(1, Math.floor(width / cellW)) : 0) : 1
/// Stand-in for a grid slot with no album — the last row of a group is
/// rarely full, and since the cell count is now CONSTANT (that is what
@@ -106,6 +119,11 @@ Item {
property int rebuildCount: 0
function rebuild() {
// No width yet: build NOTHING rather than something guaranteed wrong.
// One empty frame beats a full-length one-column grid that has to be
// discarded — and in the common case the width IS known here, so this
// never fires and the synchronous first build is unchanged.
if (cols <= 0) return
var _t0 = Date.now()
var out = []
var flatOut = []
@@ -144,6 +162,7 @@ Item {
root.rebuildCount += 1
console.info(colTiming, "[coltiming] rebuild #" + root.rebuildCount
+ " surface=" + root.surface + " grouped=" + root.grouped
+ " cols=" + per
+ " albums=" + flatOut.length + " entries=" + out.length
+ " in " + (Date.now() - _t0) + "ms")
report()