---
key: scan.getoraddfolder-db-lookup
title: 2026-07-20 — `ILibraryRepository.GetOrAddFolder` resolves the folder from the DB, not the caller's `LibraryPath.LibraryFolders` navigation (#488)
status: active
since: '2026-07-20'
supersedes: none
superseded-by: none
rule: '`ILibraryRepository.GetOrAddFolder` resolves the existing folder via a DB query on `(LibraryPathId, Path)`, not the caller''s `LibraryPath.LibraryFolders` in-memory navigation, since that navigation is only eager-loaded on the local scan path and is null on remote (Jellyfin) callers.'
signals: 'GetOrAddFolder, LibraryFolders navigation, ArgumentNullException, folder lookup contract, remote scanner crash, eager-load assumption · paths: `ILibraryRepository.GetOrAddFolder`, `LibraryRepositoryTests`, `JellyfinMusicVideoLibraryScanner` · issues: #488'
mechanics: '`LibraryRepositoryTests` (`LibraryPath.LibraryFolders == null` case); test coverage in `LibraryRepositoryTests`'
---

`GetOrAddFolder` looked the existing folder up by reading `libraryPath.LibraryFolders` in memory. That
navigation collection is only eager-loaded on the **local** scan path — `LibraryRepository.GetLibrary`
`.Include(l => l.Paths).ThenInclude(p => p.LibraryFolders)` — which every `*FolderScanner` goes through. The
**remote** (Jellyfin) sync path takes its `LibraryPath` straight off the `JellyfinLibrary` entity, whose
`Paths[].LibraryFolders` is **null**, so `.Filter(...)` on it hit `Enumerable.Where(null, …)` and threw
`ArgumentNullException('source')` on the very first item of every Jellyfin music-video scan.
`JellyfinMusicVideoLibraryScanner` is the only *remote* caller of `GetOrAddFolder` (it does not derive from the
`MediaServer*LibraryScanner` base that the movie/TV/other-video remote scanners share), which is why no other
remote scanner tripped it, and the feature had never run in prod, CI, or locally.

- **Fix the repository, not the caller.** The lookup now queries `dbContext.LibraryFolders` by
  `(LibraryPathId, Path)` (the method already opened a `dbContext` for the insert). This removes the *implicit,
  undocumented loading contract* entirely — correct for all nine callers — rather than the caller-side option
  (eager-load `LibraryFolders` on the Jellyfin path), which would have left the contract intact for the next
  remote scanner to trip. The contract change is now **stated** on `ILibraryRepository.GetOrAddFolder`.
- **`null` ≠ empty — do not coerce.** A `null` navigation meant *unknown*, not *known-absent*; treating it as an
  empty collection would fall through to `CreateNewFolder` and insert a duplicate `LibraryFolder` for a path
  that already exists (there is no unique constraint behind it). Querying the DB distinguishes the two.
- **No new hot-path cost.** Every local scanner already calls `GetParentFolderId` (a DB query with the same
  `(LibraryPathId, …)` shape) once per folder immediately before `GetOrAddFolder`, so the folder granularity was
  never served purely from memory; this adds one indexed lookup per folder, not a per-file query. As a bonus the
  DB lookup also sees folders created earlier in the *same* scan, which the load-time in-memory snapshot could
  not.
- **Tests.** `LibraryRepositoryTests` (real in-memory-SQLite `TvContext`) drives `GetOrAddFolder` with
  `LibraryPath.LibraryFolders == null` — the exact remote-path shape — and asserts it creates the folder and is
  idempotent on re-scan (no duplicate row). Proven non-vacuous by restoring the navigation-collection lookup and
  watching both tests fail with the issue's `ArgumentNullException('source')`. Note: `CleanEtagsForLibraryPath`
  still reads `libraryPath.LibraryFolders` directly, but it is only reached on the local (eager-loaded) path, so
  it is not affected; left as-is (out of #488 scope).
