feat(source): wire My QBZ through the seam, and land the owner's container rules

Stage 2. The measured win: opening a 34-item collection now costs **2** library.db
opens instead of 23-34 — the local source owns a pooled handle and hands out no
guard, only `with(|db| ...)`, so a `!Send` `&LibraryDatabase` cannot cross an
await either. Bug 1 from the seam design is dead by construction, not by care.

The owner's rules, decided today, now live in one place instead of eight:
- **Collection** takes albums, singles and EPs from ANY source — qobuz, folders,
  Plex, and whatever is added next. **Mixtape** takes albums, playlists and
  tracks. This was a single boolean (`restrict_to_mixtape`) that could express
  neither release types nor "collections accept local".
- An unsupported combination must never be VISIBLE: the picker stops offering
  the impossible action, and the error stays a log-only safety net. Owner: "el
  bloqueo no debería ser visible nunca, más bien esto hay que ajustarlo en la UI".
- **Ephemeral honours its name** — playable, queueable, and offerable to nothing
  else.
- A row's source comes from the ROW. Eight QML sites built the add-to-mixtape
  payload with `"source": "qobuz"` written as a literal, so a Plex or local
  track was stored as Qobuz and afterwards could not play at all: the app asked
  the Qobuz API for an id that never existed there.

`QobuzSource` reads the client through a lens on every call instead of holding a
clone. That is not style: `qbz-core` REPLACES its client (core.rs:346 and :384,
both on paths that run before `set_session`), so a cached clone goes stale and
fails silently — the hazard the crate's own doc comment described.

The lens is installed in `on_boot`, first of everything, and finding out why is
the lesson of this commit. The gate was green — build, 142 tests, both QML
audits, a clean offscreen smoke — and the feature was visibly broken: every
Qobuz row showed "No results found", an empty Quality cell and a wrong "SINGLE"
type, while the Plex rows resolved fine. The registry is a OnceLock, so whoever
touches it first BUILDS it, and one built without the lens leaves Qobuz
permanently detached. Only driving the real app surfaced it; the crate's error
string named the cause exactly, which is the one thing that made it a ten-minute
fix instead of an afternoon.

cc
This commit is contained in:
vicrodh committed 2026-07-30 17:14:40 -06:00
1 parent f1b4241342
commit b6cf06ff2a
20 files changed
+1574 -172

No files matched your search

+31 -3
View File
@@ -89,6 +89,25 @@ Rectangle {
}
}
// SOURCE of the now-playing track for the MyQBZ AddItem payload
// ("qobuz" | "local"), read off the queue document's `current` row — the
// same row the heart above reads — and never written as a literal. The
// full bar carries the long-form rationale (PlayerBar.qml `npSource`); the
// short version: `QbzPlayer` publishes no source word, `isLocal === false`
// proves a Qobuz catalog id, and `isLocal === true` conflates four id
// namespaces (local row / Plex / ephemeral / offline-cached Qobuz) that
// this bar cannot separate without sniffing the id. "" = unknown, and the
// menu then does not offer "Add to mixtape" at all.
readonly property string npSource: {
try {
var d = JSON.parse(QbzQueue.queueJson)
if (d && d.current && d.current.id === QbzPlayer.npTrackId)
return d.current.isLocal === true ? "" : "qobuz"
} catch (e) {
}
return ""
}
function fmt(secs) {
var m = Math.floor(secs / 60)
var s = Math.floor(secs % 60)
@@ -480,8 +499,15 @@ Rectangle {
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
{ "label": QbzSession.tr("Add to mixtape", QbzSession.trRev), "icon": "cassette-tape", "action": "mixtape" },
]
// Only when the track's source is KNOWN (see `npSource`).
if (root.npSource !== "") {
m.push({
"label": QbzSession.tr("Add to mixtape", QbzSession.trRev),
"icon": "cassette-tape",
"action": "mixtape"
})
}
if (QbzPlayer.npAlbumId !== "") {
m.push({
"label": QbzSession.tr("Add album to collection", QbzSession.trRev),
@@ -504,9 +530,11 @@ Rectangle {
// MyQBZ AddItem, built here from the now-playing state:
// `npArtworkPath` is a file:// CACHE path, so it is NOT the
// artworkUrl — the store would keep a dead local path.
if (id !== "")
// Source from the queue's current ROW (`npSource`), not a
// literal; the entry is absent when that row cannot answer.
if (id !== "" && root.npSource !== "")
QbzMyQbzAdd.open(JSON.stringify([{
"itemType": "track", "source": "qobuz",
"itemType": "track", "source": root.npSource,
"sourceItemId": id, "title": QbzPlayer.npTitle,
"subtitle": QbzPlayer.npArtist, "artworkUrl": "",
"year": null, "trackCount": null