Anime Cron
Anime automation pipeline: fetch airing episodes from Nyaa, sync watch progress back to AniList, queue Blu-ray upgrades via SeaDex, and archive preservation-group releases with unlimited seeding.
Methods:
fetch-airing— check AniList CURRENT list → search Nyaa for the next episode of each show → add to Transmission. Skips shows not yet aired. Supports multi-episode catchup and name-based dedup. Optional Telegram notifications when an episode is queued or overdue.mark-watched— scan recently completed Transmission torrents → update AniList progress. Matches by show title via AniList search.upgrade-bd— check AniList COMPLETED list against SeaDex → queue BD release downloads for shows that have a recommended release not yet on disk. Idempotent (Transmission deduplicates).fetch-archive— sweep Nyaa for every release credited to a set of preservation groups (default Kineko Video + LonelyChaser), queue them intoarchiveContainerDir, and switch them to unlimited ratio/idle seeding so an archive never stops seeding. Matches on the leading[..]credit, so collabs and suffixed aliases are caught but a title that only mentions the group is not. Idempotent via infoHash dedup against live Transmission state.disk-stats— query Transmission for all anime torrents and return total/downloaded/remaining disk usage grouped by status.
Requires a running Transmission RPC endpoint and an AniList account.
mark-watched and fetch-airing Telegram alerts require anilistToken.
Set telegramModel to a @magistr/telegram-send model instance name to
enable notifications.
2026.09.01.3
- Republish of 2026.09.01.2, no code change. The 2026.09.01.2 publish was
skipped when
fmt:checkfailed on the merge commit (a deno 2.8/2.9 markdown reflow difference in this file), andextension-publishonly fires on the commit that carries the version bump, so the registry never received it.
Global Arguments
| Argument | Type | Description |
|---|---|---|
| anilistUser | string | AniList username |
| maxCacheAgeDays | number | How stale the cached watch list may be before fetch-airing fails instead of falling back to it |
| anilistToken? | string | AniList personal access token — required for update-progress. Get at: https://anilist.co/settings/developer |
| transmissionRpcUrl | string | Transmission RPC URL (e.g. http://host:9091/transmission/rpc) |
| transmissionUser | string | Transmission RPC username |
| transmissionPass | string | Transmission RPC password |
| archiveContainerDir | string | Download dir for fetch-archive preservation rips, as the Transmission CONTAINER sees it (e.g. /anime/kineko) — not the host path it is bind-mounted from |
| animeContainerDir | string | Download dir prefix INSIDE the Transmission container for anime (e.g. /anime/tv) |
| preferredResolution | number | Preferred resolution: 720/1080/2160 |
| telegramModel | string | Swamp model name for Telegram send (@magistr/telegram-send instance). Set to empty string to disable notifications. |
Resources
2026.09.01.1
parseEpisodenow reads theSxxExxform. The bare E-prefix pattern documented itself as covering "S01E05", but\bbefore theEnever fires when a digit precedes it, so everyS01E09-style release parsed asnullandpickBesthad nothing to rank. Seen live on 2026-09-01: Koukaku Kidoutai (2026) ep 9 — Erai-raws (the group behind eps 1–8, dash form) never posted, and all nine other 1080p releases (VARYG, Judas, DKB, ToonsHub, Ironclad, Cattleya) usedS01E09, so the show sat innot-foundfor five hourly runs. A dedicated\bS\d{1,2}E(\d{1,3})\bcheck now runs right after the- NNfansub convention.
2026.08.22.2
An inferred airing time no longer drives an alert. The not-found branch derives
airedAtSecfromnextAiringAt, but on a cache-backed run that timestamp is projected — and on a SEEDED list it is simply the capture instant. WithepsBehind = 0it therefore measured the age of the cache rather than of the episode, so past the 30-minute grace every show read as overdue on every run: 13 shows produced an identical "Aired 298min ago — not yet on Nyaa" every hour.The overdue Telegram alert is now gated on
listSource === "anilist". The not-found outcome is still recorded, so the run report shows exactly what was missing; only the page is withheld until AniList answers and the times are real again. The "queued" alert is deliberately NOT gated — a started download is a fact regardless of where the list came from.New
alertsSuppressedcount onfetchResult, so a withheld page is visible rather than silent. It is incremented independently of whethertelegramModelis configured, which is also what makes the behaviour testable:sendTgspawnsswampviaDeno.Command, so a fakecontext.runModelobserves nothing and such a test passes vacuously. The new test pairs the cache case with an AniList-path positive control.listSourceandlistAgeSecondsare now declared inFetchResultSchema. They shipped in 2026.08.22.1 and worked — undeclared keys pass through — but carried no schema or description.
2026.08.22.1
Merged the two divergent copies of this model into one. The main repo's
extensions/models/anime_cron.tswas the LIVE file and this package was inert; each had work the other lacked. Reconciled with the repo behaviour kept where they conflicted:pickBestrestores the HARD resolution floor (h.resolution >= targetRes) rather than the soft ranking tried here, and the"varyg": 7preferred-group score is back. The property test's oracle now applies the same floor — mirroring only the scoring made it nominate winners pickBest had already discarded.New
seed-watchlistmethod. Rebuilds the watch-list cache from the most recentfetchResultfor the case the cache cannot cover: AniList went down BEFORE any run had cached the real list. Progress per show comes from the strongest signal available (duplicatemeans that episode is already in Transmission, anything else means it is still owed), taking the highest across outcomes. Airing times are INFERRED from the capture, so the payload is flaggedseeded: trueand the next successful AniList read replaces it. Refuses to overwrite an existing cache withoutforce.Resource instances renamed to
watchlist-current.writeResourcetakes (spec, INSTANCE) butreadResourcetakes the INSTANCE — so the cache both read the wrong key and collided withfetchResult, which already owns the instance namecurrent.fetch-airingkeeps working while AniList is down. Every successful run now caches the CURRENT list to awatchlistresource, and a failed AniList read falls back to it instead of aborting the run. On 2026-08-22 three consecutive hourly runs failed onAniList 403and nothing downloaded for three hours; nothing about downloading needs AniList reachable, only knowing what is being watched.The cached list is projected forward, not replayed.
lastAiredEpis derived asnextAiringEp - 1, so a raw snapshot would conclude nothing new had aired and download nothing — the very failure it exists to fix. TV anime airs weekly, so episodes aired since the capture are the whole weeks elapsed sincenextAiringAt, plus the one airing at that instant. Capped at the season total so a finished show is never projected past its last episode.New
maxCacheAgeDaysglobal argument (default 14). Past that the run fails as before: shows end and new ones start, so downloading against a month-old list is worse than failing loudly. A cache with an unparseable timestamp counts as infinitely old.fetchResultgainslistSourceandlistAgeSeconds, so a run served from a stale list is visible rather than looking identical to a healthy one.No behaviour change when AniList is reachable, and AniList-down with no cache still rejects exactly as before.
Modified 1 models
2026.08.08.1
Added fetch-archive: a preservation-archive sweep that pulls every Nyaa
release credited to a set of release groups and pins it to seed forever.
Motivation: groups like Kineko Video and LonelyChaser publish fresh 16mm/35mm
film scans, LaserDisc Domesday captures and VHS restorations of material that
never got a digital release. Keeping a complete, permanently-seeding copy is the
point — but the existing fetch-airing/upgrade-bd methods are both driven off
an AniList list, and none of this catalogue is on one.
- New method
fetch-archive. Pages Nyaa's RSS (75/page,maxPagesdefault 10) once per group, keeps only releases whose leading[..]credit actually names a wanted group, dedups by infoHash across groups, queues the rest flat into the archive dir, and issues one batchedtorrent-setwithseedRatioMode: 2+seedIdleMode: 2(Transmission's "unlimited", which overrides the session defaults per torrent).- Group matching splits collabs on
& + , /and prefix-matches in both directions behind a 5-character stem floor, so a wantedLonelyChasercatches theLonelyChaser-Rawsalias and a wantedKineko Videocatches a bareKineko, while a short token cannot wildcard onto every group. A title that merely mentions a group without crediting it in the bracket is not swept. - Idempotent: it reads live Transmission state first and reports already-held
releases as duplicates.
seedForeverExisting(default true) re-applies unlimited seeding to everything already in the archive dir, so a re-run repairs torrents added before this method existed. categorydefaults to0_0(all) because these groups also post live-action scans outside the anime categories.- Reports
queuedGB/catalogGBso adryRunanswers "how much disk will this cost" before anything is queued. - Optional Telegram summary lists what was snatched (capped at 20 titles + an overflow count, to stay under Telegram's 4096-char limit).
- Group matching splits collabs on
- New global argument
archiveContainerDir(default/anime/kineko) — the download dir inside the Transmission container for these rips. NyaaHitgainssizeBytes, parsed from<nyaa:size>by the newparseNyaaSizehelper (returns 0 for anything unparseable, so a missing size can never NaN a running total).- New exported pure helpers, all unit-tested:
parseNyaaSize,decodeEntities,escapeHtml,bracketGroups,normGroup,creditsGroup.
Tests: 117 pass. Added fetch-archive cases across the contract-fixture (pure
helpers), methods (happy path, idempotent re-run, dryRun-mutates-nothing,
per-group search failure isolation, seeder floor, empty-group rejection) and
adversarial (HTML-injection escaping at the Telegram subprocess boundary, group
impersonation outside the credit bracket, credential-leak sweep) suites. The
sanity: model exposes exactly the N documented methods pin moved 4 → 5, and
the two NyaaHit literal builders in the existing suites gained sizeBytes.
Modified 1 models
Release 2026.07.16.2 — align model versions with manifests
Maintenance release across the @magistr extensions. For most packages this
carries no functional change: the only edit is the model's version: field,
brought back in line with its manifest version so the published model type
version and the package version no longer drift.
Functional changes in this release are limited to:
anime-cron: normalizeTitle now strips a ": subtitle" suffix and a trailing parenthesized year before comparison, fixing dedup false-misses where the torrent title carries a subtitle or year that the AniList romaji does not.
arckit: first publish. Standalone ArcKit port — a 12-phase architecture governance state machine with 65 bundled templates, driven by a bundled skill.
Also tracks three extensions (kaiten, observability-agent, music-library) that previously existed only as untracked working-tree directories, recovered from stashes.
Merge pull request #38 from umag/feat/anime-cron
Add @magistr/anime-cron: AniList+Nyaa+SeaDex anime automation pipeline
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License declared1/1earned
- Verified public repository2/2earned