feat(media-cache): one cache table for every remote server

The Local Library union works by ATTACHing a remote server's mirror and
UNIONing it with local_tracks, and today that mechanism names Plex
inside a shared crate: `get_albums_metadata_page(..., plex_cache_path)`
with `FROM plex_cache.plex_cache_tracks` hardcoded twice. Copying it per
source means three Option<&Path> parameters and six UNION arms, and
every future source pays again.

This is the generic form: one table with a `source` discriminator, one
ATTACH, one UNION arm. The schema is `plex_cache_tracks` with its
columns renamed — all 19 map 1:1 onto Jellyfin and Subsonic, which is
what made "generic" a measurement rather than a guess.

Plex is deliberately NOT migrated here. It is a live table with 17145
rows on the owner's install, and folding it in alongside two new sources
would make one commit where a regression could come from either. This
table proves itself with Jellyfin and Subsonic first; Plex moves in as
its own change with its own verification. The cost until then is a
second ATTACH in one SQL builder, which is cheaper than the risk.

Identity: the namespaced Local Library id is `FLOOR | rowid`, with
Jellyfin at 1<<41 and Subsonic at 1<<42 — disjoint from Plex's 1<<40 and
the ephemeral store's 1<<48, asserted against both. The payload is this
table's AUTOINCREMENT rowid, NOT a hash of the server's id. Plex had to
hash because its ids predate its cache, which is why resolving one costs
a table scan; a new table has no such debt, and at ~50k tracks a 40-bit
hash carries ~0.1% odds of a collision that would play the WRONG TRACK.

Writes are UPSERT on (source, item_id), never delete-then-insert: the
row id is published identity — it sits inside queue entries and
session.db — so a re-scan must not mint a new one for a track that was
already there. AUTOINCREMENT for the same reason: a freed id must never
be handed to a different track. Both are pinned by tests.

`prune_stale` keys on updated_at rather than a set of ids, so a 50k
sweep needs no giant IN(...) — with the documented precondition that it
only runs after a sweep that COMPLETED, since a dropped connection
halfway through would otherwise read as "the server deleted everything
it did not get to".

11 tests, including the id-namespace matrix against the two existing
floors and the untagged-track ordering (75 of 4924 measured Jellyfin
rows carry no track number and must not sort above track 1).

cc
This commit is contained in:
vicrodh committed 2026-08-20 07:49:55 -06:00
1 parent 0f41c44dac
commit 0dfec607c2
4 files changed
+805

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
[package]
name = "qbz-media-cache"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "One SQLite cache for every remote media server QBZ reads (Jellyfin, Subsonic, and Plex when it migrates)"
[dependencies]
rusqlite = { version = "0.31", features = ["bundled"] }
[dev-dependencies]
tempfile = "3"