A mixed-content library is not scanned #208

Closed
opened 2026-09-12 17:34:36 +02:00 by Claude · 1 comment
Collaborator

A library registered as mixed content is not scanned. Split out of #194, where it was grouped with music videos on the assumption that it was another resolver; it is not.

Why it is a different shape

A mixed library writes mixed.collection. Enum.TryParse<CollectionType> fails on that string, so CollectionType is null for the whole walk — and null is not "no kind", it is the case every resolver has its own branch for:

  • SeriesResolver promotes a directory holding tvshow.nfo, or one holding a season folder
  • MovieResolver.ResolveMultiple aborts entirely when it sees tvshow.nfo or season.nfo beside the files
  • LibraryManager.ResolvePaths tries the multi-item resolvers first and falls back to resolving each file on its own
  • the whole thing is ordered by ResolverPriority

So one directory is offered to every resolver in priority order until one claims it. Our crates/ephyra-app/src/scan/tree/ picks a single function per collection kind up front (movies, tv_shows, music, books, home_videos), which has no place to put "ask each in turn". This is a change to how a library's root is walked, not another arm on an existing match.

Where to start

upstream/Emby.Server.Implementations/Library/LibraryManager.cs — ResolvePaths, ResolveItem, and the IItemResolver.Priority ordering; then the collectionType is null branch in each of MovieResolver, SeriesResolver, MusicAlbumResolver, AudioResolver, PhotoAlbumResolver, GenericFolderResolver.

Verifying

cargo xtask reference gen-library-kinds (xtask/src/reference/kinds.rs) already lays a tree down, stands up its own container over a cp -a copy of snapshots/default, registers libraries, scans, and writes upstream's rows and the listing they were resolved from into crates/ephyra-app/corpus/library-kinds.json; crates/ephyra-app/tests/library_kinds.rs replays our walk over that listing. It registers four libraries today. Adding a mixed one is the natural way to get an answer key here — and a tree holding a film, a series with a tvshow.nfo, a loose episode and an album beside each other is what makes the priority order observable.

Note mixed is registerable (it is a CollectionTypeOptions member, unlike folders), so Collection::REGISTERABLE and crates/ephyra-http/src/routes/library/structure/editing.rs are part of this — and crates/ephyra-http/tests/virtual_folders.rs now has a test that posts every registerable kind and reads the configuration back, which will need the new kind added to it.

A library registered as **mixed content** is not scanned. Split out of #194, where it was grouped with music videos on the assumption that it was another resolver; it is not. ## Why it is a different shape A mixed library writes `mixed.collection`. `Enum.TryParse<CollectionType>` fails on that string, so `CollectionType` is **null** for the whole walk — and null is not "no kind", it is the case every resolver has its own branch for: - `SeriesResolver` promotes a directory holding `tvshow.nfo`, or one holding a season folder - `MovieResolver.ResolveMultiple` aborts entirely when it sees `tvshow.nfo` or `season.nfo` beside the files - `LibraryManager.ResolvePaths` tries the multi-item resolvers first and falls back to resolving each file on its own - the whole thing is ordered by `ResolverPriority` So one directory is offered to every resolver in priority order until one claims it. Our `crates/ephyra-app/src/scan/tree/` picks a single function per collection kind up front (`movies`, `tv_shows`, `music`, `books`, `home_videos`), which has no place to put "ask each in turn". This is a change to how a library's root is walked, not another arm on an existing match. ## Where to start `upstream/Emby.Server.Implementations/Library/LibraryManager.cs` — `ResolvePaths`, `ResolveItem`, and the `IItemResolver.Priority` ordering; then the `collectionType is null` branch in each of `MovieResolver`, `SeriesResolver`, `MusicAlbumResolver`, `AudioResolver`, `PhotoAlbumResolver`, `GenericFolderResolver`. ## Verifying `cargo xtask reference gen-library-kinds` (`xtask/src/reference/kinds.rs`) already lays a tree down, stands up its own container over a `cp -a` copy of `snapshots/default`, registers libraries, scans, and writes upstream's rows **and the listing they were resolved from** into `crates/ephyra-app/corpus/library-kinds.json`; `crates/ephyra-app/tests/library_kinds.rs` replays our walk over that listing. It registers four libraries today. Adding a `mixed` one is the natural way to get an answer key here — and a tree holding a film, a series with a `tvshow.nfo`, a loose episode and an album beside each other is what makes the priority order observable. Note `mixed` is registerable (it is a `CollectionTypeOptions` member, unlike `folders`), so `Collection::REGISTERABLE` and `crates/ephyra-http/src/routes/library/structure/editing.rs` are part of this — and `crates/ephyra-http/tests/virtual_folders.rs` now has a test that posts every registerable kind and reads the configuration back, which will need the new kind added to it.
Author
Collaborator

