Skip to content

0.10.0

Choose a tag to compare

@Dim145 Dim145 released this 25 Oct 23:43
· 47 commits to main since this release

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's language, Sonarr's and Radarr's
    answers, the .nfo export 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.ttlContinuing and
    refresh.ttlEnded say 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.md says 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, altdvd and
    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
    by scripts/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.md says 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 metrics scope reads GET /api/v1/admin/metrics and
    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 in x-request-id and written beside every
    log line the request produces, at every AMS_LOG level (request_id on
    the request span, which the JSON format files under span). The
    x-request-id a proxy in AMS_TRUSTED_PROXIES sends is kept, so its log
    and the server's name a request alike, and its traceparent names 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 and lcov.info is 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),
    in AMS_BACKUP_DIR — backups beside the SQLite database, /data/backups
    in the image — or, among several instances whose media are in a bucket, in
    that bucket under backups/. With AMS_BACKUP_PASSPHRASE set, 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 task backup.run is on the Jobs page, the
    dashboard says when the newest archive is older than twice the schedule,
    /api/v1/admin/health carries lastBackupAt, a scrape
    ams_backup_age_seconds, and the webhooks are told backup.completed and
    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 —
    /health aside — 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.restored and backup.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.beforeUpgrade is turned off. An archive that cannot be made stops
    the start; AMS_BACKUP_BEFORE_UPGRADE=false starts without it.
    migrate run does 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|run and version, beside
    healthcheck and transfer; with no command the binary serves, as
    before. What changes accounts, keys or locks goes through the server's own
    API in the process, as client: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.md gives the docker exec and
    docker compose run forms 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.md says 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 under ams-
    and their name. Through the API: POST /items/{id}/orders, PUT and
    DELETE /items/{id}/orders/{key}; the audit trail says order.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 with meta=episodes — and the .nfo export 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.nfo carries <displayorder>, and the
    group's id for a TMDB group; a special an order places among the seasons
    is written with Kodi's displayseason and displayepisode.
  • TMDB numbers a series TheTVDB has stopped answering for, after
    numbering.fallbackAfter refreshes in a row — three unless set, twenty
    hours apart at the least, 0 never — each episode keeping the TheTVDB id
    it had, until TheTVDB answers again. The audit trail says
    item.numbering_fallback and item.numbering_restored, a notification
    destination that wants numbering.fallback is 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.md says what 1.0 promises, and how something goes: under
    /api/v1 a 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, deprecated in the OpenAPI document, a
    Deprecation header 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 and AMS_* variables keep their
    names, a renamed one read a minor longer with a warning; migrations only
    go forward. CONTRIBUTING.md says 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, and GET /api/v1/items/{id}/changes; kept a year
    unless history.keepDays says otherwise, 0 for 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), and GET /api/v1/items/{id}/provenance
    carries them under diverging.
  • 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, g then b, c or s for the catalogue, the
    schedule or the season chart, r for a work at random, t for 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 Movie or 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.ts measures 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's pg_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 bad finds 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
    as S05E14 — 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 as person, keyword, runtimeMin/runtimeMax,
    country, certification (with region), collection and
    sort=runtime|lastAired|episodes; GET /api/v1/facets adds the
    countries and the ratings to choose from.
  • The task search.rebuild writes 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 with 409
    (work_merged), naming the kept work. The journal (Merge), the audit
    trail (item.merged) and, with arr.pushRefresh, Sonarr or Radarr hear
    of it (migration 0040).
  • A merge is undone from the kept work's editor for merge.undoDays days —
    14 unless set, 0 for 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 (410 past 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, …/split and …/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.castLimit of 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.md says 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's images). 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.avif on (off until it
    is set), the task media.avif makes, 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 (the avif-asm feature), 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) and characterart (Fanart.tv's, filed as clear art until
    now), shown in the editor's Artwork tab and offered when adding a
    picture, and exported as keyart.jpg, discart.png with disc.png
    beside it, and characterart.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.updated in the audit trail; /api/v1/editorial reads and
    writes them, a thing nothing is written about answering null
    (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 when OTEL_EXPORTER_OTLP_ENDPOINT names 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 W3C traceparent. A trace is named by the
    request's id, and a trusted proxy's traceparent is joined. Only the
    spans leave, every address in them masked; OTEL_SERVICE_NAME,
    OTEL_TRACES_SAMPLER and the rest of OpenTelemetry's variables apply.
    See docs/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/logs answers 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 the trace_id and request_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, and ams_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 by promtool, 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; g then 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, PUT and DELETE /api/v1/account/preferences
    (migration 0045).
  • The Jobs page follows the tasks as they run, whichever instance runs
    them: GET /api/v1/admin/tasks/stream sends, 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:00 to 06: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. PUT and DELETE /api/v1/tasks/{id}/schedule,
    POST /api/v1/tasks/schedules/preview; kept in task_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 task import.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.txt or 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. GET and POST /api/v1/import/rules, PUT and
    DELETE /api/v1/import/rules/{id}, POST /api/v1/import/rules/{id}/run,
    POST /api/v1/import/rules/preview; kept in import_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 its totals, 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 setting imdb.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 .nfo document,
    and on its page IMDb's rating of it, marked by number, with a link to
    IMDb. GET /api/v1/items/{id}/imdb/episodes gives 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 /__styleguide in a development build or
    one made with vite 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 as mcp.called. It takes a key with the
    new mcp scope, 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 with nextCursor; 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_ID is set, Tenrai otherwise, and Jikan — whose public
    service closed on 1 October 2026; AMS_JIKAN_UPSTREAM is for an instance
    of your own — only as the last way in while mal.fallback is 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/sources lists
    tenrai then — 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 at info, where it was
    debug and 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
    follows DROP TABLE, CREATE TEMP TABLE and the name a renamed table
    takes; the provider fan-out a refresh awaits is boxed, so a debug build's
    futures stay small; CONTRIBUTING.md says 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.
  • transfer reads its source as of one moment — one read transaction,
    REPEATABLE READ on 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: --help lists every command, and an
    unknown command exits with 2. An archive is put together beside the
    archives, in AMS_BACKUP_DIR, rather than in the system's temporary
    directory; compose.multi.yaml mounts a volume at /data for it. The
    backup settings have a panel of their own on the Settings page.
  • media_episode_order is gone (migration 0037): a transfer between
    databases, and a backup, carry media_order, media_order_episode and
    media_numbering in its place.
  • POST /api/v1/items/{id}/refresh and POST /api/v1/items/{id}/sync are
    documented as answering RefreshAnswer and SyncAnswer: the work, or
    the sync's outcome, as before — or, with dryRun, 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 unless sort is given (sort=relevance says 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.md has 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's id, their
    portrait, what they did (jobs) and the name they were matched by, and
    the palette leads to their page whether TMDB knows them or not; the
    list's person filter 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;
    tmdbId is absent for somebody TMDB does not know, and each of roles
    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 given record, 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_asset keeps each picture's colour and sketch, and media_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/status says 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 its slug. 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=warn in the default filter); AMS_LOG decides as before.
  • /.well-known/oauth-protected-resource,
    /.well-known/oauth-authorization-server and
    /.well-known/openid-configuration answer 404 rather than the
    interface's page.
  • ams_http_requests_total and its kin count /mcp under
    surface="mcp".

Removed

  • The title index of 0022 (SQLite's media_search and its triggers,
    PostgreSQL's five trigram indexes on the works' titles), which the search
    no longer reads.

  • episodes in GET /api/v1/calendar, deprecated since 0.8.0: the
    episodes are the entries of entries whose kind is episode, 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 a 404 only when every source that could resolve it —
    TheTVDB, Skyhook, TMDB — answered that there is no such series, and a
    502 when one of them timed out, failed or refused the connection: Sonarr
    takes a series for deleted on a 404 and keeps what it has on anything
    else. A search nothing answered while a source did not is a 502 rather
    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
    en until 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 #watch or #details in 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 a style prop 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 metrics scope
    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 rmcp 3.7, past GHSA-4432-vg92-fqh6 (an
    unauthenticated memory exhaustion in 3.0–3.5) and the earlier
    DNS-rebinding and session-leak advisories; /mcp answers 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-webpki 0.103.16, webpki-roots and webpki-root-certs 1.0.11,
    ctutils 0.4.5 and tokio 1.54.1, past their advisories
    (RUSTSEC-2026-0381, -0382, -0383, -0385, -0386); the interface's zod
    and the documentation site's vite, zod and undici likewise. The advisory on
    crypto-bigint 0.5 (RUSTSEC-2026-0387) is set aside with its reason:
    p256 only verifies public signatures and age encrypts with a
    passphrase here, and the fix waits for openidconnect and age.
  • 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-toolchain they 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 plain http only 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 needs https: the
    destination is refused when it is saved, and a post says cleartext
    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.