Account
Logging in
MyAnimeList's website login has a captcha, so pymal can't log in with a password. There are two ways to act as your account; pick one:
| Official API (OAuth) | Browser cookies (experimental) | |
|---|---|---|
| Setup | An API app, once | A cookie export of your browser |
| Lasts | Tokens refresh themselves | Until the session expires |
| Works | Everything below | Status, score and progress only |
| Verified | Written from MAL's API documentation and tested against mocked answers | Run against a real account (read, add, edit, delete) |
If both are set, the API is used.
The official API
pymal uses MAL's API v2 here.
1. Create an API app (once). Open https://myanimelist.net/apiconfig and create an app:
| Field | What to enter |
|---|---|
| App Type | other gives a client ID only (simplest). web also gives a client secret; pass it as client_secret=. |
| App Description | 50–500 characters, no special characters |
| App Redirect URL | http://localhost/callback (nothing has to listen there) |
| Commercial / Non-Commercial | non-commercial, for personal use |
| Homepage URL, logo, policy URLs | optional |
Creating an app means accepting MAL's API License and Developer Agreement, so read it first. It applies only to people who register an app (the OAuth route); reading titles, searching, seasons and public lists, and the cookie route, don't involve it. The app gives you a client ID (and a secret for the web type); keep them private, and don't share one client ID between people.
2. Log in (once).
account = Account("your-username")
account.authorize("your-client-id") # add client_secret="…" for a web app
authorize shows a link (and opens it). MAL always asks you to log in on that page: log in
as the account you named, allow the app, and your browser is sent to the redirect URL
(it shows "can't connect"; that is expected). Copy the whole address from the address bar and paste
it into the terminal. pymal checks that you logged in as the account you named, and refuses
(saving nothing) if it was somebody else.
The tokens are saved to ~/.config/pymal/<username>.json (readable only by you, or
$XDG_CONFIG_HOME/pymal/; change it with Account(name, token_file=…)). They are loaded
automatically next time and refreshed when they expire, so you only do this once.
If a refresh is refused (MyAnimeListApiError, for example the app was deleted or the refresh token was revoked), log in again with authorize.
Other ways to provide tokens
- Already have tokens: account.login(client_id, access_token, refresh_token, save=True).
- A server or other machine with no browser: run authorize on your own machine and copy the token
file to the same path on the server (authorize needs a terminal to paste into).
- Only a client ID, no login: account.login(client_id) reads public data of any account.
Without any login, the public anime/manga list of any account can still be read (as bare entries), but changing a list or reading your private data needs one of the two routes.
Security. The token file lets anyone who has it edit your list. Don't commit or share it. To cut off access, delete the file and delete the app on the apiconfig page.
Experimental: without the API, with a browser cookie
If you can't or won't create an API app, log in to MyAnimeList in your browser (you solve the captcha there) and give pymal the login cookies of that browser.
account = Account("your-username")
account.login_with_cookie("/path/to/cookies.txt", save=True) # save=True keeps it in the token file
What to pass as the first argument:
- the path of a cookie export of your browser (cookies.txt, Netscape format, made by an
extension such as "Get cookies.txt"). Only the login cookies of myanimelist.net are read from it;
- or just the value of the MALSESSIONID cookie (developer tools → Application/Storage → Cookies);
- or a dict of cookies, or the whole Cookie header string of a request your browser sends to
myanimelist.net (Network tab → any request → Request Headers → Cookie).
MAL only treats a request as logged in if the is_logged_in=1 flag cookie is sent as well; pymal
adds it automatically. If MAL still doesn't see you as logged in, pass your browser's exact
User-Agent with user_agent="Mozilla/5.0 …" (a recent Chrome one is the default).
This reads your lists, including a private one, and can add, change and delete entries, but only the status, score and progress (episodes, or chapters and volumes) work; tags, comments, dates, priority and rewatch/reread values need the API. Every change is read back from your list, and pymal raises an error if it didn't happen.
The session expires: when pymal says MAL refused the session, log in in the browser again and pass the new cookies. This route uses MAL's website endpoints, which are not documented, can change without notice, and fall under MAL's website Terms of Use rather than the API agreement. If a client ID is set as well, the API is used. Treat the cookies like a password: anyone who has them is logged in as you.
Troubleshooting
| Message | Meaning |
|---|---|
You logged in as 'x', not 'y' |
The browser logged in as another account. Log in as the named one on MAL's page. |
The state doesn't match |
The pasted address is from another attempt. Run authorize again and paste the new one. |
MAL refused the token request (HTTP 4xx) |
Wrong client ID/secret, a used or expired code, a redirect URL that doesn't match the app, or a refresh token that no longer works. Log in again with authorize. |
UnauthenticatedAccountError |
No credentials at all (or no access token for something that needs one): log in first. |
MAL doesn't see the MALSESSIONID cookie as logged in |
The cookie is wrong or expired. Export it again from a logged in browser. |
MyAnimeListApiError (.status_code) |
MAL refused the request; the message includes MAL's answer. |
SiteUnavailableError |
MAL can't be reached, or keeps answering with a server error. |
Constructor
Returns a singleton keyed by username. token_file changes where the tokens are
saved and loaded from.
Attributes
| Attribute | Type | Description |
|---|---|---|
username |
str |
MAL username |
animes |
AccountAnimes |
The anime list (lazy-loaded set) |
mangas |
AccountMangas |
The manga list (lazy-loaded set) |
friends |
AccountFriends |
The friends list (lazy-loaded set, first 100) |
user_id |
int |
MAL user id (loaded on first access) |
is_auth |
bool |
Whether there is an access token (list editing possible) |
has_api_access |
bool |
Whether there is at least a client ID |
has_session |
bool |
Whether there is a website session cookie |
web |
WebSession or None |
The website session of the cookie login (pymal.web_session) |
Methods
authorize(client_id, client_secret=None, redirect_uri=None, open_browser=True)— the one-time login described above.login_with_cookie(session_id, save=False, user_agent="")— experimental login with browser cookies (a cookie export path, theMALSESSIONIDvalue, a dict or aCookieheader), see above.login(client_id, access_token="", refresh_token="", client_secret="", save=False)— use tokens you already have.api_pages(path, params=None)— likeapifor a paged answer ({"data": [...], "paging": ...}): iterates over all the items, following the pages.api(method, path, params=None, data=None)— call any API v2 endpoint, e.g.account.api("GET", "/users/@me"); returns the decoded JSON.me(fields="anime_statistics") -> dict— your profile and list statistics (needs a token).suggestions(limit=100)— animes MAL suggests to you, asAnimeobjects (needs a token).anime_ranking(ranking_type="all", limit=100)— the top animes (all,airing,upcoming,tv,ova,movie,special,bypopularity,favorite) asAnimeobjects.search(search_line, is_anime=True)— search anime (or manga); results already in your list are yourMyAnime/MyManga.get_image()— download the avatar as aPIL.Image.Image.reload()— re-fetch account data.
Errors: UnauthenticatedAccountError when there are no credentials,
MyAnimeListApiError (with .status_code) when the API refuses a request, and
SiteUnavailableError when MAL can't be reached.
List objects
account.animes and account.mangas are set-like objects. Their entries are
MyAnime / MyManga objects, already filled with your data.
len(account.animes) # number of entries
anime in account.animes # membership test (Anime, MyAnime or ID)
account.animes.reload() # re-fetch from MAL
account.animes.watching # also completed, on_hold, dropped, plan_to_watch
account.mangas.reading # also completed, on_hold, dropped, plan_to_read
# Set operations
account.animes | friend.animes # union
account.animes & friend.animes # intersection
account.animes - friend.animes # difference
account.animes ^ friend.animes # symmetric difference