Skip to content

Releases: Dim145/plex-auto-genres

v2.7.0 — arr-metadata-server by key

Choose a tag to compare

@Dim145 Dim145 released this 18 Oct 00:47

arr-metadata-server 0.8.1 lets its Plex provider ask for keys (AMS_PLEX_AUTH=apikey)
without shutting Plex out: Plex is given the provider's address with a key at its end,
…/plex/<key>. This release follows it: this app can now be let in by a key as well as by
its address.

Setting it up

With AMS_PLEX_AUTH=apikey on the server:

  1. Under Keys & network, issue this app a key of its own, one that only reads.
  2. Set it as ARR_METADATA_API_KEY. The address of the Plex panel, …/plex/<key>,
    works as ARR_METADATA_URL too, but the server's guide keeps that key for Plex alone:
    revoking it would shut both out.
  3. Config › Checks says whether the server takes it.

The key travels in an X-Api-Key header, never in an address. An address carrying one is
shown with /plex/*** — in the console, the doctor and the logs — and so is a key in its
query. A key carries its own settings on the server, the language the genres come in among
them.

A server that lets callers in by address, its default, needs nothing new.

What else changes

  • A refusal says which way in failed, and which setting to change: a key missing, or
    one the server does not know; an address not on its list, or a key switched off or run
    out; a key whose account is no longer active. A refusal the server did not send — from a
    reverse proxy in front of it, and the password it asks for — is now said to come from
    there, rather than blamed on the server.
  • ARR_METADATA_URL and ARR_METADATA_API_KEY naming two different keys are refused, with
    the reason, by the libraries that read from ams and by the doctor.

Upgrading

Pull and restart: nothing to change.

v2.6.0 — your own catalogue as a source

Choose a tag to compare

@Dim145 Dim145 released this 17 Oct 12:37

A library can now read its genres from an arr-metadata-server: the self-hosted
metadata server that answers Sonarr, Radarr and Plex from a catalogue of its own, in which
every correction locked by hand wins over TMDB, TheTVDB and the arr services. Those
corrections now reach the genres this app writes. It holds films, series and anime, so any
library can read from it, alone or ahead of the others.

Setting it up

  1. ARR_METADATA_URL is the server's address as this app reaches it — two containers
    on one Docker network reach it by name, http://arr-metadata-server:8080. The address
    its Keys & network page gives Plex, ending in /plex, works too.
  2. Let this app in. The server's Plex provider admits callers by address: add the
    address the server sees this app call from under Keys & network › Allowed
    addresses
    , beside Plex's. A server that asks for a key instead
    (AMS_PLEX_AUTH=apikey) takes one issued there in ARR_METADATA_API_KEY.
  3. Add ams to a library's sources. In the console, Reads from is now an ordered
    list: move, drop or add each source. Config › Checks asks the server and says what
    is still in the way — a refused address, a host name it does not answer to, a provider
    switched off, a redirect.
{ "library": "Films", "type": "standard-movie", "providers": ["ams", "tmdb"] }

An item is asked for by the ids Plex gives it — a film by its TMDB or IMDb id, a series by
its TheTVDB or TMDB id, an item matched by the server's own agent by the key that agent
gave it — and by title and year otherwise. Asking keeps the work in the server's
catalogue, as when Plex asks, so a title search takes a match only with the item's title,
accents aside, from a year either side. An item with no year, or a work found under
another of its titles, is left to the next source, or to a binding.

What else changes

  • A source that refuses this app — an address the server does not let in, a wrong
    TMDB key — sits out the rest of the run after saying why, once in the log and on the
    run's report. It used to be asked again for every title, and its titles were failed and
    kept behind the retry backoff. A title the other sources all miss now waits for the next
    run; in a library that merges its sources, a title that needs the refusing one waits
    too, without the others being asked for it.
  • TMDB asks for an item as what Plex holds it as. In an anime library that is a Plex
    film section, it looked among its series, so a film's id could fetch another work.
  • Bindings: the picker searches as what Plex holds the item as, an anime library
    reading ams takes TheTVDB and IMDb pins alike, and AniDB pins are offered only where
    Jikan or AniList can read them.
  • Stopping a run — a cancel, or the give-up — no longer loses an item whose write was
    already with Plex: it finishes, and is counted and cached. A server stop arriving right
    after a job was cancelled no longer hangs.

Upgrading

Pull and restart: nothing to change unless you want the new source.

A run asks again only for what changed on this side. Titles already tagged keep their
genres until they are asked again — Forget them in the library browser, or run the
library with --force — which matters in two cases:

  • an anime library that is a Plex film section and reads TMDB, to have its films read as
    films;
  • a correction locked in arr-metadata-server after a title was tagged, or a title another
    source answered while arr-metadata-server refused this app.

v2.5.0 — the database lives beside the config

Choose a tag to compare

@Dim145 Dim145 released this 01 Oct 15:22

The state database moves out of /logs into /config, beside config.json. Upgrading
is a pull and one start with the old /logs mount still in place; after that the mount
can go.

Why

The database holds the only copy of the bindings and the genres decided by hand. It
lived in /logs: a directory whose name says it can be thrown away, and a second volume
to get right. A /logs mounted from the host's /tmp came back empty after a reboot,
and the app opened a new, empty database there without a word. Every binding and every
decision was gone, while config.json and its rules survived.

What changes

  • The database is plex-auto-genres.db beside the config file:
    /config/plex-auto-genres.db in the image, config/plex-auto-genres.db next to
    config/config.json outside Docker. --db / PAG_DB still point anywhere.
  • /config is the only volume the compose file needs (/posters stays optional). It
    holds everything worth keeping, so it is the one to back up.
  • The container checks at every start that /config will survive, and warns when it
    will not: only config.json mounted into it, a tmpfs, or a directory under the host's
    /tmp.

Upgrading

Stop the old container, then start the new one with your /logs mount still in place.
The database moves to /config on its own. It is copied through SQLite, so whatever the
last run left in its WAL comes along, then checked, and the old file is kept as
plex-auto-genres.db.moved. The log says:

Moved the state database from /logs/plex-auto-genres.db to /config/plex-auto-genres.db, beside the config; nothing is written to /logs any more. The old file is kept as plex-auto-genres.db.moved.

Then remove the /logs line from your compose file, and PAG_DB if a tool added it
(2.4's own default is read as unset).

The start stops, with the reason, rather than go on with an empty database when:

  • the old database is still open, because the previous container is still running;
  • it cannot be read or copied, or the copy does not check out. SQLite writes beside a
    database to open it, so a read-only /logs counts;
  • a file at /config/plex-auto-genres.db is not one of this app's databases;
  • /config would not keep it (see above).

An empty database already in /config, from a doctor run before /logs was mounted
for instance, gives way. One that holds anything is never touched: the log names both,
with what each holds, and how to keep the older one.

To go back to 2.4 afterwards, set PAG_DB=/config/plex-auto-genres.db.

v2.4.1 — replaced and ignored tags come off

Choose a tag to compare

@Dim145 Dim145 released this 24 Sep 18:20

A fix for libraries that replace their genres. Upgrading is a pull, followed by one
forced run of each library that uses "Replace existing genres".

Replaced and ignored tags stayed in Plex

With "Replace existing genres" (clearGenres) on, names covered by a rename or an
ignore rule could stay on an item after a run. The rules were applied; the write was
not what it should have been.

Plex's library listing returns only the first few tags of each item, typically two
genres of four, and every write was planned from that listing. The tags to take off
were therefore only the ones it showed: every older tag past the cut stayed on the
item, which is exactly where a renamed or ignored name tends to sit once a library
writes keywords. An item whose visible tags already matched was even reported as
unchanged.

A write that takes tags off now reads the item's own page first. That covers
"Replace existing genres", names refused by hand, and names added by hand. A write
that only adds does not need to, because Plex appends: merging libraries and rating
collections cost no extra request.

The Genres… dialog reads the item's full list too. The list it showed came from
the same listing, and fixing an item's genres from it would have dropped the ones it
did not show.

After upgrading

Items written by an earlier version keep their hidden tags until they are written
again, and the cache has no reason to rewrite them. Run each affected library once
with Force (the console's Run menu), or:

plex-auto-genres run --library "<library>" --force

v2.4.0 — genres decided by hand, and bindings that find their item

Choose a tag to compare

@Dim145 Dim145 released this 24 Sep 08:12

Two changes: genres you decide by hand, and bindings by title that actually apply.
Upgrading is a pull.

Genres decided by hand

Any item can now have its genres (or collections) decided by hand, and nothing
automatic undoes the decision until you hand the item back. There are three
instructions, the same ones Kometa
uses for this job:

  • Always write: added whatever the sources say, even when they have nothing for
    the title.
  • Never write: kept off even when a source returns it, and taken off the item if
    it is already there.
  • Set exactly: the sources are no longer asked, and the genres are exactly yours.
    In a library that writes collections this is Stop asking: collections are never
    cleared, so the names you add are written, the ones you refuse are taken off, and
    the item's other collections stay.

In the console: Genres… on any item of a library, a By hand filter, and an
Overrides page (formerly Bindings) listing every pin and every decision. From the CLI:

plex-auto-genres manual Anime "Monster" --add Psychological --remove Kids --note "the sources lump it in"
plex-auto-genres manual Anime "Monster (2004)" --lock --add Psychological --add Thriller
plex-auto-genres manuals --library Anime
plex-auto-genres unmanual Anime "Monster"

Worth knowing:

  • A refused name does not use up one of the maxGenres slots, and a name you add is
    never cut by the cap.
  • A decision is part of what the cache remembers about the item. It applies on the
    next run, even when it was saved while a run was going, and editing only its note
    asks the sources nothing.
  • A name matches a tag with or without PLEX_COLLECTION_PREFIX, so you can refuse a
    tag the app wrote, one you made, or a star-rating collection alike. The
    rating-collections pass honours refusals too.
  • Handing an item back asks the sources again from the next run. Where a library
    merges rather than replaces (no clearGenres, and every collections library), the
    names a decision added stay on the item like any other tag: refuse them first to
    take them off.
  • The API is GET/PUT/DELETE /api/v1/manual.

bind by title applies now

bind stored the typed title as the item's key, while items are looked up by the GUID
Plex gives them. So bind Lib 'Some Title' tmdb 123 said "bound" and was never
applied, and failures suggested exactly that command.

bind and unbind now find the item the way manual does. Give a title as Plex shows
it (add the year when two items share it), or the item's key as the console and
failures show it, such as mal://1234. A title no run has seen is refused rather
than stored and ignored.

  • Pins no longer drop the item's cached result: they are part of its fingerprint. A
    pin saved while a run is under way is no longer lost, and the item keeps its name in
    the lists.
  • failures shows each item's key, suggests an id scheme the library can read, and
    still lists when the config does not load. failures --retry and the console's
    retry keep the rows they reset.

If you ever ran bind with a title before this version, that pin was probably
never applied. doctor now lists pins on keys no run has seen, and bindings marks
them. Remove one with unbind and that exact key, then bind the item again by title.

Upgrading

The database gains one table, for decisions, when it is first opened. The first run after
the upgrade re-resolves each item that has a pin, once, since pins are now part of its
cache fingerprint.

v2.3.0 — keywords as a preference, and a tag threshold you can set

Choose a tag to compare

@Dim145 Dim145 released this 22 Sep 00:39

Three changes to what a keyword-tagged library writes, from a merging anime library in
use. Upgrading is a pull.

useKeywords is a preference now, not an ultimatum

It meant keywords instead of genres, and nothing at all when the source had no keyword
for that title. AniList counts only the tags its community agrees on strongly enough,
and a great many TMDB entries carry no keyword, so each source went quiet on a large
share of titles. Where both did, the title was tagged with nothing and recorded as a
failure under backoff.

It now reads as a preference: the finer vocabulary where a source has one, that
source's own genres where it does not
. A merging library writes something for every
title again, from both of its sources.

AniList's tag threshold is yours to set

AniList shows a tag with the share of its community that agreed with it. Only tags at
70% or more were written, a figure hard-coded with nothing anywhere to say so — which is
why a tag plainly visible on AniList's own page could fail to reach Plex.

ANILIST_TAG_RANK="20"   # default 70

Lower it to take much more of what AniList knows, and accept the noise that comes with
it. It joins the settings fingerprint when it is not the default, so changing it re-tags
the libraries that read AniList.

A failure message that pointed at the wrong thing

"returned no usable genres (all filtered by your ignore rules?)" was shown even when the
rules were innocent. When they really are the cause it now says so as a fact, with how
many names they dropped. To see what a source returns for one title without running a
pass:

plex-auto-genres query "<title>" --type anime --keywords

Also

The example config and several docstrings use invented names for their examples. Two
genre posters nothing referred to are gone; a collection with no poster file is
reported as missing and skipped, as always.

v2.2.2 — AniList paced at what it actually serves

Choose a tag to compare

@Dim145 Dim145 released this 21 Sep 23:33

AniList refusing a large library turned out to be our fault, not its rate limit.
Upgrading is a pull; nothing to configure.

AniList was being asked three times too fast

Its documentation says 90 requests a minute. The API has been "in a degraded state" for
a long time and actually serves 30, which is what its X-RateLimit-Limit header
reports on every response. Our limiter was built for the documented figure, so a library
reading AniList was paced three times over what it would accept, and got refused for it.

It now starts at what AniList serves, and every response retunes the pacing — up or
down — for any source that advertises a limit and documents the window that figure
counts over. The day AniList is restored to 90, the next run speeds back up on its first
answer, with no release and no setting to change.

Expect AniList to be slower than before: one request every two seconds, answering,
instead of a faster one being turned away. Falling back, it is only asked about the
titles MyAnimeList missed, so a run barely changes length. Merging, it is asked about
every title and it will set the pace.

No API key would have helped

The question that started this was whether an AniList key could raise the limit, the way
TMDB_API_KEY unlocks TMDB. It could not. AniList's OAuth exists to read and write a
user's own lists, not to buy throughput, and a rate-limit raise is a manual request its
maintainers say they are not currently accepting.

Jikan and TMDB were checked too, and are right as they are

Both were verified against their live endpoints and their documentation:

Source Sends a limit header Paced at
AniList yes, per minute what it reports, 30 today
Jikan no 3 a second, 60 a minute
TMDB no 40 a second

Jikan answers with cache headers alone and documents exactly the figures we use. TMDB
retired its old cap in 2019 and now documents "somewhere in the 40 requests per second
range", with nothing on the wire and an instruction to respect the 429 — which this
tool has always done. Neither had anything to adapt.

Worth knowing when Jikan refuses anyway, from its own documentation: "It's still
possible to get rate limited from MyAnimeList.net instead." No pacing on this side
changes that; a second source does.

One hardening

Reading any header called X-RateLimit-Limit and assuming it means "per minute" is a
guess. The same name counting over ten seconds would pace us six times too fast, exactly
when a service is asking for less. A source now declares the window its header counts
over, and the header is read only from a source that has said what it means.

v2.2.1 — what a review and a tool pass found in 2.2.0

Choose a tag to compare

@Dim145 Dim145 released this 21 Sep 22:19

A correction release: a full review of everything since 2.1.1, plus semgrep, bandit,
gitleaks, trivy, hadolint, shellcheck, actionlint and the dependency audits over the
repository. Fifteen defects, all from 2.2.0, all fixed. Upgrading is a pull.

If you run TMDB in a language other than English, upgrade

2.2.0 compared genre names on ASCII letters and digits alone, so a genre written in
Japanese, Cyrillic, Greek or Korean reduced to nothing and was dropped. With
TMDB_LANGUAGE set to one of those, every title in the library lost its genres and
was cached as a permanent failure
, under a message blaming ignore rules you never
wrote. Letters are now whatever the writing system calls them.

Those cached failures are still sitting behind their retry backoff. Clear them, and only
them, then run again:

plex-auto-genres failures --library "<your library>" --retry

If you have manual bindings, upgrade

The migration that widened bindings to one id per source ran as a script, where every
statement commits on its own. An interruption between dropping the old table and
renaming the new one — a container restart is enough — lost every pinned id in
silence
, and a leftover scratch table made the database refuse to open at all. It is
one transaction now, and it cleans up after an interrupted attempt.

A pinned id is no longer overridden by a guess

The rule that a hand-pinned title is only ever asked of the sources that can read its id
was written into the merge path alone. In the default mode, a pinned source that was
rate limited handed the title to the next source, which searched by name and wrote back
exactly the match the binding existed to override. The rule now holds in both modes, and
a pin no configured source can read says so instead of caching the title as dead.

The rest, in the order a title meets them

  • An answer carrying no genres stopped the fallback chain, so a title whose AniList tags
    were all below the agreement threshold failed for "no usable genres" without the
    sources below it ever being asked.
  • A merge that lost a source to a transient error was cached as complete, freezing that
    title on a subset of its sources for good. It is deferred and completed next run.
  • A merged result took its rating from whichever source had one, which could write one
    record's score beside another record's id.
  • The parallel merge discarded answers already in hand when one source failed in an
    unexpected way, and the connection pool was sized for one request per title while a
    merge asks every source at once — enough to time out and stand a healthy source down.
  • The Plex writer still compared tags by case alone, so on a collection library the
    duplicate collections 2.2.0 set out to remove were appended straight back.
  • The bindings page removed every pin on an item when asked to remove one row, and the
    config editor kept a merge mode alive after hiding its control.
  • Two rename rules that differ only in punctuation cancelled each other in silence; that
    is refused now, with both keys named.

Also

TMDB_LANGUAGE joins the settings fingerprint, since it decides the very strings
written to Plex: if yours is not en-US, this release re-tags those libraries once, on
purpose. doctor warns when useKeywords sits behind a source that has no keywords and
will answer first.

From the tools: semgrep found two shell injections in the release workflow, where a ref
name reached a shell through interpolation; those values go through the environment now.
Trivy found six advisories in the image, all against pip, which nothing at runtime
invokes — it is gone from the final stage, with 6.7 MB and a package installer. Bandit,
gitleaks over the tree and the whole history, hadolint, shellcheck, actionlint,
pip-audit and pnpm audit came back clean, and semgrep now reports nothing anywhere in
the repository.

v2.2.0 — merge several sources, AniList tags, and one collection per genre

Choose a tag to compare

@Dim145 Dim145 released this 21 Sep 20:27

Three changes to what ends up written on a title, all from a library in use. Upgrading
is a pull: the defaults are the old behaviour, and no cached item is invalidated by the
upgrade itself.

Several sources at once, not just one after another

providers has always been a preference order. providerMode now says what that order
means:

Mode What a title costs What it returns
fallback (default, and what 2.1 did) one request, until a source answers the first answer
merge one request per source every source's genres, pooled
{ "library": "Animes", "type": "anime",
  "providers": ["jikan", "anilist", "tmdb"], "providerMode": "merge",
  "overrides": { "maxGenres": 10 } }

In the console it is a switch under the source presets, and with it on, the TMDB
fallback reads "…and TMDB" rather than "…then TMDB", because that is what it now does.

Merging is worth a cap — three sources can hand back thirty names for one title. One
rule is worth knowing: a title you bound by hand is only asked of the sources that can
read the id you pinned. The others would search by title and merge back the very match
the binding exists to override.

Keywords are a property of the source, not of the library type

useKeywords meant TMDB's keywords. AniList's community tags are the same idea, and
its genre list is a dozen broad buckets that say little across a whole library. AniList
now returns its tags instead of those genres when keywords are asked for, still
filtered to high-agreement, non-spoiler tags.

So the rule is no longer about the library type: useKeywords needs a source that has
keywords — TMDB or AniList — and the error says which sources the library actually
reads. MyAnimeList has none, its themes and demographics being part of the genres it
returns already.

One genre, one collection

Two sources spell one idea differently and Plex grows two collections: Boys Love
beside Boys' Love. Genre names are now compared on letters and digits alone, so
case, accents, punctuation and spacing stop splitting a genre in two. The first
spelling a source returned is the one written, and a rename rule reaches every spelling
of its key — one "sci fi" line catches Sci-Fi, SciFi and sci fi.

Words that genuinely differ cannot be guessed, so a localized source beside
English-only ones still needs a line each:

"defaults": { "anime": { "replace": { "comedie": "Comedy" } } }

One line covers Comédie, comedie and COMÉDIE. The other way out is to stop the
mismatch at the source: TMDB_LANGUAGE=en-US makes TMDB agree with MyAnimeList and
AniList, which are English-only.

Items already in the cache keep the tags they have — the upgrade deliberately does not
invalidate them. To re-tag a library with the folding applied, run it once with
--force; collections left empty afterwards are Plex's to tidy.

v2.1.1 — TMDB keywords reach an anime library that falls back to TMDB

Choose a tag to compare

@Dim145 Dim145 released this 21 Sep 19:52

One correction, reported from the console: useKeywords was refused on any anime
library, so the option was hidden in the library editor and rejected on save.

That rule was right while nothing anime-shaped could produce keywords. It went out of
date in 2.1.0, which let an anime library end its chain with TMDB as a last resort —
and TMDB has keywords.

The rule now follows the sources rather than the library type. useKeywords needs
tmdb among the library's providers, whatever that library is, and the error names the
sources it does read instead of talking about types. In the console the toggle appears
whenever TMDB is in the chain:

{ "library": "Animes", "type": "anime", "providers": ["jikan", "anilist", "tmdb"],
  "useKeywords": true }

It covers exactly the titles TMDB itself answered for. The anime sources above it are
untouched, and they still return their own genres. TMDB can return 50 keywords for one
title, so pair it with overrides.maxGenres — doctor warns when you have not.

Removing TMDB from the chain clears the flag with it, in the editor, so the pair that
cannot work is not one you can build by hand.

Nothing else changes. No configuration that was valid before becomes invalid, and
upgrading is a pull.