rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.
cc
Preserve per-disc and collection artwork, multi-value genres and remote metadata across Local, Plex, Jellyfin and Subsonic.
Apply source, format and quality filters before pagination, reject invalid CUE rows, and keep Qt library surfaces aligned with the selected physical version.
cx
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