Files
qbz/crates/qbz-source/Cargo.toml
T
vicrodh f1b4241342 feat(source): add qbz-source, the source-agnostic media seam
Stage 1 of the seam the owner asked for: "son vistas súper híbridas donde se
mezcla Qobuz/Local(Folders/Plex) ... hay que hacer una solución agnóstica
porque no tardamos en meter a Jellyfin y quizás Navidrome". Nothing is wired
yet — this lands the crate, its three source implementations and its tests.
`qbz-qt` and `qbz` are untouched, so this commit changes no behaviour.

The problem it exists to end, measured rather than assumed: 129 source-branching
sites across 30 files in qbz-qt alone, SEVEN distinct item->tracks resolvers
none of which can reach the others, FIVE non-interchangeable artwork pipelines
over one correct taxonomy, and six incompatible source vocabularies — three of
which cannot express Plex at all, including the PERSISTED one (`AlbumSource` is
`Qobuz|Local`). Every hybrid view re-wires the per-source handling by hand,
which is why something is always missing.

One `Source` trait covering the four concerns together — tracks, metadata and
origin, artwork, playback — because splitting them is exactly what lets a view
forget one. A `SourceRegistry` with claim-priority ordering (Qobuz, Plex, then
Local; Plex-before-Local is load-bearing). `RegistryResolver` implements
`qbz_mixtape::ItemResolver`, so that crate and the Slint glue stay byte-
untouched and keep compiling.

The three measured bugs die structurally, not by discipline:
- The local DB was reopened PER CALL, so a My QBZ collection logged one
  "Opening library database" per item (34 measured live). `LocalSource` owns a
  pooled handle, and it hands out no guard — only `with(|db| ...)` — so a
  `!Send` `&LibraryDatabase` provably cannot cross an await. `qbz-mixtape`'s
  sync-resolver constraint becomes an API property instead of a comment.
- A `plex:<hash>` album key reached an API that wants a rating key. `MediaRef`
  has no public constructor: only `registry.claim` mints one, and
  `PlexSource::claim` rejects that shape in track position. The survey also
  found a second, undiscovered face of the same bug — "Go to album" on a Plex
  track row opens an empty page.
- Local rows in a hybrid list rendered placeholder discs. Traced to its real
  mechanism: those items are stored with `artwork_url: None` and the only
  backfill is behind a Qobuz-client gate, so offline or pre-login the art is on
  disk and never shown. `Source::artwork` is one call per row whatever the
  source, and keeps the cheap/cold split the local path already had.

Adding Jellyfin is one new file, four diff lines inside this crate, and one
line in `auth_qt.rs` — and the design states this only holds BECAUSE artwork
sits on the trait.

36 tests. The id tests derive their fixtures from `PLEX_TRACK_ID_FLOOR` instead
of transcribing them, which is not pedantry: the survey's own id table — the
artefact meant to be the definitive reference — paired a namespaced id with the
wrong rating key, and the transcribed test caught it on the first run.

Design and adversarial review (6 BLOCKER / 10 MAJOR / 7 MINOR, all corrected):
qbz-nix-docs/qt-frontend/2026-07-30-source-seam/.

cc
2026-07-30 16:14:30 -06:00

29 lines
1.2 KiB
TOML

[package]
name = "qbz-source"
description = "The source-agnostic seam: one trait + one registry over Qobuz, local files and Plex"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
# QueueTrack, mixtape::*, plex_thumb_url. No internal deps of its own.
qbz-models = { path = "../qbz-models" }
# QobuzClient (Clone) — the catalog arm.
qbz-qobuz = { path = "../qbz-qobuz" }
# LibraryDatabase / LocalTrack / thumbnails — the local arm.
qbz-library = { path = "../qbz-library" }
# Plex cache reads + plex_resolve_track_media — the Plex arm.
qbz-plex = { path = "../qbz-plex" }
# `ItemResolver` — this crate IMPLEMENTS it (mixtape_adapter.rs). qbz-mixtape is
# NOT modified and NOT absorbed; the dependency is one-directional so the `qbz`
# (Slint) crate keeps compiling untouched.
qbz-mixtape = { path = "../qbz-mixtape" }
async-trait = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
# ABSENT ON PURPOSE (design §8): qbz-audio, qbz-player (the PROTECTED backend is
# not linked, so it cannot be reached even by accident), qbz-core, qbz-app,
# tokio (this crate never spawns), reqwest, qbz-i18n, qbz-cache, rusqlite.