docs(489): spec + implementation plan for Jellyfin mixed-content libraries #490

Merged
timothy merged 1 commits from docs/489-mixed-libraries-spec into main 2026-07-20 18:36:06 +02:00
Owner

Docs-only. Adds the design spec and the bite-sized TDD implementation plan for #489.

  • docs/superpowers/specs/2026-07-20-jellyfin-mixed-libraries-design.md
  • docs/superpowers/plans/2026-07-20-jellyfin-mixed-libraries.md

No code, no API change, no route change — nothing for blazor-route-parity.md or api-conventions.md to record yet. docs/decisions.md is written by the implementation PR (plan Task 7), not this one, so the decision lands with the code that realises it.

Why this shape

A Jellyfin library typed mixed is currently dropped by JellyfinApiClient.Project()'s _ => None with no log line, which is why /data/music never reached ErsatzTV (#474) and why /data/standup needed a local-library workaround. Both libraries are genuinely mixed — shows, concert movies and music videos together — so the fix is to model that rather than to re-type them at the source.

Four findings did the design work:

  1. Classification is not guesswork. IJellyfinApi already queries parentId + includeItemTypes ("Movie" / "Series" / "MusicVideo"), so a mixed library is read once per type and Jellyfin returns disjoint, authoritative sets.
  2. No DB migration. MediaItem is table-per-type with no discriminator and LibraryPathId on the abstract base; MediaItemRepository.GetAllTrashedItems already COALESCEs across every subclass id for one LibraryPathId. Heterogeneous contents were always legal.
  3. MediaKind is dispatch + presentation, not structure. Scheduling, playout, collections, smart collections and search contain zero references to it.
  4. Reconciliation is type-scoped (GetExistingMovies(library) and friends), so several scanners over one library cannot cross-delete.

Result: add LibraryMediaKind.Mixed, map mixed/null to it, and compose the three existing scanners. No new scanner, no migration, and — verified — no SPA source change, because libraryMediaIcon / formatLibraryMediaKind both have default: arms and MEDIA_KIND_OPTIONS is an allowlist that correctly omits Mixed.

Deliberate scope limit

Jellyfin only. Local mixed libraries are excluded on purpose: every local scanner shares LocalFolderScanner.VideoFileExtensions, so the movie scanner would claim episode files, and LibraryFolder rows are keyed by LibraryPathId with no kind, so two scanners over one path would thrash each other's etags. Neither hazard exists remotely.

Stated open risk

JellyfinMusicVideoLibraryScanner's reconciliation is not traced. It is the odd one out — standalone, no MediaServer* base, no etag, path-keyed — so it is the one place cross-type deletion could hide. Plan Task 6 Step 1 closes it by reading the code, and says explicitly to stop and report if any query is scoped by LibraryPathId alone. A negative finding there is a deliverable, not a blocker to route around.

Sequencing

Blocked by #488 — the Mixed arm invokes the music-video scanner, which currently throws ArgumentNullException on its first item. Tasks 1–5 and 7 are independent of it; Task 6 is not.

Refs #489

Docs-only. Adds the design spec and the bite-sized TDD implementation plan for #489. - `docs/superpowers/specs/2026-07-20-jellyfin-mixed-libraries-design.md` - `docs/superpowers/plans/2026-07-20-jellyfin-mixed-libraries.md` No code, no API change, no route change — nothing for `blazor-route-parity.md` or `api-conventions.md` to record yet. `docs/decisions.md` is written by the implementation PR (plan Task 7), not this one, so the decision lands with the code that realises it. ## Why this shape A Jellyfin library typed `mixed` is currently dropped by `JellyfinApiClient.Project()`'s `_ => None` **with no log line**, which is why `/data/music` never reached ErsatzTV (#474) and why `/data/standup` needed a local-library workaround. Both libraries are genuinely mixed — shows, concert movies and music videos together — so the fix is to model that rather than to re-type them at the source. Four findings did the design work: 1. **Classification is not guesswork.** `IJellyfinApi` already queries `parentId` + `includeItemTypes` (`"Movie"` / `"Series"` / `"MusicVideo"`), so a mixed library is read once per type and Jellyfin returns disjoint, authoritative sets. 2. **No DB migration.** `MediaItem` is table-per-type with no discriminator and `LibraryPathId` on the abstract base; `MediaItemRepository.GetAllTrashedItems` already `COALESCE`s across every subclass id for one `LibraryPathId`. Heterogeneous contents were always legal. 3. **`MediaKind` is dispatch + presentation, not structure.** Scheduling, playout, collections, smart collections and search contain zero references to it. 4. **Reconciliation is type-scoped** (`GetExistingMovies(library)` and friends), so several scanners over one library cannot cross-delete. Result: add `LibraryMediaKind.Mixed`, map `mixed`/null to it, and compose the three **existing** scanners. No new scanner, no migration, and — verified — no SPA source change, because `libraryMediaIcon` / `formatLibraryMediaKind` both have `default:` arms and `MEDIA_KIND_OPTIONS` is an allowlist that correctly omits `Mixed`. ## Deliberate scope limit **Jellyfin only.** Local mixed libraries are excluded on purpose: every local scanner shares `LocalFolderScanner.VideoFileExtensions`, so the movie scanner would claim episode files, and `LibraryFolder` rows are keyed by `LibraryPathId` with no kind, so two scanners over one path would thrash each other's etags. Neither hazard exists remotely. ## Stated open risk `JellyfinMusicVideoLibraryScanner`'s reconciliation is **not** traced. It is the odd one out — standalone, no `MediaServer*` base, no etag, path-keyed — so it is the one place cross-type deletion could hide. Plan Task 6 Step 1 closes it by reading the code, and says explicitly to **stop and report** if any query is scoped by `LibraryPathId` alone. A negative finding there is a deliverable, not a blocker to route around. ## Sequencing Blocked by #488 — the `Mixed` arm invokes the music-video scanner, which currently throws `ArgumentNullException` on its first item. Tasks 1–5 and 7 are independent of it; Task 6 is not. Refs #489
timothy added 1 commit 2026-07-20 00:50:16 +02:00
docs(489): spec + implementation plan for Jellyfin mixed-content libraries
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 10s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 11s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 34s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 36s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 34s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (pull_request) Failing after 10m13s
a4ad12f3dc
Design and bite-sized TDD plan for supporting Jellyfin libraries whose
collection type is 'mixed' (currently dropped silently by
JellyfinApiClient.Project), so music and standup content is ingested while
staying segregated from the main Movies / TV Shows libraries.

Key findings that shaped the design:
- Jellyfin classifies server-side via parentId + includeItemTypes, so the
  three per-kind scanners see disjoint authoritative sets. No inference.
- No DB migration: MediaItem is TPT keyed on LibraryPathId, so
  heterogeneous items under one library path are already legal.
- MediaKind is dispatch + presentation only; scheduling, playout,
  collections and search carry zero references to it.
- Scoped to Jellyfin deliberately: local mixed libraries would have the
  scanners claim each other's files and thrash LibraryFolder etags.

Blocked by #488 (JellyfinMusicVideoLibraryScanner crashes on first item).

Refs #489
timothy merged commit 56dff2c49f into main 2026-07-20 18:36:06 +02:00
timothy deleted branch docs/489-mixed-libraries-spec 2026-07-20 18:36:09 +02:00
Sign in to join this conversation.