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.