A MyAnimeList API v2 client for Elixir, covering both anime and manga. The media type is the first argument on every call:
MyAnimeList.search(:anime, "frieren", client_id)
MyAnimeList.get(:manga, 2, client_id)
MyAnimeList.list_statuses(:anime, access_token)Three levels of access map to three argument shapes:
- Public data (search, details) needs only your app's
client_idstring — sent as theX-MAL-CLIENT-IDheader, no user login. - OAuth (
authorize_url/3,exchange_code/3,refresh_token/3) needs aMyAnimeList.Clientwith yourclient_id/client_secret/redirect_uri. - List sync (
list_statuses/3,update_list_status/5, …) needs a useraccess_tokenstring.
Errors are values ({:error, reason}) — this module never raises.
Testing
Every request merges req_options (from the Client, a per-call
:req_options, or config :myanimelist, :req_options), so you can point
it at Req.Test:
# config/test.exs
config :myanimelist, req_options: [plug: {Req.Test, MyAnimeList}]
Summary
Functions
MAL authorization URL to redirect the user to.
Whether a usable client id / access token was provided.
Return a non-expired token, refreshing via refresh_token/3 if the
current one is within a minute of expiry (or has no expires_at). The
returned token may be the same struct — persist it if it changed.
Exchange an authorization code for a MyAnimeList.Token.
Full metadata for a title id, normalized for enrichment. Common fields:
id, url, title, cover_url, synopsis, genres, score, rank, popularity, num_users, media_type, status, year, year_start, year_end. Manga adds
num_chapters, num_volumes, authors; anime adds num_episodes, studios
(the other media's fields are present as nil/[]).
The authenticated user's list as %{mal_id => score} (score 0 = on the
list but unrated; absent = not on the list).
The authenticated user's whole list as %{mal_id => list_status_map}
(the raw list_status object with "status", "score", the progress
field, …). Paginates. Best-effort — returns {:ok, map} even on a
partial/failed fetch.
The authenticated user's current progress for a title (episodes watched /
chapters read), or 0 if it's not on their list. Useful to seed a push
high-water mark so linking never regresses existing MAL progress.
A fresh PKCE code verifier (also used verbatim as the challenge).
True when the client has both an id and a secret configured.
Public MAL page URL for a title.
The my_list_status field that carries a user's progress for media
("num_episodes_watched" for anime, "num_chapters_read" for manga) —
handy when building an update_list_status/5 fields map.
Exchange a refresh token for a fresh MyAnimeList.Token.
Search media by title. Returns
{:ok, [%{id, title, cover_url, score, media_type, year}]}.
MAL publication/airing status → display label (anime + manga).
The valid list-status values MAL accepts for media.
Update the authenticated user's list entry (PATCH /{media}/{id}/my_list_status). fields is a plain map, e.g.
%{num_episodes_watched: 12, status: "watching"} for anime or
%{num_chapters_read: 12, status: "reading"} for manga.
The authenticated user's MAL username, or nil.
Year range for display: "1997", "1997–2003", or "1997–" (ongoing).
Types
Functions
@spec authorize_url(MyAnimeList.Client.t(), String.t(), String.t()) :: String.t()
MAL authorization URL to redirect the user to.
Whether a usable client id / access token was provided.
@spec ensure_fresh(MyAnimeList.Client.t(), MyAnimeList.Token.t(), keyword()) :: {:ok, MyAnimeList.Token.t()} | {:error, term()}
Return a non-expired token, refreshing via refresh_token/3 if the
current one is within a minute of expiry (or has no expires_at). The
returned token may be the same struct — persist it if it changed.
@spec exchange_code(MyAnimeList.Client.t(), String.t(), String.t(), keyword()) :: {:ok, MyAnimeList.Token.t()} | {:error, term()}
Exchange an authorization code for a MyAnimeList.Token.
Full metadata for a title id, normalized for enrichment. Common fields:
id, url, title, cover_url, synopsis, genres, score, rank, popularity, num_users, media_type, status, year, year_start, year_end. Manga adds
num_chapters, num_volumes, authors; anime adds num_episodes, studios
(the other media's fields are present as nil/[]).
Options: :req_options.
The authenticated user's list as %{mal_id => score} (score 0 = on the
list but unrated; absent = not on the list).
The authenticated user's whole list as %{mal_id => list_status_map}
(the raw list_status object with "status", "score", the progress
field, …). Paginates. Best-effort — returns {:ok, map} even on a
partial/failed fetch.
The authenticated user's current progress for a title (episodes watched /
chapters read), or 0 if it's not on their list. Useful to seed a push
high-water mark so linking never regresses existing MAL progress.
@spec new_code_verifier() :: String.t()
A fresh PKCE code verifier (also used verbatim as the challenge).
@spec oauth_ready?(MyAnimeList.Client.t()) :: boolean()
True when the client has both an id and a secret configured.
Public MAL page URL for a title.
The my_list_status field that carries a user's progress for media
("num_episodes_watched" for anime, "num_chapters_read" for manga) —
handy when building an update_list_status/5 fields map.
@spec refresh_token(MyAnimeList.Client.t(), String.t(), keyword()) :: {:ok, MyAnimeList.Token.t()} | {:error, term()}
Exchange a refresh token for a fresh MyAnimeList.Token.
Search media by title. Returns
{:ok, [%{id, title, cover_url, score, media_type, year}]}.
Options: :limit (default 10), :req_options.
MAL publication/airing status → display label (anime + manga).
The valid list-status values MAL accepts for media.
@spec update_list_status(media(), String.t(), term(), map(), keyword()) :: {:ok, map()} | {:error, term()}
Update the authenticated user's list entry (PATCH /{media}/{id}/my_list_status). fields is a plain map, e.g.
%{num_episodes_watched: 12, status: "watching"} for anime or
%{num_chapters_read: 12, status: "reading"} for manga.
The authenticated user's MAL username, or nil.
Year range for display: "1997", "1997–2003", or "1997–" (ongoing).