Enrich a library
Enrichment fills what the volume does not hold: which title a folder
is, its plot, its ratings, its art, and its people. The operator asks
metadata providers and writes the answers beside the media, as the
.nfo sidecars and art files Kodi and Jellyfin read. The volume stays
the source of truth, and the catalog is derived from it.
1. Declare a provider
A MetadataProvider is one account with one provider. The key is in
a Secret in the same namespace:
apiVersion: v1
kind: Secret
metadata:
name: tmdb-api-key
namespace: media
type: Opaque
stringData:
token: <your key>
---
apiVersion: library.liken.sh/v1alpha1
kind: MetadataProvider
metadata:
name: tmdb
namespace: media
spec:
tmdb:
secretRef:
name: tmdb-api-key
The providers, and the facts each one serves:
tmdb, The Movie Database: identity, overview, certification, its own rating, credits, poster, backdrop, logo, season posters, episode stills, and the people’s ids, biographies, and headshots. Every library starts here, because identification happens through TMDb.omdb, OMDb: overview, certification, and the IMDb, Rotten Tomatoes, and Metacritic ratings. It answers on an IMDb id, which TMDb supplies. The free tier allows a thousand calls a day.fanart, Fanart.tv: art only, and the one source of clear art, banners, landscapes, disc art, and season banners.tvmaze, TVmaze: series only, and it needs no account. Declare it with an empty block,tvmaze: {}.
The operator reads the Secret once per pass, to check the provider
answers. The key reaches an enricher container through a
secretKeyRef, so no standing pod holds it.
$ kubectl -n media get metadataproviders
NAME PROVIDER READY REASON AGE
tmdb tmdb True Reachable 3d
omdb omdb False Refused 3d
spec.facts narrows what one account serves.
MetadataProvider describes every
field.
2. Name the sources on the Library
A Library asks the providers it names in spec.sources, in that
order:
spec:
sources:
- tmdb
- omdb
- fanart
A fact with one value, such as a plot or a certification, takes the
first provider that answers. A fact with a set of values, such as the
genres, takes the union of every provider that answers. Art takes the
first provider that holds an image. The Sources condition on the
Library reports whether every name resolves and every fact the
library needs has a provider.
3. What the enricher does
The operator creates an enrich Job named <library>-enrich when a
walk has finished and a fact still has an open gap. The Job runs its
facts in order, each in a container of its own: probe reads each
video’s streams, arrival records when a file was first seen,
identity names each title, nfo fills the sidecar, art downloads
the images, contributors fills the people, and trickplay builds
the scrub-bar thumbnails when spec.trickplay.enabled is set.
kubectl -n media get jobs -l library.liken.sh/library=movies,library.liken.sh/worker=enrich
kubectl -n media logs job/movies-enrich -c nfo
Identification
The identity fact asks TMDb for the folder’s title and runs a fixed
sequence of tests: the title, then the year, then a year on either side,
then the runtime within five minutes. One survivor is the answer, and
its reason is recorded. Several survivors become candidates in
.liken/identity.yaml, and the title counts in status.waiting until
a person names the right uniqueid in the .nfo. A title no provider
can name counts in status.unresolved.
Once a title has a TMDb id, one more call writes every other database’s id into the sidecar, so OMDb and Fanart.tv become reachable with no account of their own.
The write rule
Each fact owns a fixed group of elements in the sidecar and writes
nothing outside it. The overview fact owns the plot, the tagline,
the genres, the studios, the premiere date, and the runtime. Each
rating owns its one element. credits owns the actors, directors,
and writers. Before a fact writes, it checks that the group still
hashes to what it wrote last time. If another writer changed it, the
fact records a fight and writes nothing. status.fights counts
them.
An art file that already exists is never replaced. The fact records it as answered and downloads nothing.
When a fact asks again
A miss stands for thirty days and an error for one day, then the fact
asks again. An attempt made before a title’s release date stands only
until that date. To ask one fact again for every title now, set its
time in spec.refresh:
spec:
refresh:
overview: "2026-09-06T00:00:00Z"
Every attempt of that fact before the time no longer counts. The fact rewrites its own files and rows in place, and nothing is deleted.
4. The Jellyfin handover
Jellyfin reads the sidecars and art this operator writes, under the
same names its own scraper uses. Turn off Jellyfin’s
SaveLocalMetadata for a library this operator enriches. With it
on, two writers change the same sidecars, and the fight check leaves
the file to Jellyfin.
Reading progress
kubectl -n media get library movies -o jsonpath='{.status.gaps}'
kubectl -n media get library movies -o jsonpath='{.status.waiting} {.status.unresolved} {.status.fights}'
kubectl -n media get library movies -o jsonpath='{.status.conditions[?(@.type=="Sources")]}'
status.gaps counts, per fact, the rows still to fill. The operator
creates the next enrich Job while any count is above zero.