mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-12 15:50:01 -08:00
258 lines
8.7 KiB
Python
258 lines
8.7 KiB
Python
import logging
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
from httpx import Client
|
|
|
|
from ....core.config import AnilistConfig
|
|
from ....core.utils.graphql import (
|
|
execute_graphql,
|
|
)
|
|
from ..base import BaseApiClient
|
|
from ..params import (
|
|
MediaAiringScheduleParams,
|
|
MediaCharactersParams,
|
|
MediaRecommendationParams,
|
|
MediaRelationsParams,
|
|
MediaSearchParams,
|
|
UpdateUserMediaListEntryParams,
|
|
UserMediaListSearchParams,
|
|
)
|
|
from ..types import MediaSearchResult, UserMediaListStatus, UserProfile
|
|
from . import gql, mapper
|
|
|
|
logger = logging.getLogger(__name__)
|
|
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
|
|
|
|
|
user_list_status_map = {
|
|
UserMediaListStatus.WATCHING: "CURRENT",
|
|
UserMediaListStatus.PLANNING: "PLANNING",
|
|
UserMediaListStatus.COMPLETED: "COMPLETED",
|
|
UserMediaListStatus.DROPPED: "DROPPED",
|
|
UserMediaListStatus.PAUSED: "PAUSED",
|
|
UserMediaListStatus.REPEATING: "REPEATING",
|
|
}
|
|
|
|
# TODO: Just remove and have consistent variable naming between the two
|
|
search_params_map = {
|
|
# Custom Name: AniList Variable Name
|
|
"query": "query",
|
|
"page": "page",
|
|
"per_page": "per_page",
|
|
"sort": "sort",
|
|
"id_in": "id_in",
|
|
"genre_in": "genre_in",
|
|
"genre_not_in": "genre_not_in",
|
|
"tag_in": "tag_in",
|
|
"tag_not_in": "tag_not_in",
|
|
"status_in": "status_in",
|
|
"status": "status",
|
|
"status_not_in": "status_not_in",
|
|
"popularity_greater": "popularity_greater",
|
|
"popularity_lesser": "popularity_lesser",
|
|
"averageScore_greater": "averageScore_greater",
|
|
"averageScore_lesser": "averageScore_lesser",
|
|
"seasonYear": "seasonYear",
|
|
"season": "season",
|
|
"startDate_greater": "startDate_greater",
|
|
"startDate_lesser": "startDate_lesser",
|
|
"startDate": "startDate",
|
|
"endDate_greater": "endDate_greater",
|
|
"endDate_lesser": "endDate_lesser",
|
|
"format_in": "format_in",
|
|
"type": "type",
|
|
"on_list": "on_list",
|
|
}
|
|
|
|
|
|
class AniListApi(BaseApiClient):
|
|
"""AniList API implementation of the BaseApiClient contract."""
|
|
|
|
def __init__(self, config: AnilistConfig, client: Client):
|
|
super().__init__(config, client)
|
|
self.token: Optional[str] = None
|
|
self.user_profile: Optional[UserProfile] = None
|
|
|
|
def authenticate(self, token: str) -> Optional[UserProfile]:
|
|
self.token = token
|
|
self.http_client.headers["Authorization"] = f"Bearer {token}"
|
|
self.user_profile = self.get_viewer_profile()
|
|
if not self.user_profile:
|
|
self.token = None
|
|
self.http_client.headers.pop("Authorization", None)
|
|
return self.user_profile
|
|
|
|
def is_authenticated(self) -> bool:
|
|
return True if self.user_profile else False
|
|
|
|
def get_viewer_profile(self) -> Optional[UserProfile]:
|
|
if not self.token:
|
|
return None
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.GET_LOGGED_IN_USER, {}
|
|
)
|
|
return mapper.to_generic_user_profile(response.json())
|
|
|
|
def search_media(self, params: MediaSearchParams) -> Optional[MediaSearchResult]:
|
|
variables = {
|
|
search_params_map[k]: v
|
|
for k, v in params.__dict__.items()
|
|
if v is not None and not isinstance(v, Enum)
|
|
}
|
|
|
|
# handle case where value is an enum
|
|
variables.update(
|
|
{
|
|
search_params_map[k]: v.value
|
|
for k, v in params.__dict__.items()
|
|
if v is not None and isinstance(v, Enum)
|
|
}
|
|
)
|
|
|
|
# handle case where is a list of enums
|
|
variables.update(
|
|
{
|
|
search_params_map[k]: list(map(lambda item: item.value, v))
|
|
for k, v in params.__dict__.items()
|
|
if v is not None and isinstance(v, list) and isinstance(v[0], Enum)
|
|
}
|
|
)
|
|
|
|
variables["per_page"] = params.per_page or self.config.per_page
|
|
|
|
# ignore hentai by default
|
|
variables["genre_not_in"] = (
|
|
list(map(lambda item: item.value, params.genre_not_in))
|
|
if params.genre_not_in
|
|
else ["Hentai"]
|
|
)
|
|
|
|
# anime by default
|
|
variables["type"] = params.type.value if params.type else "ANIME"
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.SEARCH_MEDIA, variables
|
|
)
|
|
return mapper.to_generic_search_result(response.json())
|
|
|
|
def search_media_list(
|
|
self, params: UserMediaListSearchParams
|
|
) -> Optional[MediaSearchResult]:
|
|
if not self.user_profile:
|
|
logger.error("Cannot fetch user list: user is not authenticated.")
|
|
return None
|
|
|
|
# TODO: use consistent variable naming btw graphql and params
|
|
# so variables can be dynamically filled
|
|
variables = {
|
|
"sort": params.sort.value
|
|
if params.sort
|
|
else self.config.media_list_sort_by.value,
|
|
"userId": self.user_profile.id,
|
|
"status": user_list_status_map[params.status] if params.status else None,
|
|
"page": params.page,
|
|
"perPage": params.per_page or self.config.per_page,
|
|
"type": params.type.value if params.type else "ANIME",
|
|
}
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.SEARCH_USER_MEDIA_LIST, variables
|
|
)
|
|
return mapper.to_generic_user_list_result(response.json()) if response else None
|
|
|
|
def update_list_entry(self, params: UpdateUserMediaListEntryParams) -> bool:
|
|
if not self.token:
|
|
return False
|
|
score_raw = int(params.score * 10) if params.score is not None else None
|
|
variables = {
|
|
"mediaId": params.media_id,
|
|
"status": user_list_status_map[params.status] if params.status else None,
|
|
"progress": int(float(params.progress)) if params.progress else None,
|
|
"scoreRaw": score_raw,
|
|
}
|
|
variables = {k: v for k, v in variables.items() if v is not None}
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.SAVE_MEDIA_LIST_ENTRY, variables
|
|
)
|
|
return response.json() is not None and "errors" not in response.json()
|
|
|
|
def delete_list_entry(self, media_id: int) -> bool:
|
|
if not self.token:
|
|
return False
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT,
|
|
self.http_client,
|
|
gql.GET_MEDIA_LIST_ITEM,
|
|
{"mediaId": media_id},
|
|
)
|
|
entry_data = response.json()
|
|
|
|
list_id = (
|
|
entry_data.get("data", {}).get("MediaList", {}).get("id")
|
|
if entry_data
|
|
else None
|
|
)
|
|
if not list_id:
|
|
return False
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT,
|
|
self.http_client,
|
|
gql.DELETE_MEDIA_LIST_ENTRY,
|
|
{"id": list_id},
|
|
)
|
|
return (
|
|
response.json()
|
|
.get("data", {})
|
|
.get("DeleteMediaListEntry", {})
|
|
.get("deleted", False)
|
|
if response
|
|
else False
|
|
)
|
|
|
|
def get_recommendation_for(self, params: MediaRecommendationParams):
|
|
variables = {"mediaRecommendationId": params.id, "page": params.page}
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.GET_RECOMMENDATIONS, variables
|
|
)
|
|
return response
|
|
|
|
def get_characters_of(self, params: MediaCharactersParams):
|
|
variables = {"id": params.id}
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.GET_CHARACTERS, variables
|
|
)
|
|
return response
|
|
|
|
def get_related_anime_for(self, params: MediaRelationsParams):
|
|
variables = {"id": params.id}
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.GET_MEDIA_RELATIONS, variables
|
|
)
|
|
|
|
def get_airing_schedule_for(self, params: MediaAiringScheduleParams):
|
|
variables = {"id": params.id}
|
|
response = execute_graphql(
|
|
ANILIST_ENDPOINT, self.http_client, gql.GET_AIRING_SCHEDULE, variables
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
from httpx import Client
|
|
|
|
from ....core.config import AnilistConfig
|
|
from ....core.constants import APP_ASCII_ART
|
|
from ..params import MediaSearchParams
|
|
|
|
anilist = AniListApi(AnilistConfig(), Client())
|
|
print(APP_ASCII_ART)
|
|
|
|
# search
|
|
query = input("What anime would you like to search for: ")
|
|
search_results = anilist.search_media(MediaSearchParams(query=query))
|
|
if not search_results:
|
|
print("Nothing was finding matching: ", query)
|
|
exit()
|
|
for result in search_results.media:
|
|
print(
|
|
f"Title: {result.title.english or result.title.romaji} Episodes: {result.episodes}"
|
|
)
|