From 368935e6f4ccf22b774a09f2daa28452ef7e3e66 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Mon, 7 Sep 2026 00:11:20 +0200 Subject: [PATCH] fix(library): scope a library listing to that library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening a library that is not Music, Movies or TV served whatever happened to be cached — films under Books, albums under Photos — rather than the library's own contents. The cached-browse query matched a library parent with an EXISTS that never referenced the item: OR EXISTS (SELECT 1 FROM libraries l WHERE l.id = ? AND l.server_id = i.server_id) It asks only whether a library with the requested id exists, so it is true for every cached row the moment the parent is any library. The three typed libraries concealed it because their landing pages pass include_item_types, which narrowed the result to albums or films or series; the generic library page passes none, so nothing narrowed it at all. `library_id` now decides wherever the cache kept one. That is the server's own answer, and the only thing that can scope a library whose type has no mapping (Books, Photos, Collections) or none at all — a mixed library, where Jellyfin sends CollectionType null. The collection_type/item_type taxonomy stays as the fallback for rows written before the link was stored, and a library with neither matches nothing and falls through to the server, which does know what is in it. The taxonomy is now one macro shared with the downloaded listing. That listing had the identical defect and it was fixed there alone (DR-167) — the comment there even says the mapping "is needed in two places that must agree", which was true of a third place nobody looked at. One existing assertion changed rather than being worked around: UT-206 expected a lib-2 album back from a lib-1 listing, which only held because of this bug. It is about parameter binding order, so it keeps testing exactly that, now with an album that is really in lib-1. --- docs/requirements.md | 5 +- src-tauri/src/repository/offline.rs | 187 ++++++++++++++++++++++++++-- 2 files changed, 181 insertions(+), 11 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index d5ff2b5f0..3b094aa65 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -474,6 +474,7 @@ Internal architecture, components, and application logic. | DR-274 | Startup shows the picker only when the last-used profile has a PIN, or when more than one profile exists and the setting asks for it; otherwise it resumes exactly as before. The feature is invisible to a single-account install, which is what makes it safe to ship without a migration anyone has to think about | Auth | UR-082 | Proposed | | DR-275 | Idle re-lock separates "the UI is locked" from "who is the active profile", so audio keeps playing and keeps reporting as the account that started it while the screen is locked. Lockscreen transport controls keep working untouched, because nothing on a lockscreen browses or starts new content — the locked UI refuses only what reaches past the current queue. The timer starts when playback stops rather than when the UI goes quiet, and unlocking to a *different* profile stops playback. It lives in Rust beside the player state machine: it needs authoritative playback state, and a frontend timer dies with the WebView on Android | Player | UR-083 | Proposed | | DR-276 | The picker and PIN pad render an opaque `unlock_method` and an `UnlockOutcome` union the backend returns; the frontend never compares a PIN, counts an attempt, or infers that an account without a PIN is a child's. "Child account" is not modelled at all — a child profile is simply one with no PIN — so no role taxonomy is invented on either side of a boundary that has leaked taxonomy before | Frontend | UR-082, UR-083 | Proposed | +| DR-277 | A library listing is scoped to that library. The cached-browse query matched a library parent with an `EXISTS` that never referenced the item — it asked only whether a library with the requested id existed — so the clause was true for every cached row on the server. Music, Movies and TV concealed it because their landing pages pass `include_item_types`, which narrowed the result; the generic library page passes none, so opening Books, Photos, Collections or a mixed library served whatever happened to be cached. The stored `library_id` now decides wherever the cache kept one, because that is the server's own answer and the only thing able to scope a library whose type has no mapping or none at all; the `collection_type` ↔ `item_type` taxonomy is the fallback for rows written before it was stored, and a library with neither matches nothing and falls through to the server. The taxonomy itself is now a single macro shared with the downloaded listing, which had the identical defect fixed in isolation (DR-167) while this path kept it | Repository | UR-007 | Done | | DR-198 | The webview runs under a real Content-Security-Policy, and the asset protocol is scoped to the one directory it still serves. `csp` was `null`, which disables CSP entirely: any script that reached the web layer — through a future `{@html}`, a dependency, or a devtools paste — would have inherited the whole IPC surface, and with it the user's session. `script-src 'self'` (Tauri injects a nonce for SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed) plus `object-src`/`frame-src 'none'` and `base-uri 'self'` is the part that is genuinely restrictive. `img-src`/`media-src`/`connect-src` cannot be: the Jellyfin origin is typed in by the user at run time and is commonly plain `http` on a LAN, so they allow `http:`/`https:` — a wide grant for *data*, but one that still bars `file:`, `filesystem:` and scripting schemes, and leaves `script-src` untouched. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes (including `app.html`'s `display: contents` wrapper) into markup; this is safe only while no `