Repository navigation
0.10.0
Added
- The tagline in every language TMDB describes a work in.
media_translation
carries it (migration 0035), and it is laid over the work as the title and
the overview are — by the native API'slanguage, Sonarr's and Radarr's
answers, the.nfoexport and the Kometa file — a tagline locked for one
language standing over TMDB's in it. Plex is given the tagline in the
language it asks for when the catalogue holds one in it, where it was given
none outside the stored language. - A work's copy of TMDB's answer is six months old at the most, as TMDB's
terms of use ask. A work refreshed longer ago than 180 days — an ended
series kept for a year, or ten — is given a light refresh by the sweep,
after the works that are due and within the same batch: TMDB alone, as a
sync asks it, the other sources keeping what they last gave; the run's
entries say which works were refreshed so.refresh.ttlContinuingand
refresh.ttlEndedsay it in their description, and the Settings page says
it when either is set past six months. - Each source's terms of use, in a line with a link, on Administration ›
Sources;docs/sources.mdsays what the server does with each source's
data, and what it never does. - The editor's Seasons tab lists every picture attached to a season, the
hidden ones struck through with a way to show them again, and hides a
source's picture as the Artwork tab hides the work's own: a season's
hidden picture could be shown again by the API alone. - Sonarr v5 is offered TheTVDB's two further alternate orders,
altdvdand
alttwo("Alternate DVD Order", "Alternate Order 2"), which TheTVDB's own
episodes endpoint answers empty for (thetvdb/v4-api#340): when it does,
the order is read season by season from each season's own record, one
call a season, as the real Skyhook serves it. .github/workflows/parity.yml: every Monday, and on demand, the real
Skyhook's second edition is set beside this server's for four real series
byscripts/e2e-skyhook-v2.sh, and any difference that is not an addition
fails the run. The Office (US) joins the script's default series for its
two alternate orders: a season type the real Skyhook offers and this
server does not is a failure, The Office's "Superfan Order" included. It
needs the same repository secrets as the end-to-end job, and stands aside,
saying so, without them.docs/sonarr-radarr.mdsays what happens when Sonarr is upgraded to v5 —
every series refreshed once, the requests coalesced, the limit per address
and how to raise it for the day — and tells "not found" from "not
answered".- A key with the new
metricsscope readsGET /api/v1/admin/metricsand
nothing else — not the catalogue, not the settings, not the other keys, on
any surface — so what sits in Prometheus's configuration file can read the
figures and no more. Made under Keys & network by ticking Scrape; a
person's own key cannot carry the scope. An administrator's key reads them
as before. - Every request is given an id — sixteen random bytes in hexadecimal, a
trace id's shape — answered inx-request-idand written beside every
log line the request produces, at everyAMS_LOGlevel (request_idon
the request span, which the JSON format files underspan). The
x-request-ida proxy inAMS_TRUSTED_PROXIESsends is kept, so its log
and the server's name a request alike, and itstraceparentnames the
request when it sent no id; anybody else's is replaced. - CI measures how much of the crate the tests reach (
cargo llvm-cov),
informatively: the summary is in the job's log andlcov.infois kept a
week as an artifact. It never fails the run. - Backups. The server makes archives of itself — the whole database as
one SQLite file, which restores on SQLite and on PostgreSQL alike, and,
when asked, the media kept on disk — named for when and why they were
made,ams-20261019-020000-scheduled.tar.zst, with a manifest that says
the version and the last migration that made them, the engine read and
each file's SHA-256. Weekly by default, the schedule's four newest kept
(backup.schedule,backup.keep,backup.includeMedia; migration 0036),
inAMS_BACKUP_DIR—backupsbeside the SQLite database,/data/backups
in the image — or, among several instances whose media are in a bucket, in
that bucket underbackups/. WithAMS_BACKUP_PASSPHRASEset, each
archive is encrypted as age writes it (….tar.zst.age): an archive holds
every account's password hash, every key's and the webhooks' secrets.
Administration › Backups lists them, makes one now, downloads, deletes
and restores them; the taskbackup.runis on the Jobs page, the
dashboard says when the newest archive is older than twice the schedule,
/api/v1/admin/healthcarrieslastBackupAt, a scrape
ams_backup_age_seconds, and the webhooks are toldbackup.completedand
backup.failed. - Restoring an archive from the Backups page makes an archive of the
database first, then answers every request, on every door, with 503 —
/healthaside — until the rows are copied, in one transaction: a restore
that fails leaves the database as it was. An archive of a later version or
migration is refused before anything is touched; an older one is brought
up to this version's migrations first. The audit trail records
backup.restoredandbackup.deleted. - Before a start applies a new version's migrations, the database is
archived as the version before left it (ams-…-upgrade-m0037.tar.zst,
without the media), the two newest kept apart from the schedule's
(backup.keepUpgrades): always on SQLite, on PostgreSQL unless
backup.beforeUpgradeis turned off. An archive that cannot be made stops
the start;AMS_BACKUP_BEFORE_UPGRADE=falsestarts without it.
migrate rundoes the same. - A command line beside the server:
backup,restore— refused while a
server holds the database, which it does from its start, by a lock beside
an SQLite file or an advisory lock on PostgreSQL —,user list|create|reset-password|set-admin,keys create,locks export|import,config check,migrate status|runandversion, beside
healthcheckandtransfer; with no command the binary serves, as
before. What changes accounts, keys or locks goes through the server's own
API in the process, asclient:command line, with the interface's checks
and its audit trail; a password is read from standard input or asked for,
never taken as an argument.docs/cli.mdgives thedocker execand
docker compose runforms the distroless image needs. - A series' orders are orders of their own (migration 0037): each has
the key a client asks for it by, its kind, where it was read and the name
it goes by, and each of its places names an episode the work holds or
carries the record its source gave of one it does not. An order TheTVDB
files apart from the aired episodes is served whole — The Office's
"Superfan Order", 194 extended cuts that are episodes of their own on
TheTVDB, or a DVD's hour-long cut of two aired parts — where its places
named aired episodes or nothing. TheTVDB's orders go by the series' own
name for them ("Superfan Order", "Netflix Order"). The orders 0013 kept
are carried over, and each series that had one is refreshed at the next
sweep, which reads its orders as they are now. Two orders of one name are
told apart in Sonarr's and Plex's lists by the source of the one not
numbering the series — Superfan Order (TMDB). - TMDB's episode groups are orders beside TheTVDB's: listed with the series,
read by the refresh that lists them, four at a time — the editor and a
season's page ask TMDB nothing — offered to Sonarr v5 and Plex under
tmdb-and the group's id. A group that is the same order as one
of the source numbering the series, and holds no more episodes, is not
offered: that one stands for it.docs/sources.mdsays which. - Orders made here, in the editor's Seasons tab: made empty or from
another — the aired one, TheTVDB's, a TMDB group — named, their places
moved up and down and renumbered, and given episodes of their own the work
does not hold, served under TheTVDB's id when they carry one and a stable
negative one otherwise. Sonarr v5 and Plex are offered them underams-
and their name. Through the API:POST /items/{id}/orders,PUTand
DELETE /items/{id}/orders/{key}; the audit trail saysorder.created,
order.updated,order.deleted. - An order can be frozen as the aired numbering, and thawed
(POST/DELETE /items/{id}/orders/{key}/freeze;order.frozen,
order.thawed): Sonarr v4 and v5 asking for the aired order, Plex with no
order chosen, Jellyfin through the TheTVDB relay — its pages of episodes
and a series' record withmeta=episodes— and the.nfoexport are
served it, an episode that airs later keeping its aired numbers where the
order leaves them free, and an episode of the order's own given one id by
Sonarr v4 and v5 alike. It is how a series that has files in Sonarr v5 is
given another order, which Sonarr will not change on it (Sonarr#9055);
the dialog says what it does to XEM's mappings and to the files Sonarr
matched. A frozen series'tvshow.nfocarries<displayorder>, and the
group's id for a TMDB group; a special an order places among the seasons
is written with Kodi'sdisplayseasonanddisplayepisode. - TMDB numbers a series TheTVDB has stopped answering for, after
numbering.fallbackAfterrefreshes in a row — three unless set, twenty
hours apart at the least,0never — each episode keeping the TheTVDB id
it had, until TheTVDB answers again. The audit trail says
item.numbering_fallbackanditem.numbering_restored, a notification
destination that wantsnumbering.fallbackis told, Sonarr v5 is sent
metadataSource: tmdb, and the work's page and its editor carry a banner
with Back to TheTVDB (POST /items/{id}/numbering/restore). A
numbering frozen meanwhile stays TMDB's until it is given back by hand. - A season's page offers every order of the series, TMDB's groups and those
made here among them, names apart two that share a name by their source,
and marks an episode the order carries as its own. docs/api.mdsays what 1.0 promises, and how something goes: under
/api/v1a documented field keeps its meaning and its type, whatever is
added can be ignored, and whatever goes is announced a minor ahead —
under Deprecated here,deprecatedin the OpenAPI document, a
Deprecationheader on the answers that still carry it; Sonarr's,
Radarr's and Plex's surfaces are served for as long as they read them,
and change only as they do; setting keys andAMS_*variables keep their
names, a renamed one read a minor longer with a warning; migrations only
go forward.CONTRIBUTING.mdsays what to write where when something is
deprecated, and when it goes.- Each work keeps a journal of what changed in it, value by value: every
field a refresh, a sync or a person changed — of the work, of a season, of
an episode, of a credit, a picture or a title a lock names — with what it
held, what it took, when, why (refresh,sync,fallback,lock,
unlock,revert,edit,import) and who: a person, a key, a source,
or both when a person asked the sources again (migration 0038). A refresh
writes what it changed at once and nothing when it changed nothing; of a
series' episodes the journal follows the title, the overview, the air
date, the runtime and the still, and an episode that moved is one change
of its numbers. It is the editor's new Journal tab, filtered by field,
by who and by a period, andGET /api/v1/items/{id}/changes; kept a year
unlesshistory.keepDayssays otherwise,0for ever. - Revert to this value, beside a change in the journal, locks the field to
the value it held before it — through the locks' own route,
"cause": "revert", which the audit trail and the journal say; what a
source gave is never written back over its copy. - Under a field locked in every language, the editor says what the sources
give now when it is something else — TMDB now says: …, and changed
since the lock when a source moved it after the lock was set — with
Take this value, which lifts the lock. The dashboard counts the works
with a lock their source moved under, the catalogue filters them
(?missing=divergingLock), andGET /api/v1/items/{id}/provenance
carries them underdiverging. - The editor's refresh shows first what it would change: the sources are
asked and merged as a refresh asks and merges them, nothing is written,
and every value it would write over is listed with what it holds and
would hold — the locked ones are not, since no refresh reaches them. A
field ticked is locked as it stands before the refresh runs.
POST /api/v1/items/{id}/refresh?dryRun=1, and the sync's, answer the
same and write nothing. - After Change a field… over a selection, Undo is offered for eight
seconds, longer while the pointer or the focus rests on it: each work is
put back as its journal says it was, the lock it held or none
(POST /api/v1/items/{id}/changes/undo), and one changed since by
something else is left as it is. - The audit trail and a work's journal are downloaded whole, under the
filters of their page, as a CSV a spreadsheet opens or as JSON
(?format=csv,?format=json): buttons on Audit and in the
Journal tab. A cell a spreadsheet would take for a formula — a refused
sign-in's typed name included — is written so it is not. - A work's page on a phone keeps a bar along the foot of the screen — where
to watch it, its trailer, its address — in place of the site's own, and a
row of its sections under the bar at the top that follows the reader down
the page. The rest of a long cast, the other names, the keywords and the
identifiers past the first few are folded, and the browser's find in page
unfolds them on a match (a<details>where the browser cannot). - The schedule's month is a table a screen reader walks by weekday and
week, its days named in full, each episode's hour a<time>, small
posters in the days wide enough to hold them; a list of the month's days
is a press away; and an episode opens a look at itself beside the day —
what it is, when, where, the way to its page — rather than leaving the
month. - Keys:
/to search,gthenb,corsfor the catalogue, the
schedule or the season chart,rfor a work at random,tfor today in
the schedule, and?for the list of them. None fires while something is
being typed. The arrow keys move between the cards of a grid or a shelf
of posters, by where the cards are drawn; Tab still stops at each card. - Read the catalogue's texts in another language than the interface's:
"Read texts in…" in the language menu passes the chosen language on every
read of the catalogue — titles and synopses where a translation is held —
and leaves the administration alone. - Posters of works for adults — marked so, or rated 18 and over — are
blurred until pressed, or until "Show adult posters" is turned on in the
language menu; the front page never features one for a reader who has
not. - A work's page carries schema.org's description of a
Movieor a
TVSeries— name, poster, synopsis, release, genres, director, rating —
for search engines, only while the site is public. - The front page says what is new rather than what the catalogue holds:
what airs this week, the series back this week, the selections, the
season's new works and the newest arrivals with a trailer — and its
featured picture steps back and darkens as the page is scrolled past it,
where the browser drives animations by scrolling and the reader allows
motion. - A reader whose system asks for more contrast gets it: the quietest text
rises to the tier above it, the hairlines become edges a field can be
found by — 3:1 against every surface — and the grain and the washes go.
Under forced colours (Windows' contrast themes) buttons, choices, chips,
lamps and switches keep an edge in the system's button colour, a chosen
option and a switch that is on wear its selection colours, and the
score's arc and a missing poster's stand-in keep their drawing. - A button that shows only a glyph says its name beside it, to a pointer
that rests on it and to the keyboard that reaches it, where the browser's
own tooltip came late and never to a keyboard. - The installed app's icon carries the number of episodes airing today
while the app is behind another, and drops it when the app comes back: no
notification, no permission, nothing sent. frontend/e2e/vitals.spec.tsmeasures the Core Web Vitals of the front
page, the catalogue, a work and the season chart, and holds them to
Google's "good": LCP within 2.5 s, CLS under 0.1.- A search that ranks, and forgives. A term is found in every name a
work goes by — its title, original title, sort title, each translation's
and each alternative title, and the titles locked by hand — whatever its
accents, its case and its punctuation, in the people the credits name and
in the episodes' titles, from one index both engines read (search_doc,
migration 0039; SQLite's FTS5 trigram table, PostgreSQL'spg_trgm).
What it finds comes in order: the name that is the term, then one that
starts with it, then one with a word that does, then one that holds it,
the better known first among those alike; a year at the end of a term
puts that year's work first. When the letters find nothing a term was
typed to find, the names sharing most of its trigrams are offered:
breking badfinds Breaking Bad. GET /api/v1/search, the search box's: works, people and episodes, each
kind ranked on its own, a work with the other title it was found by. The
command palette lists the three kinds under their headings — an episode
asS05E14 — Ozymandias, opening its page; somebody credited, opening
theirs — and@looks for people alone.- The catalogue's further filters, in a "More filters" sheet: a person
credited (a TMDB id, or a name whatever its case and accents), a keyword,
a running time, the country a work was made in, the rating the reader's
country gives it, the collection a film belongs to; and the orders by
running time, by when a series last aired and by how many episodes it
has. The API takes them asperson,keyword,runtimeMin/runtimeMax,
country,certification(withregion),collectionand
sort=runtime|lastAired|episodes;GET /api/v1/facetsadds the
countries and the ratings to choose from. - The task
search.rebuildwrites the search index again from the whole
catalogue, the search answering throughout. - Two records of one work are found, merged and taken apart again. Two
films, or two series, are proposed as one when they are filed under one
identifier elsewhere — TMDB's, TheTVDB's, IMDb's, AniList's,
MyAnimeList's or Fan-Kai's, held or last said to be theirs — or under one
title a year apart at the most; a film and a series under one IMDb id are
pointed out, and never merged. The dashboard counts them, N works look
like another one, and leads to the catalogue narrowed to them
(missing=duplicates), each row naming the other; Not the same work
sets a pair aside for good.GET /api/v1/admin/duplicates,
POST /api/v1/admin/duplicates/dismiss. - Merge…, from a row of the catalogue or from a pair, sets the two side
by side and keeps the record its sources fill over one entered by hand,
which nothing refreshes; then the one somebody put something into by hand,
else the one filed under more identifiers, else the older — unless told
otherwise. Each field that differs can be kept from either side, a value
taken from the other locked. One transaction moves seasons, episodes,
pictures, credits, titles, translations, ratings, orders, places in
selections and the journal onto the kept work, its own standing wherever
both hold the same; the other stays as a switched-off record no list
shows, and its id, its slug and its identifiers answer with the kept work
for good: on the site, which puts the kept work's address in place of the
old one, in the API, and to Sonarr, Radarr and Plex, each answered under
the id it asked by. A write under the old id is refused with409
(work_merged), naming the kept work. The journal (Merge), the audit
trail (item.merged) and, witharr.pushRefresh, Sonarr or Radarr hear
of it (migration 0040). - A merge is undone from the kept work's editor for
merge.undoDaysdays —
14 unless set,0for never: the record comes back as it was, what
changed on the kept one since stays, and the two are not proposed again.
POST /api/v1/items/{id}/unmerge(410past the window),
GET /api/v1/items/{id}/merges. - People, each a record of their own (migration 0041). Everybody the
credits name is one person, whatever the credits call them: a record
with the ids TMDB, TheTVDB and IMDb file them under, the other names they
go by and their name in their own script, which every credit is filed
under as it is written. A credit the sources give again keeps the person
it had, so a credit split off a homonym, given to somebody by hand, or
merged with its person into another stays where it was put. At the
upgrade, a record is made for each TMDB id the credits hold and for each
name the others give; a credit by name alone is taken for the one person
known by a TMDB id who goes by that name — and split off by hand where
that is a homonym. - A person's page reads their work by job — acting, directing, writing,
producing, each with how many works — in the order of the years, the
titles or the scores, films or series apart, a guest's episodes gathered
under their series with how many and which years; their name in their
own script is marked with its language. What TMDB says of them is asked
the first time their page is read and kept a month. A person TMDB does
not know has a page too, at/people/{id}, and the cast on a work's page
leads to it;/person/{tmdbId}still leads where it did. - The index of people,
/people, from the Browse menu: by letter and by
job, the most credited first or by name, and a name found as the search
finds it (GET /api/v1/people). - Administration › People: everybody, those known by no TMDB id and those
who share a name — the likely homonyms and duplicates; two records merged
into one, credits split off onto a record of their own, somebody entered
or put right by hand with their ids, each on the audit trail
(person.created,person.updated,person.merged,person.split,
person.credited). The editor's People tab leads each credit to its
person and gives a credit typed by name the person it is of.
POST /api/v1/people,PUT /api/v1/people/{id},
POST /api/v1/people/{id}/merge,…/splitand…/credits. - Each episode's guests, director and writers, read from the season
documents TMDB already answers: credited on that episode — the work's
own, as TheTVDB may number it — and not on the series, whose cast is what
it was. - TheTVDB's characters, the first
tvdb.castLimitof them (25 when
unset): matched into the cast another source gave by name, whatever its
accents, its case and the order of its words, giving the person
TheTVDB's id; those the cast lacks are added after it, and the editor's
sources credit TheTVDB beside the source of the cast. - The NFO's
<actor>names the person by<tmdbid>and<imdbid>where
their record has them. - This server's own pages are given copies of the pictures it keeps, at
the widths they are drawn at — a poster at 240, 480 and 960 pixels, a
background at 640, 1280 and 1920, a logo at 400 and 800, a still at 320
and 640, a photograph of the cast at 120 and 240 — as WebP, never wider
than the picture nor than 1920 pixels. A copy is named by its picture's
hash and its width (/media/<hash>-480.webp), made the first time a page
asks for it, and kept beside the picture; the one a page asks for first,
a poster's 480 or a background's 1280, is made as the picture is stored.
On the development catalogue, at 1440 pixels on a dense screen, the home
page's pictures weigh half what they did and a work's 58 % less. Clients
are given the originals, as before;docs/media.mdsays how the copies
are made and kept. - What a page draws while a picture loads: the colour most of it is and a
ThumbHash sketch of it, worked out as the picture is stored and given by
the native API with its size (pictures, beside a work'simages). The
interface paints them under the picture and gives its box its
proportions before a byte of it arrives; a poster card's frame takes the
poster's colour rather than its genre's. - Prepare the pictures (
media.variants), on Tasks and on the Media
page, which says how many pictures kept before this version have neither
a placeholder nor their first copy: it prepares them a few at a time, can
be stopped, and takes up where it stopped. It runs only when somebody
runs it. - AVIF copies at night: with the new setting
media.avifon (off until it
is set), the taskmedia.avifmakes, each night at 03:00 UTC and on
request, an AVIF copy of every poster at 480 and 960 pixels and of every
background at 1280 and 1920, one at a time on one core, a third lighter
than the WebP. A page offers them only where they exist. The container
image is built with rav1e's assembly (theavif-asmfeature), which
makes a copy in less than half the time. - Key art, disc art and character art:
keyart(a textless poster of
TMDB's or Fanart.tv's, or one put on by hand),discart(Fanart.tv's
moviedisc) andcharacterart(Fanart.tv's, filed as clear art until
now), shown in the editor's Artwork tab and offered when adding a
picture, and exported askeyart.jpg,discart.pngwithdisc.png
beside it, andcharacterart.png— none of them ever a<thumb>of the
.nfo. Fanart.tv's 4K backgrounds are read too, after its HD ones. - Plex is given a
backgroundSquare: the middle of a work's lead
background, 1080 pixels a side, where this server keeps the background. - A genre, a network or a studio, a year and a decade have pages of their
own:/genre/drama,/network/hbo,/year/1999,/decade/1990. Each
leads with its name — a genre's over a band in the colour its chips
wear — how many works it holds, its programmer's note and the five rated
highest, then lists every one of them in the catalogue's own list, its
filters, orders and views, with the page's subject always applied. A
work's genres, network and year lead there, as do the genres of the
Browse menu, and a link to one carries a preview of it wherever a reader
with no credential may see the catalogue.GET /api/v1/genres/{slug},/networks/{slug},
/years/{year}and/decades/{year}answer them. - The programmer's note: a few lines in the catalogue's own voice, set
apart in brass, on the work the front page features, on each selection
and on each genre's, network's, year's and decade's page — written for
each language, the English one standing in for a language with none. A
restricted Markdown (paragraphs, emphasis, links to pages of the site),
checked as it is saved and never read as HTML. An editor writes it in
place on the page, with what it will look like beneath the field, or
from Administration › Catalogue › Programmer's notes; each change is
editorial.updatedin the audit trail;/api/v1/editorialreads and
writes them, a thing nothing is written about answeringnull
(migration 0043). - Two works side by side,
/compare?a=…&b=…, from a work's Compare with
another work: their scores source by source, running times, seasons and
episodes, networks or studios, genres, and the people both credit with
what each did on each.GET /api/v1/compare. - Traces of each request, sent over OTLP to Tempo, Jaeger or an
OpenTelemetry collector whenOTEL_EXPORTER_OTLP_ENDPOINTnames one —
off otherwise, and nothing built. A request is a server span named by its
route, and each call it makes to a provider a client span under it, which
carries the trace on in a W3Ctraceparent. A trace is named by the
request's id, and a trusted proxy'straceparentis joined. Only the
spans leave, every address in them masked;OTEL_SERVICE_NAME,
OTEL_TRACES_SAMPLERand the rest of OpenTelemetry's variables apply.
Seedocs/monitoring/README.md. - Each JSON log line written in a request begins with its
trace_id,
which leads from a line in Loki to its trace in Tempo. - Administration › Server log: the last two thousand lines the server
wrote, by level and by words, followed as they come, a request's lines
one press away, downloaded as text or JSON lines; every secret in them
masked.GET /api/v1/admin/logsanswers the same, for an administrator. - Error reports to a Sentry or a GlitchTip, off until a DSN is set under
Settings › Error reports (sentry.dsn, written once and never shown,
sentry.environment,sentry.sampleRate), and taken at once on every
instance: each error line, masked, with thetrace_idandrequest_id
that find it in the log and the traces. - Figures:
ams_http_responses_total{surface,status}, the database's pool
(ams_db_pool_size,_idle,_max, andams_db_pool_wait_seconds),
ams_queue_depth{kind}for the refreshes due, the media to fetch and
the webhook deliveries,ams_refresh_failed, and
ams_tls_expiry_seconds{certificate}. docs/monitoring/alerts.yml: eleven alerts — down, no leader or two,
server errors, a source failing, failed refreshes rising, Valkey
unreachable, the pool exhausted, a certificate ending, a stale backup —
checked bypromtool, with tests; the Grafana dashboard gains the
statuses, the pool, the queues, the certificates and the archives.- A field of the work editor is edited where it is shown: its value opens a
box in its place, and the change is kept, marked in brass on the field
and its tab, until the bar along the foot of the page saves it — Save
and stay (⌘S), or Save and open the next work of the catalogue's list
(⇧⌘↵). Leaving with a change unsaved asks first. - The editor can show the work's public page beside it, from 1280 pixels
of screen, or over it below that; it reloads after each save. - The administration's tables keep their head in view, are ordered by a
column's head, hide and show their columns and set their density — kept
with the account, in every browser — become cards on a narrow screen,
and are walked a row at a time with the keyboard. - The catalogue keeps its search, filters and order in the address; it
offers a few views, and saves your own under a name, with your account. - The palette does what the page is about: refresh a work or the rows
chosen, lock its overview as it is, enable or disable it, add it to a
selection, run a task. Each command shows its keys, which work without
it;gthen a letter goes to a place. - The catalogue lists the works whose pictures or theme the media store
gave up fetching (?missing=mediaFailed), and says beside each of its
filters how many works it leaves (GET /api/v1/admin/gaps). - What a person keeps of their own way of working is kept with their
account:GET,PUTandDELETE /api/v1/account/preferences
(migration 0045). - The Jobs page follows the tasks as they run, whichever instance runs
them:GET /api/v1/admin/tasks/streamsends, as server-sent events named
tasks, the whole board at once and again whenever a run starts, moves
on or ends — read from the history every instance writes and the holds
they share — with a comment every fifteen seconds and a new stream every
five minutes. A running task's card draws how far it has got, and offers
Cancel, Log and, once it has run, Run again. Where the stream
cannot be opened the page asks every few seconds instead, and a hidden
tab is followed by neither. - The sweep, the anime identifier list, IMDb's ratings and the archive can
be kept to a window of the day (01:00to06:00) or run at the
moments a cron expression names (0 4 * * 0), in UTC: set from their
card, which says the next three runs in the reader's own time as the
expression is written.PUTandDELETE /api/v1/tasks/{id}/schedule,
POST /api/v1/tasks/schedules/preview; kept intask_schedule
(migration 0044), so every instance obeys the one set on another. The
task that keeps the catalogue's order current,listing.relist, is
listed with the others. - Import many, on the Import page: ids pasted, a CSV or JSON file dropped
— Radarr's, Sonarr's, IMDb's and Trakt's exports read by their columns —
or works picked from the search are set beside the catalogue (already
here, to import, not found, not an id, film or series?, repeated), then
imported as one run of the taskimport.batch: into the hand-made
selections chosen, switched on or not, each series' episodes fetched at
once in a language chosen. Two thousand lines a batch.
POST /api/v1/items/import/preview,POST /api/v1/items/import/batch. - Import rules, at the foot of Lists: TMDB's most popular films or series
of the last week to year, or another server's list followed by its
ids.txtor an Atom or RSS feed, brought on a schedule into a hand-made
selection — in place of its works or beside them — each run a run of
import.rules.GETandPOST /api/v1/import/rules,PUTand
DELETE /api/v1/import/rules/{id},POST /api/v1/import/rules/{id}/run,
POST /api/v1/import/rules/preview; kept inimport_rule(migration
0044). - The media by work: the Media page ranks the works by what their media
and copies weigh, by how many they keep or by how many were given up on
(GET /api/v1/media/works), a work's media list gives itstotals, and
a dashboard tile says what is kept, what it weighs, names the three
heaviest works and leads to the works whose pictures were given up on. - IMDb's titles by region and its episodes, two more of its
non-commercial lists, each off until switched on (imdb.akas,
imdb.episodes) and each downloaded weekly by a task of its own —
IMDb titles by region, IMDb episodes — that can be run by hand,
given a window or a cron expression, and stopped. Neither list is ever
held whole: half a gigabyte and fifty-five megabytes are read as they
arrive, a line at a time, and only the works and series held here are
kept (migration 0046), written a batch at a time. Radarr is given a
film's titles in the regions of the new settingimdb.regions— per key
and per address too; unset, the country of the language it is answered
in — among its alternative titles, each once; Sonarr is given none,
since each alias would cost it a search. The data stays out of the
search, the feeds, the exports and the relays, as IMDb's terms ask. - An episode's IMDb id, matched by its season and number in the official
order where IMDb numbers it alike — never a special, nor a number IMDb
gives twice, nor a season IMDb counts with fewer episodes than the
catalogue but the last:<uniqueid type="imdb">in its.nfodocument,
and on its page IMDb's rating of it, marked by number, with a link to
IMDb.GET /api/v1/items/{id}/imdb/episodesgives them; IMDb's daily
ratings list keeps the episodes' ratings while the episodes are read. - Lists downloaded whole on Administration › Sources: what IMDb's three
lists and the anime identifier list bring, whether each is on, when it
last landed and ran, and when it comes next
(GET /api/v1/sources/datasets). - A styleguide for whoever works on the interface: every component in each
of its states — at rest, under the pointer, reached by the keyboard,
pressed, disabled, waiting, failed and empty — in both themes and both
languages, side by side, at/__styleguidein a development build or
one made withvite build --mode styleguide. What ships never has it. - The interface is held to pictures: CI compares every gallery of the
styleguide, and a few pages as a visitor and an administrator first meet
them, with pictures drawn in the official Playwright image; Draw the
pictures again, a workflow run by hand, draws them anew and opens a
pull request with the ones that changed. - Five glyphs: compare, now beside a work's Compare with another work,
bookmark, keyboard, filter and history. - The catalogue for agents. An MCP server at
/mcp, off until an
administrator switches it on (mcp.enabled, on Keys & network):
Claude Code, Open WebUI or any agent that speaks the Model Context
Protocol can search the catalogue, read a work and its episodes, the
week's schedule, the curated lists, where a value came from, the
server's health and what needs a hand — eight tools that only read, each
call written in the audit trail asmcp.called. It takes a key with the
newmcpscope, in a header; given that scope alone, a key opens/mcp
and nothing else. The panel makes one and gives the line that connects
Claude Code. See Connecting Claude Code or Open WebUI in the API's
documentation. - Pages by cursor. The catalogue's list, the audit trail and a
notification destination's log answer withnextCursor; given back as
cursor, the next page begins just after the last row read, whatever
was added or removed meanwhile. The offset keeps working.
Changed
- MyAnimeList is reached through Tenrai before Jikan: the official API when
AMS_MAL_CLIENT_IDis set, Tenrai otherwise, and Jikan — whose public
service closed on 1 October 2026;AMS_JIKAN_UPSTREAMis for an instance
of your own — only as the last way in whilemal.fallbackis on. The
hints, the Sources page and the documentation say so. - The public footer credits each source on its own terms: TheTVDB with a
direct link, TVmaze with its licence named (CC BY-SA 4.0), MyAnimeList
"via Tenrai" while the mirror serves —GET /api/v1/sourceslists
tenraithen — beside the notices TMDB and IMDb require. - The interface ships its fonts as woff2 alone. Libre Bodoni's faces are
declared by the interface rather than imported from the package, whose
stylesheets listed a woff beside every woff2 — twelve files for browsers
nobody runs it in. - The lines a request writes to the log carry its method, address and id
at the default level: the request span is atinfo, where it was
debugand never entered. - Internal, with nothing to see from outside: CI's PostgreSQL job runs every
test that opens a database or builds the whole server — the native API's
handlers, the Plex provider, the relays, the services, the clients'
authority, the update check, the settings reference, the arr history —
where it ran the repositories and the parity test alone, each server a
test builds given a PostgreSQL database of its own; CI typechecks the
end-to-end suite (npm run typecheck:e2e), which Playwright compiles
without checking, and the fourteen strictness errors nobody saw are
fixed; four functions and a constant kept under#[allow(dead_code)]are
gone, and six fields TMDB's models read nowhere; the transfer's own test
followsDROP TABLE,CREATE TEMP TABLEand the name a renamed table
takes; the provider fan-out a refresh awaits is boxed, so a debug build's
futures stay small;CONTRIBUTING.mdsays how a release is cut. - The OpenAPI document gives the values of the fields the native API fills
from a fixed set, where it said "string": a site's access, what an API
asks of a caller, which relay and whose key it asks with, how MyAnimeList
is reached, the media store's backend and what is kept of each of a work's
media, the clients' door's certificate, a cache's space, a destination's
kind and why a test of it was not sent, a run's status and its entries'
outcome, a task's id, a push's outcome, a setting's source. Nothing the
server sends or accepts has changed; a client generated from the document
gets the values, and the interface's types take them from it instead of
repeating them by hand. transferreads its source as of one moment — one read transaction,
REPEATABLE READon PostgreSQL — and a table a chunk at a time rather
than whole: a database a server is writing to copies without an episode
whose season was read before it existed, and a large table no longer
passes through memory in one piece.- The command line is parsed by clap:
--helplists every command, and an
unknown command exits with 2. An archive is put together beside the
archives, inAMS_BACKUP_DIR, rather than in the system's temporary
directory;compose.multi.yamlmounts a volume at/datafor it. The
backup settings have a panel of their own on the Settings page. media_episode_orderis gone (migration 0037): a transfer between
databases, and a backup, carrymedia_order,media_order_episodeand
media_numberingin its place.POST /api/v1/items/{id}/refreshandPOST /api/v1/items/{id}/syncare
documented as answeringRefreshAnswerandSyncAnswer: the work, or
the sync's outcome, as before — or, withdryRun, what they would write
over.- The bar at the top is calmer: the palette's ⌘K is said in the search
field beside its/, and the languages are one small menu; between a
laptop's narrowest width and 1280 px the search is the button that opens
it under the bar, which no longer runs past the edge of the screen. - Signing in and signing up say what is missing or wrong under each field
once it has been left, or when the form is sent, in the interface's
language, and put the cursor in the first field to fix; the password
fields are named for password managers (current-password,
new-password). - Alternative titles lead to a search of the catalogue, and the directors
and writers named under a synopsis to everything the catalogue holds with
them. - Nothing that says something is smaller than twelve pixels: a field's
name, a chip, a rating on a card were eleven, a vote count eight. - Counts follow each language's grammar (
Intl.PluralRules): French says
« 0 œuvre », and a new language can give its own forms. - Going back slides the page the other way it came, and the scroll position
is put back once the page it belongs to is drawn; dialogs, sheets and
menus rise in and fall away. None of it moves for a reader who asked for
less motion. - Lists open in the interface's colours where the browser lets a select be
drawn, and in the system's elsewhere. - Text no longer jumps when the typefaces arrive: the system fonts drawn
meanwhile are reshaped to Libre Bodoni's and Public Sans's measurements. - Headings break into even lines and paragraphs no longer end on a lone
word, where the browser can; the page keeps room for its scrollbar, so a
short page and a long one line up. - A long grid of posters lays out only the rows near the screen, and a
poster's frame is tinted by the work's genre while the picture loads. The
front page's rows keep their place while their reading is out, and the
catalogue's count of results its line before the count is known: each row
arrived above those already drawn and pushed the page down, a shift the
Core Web Vitals spec now holds the pages to. - An empty screen says why — nothing yet, nothing under these filters, a
failure — and a failure with no network says so. GET /api/v1/items?term=is in the order of how well each work answers
the term unlesssortis given (sort=relevancesays it); a term of
punctuation alone finds nothing where it found every work, and a term
shorter than three letters is looked for at the start of a name. Sonarr's,
Radarr's and Plex's searches list the work held here by that very name
before a better known one that only starts with it, and before TMDB's
answers.- The catalogue lists its filters' chips at the head of the filters, with
"Clear all"; shows six columns from 1440 pixels; and asks for the next
page as the end of the list comes near, the button staying for whoever
reaches it first. - The first start after the upgrade writes the search index in the
background, a few hundred works a pass: a catalogue of fifty thousand
works is searchable whole after about four minutes, and finds what is
already written meanwhile. - The bundled SQLite is compiled without its memory statistics
(SQLITE_DEFAULT_MEMSTATUS=0), whose global mutex was the ceiling under
many searches at once;docs/perf/README.mdhas the measurements. - The search finds a person by every name they go by — TMDB's other names,
their name in their own script — once, under their record, and a credit a
lock hides names nobody: somebody whose every credit is hidden is not
found.GET /api/v1/search's people carry their record'sid, their
portrait, what they did (jobs) and the name they werematchedby, and
the palette leads to their page whether TMDB knows them or not; the
list'spersonfilter takes a record's id too, and leaves out what a
lock hides. GET /api/v1/people/{id}takes a record's id as well as a TMDB id;
tmdbIdis absent for somebody TMDB does not know, and each ofroles
carries its credit's id and, for an episode's credit, the season, the
episode, its title and the day it aired. Whoever may edit the catalogue
is also givenrecord, the record as it is kept.- The first start after the upgrade writes the people's rows of the search
index, and the index of people's counts, again in the background, as the
works are written. - No JPEG thumbnail (
-t.jpg) is made any more: the pages ask for the WebP
copies. Those already made are still served. - A picture is decoded with limits: past 16,384 pixels a side, 100 million
pixels or 256 MB of pixels it is not decoded at all, and is served whole. media_assetkeeps each picture's colour and sketch, andmedia_variant
lists the copies (migration 0042); a database transfer carries it. A
backup carries no copy, which is made again when asked for.- The Media page counts the copies and what they weigh, and
GET /api/v1/media/statussays so (variants,unprepared,avif,
preparing). - A genre is one genre whatever language its works were fetched in:
TMDB's "Drama" and "Drame", its "Science Fiction" and AniList's "Sci-Fi",
TheTVDB's "Children" and TMDB's "Kids" are listed under one slug,
offered once and counted once, and a filter by any of their names finds
them all.genre=takes any name a genre goes by, or its slug, and each
genre of the facets carries itsslug. Every work is listed again once
in the background after the upgrade; the genres a work is served with to
Sonarr, Radarr, Plex and the NFO files do not change. The similar works
count a genre shared whatever language each names it in. - A work's page shows a series' combined genre as the two it stands for —
"Action & Adventure" is Action and Adventure — each leading to its page,
as the catalogue already filed them. - A panic is written as an error line, under the request it happened in,
before the usual message on standard error. - The sweep's cadence is read from its runs in the database, so it holds
when another instance becomes the leader or the server restarts; the
archive's schedule is looked at every minute. - The Jobs page's history and the Media page's tables are drawn as the
administration's other tables: the head stays in view, columns hide, a
narrow screen gets cards, and the rows are walked with the keyboard —
Enter opens what a run did, or a work's editor. - The IMDb ratings' line under the further sources counts the ratings
kept, episodes included, rather than calling them works. - The colours that say a state are named for it — done, wants a look,
failed — and the interface asks for them by that name wherever it says
one. The lint refuses a bracketed size, corner or colour where the theme
has a step for it, and writes the step in. - The log no longer writes the MCP SDK's own lines below a warning
(rmcp=warnin the default filter);AMS_LOGdecides as before. /.well-known/oauth-protected-resource,
/.well-known/oauth-authorization-serverand
/.well-known/openid-configurationanswer404rather than the
interface's page.ams_http_requests_totaland its kin count/mcpunder
surface="mcp".
Removed
-
The title index of 0022 (SQLite's
media_searchand its triggers,
PostgreSQL's five trigram indexes on the works' titles), which the search
no longer reads. -
episodesinGET /api/v1/calendar, deprecated since 0.8.0: the
episodes are the entries ofentrieswhosekindisepisode, beside
the films' releases.
Fixed
- Every control is named with the words it shows (WCAG 2.5.3, Label in
Name), as axe-core 4.14 now checks: a work's card is named by its poster,
rating, title and year rather than "Open …"; the language button says its
code without brackets; a day of the schedule's week is named by its short
weekday and date before the month and what is on; the season chart writes
a week's month as its axis does, and draws the count its name already
says; in the administration, a row of the catalogue is named with its
year, a field of the editor by what pressing it does and then by the value
it shows, an order's copy by the words on its button, and a log line's
trace by the id as far as it shows it. - Sonarr is never told that a series does not exist when a source could not
be asked. A series the catalogue holds is answered from it, as before; an
id nobody holds is a404only when every source that could resolve it —
TheTVDB, Skyhook, TMDB — answered that there is no such series, and a
502when one of them timed out, failed or refused the connection: Sonarr
takes a series for deleted on a404and keeps what it has on anything
else. A search nothing answered while a source did not is a502rather
than an empty list, which Sonarr reads as "no results"; a name the
catalogue holds is still answered. On both editions of the surface. - An
imdb:lookup for a series is fetched once when asked for twice at
once, as a lookup by the other ids already was. - The page says which language it is read in from its first paint: the
stored choice sets<html lang>before the interface loads, where it was
enuntil it had. - The editor's rail of seasons scrolls within itself on a wide screen: stuck
beside the fields, a rail of forty seasons kept its last ones under the
fold, reachable only at the page's end. - French said "1 en échec" for none and "0 jours", "0 images"; counts of
nought are now singular. The light theme's washes were the dark theme's
colours. - A shared address no longer carries the section the reader was at: pressing
Watch or a section chip left#watchor#detailsin it, and the
recipient landed mid-page. - The catalogue keeps its titles readable beside the sidebar between 1024
and 1280 pixels, where they were cut to a few letters: the identifiers
and the time of the last change show from 1280 pixels, and the title
column always keeps a width of its own. - Every dialog is named by its title, which a screen reader says as it
opens: they were announced as a dialog and nothing more. - Lists to choose from show their chevron, in each theme's colour, on the
field's own ink, with their words centred: the chevron had never been
drawn, and the field had lost its fill. - A disabled field looks disabled, and a field the page says is wrong
wears the error's edge everywhere, not only on the sign-in forms. - A plate's shadow carries the accent as it is, not as it was before it
was lifted.
Security
- The content security policy allows no inline styles:
style-src 'self',
where it was'self' 'unsafe-inline'. The interface's styles are its
stylesheet, and what React writes through astyleprop goes through the
CSSOM, which the policy does not judge; nothing was ever inline. The API's
documentation alone,/api/docs, keeps the allowance for the styles
Swagger UI writes as it draws — chosen by path, scripts this origin's
there as everywhere. - A scrape no longer needs an administrator's key: see the
metricsscope
above. - The server's pages may be framed by the server itself, and still by
nobody else (frame-ancestors 'self',X-Frame-Options: SAMEORIGIN),
for the editor's preview. - The MCP SDK is pinned to
rmcp3.7, past GHSA-4432-vg92-fqh6 (an
unauthenticated memory exhaustion in 3.0–3.5) and the earlier
DNS-rebinding and session-leak advisories;/mcpanswers only under the
server's own names, refuses other sites' pages, takes no cookie, and is
rate limited like the API. - Where the backups' directory cannot be written, a backup is put together
in the system's temporary directory under a name nobody can guess, in a
directory only the server's user may read, made afresh each time —
never under a fixed name another user of the machine could have made
first, nor swept there beyond the server's own leftovers: a link planted
under the old name would have led the sweep into a directory of the
server's. rustls-webpki0.103.16,webpki-rootsandwebpki-root-certs1.0.11,
ctutils0.4.5 andtokio1.54.1, past their advisories
(RUSTSEC-2026-0381, -0382, -0383, -0385, -0386); the interface'szod
and the documentation site'svite,zodandundicilikewise. The advisory on
crypto-bigint0.5 (RUSTSEC-2026-0387) is set aside with its reason:
p256only verifies public signatures and age encrypts with a
passphrase here, and the fix waits foropenidconnectandage.- The workflows keep no token in a checkout, give the image's write right
to the jobs that push it rather than to the whole workflow, pin the
databases their tests start by digest, and run one at a time where two
would collide. Rust is installed by rustup through an action of the
repository's own,.github/actions/rust-toolchain: the commits of
dtolnay/rust-toolchainthey were pinned to had been left on no branch
of it as its branches were rewritten, where nothing told them from a
fork's. The minimum version's job reads the version from Cargo.toml. - What lets anybody post as the server — a Gotify application's token, an
ntfy topic's token, the address of a Discord or Slack webhook, which holds
its own — goes over plainhttponly to an address of the home network
(a private or Tailscale address, a name without a dot,.lan,.local,
.home.arpa,.internal). Anywhere else it needshttps: the
destination is refused when it is saved, and a post sayscleartext
instead of sending it. - A batch from Discover goes into twenty selections at the most; one that
names more is refused before any is looked up.