Writing · 6 Sep 2026
Three small apps that keep one anime list honest
AniYears, mal-rater, and a review-first Crunchyroll to MAL sync: three tools around one MyAnimeList account, built on one API and one rule.

I keep my anime history on MyAnimeList, and for years that list was a mess. Hundreds of shows marked completed with no score. A year's worth of watching I could not have summarised if you asked me. Episodes I had watched on my phone that never made it into the list at all. Rather than fix any of it by hand, I built three tools, and they turned into a small suite around one account.
AniYears reads the list and tells me what my year looked like. mal-rater fills in the scores I never got around to. A private Crunchyroll to MAL sync keeps the list matching what I actually watched. Three different runtimes and three different problems, held together by one API and one rule I keep coming back to: propose first, write only on purpose.
One API, two doors
Everything here talks to the official MyAnimeList v2 API, and the useful thing to know about that API is that it has two doors.
For reads that do not need to be you (a title, a synopsis, an episode count), the request carries only an X-MAL-CLIENT-ID header. No login, no token dance. For writes (changing a score, marking episodes watched), it is full OAuth2, authorization code with PKCE. One wrinkle the docs do not advertise: MAL does not support the S256 challenge method, so these apps use plain, where the verifier and the challenge are the same string.
I also learned where not to go. Jikan, the community MAL API, is lovely for casual use and rate-limits into failure under bulk load, so it survives here only as a fallback. For voice actor data I go to AniList's GraphQL API instead. This is the kind of thing you only find out by hammering an endpoint at 3am.
AniYears: a year in review that runs itself
AniYears is the sibling of earshot, my audiobook retrospective: same shape, different source. It is a single-user site for my own list (Spidrex, if you want to look), built as a Vite and React SPA and deployed as a static-assets-only Cloudflare Worker. No server at runtime, no API call from the browser. Every number on the page was baked at build time.
The build is three Node scripts.
Pull hits the MAL v2 API with the client id and pages through the whole list.
Process joins each entry against the manami-project anime-offline-database, an open dataset of about 60MB, to recover studios, genres, seasons, and cover art. Then it computes the stats: days watched, score distribution, the best show of each year, top studios and genres. It preserves prior finish dates on purpose, so a refresh can never wipe the timeline it built last time.
Enrich pulls main-character voice actors from AniList. That call is slow, so it is rate-limited and resumable through a cache committed to the repo. I would rather never redo work I have already paid for.
A daily GitHub Actions job runs all three, commits whatever JSON changed, and redeploys. The site fetches those static files and renders a hero, some fun facts, a poster wall, and a shareable "wrapped" card. It is copyright-safe by construction: my metadata, my scores, and a link back to MAL for everything else.
mal-rater: scoring from the keyboard
mal-rater exists because of one nagging number: hundreds of shows marked completed and sitting at score 0.
It is a single Cloudflare Worker, no framework, no bundler, serving a vanilla JS page. It is multi-user: anyone can sign in with MAL OAuth. The tokens never touch the browser. They live server-side in Workers KV, keyed to an opaque session id cookie, and refresh themselves when MAL answers with a 401. The queue is every completed-but-unscored title on your list. Each card shows the cover, titles, synopsis, genres, and the community mean, and card payloads are cached at the edge for a week.
The point is speed. Press 1 to 9 to score, 0 for a ten, S to skip. The next card is already prefetched while you decide. Each score is a PATCH straight back to your list, and the queue only advances on success, so a failed write cannot slip quietly behind the next card. Rated and skipped ids persist locally, so the backlog keeps shrinking across sessions. It turns a chore into a rhythm.
crunchyroll-mal-sync: review first, always
This is the one I am most careful about. It bridges my Crunchyroll watch history to MAL, and nothing is written until I approve it.
It runs entirely on GitHub Actions, in Python. A weekly job pulls watch history from Crunchyroll's unofficial API. That history is account-level, so whatever I watched on my phone comes along for free. Each series is matched to a MAL entry with a dependency-free token-overlap score, and anything low-confidence, or anything that looks like a season split (Crunchyroll and MAL do not always agree on where one season ends and the next begins), gets flagged instead of assumed.
A second job builds the proposal: it opens one GitHub issue with a checklist and emails me a digest. The gate is deliberately dumb and safe. I tick the rows I believe, comment /approve, and only then does the apply job parse the ticked boxes and write to MAL. Episode counts never regress. A comment from anyone other than me is ignored.
Token overlap is right most of the time, but "most of the time" is how an automated process quietly marks the wrong season as watched for months. Let automation do the gathering, keep a human on the trigger.
The through-line
A retrospective that reads, a rater that writes, a sync that proposes and waits. One API, two auth doors, and a bias toward review over autopilot. None of these apps is large. Together they keep one list true.
AniYears is live at aniyears.danmat.dev, source at github.com/DanMat/aniyears. mal-rater is at mal-rater.danmat.dev, bring your own backlog, source at github.com/DanMat/mal-rater. The sync stays private, but the pattern is portable: pull, propose, gate on a comment, apply.