* 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>
178 lines
8.1 KiB
Markdown
178 lines
8.1 KiB
Markdown
---
|
|
title: Local NFO Metadata
|
|
description: How Silo reads Kodi/Jellyfin-style NFO sidecar files and merges them with online metadata providers.
|
|
summary: Supported NFO fields, merge and refresh semantics, and the naming-supplies-structure contract for curated libraries.
|
|
tags:
|
|
- silo
|
|
- docs
|
|
- wiki
|
|
- libraries
|
|
- metadata
|
|
- nfo
|
|
audience:
|
|
- operator
|
|
- end-user
|
|
last_reviewed: 2026-07-12
|
|
related:
|
|
- ../index.md
|
|
- media-folder-and-naming.md
|
|
---
|
|
|
|
# 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):
|
|
|
|
1. `movie.nfo` / `tvshow.nfo` in the item's folder
|
|
2. `<media basename>.nfo` next to the media file (e.g. `My Movie (2021).nfo`)
|
|
|
|
For series, Silo additionally reads:
|
|
|
|
- `season.nfo` inside each season directory (`<season>` root: title, plot)
|
|
- `<episode basename>.nfo` next to each episode file (`<episodedetails>` root: title,
|
|
plot, air date, runtime, ratings)
|
|
- Season posters: `poster.jpg` (or `folder`/`cover`) inside the season directory, or
|
|
`seasonNN-poster.jpg` in 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">` | movie, series | Trusted identity anchor for matching |
|
|
| `<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](media-folder-and-naming.md)).
|
|
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 `SxxEyy`
|
|
pattern or a `Season NN` folder routes a file to the series lane; everything else is a
|
|
movie. An NFO never flips the type: a `tvshow.nfo` sitting 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.nfo` carries 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 provider
|
|
- `internal/metadata/merge.go` — merge semantics and field locks
|