Skip to content

mal

MALAlternativeTitles(en=None, ja=None, synonyms=None) dataclass

Bases: DataClassJsonMixin

A json-serializable class that holds alternative anime titles.

Attributes:

Name Type Description
en Optional[str]

The (offical) english variant of the name

ja Optional[str]

The (offical) japanese variant of the name

synonyms Optional[List[str]]

Other synonymous names

MALAnime(id, title, media_type, num_episodes, alternative_titles=None, start_season=None, my_list_status=None) dataclass

Bases: DataClassJsonMixin

A json-serializable class that holds information about an anime and the user's list status if the anime is in their list.

Attributes:

Name Type Description
id int

Id of the anime

title str

Title of the anime

media_type MALMediaTypeEnum

Media type of the anime

num_episodes int

Number of episodes the anime has, if unknown it is 0

alternative_titles Optional[MALAlternativeTitles]

Alternative titles for an anime

start_season Optional[MALStartSeason]

Season/Year the anime started in

my_list_status Optional[MALMyListStatus]

If the anime is in the user's list, this holds the information of the list status

MALMediaTypeEnum

Bases: Enum

A enum of possible media types.

Attributes:

Name Type Description
TV
MOVIE
OVA
ONA
SPECIAL
TV_SPECIAL
MUSIC
CM
UNKNOWN

MALMyListStatus(num_episodes_watched, tags, status, score) dataclass

Bases: DataClassJsonMixin

A json-serializable class that holds a user's list status. It accompanies MALAnime.

Attributes:

Name Type Description
num_episodes_watched int

Watched episodes, this number may exceed that of the num_episodes in MALAnime as it can be abitrarily large.

tags List[str]

List of tags associated with the anime

status MALMyListStatusEnum

Current status of the anime

score int

The user's score of the anime

MALMyListStatusEnum

Bases: Enum

A enum of possible list states.

Attributes:

Name Type Description
WATCHING
COMPLETED
ON_HOLD
DROPPED
PLAN_TO_WATCH

MALSeasonEnum

Bases: Enum

A enum of possible seasons.

Attributes:

Name Type Description
WINTER
SPRING
SUMMER
FALL

MALStartSeason(season, year) dataclass

Bases: DataClassJsonMixin

A json-serializable class that holds a season/year combination indicating the time this anime was aired in.

Attributes:

Name Type Description
season MALSeasonEnum

Season of airing

year int

Year of airing

MALUser(id, name, picture=None) dataclass

Bases: DataClassJsonMixin

A json-serializable class that holds user data.

Attributes:

Name Type Description
id int

Users's id

name str

Users's name

picture Optional[str]

Users's profile picture

MyAnimeList(client_id=None)

MyAnimeList api client that implements some of the endpoints documented here.

Attributes:

Name Type Description
API_BASE

