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 _ => Nonewith 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:
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.
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.
MediaKind is dispatch + presentation, not structure. Scheduling, playout, collections, smart collections and search contain zero references to it.
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.
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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.mddocs/superpowers/plans/2026-07-20-jellyfin-mixed-libraries.mdNo code, no API change, no route change — nothing for
blazor-route-parity.mdorapi-conventions.mdto record yet.docs/decisions.mdis 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
mixedis currently dropped byJellyfinApiClient.Project()'s_ => Nonewith no log line, which is why/data/musicnever reached ErsatzTV (#474) and why/data/standupneeded 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:
IJellyfinApialready queriesparentId+includeItemTypes("Movie"/"Series"/"MusicVideo"), so a mixed library is read once per type and Jellyfin returns disjoint, authoritative sets.MediaItemis table-per-type with no discriminator andLibraryPathIdon the abstract base;MediaItemRepository.GetAllTrashedItemsalreadyCOALESCEs across every subclass id for oneLibraryPathId. Heterogeneous contents were always legal.MediaKindis dispatch + presentation, not structure. Scheduling, playout, collections, smart collections and search contain zero references to it.GetExistingMovies(library)and friends), so several scanners over one library cannot cross-delete.Result: add
LibraryMediaKind.Mixed, mapmixed/null to it, and compose the three existing scanners. No new scanner, no migration, and — verified — no SPA source change, becauselibraryMediaIcon/formatLibraryMediaKindboth havedefault:arms andMEDIA_KIND_OPTIONSis an allowlist that correctly omitsMixed.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, andLibraryFolderrows are keyed byLibraryPathIdwith 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, noMediaServer*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 byLibraryPathIdalone. A negative finding there is a deliverable, not a blocker to route around.Sequencing
Blocked by #488 — the
Mixedarm invokes the music-video scanner, which currently throwsArgumentNullExceptionon its first item. Tasks 1–5 and 7 are independent of it; Task 6 is not.Refs #489