---
key: scan.jellyfin-mixed-content-library
title: 2026-07-20 (#489) — Jellyfin mixed-content libraries map to one library holding many kinds
status: active
since: '2026-07-20'
supersedes: none
superseded-by: none
rule: A Jellyfin library whose collection type is `mixed` (or absent) maps to one ErsatzTV library of `LibraryMediaKind.Mixed`, scanned by running the movie/television/music-video scanners in sequence against that single library — a library is a place, not a media kind.
signals: 'mixed-content library, `LibraryMediaKind.Mixed`, per-kind sequential scan, per-kind scan dispatch, silent-success bug · paths: `JellyfinApiClient.Project()`, `ScanMixedLibrary`, `SynchronizeJellyfinLibraryByIdHandler`, `ScanLocalLibraryHandler` · issues: #489, #474, #488'
mechanics: local mixed libraries deliberately unsupported (`LocalFolderScanner.VideoFileExtensions` hazard); `LibraryMediaKind.Mixed` dispatch in the Jellyfin sync handlers; per-kind scanners queried by `parentId` + `includeItemTypes`
---

A Jellyfin library whose collection type is `mixed` — or absent — now maps to `LibraryMediaKind.Mixed`
instead of being dropped by `JellyfinApiClient.Project()`'s `_ => None`. Scanning it runs the movie,
television and music-video scanners in sequence against that one library.

- **A library is a place, not a media kind.** One physical path ↔ one Jellyfin library ↔ one ErsatzTV
  library, whose contents are heterogeneous. This is what keeps music and standup content segregated from
  the main `Movies` and `TV Shows` libraries, which was the actual goal (#474). The previous workaround was
  a *local* library pointed at the same tree, which bypassed Jellyfin, scanned the content twice and
  mis-modelled shows as movies.
- **The classification is Jellyfin's, not ours.** Each scanner queries `parentId` + `includeItemTypes`
  (`"Movie"` / `"Series"` / `"MusicVideo"`), so the three passes receive disjoint, authoritative sets.
  Nothing is inferred from folder shape or NFO contents. This is why mixed support is tractable at all — an
  earlier reading that it would require guessing per item was wrong.
- **No migration.** `MediaItem` is table-per-type with no discriminator and `LibraryPathId` on the abstract
  base, so heterogeneous items under one `LibraryPath` were always legal;
  `MediaItemRepository.GetAllTrashedItems` already `COALESCE`s across every subclass id for a single path.
- **No cross-deletion.** Reconciliation is type-scoped (`GetExistingMovies(library)` and friends), so a pass
  over one kind cannot trash another kind's items in the same library.
- **`MediaKind` is dispatch + presentation only.** Scheduling, playout, collections, smart collections and
  search hold zero references to it; search keys off the item's own subclass and the SPA's browse screens
  use a per-item `LibraryBrowseMediaType`. Adding a kind is therefore cheap.

**Deliberately scoped to Jellyfin. Local mixed libraries are NOT supported** and `Mixed` is absent from the
SPA's local-library media-kind options. Locally the same approach is unsafe: every local scanner shares
`LocalFolderScanner.VideoFileExtensions`, so the movie scanner would claim episode files, and `LibraryFolder`
rows are keyed by `LibraryPathId` with no notion of kind, so two scanners over one path would thrash each
other's etags. Neither hazard exists remotely — among the remote scanners only
`JellyfinMusicVideoLibraryScanner` touches `LibraryFolder` at all. (The existing `Images` + `OtherVideos`
shared-path exemption in `LocalLibraryHandlerBase.AreSubPaths` is the closest local analogue, and it already
carries that etag contention.)

**Failure semantics of the `Mixed` arm**: one kind failing does *not* skip the others — a broken music-video
scan must not prevent the movies in the same library from being ingested — but the library as a whole then
reports failure and `LastScan` is not stamped. `ScanCanceled` aborts the sequence immediately, since a user
cancellation is not one kind failing.

**Also fixed here**: `ScanLocalLibraryHandler` and `SynchronizeJellyfinLibraryByIdHandler` both ended their
dispatch switch with `_ => Unit.Default`, returning **success** for an unhandled kind and stamping `LastScan`
as though a scan had run. Both now return a `BaseError`. That silent success is precisely how a missing
`Mixed` arm would have hidden, so removing it is part of the feature, not a drive-by.

**Known limitations of the `Mixed` arm**, surfaced by the adversarial review and accepted rather than
fixed here:

- **Scan progress resets twice.** Each per-kind scanner independently drives `_scannerProxy.UpdateProgress`
  from 0 to 1 over its own item set, so a mixed library's progress bar fills and resets three times. The
  local scanner solves this by threading `progressMin`/`progressMax` per path (`ScanLocalLibraryHandler`),
  but the Jellyfin `ScanLibrary` signature has no such parameter, so fixing it means changing three scanner
  interfaces. Cosmetic, and deliberately out of scope.
- **One permanently-failing kind forces the healthy kinds to rescan forever.** `ScanMixedLibrary` returns
  `Left` if any arm failed, so `LastScan` is never stamped and the whole library re-scans every interval.
  Single-kind libraries already behave this way; `Mixed` widens the blast radius to the other two kinds.
  Accepted: the alternative — stamping `LastScan` on partial success — would silently mask a broken kind,
  which is worse.
- **`JellyfinMusicVideoLibraryScanner` performs no reconciliation at all** — no `GetExisting*`, no trash
  sweep. This is *why* it cannot cross-delete in a mixed library, but it also means a music video removed
  from Jellyfin is never removed from ErsatzTV. Pre-existing and orthogonal to this change; it belongs with
  that scanner's other gaps (no `ItemId`/`Etag`, path-keyed identity — see #488).
