MyAnimeList API v2 client — anime list scrobbling.
OAuth is authorization-code + PKCE (MAL only supports the "plain" challenge,
so verifier == challenge). The owner ships a MAL_CLIENT_ID (and secret if
their app has one); each user authorizes their own account once via
laev mal login, and the tokens live in ~/.laev/mal.json, auto-refreshed.
Everything returns values, never raises.
Summary
Functions
One anime, as laev needs to describe it: %{title:, episodes:, seconds:, status:, score:} — or nil. Public data, so it works logged out too.
True once the user has logged in (a token is stored).
The MAL authorize URL to open in the browser.
Episodes the user has watched of this anime on MAL (0 if unknown).
Exchange an auth code for tokens and persist them.
Every anime on the user's list, newest fields and all: [%{mal_id:, title:, status:, episodes_watched:, episodes:, episode_seconds:, kind:}].
The user's list status for an anime: %{status, episodes_watched, score}
or nil (not on list / not logged in).
Run the full login: open the browser to MAL, wait for the redirect on the
local listener, exchange the code. Returns {:ok, username} | {:error, reason}.
open_browser is a 1-arg fn given the authorize URL.
Forget the stored tokens (logout).
A PKCE verifier (also used verbatim as the plain challenge).
What to send for "watched episode N of M": the count, the status, and a finish date on the episode that completes it.
Set the user's score (1–10) for an anime.
Record progress: set watched count and status. Never regresses a higher
count already on MAL. total (episode count, optional) decides whether
finishing marks the show completed.
Set the list status of an anime — "watching", "on_hold", "completed", "dropped", "plan_to_watch" — without touching the episode count.
The logged-in user's MAL username, or nil.
Functions
One anime, as laev needs to describe it: %{title:, episodes:, seconds:, status:, score:} — or nil. Public data, so it works logged out too.
True once the user has logged in (a token is stored).
The MAL authorize URL to open in the browser.
Episodes the user has watched of this anime on MAL (0 if unknown).
Exchange an auth code for tokens and persist them.
Every anime on the user's list, newest fields and all: [%{mal_id:, title:, status:, episodes_watched:, episodes:, episode_seconds:, kind:}].
Paged a thousand at a time — a long-standing list runs to several hundred
entries, and MAL will hand them all over, just not at once. nsfw: true
because a list laev imports has to be the list the user actually has.
The user's list status for an anime: %{status, episodes_watched, score}
or nil (not on list / not logged in).
Run the full login: open the browser to MAL, wait for the redirect on the
local listener, exchange the code. Returns {:ok, username} | {:error, reason}.
open_browser is a 1-arg fn given the authorize URL.
Forget the stored tokens (logout).
A PKCE verifier (also used verbatim as the plain challenge).
What to send for "watched episode N of M": the count, the status, and a finish date on the episode that completes it.
The last episode is the whole point — a list that still says watching after the finale is a list you have to go and tidy by hand — so reaching the episode count flips the entry to completed and dates it today. An anime whose total MAL doesn't know (a running series, a long-form shounen) stays watching, because nothing here can tell when it ends.
Set the user's score (1–10) for an anime.
Record progress: set watched count and status. Never regresses a higher
count already on MAL. total (episode count, optional) decides whether
finishing marks the show completed.
Adds the anime to the list if it isn't on it — MAL's PATCH is an upsert, so starting something you never planned to watch just appears as watching.
Returns {:ok, fields} | {:error, reason} | :skip.
Set the list status of an anime — "watching", "on_hold", "completed", "dropped", "plan_to_watch" — without touching the episode count.
The logged-in user's MAL username, or nil.