The library bus
No pod this operator creates holds an API credential. Every fact a
scanner records about a library, every choice a person makes on a
screen, and every position the progress store records reaches the
operator over the bus. The bus is the broker media-operator runs,
and this page is the contract of this operator’s tree on it: every
topic, who writes it, who reads it, and the shape of each payload.
Your program can connect the same way, with a plain MQTT client and no
Kubernetes credentials. A media browser of another make that publishes
a play request gets the same service the project’s browser gets.
The topic base
Every topic extends one base, liken/library by default. The
operator reads the base from LIBRARY_TOPIC_BASE and passes it to
every pod it creates, so the whole tree moves together when a cluster
chooses another base. media-operator’s tree extends liken/media,
so the two operators’ trees stay disjoint on the same broker. This page
writes topics without the base.
The rules that shape the tree are media-operator’s, and
The media bus
states
them: state is retained and events are not, the topic names the object
and the payload does not, and a writer that leaves retained state
behind names an availability topic as its MQTT Last Will. This tree
follows the same three rules, with one exception:
who is watching
names no Last Will, because that
answer is meant to outlive the process that wrote it.
The topics
| Topic | Writer | Readers | Retained | Payload |
|---|---|---|---|---|
libraries/{namespace}/{name}/status |
the catalog pod’s reporter | the operator | yes | the library report |
catalogs/{namespace}/availability |
the catalog pod’s reporter | the operator | yes | online or offline |
libraries/{namespace}/{name}/refresh |
the operator | the catalog pod’s reporter | yes | the refresh times |
libraries/{namespace}/{name}/missing/{fact}/{job}/{index} |
a library Job’s close container |
one pod of the fact’s worker Job |
yes | a work list |
libraries/{namespace}/{name}/missing/{fact}/{job}/count |
a library Job’s close container |
the operator | yes | a work list |
players/{namespace}/{player}/play |
the media browser, or any client | the operator | no | the play request |
players/{namespace}/{player}/audience |
the media browser | the media browser, and any client that wants to know who is in the room | yes | who is watching |
plays/{namespace}/{play}/audience |
the operator | the progress role, the jellyfin role | yes | the audience |
plays/{namespace}/{play}/final |
the operator | the progress role, the jellyfin role | yes | the final status |
plays/{namespace}/{play}/recorded |
the progress role | the operator | yes | the recorded mark |
plays/{namespace}/{name}/outside |
the jellyfin role | the progress role | no | an outside play |
plays/{namespace}/{name}/mark |
the media browser | the progress role, the jellyfin role | yes | a mark |
plays/{namespace}/{name}/sent |
the jellyfin role | the jellyfin role | yes | a sent mark |
people/{person}/forget |
the operator | every namespace’s progress role | yes | the forget request |
people/{person}/forgotten/{namespace} |
the progress role | the operator | yes | the forgotten answer |
progress/{namespace}/availability |
the progress role | the jellyfin role | yes | online or offline |
The writers are the roles of this operator’s pods. The reporter is
the container beside the standing catalog agent in the namespace’s
catalog pod. The progress role is the container beside the standing
progress agent. The jellyfin role is the pod the operator runs
beside the progress store when the namespace’s Catalog names a
server in spec.jellyfin
.
The operator itself holds the one API credential.
Every retained topic is cleared with an empty payload when the object
it names is gone, which is how MQTT drops a retained message, so a
client that connects later reads nothing for a Library, a Play, or
a Person that no longer exists. The operator clears the library,
play, and people topics, and each work list once its worker has
finished. The progress role clears its own forgotten
answer when it reads an empty forget. The media browser clears its
own audience topic when the answer on it lapses. The progress role
clears each mark once it has recorded it and the retention has run,
and the jellyfin role clears its record of a sent mark with the mark.
The library report
libraries/{namespace}/{name}/status
The reporter publishes one report per Library, retained, and
rebuilds it whenever the catalog’s runs table changes and while any
replicated table keeps changing. The operator folds each report into
that Library’s status, so the fields are the ones
the Library status
carries. The
close container of a Library’s Job writes the enrich run as the
Job’s last catalog write and exits only when a catalog pod confirms
it, so a run in this report with a finish is also the proof that a
catalog pod holds every row the Job wrote.
| Field | Type | Meaning |
|---|---|---|
titles |
integer | How many titles the catalog holds for this library. |
unidentified |
integer | How many folders the last walk could not identify. |
lastWalk |
RFC 3339 time | When the scan run last finished. |
lastChange |
RFC 3339 time | When the counts last moved. A report that counts the same rows as the one before carries the same time. A reporter that has just started takes lastWalk. |
items |
integer | The item rows the catalog holds for this library after the last walk pruned. |
files |
integer | The file rows the catalog holds for this library after the last walk pruned. |
walking |
boolean | True while a walk runs, which is a scan run whose start is later than its finish. |
removedLastSweep |
integer | How many rows the last full sweep removed. |
runs |
list | One entry per worker that has run against this library, sorted by worker. Absent until a worker has run. |
gaps |
map of integers | One count per fact of the rows that fact has left to fill, counted with the Library’s refresh times
. Absent when no fact has a gap. |
oldestAttempts |
map of RFC 3339 times | The oldest attempt this library holds for each fact. The operator reads it against spec.refresh. Absent when no fact has an attempt. |
waiting |
integer | How many titles the identity fact left as candidates for a person to choose from. |
unresolved |
integer | How many titles no provider could name. |
fights |
integer | How many titles a fact left because another writer holds the elements it writes. |
Each entry of runs carries worker, job, started, and
finished, plus unidentified, removed, and failure where the
run has them. failure is one sentence on why the run failed, and it
is empty for a run that finished its work.
{
"titles": 128,
"unidentified": 9,
"lastWalk": "2026-08-29T21:04:11Z",
"lastChange": "2026-08-29T21:04:11Z",
"items": 128,
"files": 560,
"walking": false,
"removedLastSweep": 3,
"runs": [
{"worker": "enrich", "job": "movies-walk-dlcg1z2yyzgw",
"started": "2026-08-29T21:03:01Z", "finished": "2026-08-29T21:09:42Z"},
{"worker": "scan", "job": "movies-walk-dlcg1z2yyzgw",
"started": "2026-08-29T21:03:02Z", "finished": "2026-08-29T21:04:11Z",
"unidentified": 9, "removed": 3}
],
"gaps": {"art": 4, "identity": 2},
"oldestAttempts": {"identity": "2026-08-01T03:15:00Z"},
"waiting": 1,
"unresolved": 1,
"fights": 0
}
The refresh times
libraries/{namespace}/{name}/refresh
The reporter counts each gap with the Library’s
spec.refresh
, so a title a
refresh reopened is in gaps. The reporter holds no API credential and
cannot read the Library, so the operator publishes the refresh time of
each fact the Library names, retained, whenever the times change. The
reporter subscribes to the refresh topics of its own namespace and
publishes the report again when one changes, so an edit of spec.refresh
reaches gaps with no restart of the catalog pod. The payload is the value
a Job’s containers read from LIBRARY_REFRESH: one RFC 3339 time per
fact. The walk is not a fact, and it is not in the payload. A Library
that names no refresh time publishes an empty object.
{"credits": "2026-09-30T00:00:00Z", "appearances": "2026-09-30T00:00:00Z"}
A report that the reporter built before the refresh times reached it
counts with none. The operator reads oldestAttempts against
spec.refresh as well, so a fact with a refresh that has work left starts
its work from such a report too.
A work list
libraries/{namespace}/{name}/missing/{fact}/{job}/{index} and
libraries/{namespace}/{name}/missing/{fact}/{job}/count
The videos a heavy fact still needs, for the fact’s worker Job. The
close container of the library Job named {job} reads the gap of
each heavy fact the Library turns on, trickplay or appearances,
from its copy of the catalog. It publishes each video as one retained
message, numbered from 0 in the order of the video’s path, and then the
count, retained:
{"path": "Harbour Lights/Season 01/Harbour Lights - S01E02.mkv", "size": 1468006400, "durationMs": 2710000}
The path is relative to the Library’s root. The size is the one the
walk read, and the worker compares it with the file before it opens
it. The length tells the trickplay worker how many thumbnails cover the
video. The count is a decimal number, 3. The broker reads one
connection’s messages in order, so a count on the broker proves that
every video before it is there too. The close container reads its own
count back before it writes its enrich run, and an empty gap publishes
nothing. One list holds at most 10,000 videos.
The operator subscribes to every count. When the Job named {job}
has finished, it starts the fact’s worker as an Indexed Job with one
completion per video, and each pod reads the message at its own
index. The operator clears every topic of a list when that worker has
finished, when a newer list of the same fact replaces a list no
worker took, and when the Library is gone. The broker keeps retained
messages in memory, so a broker restart drops every list, and the
next library Job lists the videos again.
The reporter’s availability
catalogs/{namespace}/availability
online while the catalog pod’s reporter runs. The reporter names
this topic as its MQTT Last Will with offline as the payload,
publishes online once it connects, and publishes offline as it
stops. Both are retained. A pod the kubelet killed reads offline,
and every Library of the namespace reads the phase Offline with
it. The reports the reporter left behind stand: the counts describe
the catalog, and they hold until the next report replaces them.
One reporter serves every Library of its namespace, so there is one
availability topic per namespace and none per Library.
The play request
players/{namespace}/{player}/play
A play request is what a person chose on a screen. The browser holds
the catalog and no API credential, and the operator holds the
credential and no catalog, so the browser resolves the list of files
and publishes it here, and the operator joins each path to the
Library’s claim and creates the
Play
. The request is
an event and is not retained: a broker that held the last one would
replay it to the operator on every reconnect.
The operator names the topic on the browser container as
LIBRARY_PLAY_TOPIC. The Player comes from the topic and never from
the payload, so a request cannot name a Player other than the one
whose topic carried it. The operator creates a Play only for a
Player whose idle screen it holds, and only from a Library in that
Player’s namespace. A request that fails a check is reported in the
operator’s log and dropped, because the screen has no way to answer.
| Field | Type | Required | Meaning |
|---|---|---|---|
library |
string | yes | The Library the items come from, as namespace/name. The namespace must be the Player’s own. |
slug |
string | no | The catalog’s slug for the item the person chose: the movie, or the chosen episode. The operator folds it into the Play’s name, so kubectl get plays reads as titles. A request with no slug names the Play after the Player alone. |
items |
list | yes | The files to play, in order. At least one. |
items[].path |
string | yes | The path of the item’s main file, relative to the library root, exactly as the catalog stores it. An empty path, an absolute path, or one that climbs above the root is refused. |
items[].presentation |
object | no | How the item should look, in the Play’s own spec.items[].presentation field names: type, hint, title, series, season, episode, episodeTitle, year, date, art, trickplay, role, appearances, contributors, and marks. role says what the item is to the work it presents: trailer, or empty for the work itself. appearances is the path of the spans file that the appearances fact wrote for the item’s file, and contributors is the path of the library’s .contributors directory, which the spans file names each portrait under. marks is a list of spans of the item’s file, each with a kind, a start and an end in seconds from the start of the file, and a source, the provider block the span came from. An absent start is the start of the file, and an absent end is the end of the file. The operator carries marks unread, and the display merges the candidates. art, trickplay, appearances, and contributors are paths relative to the library root, and the operator joins them to the claim the way it joins path. |
people |
list of strings | no | Who is watching, as Person names. Each becomes an owner reference on the Play. A name the cluster does not hold is reported and dropped, and the Play still plays. |
aliases |
map of strings | no | The work’s ids by provider, such as {"tmdb": "1000001", "imdb": "tt0000001"}. They become the Play’s library.liken.sh/alias.{provider} annotations, and they are the identity the progress store keys on. |
season |
integer | no | The season number of an episode. It becomes the library.liken.sh/season annotation. |
episode |
integer | no | The episode number of an episode. It becomes the library.liken.sh/episode annotation. |
start |
string | no | Where the first item begins, as a decimal count of seconds. It reaches the Play’s spec.start unchanged, because the player parses it and the operator does not. Omit it to start at the beginning. |
next |
object | no | The work that follows this one, which the player offers on its scrubber. It carries the three lines of the offer card, reason, title, and detail, spelled the way the continue-watching row spells them; art, a path relative to the root of next.library; library, the namespace/name of the next work’s own Library, which may differ from library above; and request, an object the operator copies onto the Play unread. The operator joins art to the claim of next.library the way it joins an item’s art, and the player publishes request back on the Player’s commands topic when a person takes the offer. |
The project’s browser publishes one item, the film or the chosen
episode, leaves out every empty field, sends start only for a
resume, and sends next only where the page the person started from
names a natural successor: the next episode of a series, the next film
of a set, or the next member of a franchise. A series continues
through next and not through the item list, so every episode is its
own Play and its own row of progress.
{
"library": "den/movies",
"slug": "a-quiet-harbor-2014",
"items": [
{
"path": "A Quiet Harbor (2014)/A Quiet Harbor (2014).mkv",
"presentation": {
"type": "video",
"hint": "movie",
"title": "A Quiet Harbor",
"year": 2014,
"art": "A Quiet Harbor (2014)/poster.jpg",
"trickplay": "A Quiet Harbor (2014)/A Quiet Harbor (2014).trickplay"
}
}
],
"people": ["ada", "grace"],
"aliases": {"tmdb": "1000001", "imdb": "tt0000001"},
"start": "600"
}
The Play the operator creates from that request carries one item
whose URI is claim://, the Library’s namespace, /, the
Library’s claim, /, and the library root joined to the path. The
root is an absolute path, so the URI has two slashes after the claim.
For a Library in the namespace den on the claim movies with the
root /, that is
claim://den/movies//A Quiet Harbor (2014)/A Quiet Harbor (2014).mkv.
The two people become owner references, the two aliases become
annotations, and 600 becomes spec.start.
Who is watching
players/{namespace}/{player}/audience
The room on one screen, as the browser asked a person for it. The
browser is both the writer and the reader: it publishes the answer
retained, and it reads the broker’s catch-up back when it starts. A
screen pod that restarts inside the idle window draws the room it had
and asks nobody. The operator names the topic on the browser container
as LIBRARY_AUDIENCE_TOPIC, and reads none of it.
| Field | Type | Meaning |
|---|---|---|
people |
list of objects | Who is watching, in the order the answer named them. Each entry carries name, the Person every record keys on, and displayName, the name the screen draws. An empty list is the answer “nobody”, which is an answer and not the absence of one. |
at |
integer | The Unix time of the last press, in whole seconds. |
{"people": [{"name": "person-a", "displayName": "Person A"}], "at": 1757350000}
The browser writes the message when a person answers the picker, and at most once a minute while a person presses keys. The stamp is what ages, and a second either way changes nothing a reader can act on. The browser republishes what it holds whenever its bus session starts, under the stamp the message already carries, because a reconnect is not a press. When the answer lapses, three hours after the last press, the browser publishes the empty payload, which drops the retained message.
The browser acts on a retained delivery alone, which is the broker’s
catch-up on the subscription. A live delivery is the echo of its own
write and changes nothing. It takes the answer where at is inside
the idle window, and it asks again where the stamp is older. A name
the cluster’s Person list does not hold is dropped.
This topic names no availability topic as its Last Will, and that is deliberate. The answer describes the room, not the browser, so it must outlive the browser’s process. A pod the kubelet restarts reads the room back.
The audience
plays/{namespace}/{play}/audience
What the operator holds about a Play that the progress role cannot
read for itself. The operator publishes it retained as soon as the
Play exists, and again whenever it changes, so a progress role that
starts late reads it back from the broker. A Play written by
hand with the same annotations and owner references gets the same
audience.
| Field | Type | Meaning |
|---|---|---|
player |
string | The Player the Play runs on. |
library |
string | The Library the items came from, by name, or absent for a Play this operator did not create. |
people |
list of strings | The Person names on the Play’s owner references, or absent for a Play nobody claimed. |
aliases |
map of strings | The work’s ids by provider, from the Play’s alias annotations. |
season, episode |
integers | The numbers of an episode, absent for a work that has none. |
credits |
list of objects | Every credits mark of the Play’s first item, from its presentation: start and end in seconds, each absent where the mark runs from the start or to the end of the file. Absent where the item has none. The jellyfin role reads it for the watched rule. |
{
"player": "den",
"library": "movies",
"people": ["ada", "grace"],
"aliases": {"tmdb": "1000001", "imdb": "tt0000001"},
"credits": [{"start": 6204.5, "end": 6600}, {"start": 6210}]
}
The final status
plays/{namespace}/{play}/final
The Play’s last status, read off the API by the operator once the
Play’s phase is Finished or Failed, or once the Play is
deleting. Retained. It closes the gap the bus leaves: a progress role
that was down for the last report of a film still records where the
film ended. The positions are H:MM:SS, as the Play status carries
them.
{"phase": "Finished", "item": 1, "position": "1:52:10", "duration": "1:52:10"}
The recorded mark
plays/{namespace}/{play}/recorded
What the progress role wrote last for one Play, retained. The
operator holds a finalizer on every Play and releases it only when
ended is true, so a Play is never deleted before its last position
is in the store. at is the time of the write, RFC 3339 in UTC.
The mark never goes back to ended: false. Once the row is ended, the
progress role writes nothing more for that Play and publishes no new
mark, so a position report that reaches the bus after the final leaves
the row and the mark where the final put them.
{"item": 1, "position": "1:52:10", "ended": true, "at": "2026-08-29T23:01:14Z"}
An outside play
plays/{namespace}/{name}/outside
One play that ran outside the cluster, on a Jellyfin server. The
jellyfin role publishes it from the webhook Jellyfin posts to, from the
reconcile it runs each time the progress role comes online, and from a
backfill of the server’s history. The progress role records it
beside the cluster’s own Plays. It is the one message in the tree
that is not retained: a progress role that was down for one post
catches the next, and a stop repeats the final position.
The {name} segment is not a Play. It is jellyfin-{userId}-{itemId},
so one person’s progress in one item is one row that moves, and a
rewatch moves it again.
The two ids are Jellyfin Guids, in the spelling its API writes: 32 hexadecimal digits with no dashes. The webhook, the reconcile, and the backfill all write that spelling, so the three name one row.
| Field | Type | Meaning |
|---|---|---|
player |
string | jellyfin, in the column that names a Player. |
people |
list of strings | The people who watched, as Person names. |
aliases |
map of strings | The work’s ids by provider. An episode carries the series’ ids. |
season, episode |
integers | The numbers of an episode, and 0 for a work that has neither. |
position, duration |
integers | The position and the length, in seconds. |
ended |
boolean | True on a stop, which marks the row ended. |
at |
integer | The Unix time of the event, which the store writes as the recorded time. A newer at wins over what the row holds. |
A work Jellyfin reports as played carries a position at the end of
the work, whatever resume point Jellyfin holds. A mark a person set by
hand has no position of its own.
{
"player": "jellyfin",
"people": ["ada"],
"aliases": {"tmdb": "1000001", "imdb": "tt0000001"},
"season": 0,
"episode": 0,
"position": 1450,
"duration": 6730,
"ended": false,
"at": 1756508474
}
A mark
plays/{namespace}/{name}/mark
One mark a person set on one title at the media browser: watched, or
cleared. It applies to everyone at the screen. The progress role
records it as one row of the store, beside the cluster’s own Plays.
The jellyfin role reads the same message to send the mark to Jellyfin.
A mark can also name a list of a series’ episodes, which
Pick up here
publishes: see a list of episodes
.
The mark is retained, because the person pressed once and nothing
repeats the press. A progress role that is down when the person
presses reads the mark when it subscribes again. The progress role
clears the topic 24 hours after at, once it has recorded the mark.
A mark it reads after those 24 hours, it records and clears at once.
A payload that is not a mark, it clears at once and records nothing.
Every reader receives a retained mark again on each subscription
within the 24 hours, so a reader must treat a second delivery as the
same mark.
The {name} segment is not a Play. It is mark-{player}-{at}, so
each press is one topic and one row. The browser moves a second press
within the same second to the next second, so no two marks of one
screen share a name. The progress role writes a row only when the
store holds no row of that name recorded at at or later, so a mark
delivered again never changes a row.
The browser takes the branch of the tree from LIBRARY_PLAYS_TOPIC,
{base}/plays/{namespace}, which the operator sets on the browser
container, and the Player’s name from MEDIA_PLAYER_NAME.
| Field | Type | Meaning |
|---|---|---|
mark |
string | watched or cleared. |
player |
string | The Player whose screen the person pressed on, in the column that names a Player. |
people |
list of strings | The people at the screen, as Person names. Empty where nobody answered who is watching. |
aliases |
map of strings | The work’s ids by provider, as a play request names them. An episode carries the series’ ids. |
season, episode |
integers | The numbers of an episode, and 0 for a work that has neither. |
position |
integer | The duration for watched, and 0 for cleared, in seconds. |
duration |
integer | The length of the work in seconds: the duration of the audience’s play of the work where one exists, and the catalog’s running time where none does. |
at |
integer | The Unix time of the press, which the store writes as the recorded time. The row stands over every play recorded before it, and a later play replaces it. |
The row is ended at at, with the phase Finished, because no Play
runs behind it.
A mark that names one work carries no episodes field.
{
"mark": "watched",
"player": "living-room",
"people": ["ada"],
"aliases": {"tvdb": "1000002"},
"season": 2,
"episode": 5,
"position": 2760,
"duration": 2760,
"at": 1759140000
}
A list of episodes
Pick up here marks every earlier episode of a series watched and clears
every later episode the people at the screen started, in one press. The
browser publishes one mark for the whole press, on the same topic and
retained the same way, and the mark lists the episodes in episodes.
The series’ aliases, the people, the Player, and at are stated once.
The mark carries no season, episode, position, or duration of
its own: each entry of the list states its own.
| Field | Type | Meaning |
|---|---|---|
episodes |
list of objects | The episodes the mark covers, in series order. Each entry has mark, season, episode, position, and duration, with the meaning each field has on a mark of one work. |
An entry’s mark is watched or cleared, and an entry with no
mark takes the message’s. The browser states mark on every entry,
and states watched on the message. A message whose mark, or whose
entry’s mark, is neither of the two is no mark: the progress role
clears it and records nothing.
The progress role writes one row for each entry. A watched row is named
{name}-s{season}e{episode} with each number in four digits, as in
mark-living-room-1759140000-s0001e0004. A cleared row is named
{name}-cleared-s{season}e{episode}, as in
mark-living-room-1759140000-cleared-s0003e0002. Every row is recorded
at at. The browser breaks a tie in recorded time on the row’s name.
The four digits make the watched names sort in series order, and every
cleared name sorts before every watched name, so the browser reads the
last watched episode of the list as the newest, and never a later
episode the press cleared. A cleared row stands at position 0, and a
thread that stood on it would offer nothing. The role writes each
row only when the store holds no row of that name recorded at at or
later, so a list delivered again writes only the rows that are missing.
It clears the topic once, as it clears a mark of one work.
The jellyfin role writes each entry to each person at the screen, with the entry’s own mark, and publishes one sent mark for the whole list once every entry has landed. An entry Jellyfin does not hold is done with no write.
The press publishes one message, and not one mark for each episode,
because each mark’s name takes the second of its press. The browser
moves a second press within the same second to the next second, so a
mark for each of 100 episodes would carry an at up to 100 seconds in
the future. The play the same press starts would read as older than
those marks until that time had passed.
{
"mark": "watched",
"player": "living-room",
"people": ["ada"],
"aliases": {"tvdb": "1000002"},
"episodes": [
{"mark": "watched", "season": 1, "episode": 4, "position": 2760, "duration": 2760},
{"mark": "watched", "season": 2, "episode": 1, "position": 2700, "duration": 2700},
{"mark": "cleared", "season": 3, "episode": 2, "position": 0, "duration": 2760}
],
"at": 1759140000
}
A sent mark
plays/{namespace}/{name}/sent
The jellyfin role’s record that it sent one mark to Jellyfin, under the mark’s own name. The role publishes it retained once the mark’s write lands for every person Jellyfin holds, and sends no mark whose record it reads. A jellyfin role that restarts within the mark’s 24 hours reads both back, so it does not send the mark again over a toggle a person made in Jellyfin since. Jellyfin clears an item’s last played date on an unplayed toggle, so the date cannot stop a second send alone.
The role clears the record when the progress role clears the mark. A
record it reads more than 24 hours after at, it clears at once,
because the mark’s clear may have arrived while the role was down.
| Field | Type | Meaning |
|---|---|---|
at |
integer | The at of the mark, the Unix time of the press. |
{"at": 1759140000}
The forget request
people/{person}/forget
The operator’s request that one person’s rows go, to every namespace’s
progress role at once. A Person is cluster-scoped, so the topic has
no namespace. The operator holds a finalizer on every Person, and
when the Person is deleted it publishes this request retained with
the time it asked, so a store that starts later reads an ask it has
not answered. An empty payload is the clear, and every role clears its
own answer when it reads one.
{"at": "2026-08-29T21:34:02Z"}
The forgotten answer
people/{person}/forgotten/{namespace}
One namespace’s answer that the person’s rows are gone from its store,
retained, with the time of the sweep. The operator releases the
Person once every namespace that stands a store has answered, then
clears the request and every answer.
{"at": "2026-08-29T21:34:05Z"}
The progress role’s availability
progress/{namespace}/availability
online while the namespace’s progress role runs. The role names this
topic as its MQTT Last Will with offline as the payload, on the same
terms as the reporter’s. The operator does not read it: it reads the
role’s recorded marks, and a mark that stops moving is the signal it
acts on. The topic is also on the bus for a client that folds the
store’s marks and needs to know whether their writer is gone.
The jellyfin role reads it. Each online starts one reconcile, a read
of every Jellyfin user’s played and resumable items published as
outside plays, because an outside play is not retained and a progress
role that was down lost every post Jellyfin made meanwhile.
What this operator reads from the media tree
The progress role and the jellyfin role read two topics of
media-operator’s tree for every Play in their namespace, under
liken/media by default and under LIBRARY_MEDIA_TOPIC_BASE when that
operator’s base moves: plays/{namespace}/{play}/status, the playback
pod’s report with the position, and plays/{namespace}/{play}/availability,
its Last Will. The progress role joins the position with the audience
above into the rows it records. This operator writes nothing on the
media tree.
The media browser reads the Player’s own topics from the same tree,
which media-operator names in the Player’s status.idle.bus, and
Put the media browser on a screen
describes what the browser does with each.