* feat(metadata): register builtin NFO provider and broaden parsing Phases A and B of the #216 local-NFO work, implemented test-first. Registration & hint-first identity (Phase A): - Migration seeds a reserved kind='builtin' silo.builtin installation and an 'nfo' metadata capability (default_enabled=false, priority 1 for movie/series) with a partial unique index and documented Down. - In-process builtin provider registry (internal/metadata/builtin.go); buildProviders returns the registered provider for builtin rows. - Guard rails keep the reserved row out of every plugin surface (user plugin-settings, installations list, image resolvers, preload, auto-update, store Delete, mutation handlers -> 409); silo.builtin is a reserved manifest id. - Startup sync materializes legacy content_level='' chains per level, then appends builtin capabilities disabled via AppendProviderToAllChains (idempotent); resolveEnabledProvidersBy priority now respects default_enabled=false. - NFO uniqueids seed the trusted-hint machinery via IdentityHintProvider with per-mode conflict policy (stored IDs win on scheduled refresh, NFO wins on manual refresh, Identify skips NFO); ID-less candidates are excluded from provider-priority tie-breaks and nfo never counts as corroboration. - Web chain-editor empty-state gate is now server-derived so builtin providers are reachable on plugin-less servers. Parser breadth & sidecar hardening (Phase B): - Parser covers the practical Kodi/Jellyfin field set for <movie> and <tvshow>: original title, tagline, runtime, dates, content rating, genres/studios/countries/tags, multi-source ratings with scale normalization, cast with roles/order, director/credits. Empty collections stay nil so merge early-returns apply. - findNFO parses candidates and falls through on read/parse failure or root-type mismatch, so a stray movie.nfo cannot shadow tvshow.nfo; GetMetadata gains the same ContentType guard Search has. - New FieldReleaseDates lock gates Year/ReleaseDate/First+LastAirDate in merge (Go) and the edit-metadata dialog (web), closing the gap where a manual refresh re-applied NFO dates over admin corrections. - Merge-contract tests pin NFO fill semantics, genres whole-list first-provider-wins, and NFO edits propagating on manual refresh only. - Docs: new admin wiki page (supported fields, merge semantics, naming-supplies-structure contract), index bullet, sidecar wording revision, v1-scope feature-detection note. Zero behavior change while the provider is disabled (default); pinned by CI-mode and DB-gated test suites. Part of #216 AI-use disclosure: implemented with Claude Code (Fable 5) via spec-driven TDD and agent-assisted implementation. * feat(metadata): ingest local sidecar artwork and read series-depth NFO Phases C and D of the #216 local-NFO work, implemented test-first, plus the mixed-library use-case pins. Together these deliver the headline case: a series absent from every remote database (e.g. a fitness library) scans into a fully presented show -> named seasons -> titled episodes tree from NFO files and sidecar art alone. Local sidecar artwork through the S3 image cache (Phase C): - The NFO provider implements ImageProvider: poster/backdrop/logo sidecar discovery with a fixed precedence map, symlink/non-regular rejection, an 8 MiB cap, and file:// source URLs at rating 0. Generic filenames apply only via the sidecar search paths, so a shared folder.jpg in a flat multi-movie directory applies to none. - file:// becomes a live local source scheme: routed into *_source_path (never *_path), accepted by every image enqueue gate, attributed as provider "local", excluded from cached-path detection. - The image-cache processor caches local files with lexical-on-logical confinement to the library roots, open-handle reads with re-checks, the same variant widths as remote art, and stable (7-day) failure classification. Keys land under local/{contentType}/{contentID}/{hash8}/{imageType}; superseded prefixes are cleaned on re-cache and item deletion. - applyIfBetter gains a local exemption so rating-0 local art can fill matched items without being stickily displaced; ImageRequest carries additive sidecar path context. Series depth (Phase D): - SeasonsRequest/EpisodesRequest carry additive local path context (series roots, per-season directories, per-episode file paths), derived from naming at match time and reconstructed on refresh. - season.nfo supplies season name/plot; NFO season numbers are advisory (directory-derived number wins with a Warn - naming owns structure). <episodedetails> gains aired/runtime/ratings; <basename>.nfo titles episodes and <basename>-thumb.ext supplies thumbs; filename SxxEyy wins over NFO numbers. - Episode NFOs work without a season.nfo (provider seasons unioned with on-disk seasons); SynthesizeFallbackEpisodes always runs after persist so NFO-less episodes keep synthesized rows. Season/episode file:// art rides the Phase C pipeline unchanged. - Migration adds season:1/episode:1 to the builtin NFO capability's default_priority (still default_enabled=false). Mixed sports-library use case (tests only, no product change): - Pins the classification contract for one library holding movie-shaped and show-shaped content (WWE PPV events as movies next to a "WWE SmackDown" show, NASCAR/F1/FIFA with partial TVDB/TMDB data): naming decides movie-vs-series per file before any provider runs; the NFO supplies metadata/identity but never flips type (ContentType guard); the per-root Type override is the correction path. - NFO-driven type classification at scan time is recorded as an explicit deferred open question. Part of #216 AI-use disclosure: implemented with Claude Code (Fable 5) via spec-driven TDD and agent-assisted implementation. * docs(metadata): document local NFO metadata architecture Add a single as-built architecture page (docs/architecture/local-nfo-metadata.md) for the #216 local-NFO feature: the builtin registration model, hint-first identity semantics, the file:// -> S3 artwork pipeline and its deployment constraint, series depth, the mixed-library classification contract, and known limitations. This replaces the working implementation plan, the per-phase specs, and the narrow sidecar-artwork note, which were planning drafts and are left untracked; admin-facing behavior remains in the wiki. Part of #216 AI-use disclosure: planned, drafted, and consolidated with Claude Code (Fable 5) using multi-agent exploration and adversarial review. * fix(metadata): address PR review findings on NFO builtin provider Fold in the valid, low-risk fixes surfaced by automated review on #390: - imagecache: extract validateCacheRequest so CacheBytes (the local sidecar season/episode path) enforces the same episode-requires-season guard as Cache, preventing distinct episodes' art from colliding under one S3 key. - image_cache_processor: close the sidecar symlink-swap window by rejecting the opened handle unless os.SameFile matches the Lstat'd file, so a leaf swapped to a symlink can't pull an out-of-root target into the public cache. - plugins: guard the reserved builtin installation row in the store's Update, matching Delete, so its version/enabled/capabilities can never be rewritten even if a mutation slips past the HTTP layer. - cmd/silo: bound SyncBuiltinProviderChains with a 30s timeout so a stuck DB round-trip fails fast at startup instead of hanging. - metadata: panic instead of silently no-op'ing on an invalid RegisterBuiltinProvider call (init-time programmer error). - docs: correct the media-folder-and-naming NFO paragraph to state season/episode NFOs and sidecar artwork are actively read. --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
8.1 KiB
title, description, summary, tags, audience, last_reviewed, related
| title | description | summary | tags | audience | last_reviewed | related | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Local NFO Metadata | How Silo reads Kodi/Jellyfin-style NFO sidecar files and merges them with online metadata providers. | Supported NFO fields, merge and refresh semantics, and the naming-supplies-structure contract for curated libraries. |
|
|
2026-07-12 |
|
Local NFO Metadata
Silo ships a built-in NFO Files metadata provider that reads Kodi/Jellyfin-style .nfo
sidecar files. It runs inside the same provider chain as online providers (TMDB, TVDB, ...),
so one toggle and one priority per library control it. By default it sits at priority 1: the
NFO wins every field it declares, and online providers fill in whatever it leaves out.
Clients can detect whether a library type includes the NFO provider via
GET /api/v1/libraries/provider-defaults.
Where Silo looks for NFO files
For each movie or series, Silo checks (in order):
movie.nfo/tvshow.nfoin the item's folder<media basename>.nfonext to the media file (e.g.My Movie (2021).nfo)
For series, Silo additionally reads:
season.nfoinside each season directory (<season>root: title, plot)<episode basename>.nfonext to each episode file (<episodedetails>root: title, plot, air date, runtime, ratings)- Season posters:
poster.jpg(orfolder/cover) inside the season directory, orseasonNN-poster.jpgin the series root - Episode thumbnails:
<episode basename>-thumb.jpg
A sidecar only applies when its root element matches the item type: a stray movie.nfo in
a series folder is skipped in favor of tvshow.nfo, and a tvshow.nfo next to a movie
file is ignored. Files that fail to parse are skipped silently and the next candidate is
tried — a broken NFO never blocks matching.
Season and episode numbers declared inside an NFO (<seasonnumber>, <season>,
<episode>) are advisory: the directory name and the SxxEyy filename pattern decide
where metadata lands, and a mismatch is logged and resolved in the filename's favor.
Multi-episode NFO documents (several <episodedetails> blocks in one file) are not
supported; the first block is used and a warning is logged.
Supported fields
| NFO element | Applies to | Silo field |
|---|---|---|
<title> |
movie, series | Title |
<originaltitle> |
movie, series | Original title |
<tagline> |
movie, series | Tagline |
<plot> |
movie, series | Overview |
<year> |
movie, series | Year (derived from the premiere date when omitted) |
<runtime> |
movie, series | Runtime in minutes (non-numeric values ignored) |
<premiered> / <releasedate> |
movie | Release date (YYYY-MM-DD) |
<premiered> / <aired> |
series | First air date (YYYY-MM-DD) |
<mpaa> |
movie, series | Content rating |
<genre> (repeated) |
movie, series | Genres |
<studio> (repeated) |
movie, series | Studios |
<country> (repeated) |
movie, series | Countries |
<tag> (repeated) |
movie, series | Keywords |
<ratings> / <rating> |
movie, series | Ratings (named sources: imdb, tmdb/themoviedb, tomatometerallcritics, tomatometerallaudience; a legacy bare <rating> fills the IMDB slot) |
<actor> (name/role/order) |
movie, series | Cast (actor <thumb> URLs are ignored) |
<director>, <credits> |
movie, series | Crew (directors and writers) |
| `<uniqueid type="tmdb | imdb | tvdb">` |
<title>, <plot> (season.nfo) |
season | Season name and overview |
<title>, <plot>, <aired>, <runtime>, <ratings> (<episodedetails>) |
episode | Episode title, overview, air date, runtime, ratings |
Not yet supported: <set> (collections) — a planned follow-up. <userrating> is read
but not stored (Silo has no per-user rating field). Unknown elements are ignored, so
exports from Kodi, Jellyfin, or tinyMediaManager work as-is.
How NFO data merges with online providers
- NFO wins what it declares. With the NFO provider at priority 1, every field the NFO populates beats the same field from online providers.
- Online providers backfill the rest. Anything the NFO leaves out (cast, artwork,
runtime, ratings, ...) is filled by the next providers in the chain, keyed by the
<uniqueid>values when present. - Genres are all-or-nothing. The first provider that supplies any genres supplies the
whole list; later providers do not append to it. An NFO with no
<genre>entries leaves genres entirely to online providers. - Field locks are respected. Fields locked in the Edit Metadata dialog (including Year and release/air dates) are never overwritten by any provider, NFO included.
Editing an NFO after the initial match
Scheduled background refreshes only fill empty fields — they intentionally do not re-apply NFO changes over existing values. To pick up edits:
Edit the NFO, then use Refresh metadata on the item. A manual refresh re-reads the sidecar and replaces all unlocked fields with the merged provider results, NFO first.
Naming supplies structure, NFO supplies presentation
NFO files describe what an item is called and about — they do not change how files are grouped. Folder layout and filenames still decide what is a series, a season, and an episode (see Supported Media Folder Structures and Naming). This makes fully local libraries with no online match work well, for example a fitness library:
Fitness/
P90X/
tvshow.nfo # show title/plot (no <uniqueid> needed)
poster.jpg fanart.jpg
Season 01/
season.nfo # "Course A: Classic"
poster.jpg # season poster
P90X S01E01 - Chest and Back.mkv
P90X S01E01 - Chest and Back.nfo # episode title/plot
P90X S01E01 - Chest and Back-thumb.jpg # episode thumbnail
The folder and episode naming build the series/season/episode tree; the NFO sidecars
supply every name, description, and image — even when no online provider knows the
content. Episodes without an .nfo still appear with their generic "Episode N" title,
so partially curated libraries degrade gracefully.
Mixed libraries (sports example)
A single library of type Mixed can hold movie-shaped and show-shaped content side by side — the common case is sports, where pay-per-view events are movies and the weekly show is a series, and most of it has no TMDB/TVDB presence:
WWE/
WrestleMania 41 (2025)/
WrestleMania 41 (2025).mkv
movie.nfo # event title/plot (a <uniqueid> is optional)
poster.jpg
WWE SmackDown/
tvshow.nfo
Season 27/
season.nfo
poster.jpg
WWE SmackDown S27E15.mkv
WWE SmackDown S27E15.nfo
WWE SmackDown S27E15-thumb.jpg
How classification works in a mixed library:
- Naming decides the type, per file, before any metadata provider runs. An
SxxEyypattern or aSeason NNfolder routes a file to the series lane; everything else is a movie. An NFO never flips the type: atvshow.nfositting next to a movie-classified file is ignored (the root element must match the item type), not applied. - NFOs supply metadata and identity only. Once the type is fixed, the NFO provider
behaves exactly as in dedicated movie/series libraries — including season and episode
depth for the show-shaped content. An event whose
movie.nfocarries a<uniqueid type="tmdb">still gets full online enrichment, while ID-less events next to it fall back to their local NFO and artwork. - Misclassified group? Override its Type on the root in the library admin (Auto → Movie/Series) and rescan — that is the correction path; fixing the NFO alone will not re-route a file.
Tools that generate sports libraries (sportarr-style managers) should therefore write
standard SxxEyy naming for show content and standalone movie naming (Title (Year)/)
for events, and place NFOs/posters exactly as in the dedicated-library layouts above.
Source References
internal/metadata/nfo/— parser and providerinternal/metadata/merge.go— merge semantics and field locks