Landed on main: 7f06d454, aeb1197d, ef0a9ef2, 7422383b, 99f945a3, 166ad391.

What changed

A mixed library is registerable (Collection::Mixed, in REGISTERABLE) and walked by crates/ephyra-app/src/scan/tree/mixed.rs. Because mixed parses to no collection type, each directory is offered to the resolvers in upstream's priority order: series, then a film or disc rip, then a folder. The loose files are read as films or plain videos, with tracks read last.

The upstream details that cost real code, each checked against the pin in review:

  • The music, photo and book resolvers all bail when the type is not theirs. An album is therefore plain folders plus Audio, and a book or a picture is no row.
  • MovieResolver.ResolveMultiple passes supportMultiEditions: false for a null type while FindMovie hard-codes true, so the folder is weighed with editions grouped and its files are read apart.
  • Under a series, ResolveMultiple returns null, so each episode is reached on its own, with no library root and no sibling naming.
  • For a null type, an extension in both the audio and the video list is a video (AudioResolver.cs:114), and .cue is never a track.

Verified

  • Against a real upstream scan. gen-library-kinds now registers a fifth, mixed library over a 21-entry tree built to make the priority order decide. Upstream filed 28 rows, and a_mixed_library_resolves_what_upstream_resolved agrees with all 28.

    • The review predicted two fixture problems, both fixed before regenerating. The copied track's ID3 tags would have been rewritten by AudioFileProber, which the empty fetchers don't disable. And series_name is filled at write time rather than on the node.
  • Review round. Three defects fixed:

    • one file filed twice under a shared extension;
    • .cue filed as a track;
    • a track below a season named a parent with no row.

    scan_mixed.rs now asserts, on every tree in the file, that no node names a parent nothing filed.

  • In the web client (native build, loopback instance): the library opens as collectionType=mixed in the Folders view, with An Artist as a folder, the loose episode and the plain film as movies, and Some Series as a series. The episode plays with its S1:E1 label resolved.

Follow-ups filed

  • #212 — [boxset] / [playlist] directories and the .m3u family reach every library kind, and we file neither.
  • #213 — three folders the walk treats as leaves: a directory below a season, extras beside a folder that keeps its row, and a series subfolder with no video.
Landed on `main`: `7f06d454`, `aeb1197d`, `ef0a9ef2`, `7422383b`, `99f945a3`, `166ad391`. ## What changed A mixed library is registerable (`Collection::Mixed`, in `REGISTERABLE`) and walked by `crates/ephyra-app/src/scan/tree/mixed.rs`. Because `mixed` parses to no collection type, each directory is offered to the resolvers in upstream's priority order: series, then a film or disc rip, then a folder. The loose files are read as films or plain videos, with tracks read last. The upstream details that cost real code, each checked against the pin in review: - The music, photo and book resolvers all bail when the type is not theirs. An album is therefore plain folders plus `Audio`, and a book or a picture is no row. - `MovieResolver.ResolveMultiple` passes `supportMultiEditions: false` for a null type while `FindMovie` hard-codes `true`, so the folder is weighed with editions grouped and its files are read apart. - Under a series, `ResolveMultiple` returns null, so each episode is reached on its own, with no library root and no sibling naming. - For a null type, an extension in both the audio and the video list is a video (`AudioResolver.cs:114`), and `.cue` is never a track. ## Verified - **Against a real upstream scan.** `gen-library-kinds` now registers a fifth, `mixed` library over a 21-entry tree built to make the priority order decide. Upstream filed 28 rows, and `a_mixed_library_resolves_what_upstream_resolved` agrees with all 28. - The review predicted two fixture problems, both fixed before regenerating. The copied track's ID3 tags would have been rewritten by `AudioFileProber`, which the empty fetchers don't disable. And `series_name` is filled at write time rather than on the node. - **Review round.** Three defects fixed: - one file filed twice under a shared extension; - `.cue` filed as a track; - a track below a season named a parent with no row. `scan_mixed.rs` now asserts, on every tree in the file, that no node names a parent nothing filed. - **In the web client** (native build, loopback instance): the library opens as `collectionType=mixed` in the Folders view, with `An Artist` as a folder, the loose episode and the plain film as movies, and `Some Series` as a series. The episode plays with its `S1:E1` label resolved. ## Follow-ups filed - #212 — `[boxset]` / `[playlist]` directories and the `.m3u` family reach every library kind, and we file neither. - #213 — three folders the walk treats as leaves: a directory below a season, extras beside a folder that keeps its row, and a series subfolder with no video.
Sign in to join this conversation.
No description provided.