MyAnimeList (myanimelist v0.1.0)

Copy Markdown View Source

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:

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

Types

Which MAL catalog to act on.

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.

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

media()

@type media() :: :anime | :manga

Which MAL catalog to act on.

Functions

authorize_url(c, state, code_verifier)

@spec authorize_url(MyAnimeList.Client.t(), String.t(), String.t()) :: String.t()

MAL authorization URL to redirect the user to.

configured?(v)

@spec configured?(String.t() | nil) :: boolean()

Whether a usable client id / access token was provided.

ensure_fresh(c, token, opts \\ [])

@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.

exchange_code(c, code, code_verifier, opts \\ [])

@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.

get(media, id, client_id, opts \\ [])

@spec get(media(), term(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}

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.

list_scores(media, access_token, opts \\ [])

@spec list_scores(media(), String.t(), keyword()) ::
  {:ok, %{optional(integer()) => integer()}}

The authenticated user's list as %{mal_id => score} (score 0 = on the list but unrated; absent = not on the list).

list_statuses(media, access_token, opts \\ [])

@spec list_statuses(media(), String.t(), keyword()) ::
  {:ok, %{optional(integer()) => map()}}

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.

my_progress(media, access_token, id, opts \\ [])

@spec my_progress(media(), String.t(), term(), keyword()) ::
  {:ok, integer()} | {:error, term()}

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.

new_code_verifier()

@spec new_code_verifier() :: String.t()

A fresh PKCE code verifier (also used verbatim as the challenge).

oauth_ready?(client)

@spec oauth_ready?(MyAnimeList.Client.t()) :: boolean()

True when the client has both an id and a secret configured.

page_url(media, id)

@spec page_url(media(), term()) :: String.t()

Public MAL page URL for a title.

progress_key(atom)

@spec progress_key(media()) :: String.t()

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.

refresh_token(c, refresh_token, opts \\ [])

@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, query, client_id, opts \\ [])

@spec search(media(), String.t(), String.t(), keyword()) ::
  {:ok, [map()]} | {:error, term()}

Search media by title. Returns {:ok, [%{id, title, cover_url, score, media_type, year}]}.

Options: :limit (default 10), :req_options.

status_label(arg1)

@spec status_label(String.t() | nil) :: String.t() | nil

MAL publication/airing status → display label (anime + manga).

statuses(atom)

@spec statuses(media()) :: [String.t()]

The valid list-status values MAL accepts for media.

update_list_status(media, access_token, id, fields, opts \\ [])

@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.

username(access_token, opts \\ [])

@spec username(
  String.t(),
  keyword()
) :: String.t() | nil

The authenticated user's MAL username, or nil.

year_range(y, y)

@spec year_range(integer() | nil, integer() | nil) :: String.t() | nil

Year range for display: "1997", "1997–2003", or "1997–" (ongoing).