A Library is one root directory on one volume, holding media of one
kind. It names a PersistentVolumeClaim in its namespace, a directory
inside it, and the kind that directory holds. The operator reconciles
it into a CronJob whose Jobs walk the volume into the namespace’s
catalog, and it reports back what the catalog holds: how many titles,
how many folders the walk could not identify, and when it last walked.
An import can rescan one folder at once through the webhook address in
the status.
A namespace may hold many libraries, of any mix of kinds, and one
volume may hold several libraries, each with its own root. The kind
and the storage are immutable: a different volume or a different kind
is a different Library.
apiVersion: library.liken.sh/v1alpha1
kind: Library
metadata:
name: movies
namespace: media
spec:
storage:
claim: movies
root: /
kind: movies
movies: {}
The block named by kind must be present and the other kinds’ blocks
must not. Each block holds that kind’s own settings, and an empty
block is a complete one.
A Library is media of one kind, indexed into the catalog the screens read. Every kind covers one volume, a Library of franchises included: its volume holds one directory per franchise, and it names a second claim for the art the scan downloads. Create one for each volume and kind you hold.
spec
The storage this library covers, the kind of media it holds, and the settings for that kind.
| Field | Type | Required | Description |
|---|---|---|---|
storage |
object | yes | Where the media is: a claim, and a directory inside it. A franchises library names the claim that holds its checkout here, one directory per franchise, and names the claim for its art under spec.franchises.art. |
kind |
string | yes | What this library holds. The kind selects the scanner that walks the storage and the shape the catalog stores each title in. The settings block of the same name must be present, and no other. One of: movies, series, franchises. |
movies |
object | no | The settings for a library of movies, one folder per title. Present exactly when kind is movies, and empty is a complete block: every setting has a default. |
series |
object | no | The settings for a library of series, one folder per series with a folder per season inside it. Present exactly when kind is series, and empty is a complete block. |
franchises |
object | no | The settings for a library of franchises: one directory per franchise on the storage claim, each with a franchise.yaml, and the claim the scan writes the art into. Present exactly when the kind is franchises, and the art claim is its one required setting. |
sources |
[]string | no | The MetadataProviders in this namespace to ask about a title, by name, in the order they are asked: for each fact, the first provider in the list that serves it and is Ready is the one asked. The Sources condition reports a name that resolves to no provider, or a list where none serves a fact this library needs. A library that omits the list runs only the facts that need no provider. |
scan |
object | no | When the full walk of this library runs. |
trickplay |
object | no | The thumbnail sheets and the WebVTT map a scrub bar reads, built beside each video from the file alone, with no provider. |
ignore |
[]string | no | Path components to skip. The scanner leaves out any folder whose name matches an entry, and everything under it, so a volume’s non-media folders such as a recycle bin or a staging directory stay out of the catalog. |
refresh |
map[string]string | no | One time per fact, by the names status.gaps uses. For that fact, an attempt whose time is before the refresh does not count: the title is in that fact’s gap again although its file and its rows are there, the fact asks a provider again, and it rewrites its own files and rows in place. Nothing is deleted, and a fact this map does not name is untouched. A time that has not come yet waits, and the fact runs once when it arrives. |
spec.storage
Where the media is: a claim, and a directory inside it. A franchises library names the claim that holds its checkout here, one directory per franchise, and names the claim for its art under spec.franchises.art.
| Field | Type | Required | Description |
|---|---|---|---|
claim |
string | yes | The PersistentVolumeClaim in this namespace that holds the media. Any volume the cluster can mount will do, an NFS export or a CSI volume or a disk on one node. Every scanner mounts it read-only and writes nothing to it. The operator reads the PersistentVolume behind the claim and reports it in the status, because playing a title needs to know how the volume is served. |
root |
string | no | The directory inside the claim this library starts at, as an absolute path from the root of the volume. One volume may hold several libraries, each with its own root, such as /movies beside /kids-movies. Omitted, it is /, the whole volume. Default: /. |
spec.movies
The settings for a library of movies, one folder per title. Present exactly when kind is movies, and empty is a complete block: every setting has a default.
| Field | Type | Required | Description |
|---|---|---|---|
image |
string | no | The scanner image to run in place of the one this project ships for movies. Set it to run a scanner of your own, which must speak the scanner contract: mount the volume read-only, write through the catalog sidecar, and write its runs row last. Omitted, the operator runs the project’s own image. |
spec.series
The settings for a library of series, one folder per series with a folder per season inside it. Present exactly when kind is series, and empty is a complete block.
| Field | Type | Required | Description |
|---|---|---|---|
image |
string | no | The scanner image to run in place of the one this project ships for series, on the same terms as the movies image. |
spec.franchises
The settings for a library of franchises: one directory per franchise on the storage claim, each with a franchise.yaml, and the claim the scan writes the art into. Present exactly when the kind is franchises, and the art claim is its one required setting.
| Field | Type | Required | Description |
|---|---|---|---|
image |
string | no | The scanner image to run in place of the one this project ships for franchises, on the same terms as the movies image. |
art |
object | yes | Where the art the scan downloads lands. The storage claim holds the checkout and is read-only, so the art needs a claim of its own, and a screen reads this library’s art from that claim. |
spec.franchises.art
Where the art the scan downloads lands. The storage claim holds the checkout and is read-only, so the art needs a claim of its own, and a screen reads this library’s art from that claim.
| Field | Type | Required | Description |
|---|---|---|---|
claim |
string | yes | The PersistentVolumeClaim in this namespace that the scan writes the art into, under one directory per franchise with Kodi’s names. The scan Job mounts it writable, and a screen mounts it read-only. Every scan Job of this library and every screen that shows it mount it at once, so it has to allow that. |
spec.scan
When the full walk of this library runs.
| Field | Type | Required | Description |
|---|---|---|---|
schedule |
string | no | The cron expression the full walk runs on, in the cluster’s time zone; omitted, once an hour on the hour. Default: 0 * * * *. |
spec.trickplay
The thumbnail sheets and the WebVTT map a scrub bar reads, built beside each video from the file alone, with no provider.
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | no | Off by default, because a first pass reads every video of the library end to end, which is hours of CPU for a library of any size, and writes a directory of sheets beside every one of them. Turned on, the enricher Job gains a trickplay container that fills the gap one video after another. Default: false. |
status
What the volume resolved to and what the namespace’s reporter says about this library, written only by the library operator. No pod it creates holds an API credential. The catalog pod publishes a retained report on the bus, and the operator folds that report in here.
| Field | Type | Required | Description |
|---|---|---|---|
volume |
object | no | The PersistentVolume the claim is bound to. It is absent until the claim binds. Playing a title from this library needs the volume’s kind and address, so the operator reports them here and no reader has to follow the claim to its volume. |
phase |
string | no | What this library is doing. Scanning while a walk runs, Enriching while an enricher Job runs, Idle between them, Pending while the storage, the catalog pod, or the schedule is not ready, Failed when the last scan Job failed and wrote no rows, and Offline when the namespace’s reporter has left the bus. Departing means the Library is deleted and the operator holds it open while a cleanup Job takes this library’s rows out of the namespace’s catalog; the Departing condition says which step that teardown has reached. |
titles |
integer | no | How many titles the scanner’s last walk cataloged. |
items |
integer | no | How many entries this library holds: its movies, or its series and their episodes counted together. |
files |
integer | no | How many files this library holds: the video files and everything beside them, the sidecars, the artwork, the subtitles, and the trickplay directories. |
unidentified |
integer | no | How many folders the last walk could not identify: no sidecar file, and no confident parse of the folder name. They are cataloged under their folder names, so they are still browsable. |
removedLastSweep |
integer | no | How many catalog rows the scanner’s last full sweep removed. A mass delete that a partial walk caused shows here, without a shell. |
gaps |
map[string]integer | no | One count per fact of the rows that fact has left to fill, counted by the namespace’s reporter. The operator creates the enricher Job while any count is above zero, and the count falls after the scan that follows the enricher. A row a fact tried leaves the count until its attempt has passed a window: thirty days for a miss, and one day for an error. An attempt made before the item’s release date stands only until that date, so an episode a fact asked about before it aired is in the count again on the day it airs. |
waiting |
integer | no | How many titles the identity fact left as candidates for a person to choose from. The candidates are in the title’s .liken/identity.yaml. A person puts the right uniqueid into the .nfo, and the next scan identifies the title. |
unresolved |
integer | no | How many titles no provider could name. A miss is recorded with its date, and the identity fact asks again after its retry interval. |
fights |
integer | no | How many titles a fact left because another writer changed the elements it writes. The fact recorded the attempt and wrote nothing. The repair is to stop the other writer for this library, such as Jellyfin with its metadata saver on. |
lastWalk |
string | no | When the scanner last finished a full walk of the volume. |
lastChange |
string | no | When the scanner last wrote a change to the catalog. A walk that finds nothing new moves lastWalk and leaves this alone. |
runs |
[]object | no | The last run of each worker of this library, as the namespace’s reporter published it. |
webhook |
string | no | The address you give to Radarr, Sonarr, or Jellyfin so that an import rescans that one folder at once; it names the operator’s own Service and this Library, so it holds for the life of the Library, and it is reported once the storage is bound and the namespace holds one Catalog. |
conditions |
[]object | no | The typed observations the operator keeps on this library, in the standard Kubernetes form. Bound reports the storage: True when the claim exists, is bound, and its PersistentVolume was read, and False with the reason ClaimNotFound, ClaimUnbound, or VolumeNotFound. Ready reports the scanning path: True when the namespace’s catalog pod runs with every container ready, the schedule stands, and the reporter has reported this library, and False with the reason NotBound, NoCatalog, ManyCatalogs, CatalogPending, ScanPending, Offline, or NoReport. Departing reports the teardown of a deleted Library: True for as long as the operator’s finalizer holds the object open, with the reason ScanRunning, EnrichRunning, Sweeping, AwaitingEcho, or Blocked, and a message that names what the teardown waits on. Sources reports spec.sources: True when every name resolves to a MetadataProvider and one of them serves each fact this library needs, and False with the reason ProviderNotFound, ProviderNotReady, or FactNotServed. A library that names no source carries no Sources condition. |
status.volume
The PersistentVolume the claim is bound to. It is absent until the claim binds. Playing a title from this library needs the volume’s kind and address, so the operator reports them here and no reader has to follow the claim to its volume.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | The PersistentVolume’s name. |
type |
string | no | How the volume is served, which is the name of the source key on the PersistentVolume, such as nfs, csi, local, or hostPath. |
server |
string | no | The NFS server that exports the volume. Only an nfs volume has one. |
path |
string | no | The path the NFS server exports. Only an nfs volume has one. A title’s media reference is this path and the title’s own path under it. |
status.runs[]
The last run of each worker of this library, as the namespace’s reporter published it.
| Field | Type | Required | Description |
|---|---|---|---|
worker |
string | yes | Which worker ran: scan, rescan, enrich, or cleanup. |
job |
string | no | The name of the Job that ran, for kubectl describe and logs. |
started |
string | no | When that Job started its work. |
finished |
string | no | When that Job wrote its last row, which is what a Job waits to see echoed before it exits. |
unidentified |
integer | no | How many folders that run could not identify. |
removed |
integer | no | How many rows that run removed. |
failure |
string | no | Why that run failed, in one sentence. It is empty for a run that finished its work, and status.phase reads Failed while the scan run carries one. |
status.conditions[]
The typed observations the operator keeps on this library, in the standard Kubernetes form. Bound reports the storage: True when the claim exists, is bound, and its PersistentVolume was read, and False with the reason ClaimNotFound, ClaimUnbound, or VolumeNotFound. Ready reports the scanning path: True when the namespace’s catalog pod runs with every container ready, the schedule stands, and the reporter has reported this library, and False with the reason NotBound, NoCatalog, ManyCatalogs, CatalogPending, ScanPending, Offline, or NoReport. Departing reports the teardown of a deleted Library: True for as long as the operator’s finalizer holds the object open, with the reason ScanRunning, EnrichRunning, Sweeping, AwaitingEcho, or Blocked, and a message that names what the teardown waits on. Sources reports spec.sources: True when every name resolves to a MetadataProvider and one of them serves each fact this library needs, and False with the reason ProviderNotFound, ProviderNotReady, or FactNotServed. A library that names no source carries no Sources condition.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | The check this entry reports, in CamelCase. It is the key of this list, so a library carries one entry for each type. The description of the conditions field lists the types this operator publishes. Pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$. |
status |
string | yes | The verdict. Bound and Ready state a healthy fact, so True is the good verdict for both. Departing states work in progress, so its True says the teardown is running, and its reason says how far. Unknown means the operator cannot tell yet. One of: True, False, Unknown. |
observedGeneration |
integer | no | The metadata.generation this condition judged. The generation counts spec edits, so a reader can tell a verdict on the spec as it stands from a verdict on an earlier spec. |
reason |
string | no | One CamelCase word for why the condition holds this verdict, meant for a program to match on. Pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$. |
message |
string | no | The same answer in a sentence a person reads, such as the claim that is not bound or the container that will not start. |
lastTransitionTime |
string | yes | When the verdict last changed. It moves only when the status flips, not on every write, so it answers how long a library has been Ready. |
On the bus
No pod the operator creates holds an API credential. The namespace’s
catalog pod publishes one report per Library on media-operator’s
bus, under this operator’s own topic base, liken/library by default,
and the operator folds each report into that Library’s status.
| Topic | Writer | Retained | Carries |
|---|---|---|---|
libraries/{namespace}/{name}/status |
the catalog pod | yes | the library’s report |
catalogs/{namespace}/availability |
the catalog pod | yes | online or offline |
status
The report, as the operator writes it into the status: the counts, the
rows the last sweep removed, the two times, and the last run of each
worker. A scan Job writes its runs row as its last catalog write
and exits only when this report names that Job, so a report is also
the proof that the catalog pod holds every row the Job wrote.
{
"titles": 412,
"unidentified": 9,
"removedLastSweep": 3,
"lastWalk": "2026-08-29T21:04:11Z",
"lastChange": "2026-08-29T21:04:11Z",
"runs": [
{"worker": "scan", "job": "movies-scan-29310740",
"started": "2026-08-29T21:03:02Z", "finished": "2026-08-29T21:04:11Z",
"unidentified": 9, "removed": 3}
]
}
availability
online while the catalog pod’s reporter runs. The reporter names this
topic as its MQTT Last Will with offline as the payload, so a pod the
kubelet killed reads offline, and every Library of the namespace
reads Offline with it. The reports it left behind stand: the counts
describe the catalog, and they hold until the next run replaces them.