The base url of the api (https://api.myanimelist.net/v2)

CLIENT_ID

The client being used to access the api

RESPONSE_FIELDS

Corresponds to fields of MALAnime object (read here for explaination)

Parameters:

Name Type Description Default
client_id Optional[str]

Overrides the default client id

None
Info

Please note that that currently no complex oauth autentication scheme is implemented, this client uses the client id of the official MyAnimeList android app, this gives us the ability to login via a username/password combination. If you pass your own client id you will not be able to use the from_password_grant function.

Source code in api/src/anipy_api/mal.py
def __init__(self, client_id: Optional[str] = None):
    """__init__ of MyAnimeList.

    Args:
        client_id: Overrides the default client id

    Info:
        Please note that that currently no complex oauth autentication scheme is
        implemented, this client uses the client id of the official MyAnimeList
        android app, this gives us the ability to login via a username/password
        combination. If you pass your own client id you will not be able to use
        the [from_password_grant][anipy_api.mal.MyAnimeList.from_password_grant] function.
    """
    if client_id:
        self.CLIENT_ID = client_id

    self._refresh_token = None
    self._auth_expire_time = datetime.datetime.min

    self._session = Session()
    self._session.headers.update(
        {
            "X-MAL-Client-ID": self.CLIENT_ID,
        }
    )

from_password_grant(user, password, client_id=None) staticmethod

Authenticate via a username/password combination.

Parameters:

Name Type Description Default
user str

MyAnimeList username

required
password str

MyAnimeList password

required
client_id Optional[str]

Overrides the default client id

None

Returns:

Type Description
MyAnimeList

The MyAnimeList client object

Source code in api/src/anipy_api/mal.py
@staticmethod
def from_password_grant(
    user: str, password: str, client_id: Optional[str] = None
) -> "MyAnimeList":
    """Authenticate via a username/password combination.

    Args:
        user: MyAnimeList username
        password: MyAnimeList password
        client_id: Overrides the default client id

    Returns:
        The MyAnimeList client object
    """
    mal = MyAnimeList(client_id)
    mal._refresh_auth(user, password)
    return mal

from_rt_grant(refresh_token, client_id=None) staticmethod

Authenticate via a refresh token. The refresh token will be saved in MyAnimeList._refresh_token and used to periodically refresh the access token, the refresh token may change if the current one expires.

Parameters:

Name Type Description Default
refresh_token str

The refresh token

required
client_id Optional[str]

Overrides the default client id

None

Returns:

Type Description
MyAnimeList

The MyAnimeList client object

Source code in api/src/anipy_api/mal.py
@staticmethod
def from_rt_grant(
    refresh_token: str, client_id: Optional[str] = None
) -> "MyAnimeList":
    """Authenticate via a refresh token. The refresh token will be saved in
    `MyAnimeList._refresh_token` and used to periodically refresh the
    access token, the refresh token may change if the current one expires.

    Args:
        refresh_token: The refresh token
        client_id: Overrides the default client id

    Returns:
        The MyAnimeList client object
    """
    mal = MyAnimeList(client_id)
    mal._refresh_token = refresh_token
    mal._refresh_auth()
    return mal

get_anime(anime_id)

Get a MyAnimeList anime by its id.

Parameters:

Name Type Description Default
anime_id int

The id of the anime

required

Returns:

Type Description
MALAnime

The anime that corresponds to the id

Source code in api/src/anipy_api/mal.py
def get_anime(self, anime_id: int) -> MALAnime:
    """Get a MyAnimeList anime by its id.

    Args:
        anime_id: The id of the anime

    Returns:
        The anime that corresponds to the id
    """
    request = Request("GET", f"{self.API_BASE}/anime/{anime_id}")
    return MALAnime.from_dict(self._make_request(request))

get_anime_list(status_filter=None)

Get the anime list of the currently authenticated user.

Parameters:

Name Type Description Default
status_filter Optional[MALMyListStatusEnum]

A filter that determines which list status is retrieved

None

Returns:

Type Description
List[MALAnime]

List of anime in the anime list

Source code in api/src/anipy_api/mal.py
def get_anime_list(
    self, status_filter: Optional[MALMyListStatusEnum] = None
) -> List[MALAnime]:
    """Get the anime list of the currently authenticated user.

    Args:
        status_filter: A filter that determines which list status is retrieved

    Returns:
        List of anime in the anime list
    """
    params = dict()
    if status_filter:
        params = {"status": status_filter.value}

    return self._get_resource("users/@me/animelist", params, limit=20, pages=-1)

Search MyAnimeList.

Parameters:

Name Type Description Default
query str

Search query

required
limit int

The amount of results per page

20
pages int

The amount of pages to return, note the total number of results is limit times pages

1

Returns:

Type Description
List[MALAnime]

A list of search results

Source code in api/src/anipy_api/mal.py
def get_search(self, query: str, limit: int = 20, pages: int = 1) -> List[MALAnime]:
    """Search MyAnimeList.

    Args:
        query: Search query
        limit: The amount of results per page
        pages: The amount of pages to return,
            note the total number of results is limit times pages

    Returns:
        A list of search results
    """
    return self._get_resource("anime", {"q": query}, limit, pages)

get_user()

Get information about the currently authenticated user.

Returns:

Type Description
MALUser

A object with user information

Source code in api/src/anipy_api/mal.py
def get_user(self) -> MALUser:
    """Get information about the currently authenticated user.

    Returns:
        A object with user information
    """
    request = Request(
        "GET", f"{self.API_BASE}/users/@me", params={"fields": "id,name,picture"}
    )
    return MALUser.from_dict(self._make_request(request))

remove_from_anime_list(anime_id)

Remove an anime from the currently authenticated user's anime list.

Parameters:

Name Type Description Default
anime_id int

Id of the anime to be removed

required
Source code in api/src/anipy_api/mal.py
def remove_from_anime_list(self, anime_id: int):
    """Remove an anime from the currently authenticated user's anime list.

    Args:
        anime_id: Id of the anime to be removed
    """
    request = Request("DELETE", f"{self.API_BASE}/anime/{anime_id}/my_list_status")
    self._make_request(request)

update_anime_list(anime_id, status=None, watched_episodes=None, tags=None)

Update a specific anime in the currently authenticated users's anime list. Only pass the arguments you want to update.

Parameters:

Name Type Description Default
anime_id int

The anime id of the anime to update

required
status Optional[MALMyListStatusEnum]

Updated status of the anime

None
watched_episodes Optional[int]

Updated watched episodes

None
tags Optional[List[str]]

Updated list of tags, note that this ovewrites the already existing tags, if you want to retain the old ones you have to merge the old ones with the new ones yourself.

None

Returns:

Type Description
MALMyListStatus

Object of the updated anime

Source code in api/src/anipy_api/mal.py
def update_anime_list(
    self,
    anime_id: int,
    status: Optional[MALMyListStatusEnum] = None,
    watched_episodes: Optional[int] = None,
    tags: Optional[List[str]] = None,
) -> MALMyListStatus:
    """Update a specific anime in the currently authenticated users's anime
    list. Only pass the arguments you want to update.

    Args:
        anime_id: The anime id of the anime to update
        status: Updated status of the anime
        watched_episodes: Updated watched episodes
        tags: Updated list of tags, note that this **ovewrites** the already
            existing tags, if you want to retain the old ones you have to merge
            the old ones with the new ones yourself.

    Returns:
        Object of the updated anime
    """
    data = {
        k: v
        for k, v in {
            "status": status.value if status else None,
            "num_watched_episodes": watched_episodes,
            "tags": ",".join(tags) if tags is not None else None,
        }.items()
        if v is not None
    }
    request = Request(
        "PATCH", f"{self.API_BASE}/anime/{anime_id}/my_list_status", data=data
    )

    return MALMyListStatus.schema(unknown="exclude").load(
        self._make_request(request)
    )

MyAnimeListAdapter(myanimelist, provider)

A adapter class that can adapt MyAnimeList anime to Provider anime.

Attributes:

Name Type Description
mal MyAnimeList

The MyAnimeList object

provider BaseProvider

The provider object

Parameters:

Name Type Description Default
myanimelist MyAnimeList

The MyAnimeList object to use

required
provider BaseProvider

The provider object to use

required
Source code in api/src/anipy_api/mal.py
def __init__(self, myanimelist: MyAnimeList, provider: "BaseProvider") -> None:
    """__init__ of MyAnimeListAdapter.

    Args:
        myanimelist: The MyAnimeList object to use
        provider: The provider object to use
    """
    self.mal: MyAnimeList = myanimelist
    self.provider: "BaseProvider" = provider

from_myanimelist(mal_anime, minimum_similarity_ratio=0.8, use_filters=True, use_alternative_names=True)

Adapt an anime from a MALAnime to a provider Anime. This uses Levenshtein Distance to calculate the similarity of names.

Parameters:

Name Type Description Default
mal_anime MALAnime

The mal anime to adapt from

required
minimum_similarity_ratio float

The minimum accepted similarity ratio. This should be a number from 0-1, 1 meaning the names are identical 0 meaning there are no identical charachters whatsoever. If it is not met the function will return None.

0.8
use_filters bool

Use filters for the provider to cut down on possible wrong results, do note that this will take more time.

True
use_alternative_names bool

Use alternative names for matching, this may yield a higher chance of finding a match but takes more time.

True

Returns:

Type Description
Optional[Anime]

A Anime object if adapting was successfull

Source code in api/src/anipy_api/mal.py
def from_myanimelist(
    self,
    mal_anime: MALAnime,
    minimum_similarity_ratio: float = 0.8,
    use_filters: bool = True,
    use_alternative_names: bool = True,
) -> Optional[Anime]:
    """Adapt an anime from a [MALAnime][anipy_api.mal.MALAnime] to a provider [Anime][anipy_api.anime.Anime].
    This uses [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) to calculate the similarity of names.

    Args:
        mal_anime: The mal anime to adapt from
        minimum_similarity_ratio: The minimum accepted similarity ratio. This should be a number from 0-1,
            1 meaning the names are identical 0 meaning there are no identical charachters whatsoever.
            If it is not met the function will return None.
        use_filters: Use filters for the provider to cut down on possible wrong results, do note that this will take more time.
        use_alternative_names: Use alternative names for matching, this may yield a higher chance of finding a match but takes more time.

    Returns:
        A Anime object if adapting was successfull

    """
    mal_titles = {mal_anime.title}
    if use_alternative_names and mal_anime.alternative_titles is not None:
        mal_titles |= {
            t
            for t in [
                mal_anime.alternative_titles.ja,
                mal_anime.alternative_titles.en,
            ]
            if t is not None
        }
        mal_titles |= (
            set(mal_anime.alternative_titles.synonyms)
            if mal_anime.alternative_titles.synonyms is not None
            else set()
        )

    provider_filters = Filters()
    if (
        self.provider.FILTER_CAPS & FilterCapabilities.YEAR
        and mal_anime.start_season is not None
    ):
        provider_filters.year = mal_anime.start_season.year

    if (
        self.provider.FILTER_CAPS & FilterCapabilities.SEASON
        and mal_anime.start_season is not None
    ):
        provider_filters.season = Season[
            mal_anime.start_season.season.value.upper()
        ]

    if self.provider.FILTER_CAPS & FilterCapabilities.MEDIA_TYPE:
        if mal_anime.media_type not in (
            MALMediaTypeEnum.UNKNOWN,
            MALMediaTypeEnum.CM,
        ):
            if mal_anime.media_type == MALMediaTypeEnum.TV_SPECIAL:
                m_type = MALMediaTypeEnum.SPECIAL
            else:
                m_type = mal_anime.media_type

            provider_filters.media_type = MediaType[m_type.value.upper()]

    results: Set[ProviderSearchResult] = set()
    for title in mal_titles:
        if len(title) == 0:
            continue

        results |= set(self.provider.get_search(title))
        if use_filters:
            results |= set(self.provider.get_search(title, provider_filters))

    best_ratio = 0
    best_anime = None
    for r in results:
        anime = Anime.from_search_result(self.provider, r)
        provider_titles = {anime.name}

        if use_alternative_names:
            provider_titles |= set(anime.get_info().alternative_names or [])

        ratio = self._find_best_ratio(mal_titles, provider_titles)

        if ratio > best_ratio:
            best_ratio = ratio
            best_anime = anime
        elif (
            best_anime is not None
            and ratio == best_ratio
            and len(anime.languages) > len(best_anime.languages)
        ):
            # prefer anime with more language options
            best_anime = anime

        if best_ratio == 1:
            break

    if best_ratio > minimum_similarity_ratio:
        return best_anime

from_provider(anime, minimum_similarity_ratio=0.8, use_alternative_names=True)

Adapt an anime from provider Anime to a MALAnime. This uses Levenshtein Distance to calculate the similarity of names.

Parameters:

Name Type Description Default
anime Anime

The anime to adapt from

required
minimum_similarity_ratio float

The minimum accepted similarity ratio. This should be a number from 0-1, 1 meaning the names are identical 0 meaning there are no identical charachters whatsoever. If it is not met the function will return None.

0.8
use_alternative_names bool

Use alternative names for matching, this may yield a higher chance of finding a match but takes more time.

True

Returns:

Type Description
Optional[MALAnime]

A MALAnime object if adapting was successfull

Source code in api/src/anipy_api/mal.py
def from_provider(
    self,
    anime: Anime,
    minimum_similarity_ratio: float = 0.8,
    use_alternative_names: bool = True,
) -> Optional[MALAnime]:
    """Adapt an anime from provider [Anime][anipy_api.anime.Anime] to a [MALAnime][anipy_api.mal.MALAnime].
    This uses [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) to calculate the similarity of names.

    Args:
        anime: The anime to adapt from
        minimum_similarity_ratio: The minimum accepted similarity ratio. This should be a number from 0-1,
            1 meaning the names are identical 0 meaning there are no identical charachters whatsoever.
            If it is not met the function will return None.
        use_alternative_names: Use alternative names for matching, this may yield a higher chance of finding
            a match but takes more time.

    Returns:
        A MALAnime object if adapting was successfull
    """
    results = self.mal.get_search(anime.name)

    best_anime = None
    best_ratio = 0
    for i in results:
        titles_mal = {i.title}
        titles_provider = {anime.name}

        if use_alternative_names:
            if i.alternative_titles is not None:
                titles_mal |= {
                    t
                    for t in [i.alternative_titles.ja, i.alternative_titles.en]
                    if t is not None
                }
                titles_mal |= (
                    set(i.alternative_titles.synonyms)
                    if i.alternative_titles.synonyms is not None
                    else set()
                )
            titles_provider |= set(anime.get_info().alternative_names or [])

        ratio = self._find_best_ratio(titles_mal, titles_provider)

        if ratio > best_ratio:
            best_ratio = ratio
            best_anime = i

        if best_ratio == 1:
            break

    if best_ratio >= minimum_similarity_ratio:
        return best_anime