Automated media pipeline that monitors a Telegram group for torrent tracker posts, parses release metadata, and presents everything in a review-ready web dashboard.
The app reads one file, config.yaml, at CONFIG_PATH (/data/config.yaml in the image). docs/CONFIG.mdā describes every block, and media-triage config import-env -o config.yaml writes the file equivalent to your current environment variables. The environment still works on its own: with no file at CONFIG_PATH the app builds the same configuration from the variables below and in .env.exampleā .
Most posts are fine, and stopping to approve each one is the slow part. Set
AUTO_APPROVE_ENABLED=true and the service decides for itself, keeping only
the items worth thinking about.
The rule is that review is for what a check flags, plus duplicates. Missing data alone does not send an item to review. A film without its parental guide waits for it, and comes back to you only if it never arrives. A book no catalogue can classify downloads.
First the service has to be sure it is looking at the right title. TMDB is searched with the parsed title and year, and the record that comes back is only accepted when the parsed title is at least 85% alike the record's title or its original title, accents and punctuation ignored, and the years agree within one. A record found only by dropping the year is never accepted: it names a title, not necessarily this one. Everything below reads that record's data, including the IMDb id the parental guide is fetched with, so a wrong match would judge one film on another film's content.
With the match confirmed, a film or series downloads by itself when all of this holds:
MIN_GUIDE_VOTES votes;MIN_GUIDE_VOTES votes;MIN_GUIDE_VOTES votes;The 40% rule exists because the winning label hides a split vote. A category labelled Mild can have nearly half its voters calling it Moderate or worse, and IMDb publishes the breakdown.
Trip one of those rules and the item goes to the review queue with the reason
written out. Miss the data instead, and the item waits. It is re-checked once
an hour and handed back to you after AUTO_APPROVE_MAX_WAIT_HOURS with the
reason "no parental guide data after N hours". Missing data never counts as
clean: an unrated category cannot pass a threshold, and an empty keyword list
is silence, not a clean bill of health.
A match that cannot be confirmed goes straight to the review queue saying "could not confirm this is the same title". It never waits, because waiting would not make a wrong match right.
Always approved, and a post is only a magazine when it carries the written header the group puts on one. An emoji never decides a type on its own, and a post naming a video quality and a source is a film or a series whatever else it mentions.
Off by default. The catalogues have almost nothing for the Spanish titles this group posts, which leaves the decision resting on one coarse Google Books category, and "Fiction" rules nothing out.
Add them to AUTO_APPROVE_TYPES and the title is looked up on
Open Libraryā , with
Google Booksā as a second
opinion, and classified by the subjects those catalogues carry. A record
counts when the title and the author both match it. If its subjects mention
politics, sex or magic, the book goes to review. If it carries at least three
subjects and none of them do, the book downloads.
Most of this group's books get no usable record at all, so a missing classification is not a reason to ask. The book downloads, and the reasons say why the catalogue was silent, for example "no catalogue classification (lookup failed: http 429)". The post's own title is not read for flags: "La magia del orden" is a book about tidying, and a word says too little to hold a book on.
Always reviewed by hand. The duplicate check runs again at re-check time, so a title approved by hand in the meantime is not downloaded twice.
A title that names an issue is compared as one. "Men's Health EspaƱa - Octubre" posted as an ebook in 2026 is not a duplicate of the October 2024 issue already on disk: two issues are the same only when their issue numbers agree, or failing numbers, their month and year. A periodical never matches a book, and a book is matched as before, with its year and quality tags ignored.
Besides a release post, a source reads a .torrent attachment, a magnet, an
IMDb, TMDB or TVDB link, a tracker details link and a typed title. A .torrent
or a magnet is downloaded as it is only when its sender is one the source's
direct_adds allows, reviewers by default; from anyone else it is a request by
name. docs/CONFIG.mdā lists each case.
A post carries the tracker's own link to the torrent, so that torrent is what
downloads: the .torrent is fetched with the session Prowlarr keeps for that
tracker and handed to qBittorrent, and a title that a search could mis-pick
or miss is not searched for at all. Both link shapes the tracker has used are
read, the hash one (index.php?page=torrent-details...&id=<hash>) and the
numeric one (details.php?id=<n>). Prowlarr is searched only when a post has
no such link, when no Prowlarr indexer is logged in to that tracker, or when
the tracker answers the link with something other than a torrent. The session
goes only to the indexer whose configured base URL is on the link's own host
(a www. prefix aside); a link on any other host, a tracker's old domain
included, is searched for instead.
The search, whether the worker runs it after an approval or you ask for it from the web page or the bot's Releases button, is the same one: the post's title, its season, and Prowlarr's categories for the media type.
The search sends the post's title, and its year only for films, series and magazines. The tracker matches the keywords as one piece of the title, in order, and a book's year sits after the author ("Title - Author - 2026"), so a year appended to the query would find nothing.
When the rules change, the review queue still holds items decided under the
old ones. Send /rerun to the bot, or run python -m media_triage rerun,
and the service decides again every item that went to review because
auto-approval was off, its type was not on the list, no catalogue could
classify it, or a word in its title tripped the rule that 0.11.0 dropped. An
item you have already acted on is never touched. Neither is
one a check flagged, a duplicate, or one whose title match could not be
confirmed. The reply counts how many were re-queued, approved, sent back to
review and left waiting.
To decide one item again whatever sent it to review, open it in the web UI and press Rerun policy. That works on an item in the review queue, one waiting for data and one the policy rejected, and it asks the item's lookups again.
Every automatic decision is written onto the item, with the reasons and the time. The web UI and the Telegram bot both show it.
An item can be rejected right up to the moment the download finishes, and while it is being content-checked. Rejecting it drops the torrent only when this item is what added it: qBittorrent answers an add with the hash of a torrent it was already holding, and deleting that would take a library copy that was there beforehand. The download row is marked cancelled either way, so nothing brings the item back as completed later.
Notes and links to Instagram, TikTok, YouTube, X or Facebook land in the same
group. They are marked ignored and then left alone for good: never reviewed,
never re-parsed, never deleted from the group.
That status is deliberately hard to earn. A request typed in plain words is not a release post either, and it stays in the review queue where you can act on it. A bare title in capitals and a sentence saying which book you want are both in this group's history. A message is only finished with when every link in it points somewhere nothing is downloaded from, or when there is nothing in it that could be the name of anything: no capital where grammar does not need one, no year, no shouted word, no quoted phrase, no word like "peli" or "libro", and no line short enough to be a title.
Defaults live in .env.exampleā .
| Variable | Default | What it does |
|---|---|---|
AUTO_APPROVE_ENABLED | false | The master switch. Off means nothing is ever approved automatically. |
AUTO_APPROVE_TYPES | movie,series,magazine | Which types may be approved automatically. Add ebook,audiobook,comic to include books. |
MIN_GUIDE_VOTES | 5 | Parental-guide votes a severity needs before it is trusted. |
AUTO_APPROVE_MAX_WAIT_HOURS | 24 | How long an item waits for that data before you are asked. |
GOOGLE_BOOKS_API_KEY | empty | Optional key for the Google Books lookup. Keyless requests share one daily quota that stays exhausted, so without a key Google Books answers 429 and only Open Library is heard. |
The schema is managed with Alembic. The service runs alembic upgrade head on
start-up, so a new image brings its own columns with it. The migrations in
alembic/versions/ā only ever add things and are safe to
re-run.
The migration adds enum labels, and an older image cannot load a label it has never heard of. Move the rows off the new labels before starting the old image:
UPDATE media_items SET status = 'PENDING_REVIEW'
WHERE status IN ('AWAITING_DATA', 'IGNORED');
UPDATE downloads SET status = 'ERROR' WHERE status = 'CANCELLED';
The columns themselves can stay: the old code simply does not read them.
Content type
Image
Digest
sha256:dba79195eā¦
Size
81.9 MB
Last updated
6 months ago
docker pull drumsergio/media-triage