A file that appears is never noticed: there is no filesystem watcher #96

Closed
opened 2026-08-13 22:59:38 +02:00 by Claude · 0 comments
Collaborator

No crate in the workspace depends on notify or anything like it. A library is read at startup and when something asks for a scan; a file dropped into a watched directory is invisible until one of those happens.

Upstream watches every library path (LibraryMonitor), debounces, and refreshes the affected part of the tree — which is why a file copied into a Jellyfin library appears within seconds without a scan. That is the behaviour an operator expects from an install, and the reason a 12-hour scheduled scan is a backstop upstream rather than the only mechanism.

There is a route that stands in for it today — POST /Library/Media/Updated and the four beside it, which is how something outside the server reports that a file arrived — so an external watcher is already possible. That is not a substitute for the server doing it.

Related but distinct: the periodic scan itself is missing (see the scheduler issue), and a whole-library pass currently costs 42 s at 20,000 films with nothing changed (#92). A watcher makes the incremental case cheap and is the reason upstream can afford to be slow on the whole-library one.


Where to start

The scan is crates/jellyfin-app/src/scan/, and changes.rs is the change
detection a watcher would feed; crates/jellyfin-server/src/main.rs:282,532 is
where a pass is started today. A watcher is an adapter, so the port goes in
jellyfin-core and the implementation at the edge — ARCHITECTURE.md decides
which crate, and the rule that jellyfin-core touches no filesystem is not
negotiable.

Upstream is Emby.Server.Implementations/IO/LibraryMonitor.cs. Read three
things out of it rather than inventing them: the debounce (it waits before
acting, and a burst of events must not be a burst of scans), the paths it
ignores, and how it decides a file has finished being written — a file still
copying must not be probed half-written, which is the failure mode that makes a
naive watcher worse than none.

How to know it is done

  • A file copied into a watched directory on a running server appears as an item
    without any request being made, and its row is the row a scan would have
    written — compare against a scan of the same tree rather than eyeballing it.
  • A large file still being copied does not produce a row until it settles.
  • Deleting and renaming behave: the item goes, or moves, rather than being
    duplicated.
  • The existing scan tests still pass, and the watcher has tests that do not
    depend on timing luck.

House rules

CLAUDE.md is the short version and these are the parts that bite: tests run
under cargo nextest; upstream's source is the checkout in upstream/ at the
pin
(v12.0-rc5, 6d501ba4) and not the clone beside the repository;
generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and Dn references stay out of code and comments.
ARCHITECTURE.md decides which crate a thing goes in and which dependency edges
are illegal, DECISIONS.md says why a rule exists.

The reference pair is cargo xtask reference up (and reference provision,
which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,
with diff-db for the two databases. crates/jellyfin-harness/README.md says
what a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.

No crate in the workspace depends on `notify` or anything like it. A library is read at startup and when something asks for a scan; a file dropped into a watched directory is invisible until one of those happens. Upstream watches every library path (`LibraryMonitor`), debounces, and refreshes the affected part of the tree — which is why a file copied into a Jellyfin library appears within seconds without a scan. That is the behaviour an operator expects from an install, and the reason a 12-hour scheduled scan is a backstop upstream rather than the only mechanism. There is a route that stands in for it today — `POST /Library/Media/Updated` and the four beside it, which is how something outside the server reports that a file arrived — so an external watcher is already possible. That is not a substitute for the server doing it. Related but distinct: the periodic scan itself is missing (see the scheduler issue), and a whole-library pass currently costs 42 s at 20,000 films with nothing changed (#92). A watcher makes the incremental case cheap and is the reason upstream can afford to be slow on the whole-library one. --- ## Where to start The scan is `crates/jellyfin-app/src/scan/`, and `changes.rs` is the change detection a watcher would feed; `crates/jellyfin-server/src/main.rs:282,532` is where a pass is started today. A watcher is an adapter, so the port goes in `jellyfin-core` and the implementation at the edge — `ARCHITECTURE.md` decides which crate, and the rule that `jellyfin-core` touches no filesystem is not negotiable. Upstream is `Emby.Server.Implementations/IO/LibraryMonitor.cs`. Read three things out of it rather than inventing them: the debounce (it waits before acting, and a burst of events must not be a burst of scans), the paths it ignores, and **how it decides a file has finished being written** — a file still copying must not be probed half-written, which is the failure mode that makes a naive watcher worse than none. ## How to know it is done - A file copied into a watched directory on a running server appears as an item without any request being made, and its row is the row a scan would have written — compare against a scan of the same tree rather than eyeballing it. - A large file still being copied does not produce a row until it settles. - Deleting and renaming behave: the item goes, or moves, rather than being duplicated. - The existing scan tests still pass, and the watcher has tests that do not depend on timing luck. ### House rules `CLAUDE.md` is the short version and these are the parts that bite: tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin** (`v12.0-rc5`, `6d501ba4`) and not the clone beside the repository; generated files are regenerated rather than edited; a comment says what the code does, not how it got there; and `Dn` references stay out of code and comments. `ARCHITECTURE.md` decides which crate a thing goes in and which dependency edges are illegal, `DECISIONS.md` says why a rule exists. The reference pair is `cargo xtask reference up` (and `reference provision`, which is idempotent); a run is `cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR`, with `diff-db` for the two databases. `crates/jellyfin-harness/README.md` says what a diff means and what an ignore entry has to carry — every entry needs a reason and an expiry milestone, and an adds-only history is the smell the risk table names.
alex closed this issue 2026-08-16 11:18:37 +02:00
Sign in to join this conversation.
No description provided.