From 2ff580cbd239ab530713b59dbc2bf430b62b0aa5 Mon Sep 17 00:00:00 2001 From: Benex254 Date: Mon, 5 Aug 2024 09:47:06 +0300 Subject: [PATCH] feat: show episodes count in ui and get progress from server if dont exist locally --- .../cli/interfaces/anilist_interfaces.py | 61 +++++++++++-------- fastanime/cli/interfaces/utils.py | 43 +++++-------- fastanime/libs/anilist/anilist_data_schema.py | 6 ++ fastanime/libs/anilist/api.py | 11 +++- fastanime/libs/anilist/queries_graphql.py | 44 +++++++++++++ 5 files changed, 112 insertions(+), 53 deletions(-) diff --git a/fastanime/cli/interfaces/anilist_interfaces.py b/fastanime/cli/interfaces/anilist_interfaces.py index 338a303..14f327b 100644 --- a/fastanime/cli/interfaces/anilist_interfaces.py +++ b/fastanime/cli/interfaces/anilist_interfaces.py @@ -373,16 +373,28 @@ def fetch_episode(config: Config, anilist_config: QueryDict): # internal config anime: Anime = anilist_config.anime _anime: SearchResult = anilist_config._anime - + selected_anime_anilist: AnilistBaseMediaDataSchema = ( + anilist_config.selected_anime_anilist + ) # prompt for episode number episodes = anime["availableEpisodesDetail"][translation_type] - if ( - continue_from_history - and user_watch_history.get(str(anime_id), {}).get("episode") in episodes - ): - episode_number = user_watch_history[str(anime_id)]["episode"] - print(f"[bold cyan]Continuing from Episode:[/] [bold]{episode_number}[/]") - else: + episode_number = "" + if continue_from_history: + if user_watch_history.get(str(anime_id), {}).get("episode") in episodes: + episode_number = user_watch_history[str(anime_id)]["episode"] + print(f"[bold cyan]Continuing from Episode:[/] [bold]{episode_number}[/]") + elif selected_anime_anilist["mediaListEntry"]: + episode_number = str( + selected_anime_anilist.get("mediaListEntry", {}).get( + "progress" + ) # type:ignore + ) + episode_number = episode_number if episode_number in episodes else "" + print(f"[bold cyan]Continuing from Episode:[/] [bold]{episode_number}[/]") + else: + episode_number = "" + + if not episode_number: choices = [*episodes, "Back"] if config.use_fzf: episode_number = fzf.run( @@ -501,6 +513,8 @@ def provide_anime(config: Config, anilist_config: QueryDict): def anilist_options(config, anilist_config: QueryDict): selected_anime: AnilistBaseMediaDataSchema = anilist_config.selected_anime_anilist selected_anime_title: str = anilist_config.selected_anime_title + progress = (selected_anime["mediaListEntry"] or {"progress": 0}).get("progress", 0) + episodes_total = selected_anime["episodes"] or "Inf" def _watch_trailer(config: Config, anilist_config: QueryDict): if trailer := selected_anime.get("trailer"): @@ -678,7 +692,7 @@ def anilist_options(config, anilist_config: QueryDict): icons = config.icons options = { - f"{'📽️ ' if icons else ''}Stream": provide_anime, + f"{'📽️ ' if icons else ''}Stream ({progress}/{episodes_total})": provide_anime, f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer, f"{'✨ ' if icons else ''}Score Anime": _score_anime, f"{'📥 ' if icons else ''}Add to List": _add_to_list, @@ -703,19 +717,24 @@ def anilist_options(config, anilist_config: QueryDict): def select_anime(config: Config, anilist_config: QueryDict): search_results = anilist_config.data["data"]["Page"]["media"] - anime_data = { - sanitize_filename( - str(anime["title"][config.preferred_language] or anime["title"]["romaji"]) - ): anime - for anime in search_results - } + + anime_data = {} + for anime in search_results: + anime: AnilistBaseMediaDataSchema + progress = (anime["mediaListEntry"] or {"progress": 0}).get("progress", 0) + episodes_total = anime["episodes"] or "Inf" + title = str( + anime["title"][config.preferred_language] or anime["title"]["romaji"] + ) + title = sanitize_filename(f"{title} ({progress} of {episodes_total})") + anime_data[title] = anime choices = [*anime_data.keys(), "Back"] if config.use_fzf: if config.preview: from .utils import get_preview - preview = get_preview(search_results, config) + preview = get_preview(search_results, anime_data.keys()) selected_anime_title = fzf.run( choices, prompt="Select Anime: ", @@ -733,15 +752,9 @@ def select_anime(config: Config, anilist_config: QueryDict): if config.preview: from .utils import IMAGES_DIR, get_icons - get_icons(search_results, config) + get_icons(search_results, anime_data.keys()) choices = [] - for anime in search_results: - title = sanitize_filename( - str( - anime["title"][config.preferred_language] - or anime["title"]["romaji"] - ) - ) + for title in anime_data.keys(): icon_path = os.path.join(IMAGES_DIR, title) choices.append(f"{title}\0icon\x1f{icon_path}") choices.append("Back") diff --git a/fastanime/cli/interfaces/utils.py b/fastanime/cli/interfaces/utils.py index acc90df..a9b642d 100644 --- a/fastanime/cli/interfaces/utils.py +++ b/fastanime/cli/interfaces/utils.py @@ -11,8 +11,7 @@ import requests from ...constants import APP_CACHE_DIR from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema from ...Utility import anilist_data_helper -from ...Utility.utils import remove_html_tags, sanitize_filename -from ..config import Config +from ...Utility.utils import remove_html_tags from ..utils.utils import get_true_fg logger = logging.getLogger(__name__) @@ -133,22 +132,20 @@ def save_info_from_str(info: str, file_name: str): def write_search_results( - search_results: list[AnilistBaseMediaDataSchema], config: Config, workers=None + search_results: list[AnilistBaseMediaDataSchema], + titles, + workers=None, ): H_COLOR = 215, 0, 95 S_COLOR = 208, 208, 208 S_WIDTH = 45 with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: future_to_task = {} - for anime in search_results: - anime_title = ( - anime["title"][config.preferred_language] or anime["title"]["romaji"] - ) - anime_title = sanitize_filename(anime_title) + for anime, title in zip(search_results, titles): image_url = anime["coverImage"]["large"] - future_to_task[ - executor.submit(save_image_from_url, image_url, anime_title) - ] = image_url + future_to_task[executor.submit(save_image_from_url, image_url, title)] = ( + image_url + ) # handle the text data template = f""" @@ -172,9 +169,7 @@ def write_search_results( {textwrap.fill(remove_html_tags( str(anime['description'])), width=45)} """ - future_to_task[ - executor.submit(save_info_from_str, template, anime_title) - ] = anime_title + future_to_task[executor.submit(save_info_from_str, template, title)] = title # execute the jobs for future in concurrent.futures.as_completed(future_to_task): @@ -186,19 +181,15 @@ def write_search_results( # get rofi icons -def get_icons(search_results: list[AnilistBaseMediaDataSchema], config, workers=None): +def get_icons(search_results: list[AnilistBaseMediaDataSchema], titles, workers=None): with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: # load the jobs future_to_url = {} - for anime in search_results: - anime_title = ( - anime["title"][config.preferred_language] or anime["title"]["romaji"] - ) - anime_title = sanitize_filename(anime_title) + for anime, title in zip(search_results, titles): image_url = anime["coverImage"]["large"] - future_to_url[ - executor.submit(save_image_from_url, image_url, anime_title) - ] = image_url + future_to_url[executor.submit(save_image_from_url, image_url, title)] = ( + image_url + ) # execute the jobs for future in concurrent.futures.as_completed(future_to_url): @@ -209,12 +200,10 @@ def get_icons(search_results: list[AnilistBaseMediaDataSchema], config, workers= logger.error("%r generated an exception: %s" % (url, exc)) -def get_preview( - search_results: list[AnilistBaseMediaDataSchema], config: Config, wait=False -): +def get_preview(search_results: list[AnilistBaseMediaDataSchema], titles, wait=False): # ensure images and info exists background_worker = Thread( - target=write_search_results, args=(search_results, config) + target=write_search_results, args=(search_results, titles) ) background_worker.daemon = True background_worker.start() diff --git a/fastanime/libs/anilist/anilist_data_schema.py b/fastanime/libs/anilist/anilist_data_schema.py index 79347fa..6fc42a3 100644 --- a/fastanime/libs/anilist/anilist_data_schema.py +++ b/fastanime/libs/anilist/anilist_data_schema.py @@ -108,6 +108,11 @@ class AnilistCharactersEdges(TypedDict): edges: list[AnilistCharactersEdge] +class AnilistMediaList_(TypedDict): + id: int + progress: int + + class AnilistBaseMediaDataSchema(TypedDict): """ This a convenience class is used to type the received Anilist data to enhance dev experience @@ -144,6 +149,7 @@ class AnilistBaseMediaDataSchema(TypedDict): externalLinks: list[AnilistExternalLink] characters: AnilistCharactersEdges format: str + mediaListEntry: AnilistMediaList_ | None class AnilistPageInfo(TypedDict): diff --git a/fastanime/libs/anilist/api.py b/fastanime/libs/anilist/api.py index 8e24568..5e02bef 100644 --- a/fastanime/libs/anilist/api.py +++ b/fastanime/libs/anilist/api.py @@ -45,9 +45,15 @@ class AniListApi: This class provides an abstraction for the anilist api """ + session: requests.Session + + def __init__(self) -> None: + self.session = requests.session() + def login_user(self, token: str): self.token = token self.headers = {"Authorization": f"Bearer {self.token}"} + self.session.headers.update(self.headers) user = self.get_logged_in_user() if not user: return @@ -68,6 +74,7 @@ class AniListApi: def update_login_info(self, user: AnilistUser, token: str): self.token = token self.headers = {"Authorization": f"Bearer {self.token}"} + self.session.headers.update(self.headers) self.user_id = user["id"] def get_logged_in_user(self): @@ -117,7 +124,7 @@ class AniListApi: # req=UrlRequestRequests(url, self.got_data,) try: # TODO: check if data is as expected - response = requests.post( + response = self.session.post( ANILIST_ENDPOINT, json={"query": query, "variables": variables}, timeout=10, @@ -180,7 +187,7 @@ class AniListApi: # req=UrlRequestRequests(url, self.got_data,) try: # TODO: check if data is as expected - response = requests.post( + response = self.session.post( ANILIST_ENDPOINT, json={"query": query, "variables": variables}, timeout=10, diff --git a/fastanime/libs/anilist/queries_graphql.py b/fastanime/libs/anilist/queries_graphql.py index d2be616..51247d1 100644 --- a/fastanime/libs/anilist/queries_graphql.py +++ b/fastanime/libs/anilist/queries_graphql.py @@ -171,6 +171,10 @@ query ($userId: Int, $status: MediaListStatus) { } status description + mediaListEntry{ + id + progress + } nextAiringEpisode { timeUntilAiring airingAt @@ -268,6 +272,10 @@ query($query:String,%s){ id } + mediaListEntry{ + id + progress + } popularity favourites averageScore @@ -345,6 +353,10 @@ query{ month day } + mediaListEntry{ + id + progress + } endDate { year month @@ -381,6 +393,10 @@ query{ id } + mediaListEntry{ + id + progress + } popularity favourites averageScore @@ -436,6 +452,10 @@ query{ id } + mediaListEntry{ + id + progress + } popularity episodes favourites @@ -497,6 +517,10 @@ query{ description episodes genres + mediaListEntry{ + id + progress + } studios { nodes { name @@ -545,6 +569,10 @@ query{ site id } + mediaListEntry{ + id + progress + } popularity favourites averageScore @@ -599,6 +627,10 @@ query { medium large } + mediaListEntry{ + id + progress + } description episodes trailer{ @@ -689,6 +721,10 @@ query ($id: Int) { medium large } + mediaListEntry{ + id + progress + } description episodes trailer { @@ -766,6 +802,10 @@ query ($page: Int) { site id } + mediaListEntry{ + id + progress + } popularity favourites averageScore @@ -812,6 +852,10 @@ query($id:Int){ romaji english } + mediaListEntry{ + id + progress + } nextAiringEpisode { timeUntilAiring airingAt