From 1433e04f7316021bd784f3da57ece60041cb2d10 Mon Sep 17 00:00:00 2001 From: Benex254 Date: Mon, 5 Aug 2024 09:47:04 +0300 Subject: [PATCH] feat(anilist): add account intergration --- fastanime/Utility/user_data_helper.py | 6 +- fastanime/cli/commands/anilist/__init__.py | 2 + fastanime/cli/commands/anilist/login.py | 35 ++++++ fastanime/cli/config.py | 8 ++ fastanime/libs/anilist/anilist_data_schema.py | 12 +- fastanime/libs/anilist/api.py | 82 +++++++++++- fastanime/libs/anilist/queries_graphql.py | 118 ++++++++++++++++++ 7 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 fastanime/cli/commands/anilist/login.py diff --git a/fastanime/Utility/user_data_helper.py b/fastanime/Utility/user_data_helper.py index 24631d5..3240498 100644 --- a/fastanime/Utility/user_data_helper.py +++ b/fastanime/Utility/user_data_helper.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) class UserData: - user_data = {"watch_history": {}, "animelist": []} + user_data = {"watch_history": {}, "animelist": [], "user": {}} def __init__(self): try: @@ -23,6 +23,10 @@ class UserData: self.user_data["watch_history"] = watch_history self._update_user_data() + def update_user_info(self, user: dict): + self.user_data["user"] = user + self._update_user_data() + def update_animelist(self, anime_list: list): self.user_data["animelist"] = list(set(anime_list)) self._update_user_data() diff --git a/fastanime/cli/commands/anilist/__init__.py b/fastanime/cli/commands/anilist/__init__.py index dd5f446..8a5f244 100644 --- a/fastanime/cli/commands/anilist/__init__.py +++ b/fastanime/cli/commands/anilist/__init__.py @@ -3,6 +3,7 @@ import click from ...interfaces.anilist_interfaces import anilist as anilist_interface from ...utils.tools import QueryDict from .favourites import favourites +from .login import loggin from .popular import popular from .random_anime import random_anime from .recent import recent @@ -20,6 +21,7 @@ commands = { "popular": popular, "favourites": favourites, "random": random_anime, + "loggin": loggin, } diff --git a/fastanime/cli/commands/anilist/login.py b/fastanime/cli/commands/anilist/login.py new file mode 100644 index 0000000..b13bbd1 --- /dev/null +++ b/fastanime/cli/commands/anilist/login.py @@ -0,0 +1,35 @@ +import webbrowser + +import click +from rich import print +from rich.prompt import Prompt + +from ....anilist import AniList +from ...config import Config +from ...utils.tools import exit_app + + +@click.command(help="Login to your anilist account") +@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True) +@click.pass_obj +def loggin(config: Config, status): + if status: + is_logged_in = True if config.user else False + message = ( + "You are logged in :happy:" if is_logged_in else "You arent logged in :sad:" + ) + print(message) + exit_app() + if config.user: + print("Already logged in :confused:") + exit_app() + # ---- new loggin ----- + print("A browser session will be opened") + webbrowser.open(config.fastanime_anilist_app_login_url) + print("Please paste the token provided here") + token = Prompt.ask("Enter token") + user = AniList.login_user(token) + config.update_user(user) + print("Successfully saved credentials") + print(user) + exit_app() diff --git a/fastanime/cli/config.py b/fastanime/cli/config.py index 8681212..5dafa24 100644 --- a/fastanime/cli/config.py +++ b/fastanime/cli/config.py @@ -11,6 +11,9 @@ from ..Utility.user_data_helper import user_data_helper class Config(object): anime_list: list watch_history: dict + fastanime_anilist_app_login_url = ( + "https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token" + ) def __init__(self) -> None: self.load_config() @@ -59,9 +62,14 @@ class Config(object): # ---- setup user data ------ self.watch_history: dict = user_data_helper.user_data.get("watch_history", {}) self.anime_list: list = user_data_helper.user_data.get("animelist", []) + self.user: dict = user_data_helper.user_data.get("user", {}) self.anime_provider = AnimeProvider(self.provider) + def update_user(self, user): + self.user = user + user_data_helper.update_user_info(user) + def update_watch_history(self, anime_id: int, episode: str | None): self.watch_history.update({str(anime_id): episode}) user_data_helper.update_watch_history(self.watch_history) diff --git a/fastanime/libs/anilist/anilist_data_schema.py b/fastanime/libs/anilist/anilist_data_schema.py index 33eb3a7..152c5fc 100644 --- a/fastanime/libs/anilist/anilist_data_schema.py +++ b/fastanime/libs/anilist/anilist_data_schema.py @@ -17,6 +17,13 @@ class AnilistImage(TypedDict): large: str +class AnilistUser(TypedDict): + id: int + name: str + bannerImage: str | None + avatar: AnilistImage + + class AnilistMediaTrailer(TypedDict): id: str site: str @@ -49,11 +56,6 @@ class AnilistMediaNextAiringEpisode(TypedDict): episode: int -class AnilistUser(TypedDict): - name: str - avatar: AnilistImage - - class AnilistReview(TypedDict): summary: str user: AnilistUser diff --git a/fastanime/libs/anilist/api.py b/fastanime/libs/anilist/api.py index d6c624f..179aa61 100644 --- a/fastanime/libs/anilist/api.py +++ b/fastanime/libs/anilist/api.py @@ -2,14 +2,18 @@ This is the core module availing all the abstractions of the anilist api """ +from typing import Literal + import requests -from .anilist_data_schema import AnilistDataSchema +from .anilist_data_schema import AnilistDataSchema, AnilistUser from .queries_graphql import ( airing_schedule_query, anime_characters_query, anime_query, anime_relations_query, + get_logged_in_user_query, + media_list_query, most_favourite_query, most_popular_query, most_recently_updated_query, @@ -21,6 +25,7 @@ from .queries_graphql import ( ) # from kivy.network.urlrequest import UrlRequestRequests +ANILIST_ENDPOINT = "https://graphql.anilist.co" class AniListApi: @@ -28,6 +33,76 @@ class AniListApi: This class provides an abstraction for the anilist api """ + def login_user(self, token: str): + self.token = token + self.headers = {"Authorization": f"Bearer {self.token}"} + user = self.get_logged_in_user() + if not user: + return + if not user[0]: + return + user_info: AnilistUser = user[1]["data"]["Viewer"] # pyright:ignore + self.user_id = user_info["id"] # pyright:ignore + return user_info + + def update_login_info(self, user: AnilistUser, token: str): + self.token = token + self.headers = {"Authorization": f"Bearer {self.token}"} + self.user_id = user["id"] + + def get_logged_in_user(self): + if not self.headers: + return + return self._make_authenticated_request(get_logged_in_user_query) + + def get_anime_list( + self, + status: Literal[ + "CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING" + ], + ): + variables = {"status": status, "id": self.user_id} + return self._make_authenticated_request(media_list_query, variables) + + def _make_authenticated_request(self, query: str, variables: dict = {}): + """ + The core abstraction for getting authenticated data from the anilist api + + Parameters: + ---------- + query:str + a valid anilist graphql query + variables:dict + variables to pass to the anilist api + """ + # req=UrlRequestRequests(url, self.got_data,) + try: + # TODO: check if data is as expected + response = requests.post( + ANILIST_ENDPOINT, + json={"query": query, "variables": variables}, + timeout=10, + headers=self.headers, + ) + anilist_data = response.json() + return (True, anilist_data) + except requests.exceptions.Timeout: + return ( + False, + { + "Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down." + }, + ) # type: ignore + except requests.exceptions.ConnectionError: + return ( + False, + { + "Error": "There might be a problem with your internet or anilist is down." + }, + ) # type: ignore + except Exception as e: + return (False, {"Error": f"{e}"}) # type: ignore + def get_data( self, query: str, variables: dict = {} ) -> tuple[bool, AnilistDataSchema]: @@ -41,12 +116,13 @@ class AniListApi: variables:dict variables to pass to the anilist api """ - url = "https://graphql.anilist.co" # req=UrlRequestRequests(url, self.got_data,) try: # TODO: check if data is as expected response = requests.post( - url, json={"query": query, "variables": variables}, timeout=10 + ANILIST_ENDPOINT, + json={"query": query, "variables": variables}, + timeout=10, ) anilist_data: AnilistDataSchema = response.json() return (True, anilist_data) diff --git a/fastanime/libs/anilist/queries_graphql.py b/fastanime/libs/anilist/queries_graphql.py index fca5cd7..d3e2df3 100644 --- a/fastanime/libs/anilist/queries_graphql.py +++ b/fastanime/libs/anilist/queries_graphql.py @@ -3,6 +3,124 @@ This module contains all the preset queries for the sake of neatness and convini Mostly for internal usage """ +get_logged_in_user_query = """ +query{ + Viewer{ + id + name + bannerImage + avatar { + large + medium + } + + } +} +""" + +media_list_mutation = """ +mutation($mediaId:Int,$id:Int,$scoreRaw:Int,$repeat:Int,$progress:Int){ + SaveMediaListEntry(mediaId:$mediaId,id:$id,scoreRaw:$scoreRaw,progress:$progress,repeat:$repeat){ + id + status + mediaId + score + progress + repeat + startedAt { + year + month + day + } + completedAt { + year + month + day + } + + } +} +""" + +media_list_query = """ +query ($userId: Int, $status: MediaListStatus) { + Page { + pageInfo { + currentPage + total + } + mediaList(userId: $userId, status: $status) { + mediaId + + media { + id + title { + romaji + english + } + coverImage { + medium + large + } + trailer { + site + id + } + popularity + favourites + averageScore + episodes + genres + studios { + nodes { + name + isAnimationStudio + } + } + tags { + name + } + startDate { + year + month + day + } + endDate { + year + month + day + } + status + description + nextAiringEpisode { + timeUntilAiring + airingAt + episode + } + } + status + progress + score + repeat + notes + startedAt { + year + month + day + } + completedAt { + year + month + day + } + createdAt + + } + } +} +""" + + optional_variables = "\ $page:Int,\ $sort:[MediaSort],\