diff --git a/fastanime/Utility/data.py b/fastanime/Utility/data.py index aca936d..289e56b 100644 --- a/fastanime/Utility/data.py +++ b/fastanime/Utility/data.py @@ -9,6 +9,3 @@ anime_normalizer = { "Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka", 'Hazurewaku no "Joutai Ijou Skill" de Saikyou ni Natta Ore ga Subete wo Juurin suru made': "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made", } - - -anilist_sort_normalizer = {"search match": "SEARCH_MATCH"} diff --git a/fastanime/cli/__init__.py b/fastanime/cli/__init__.py index 579308d..ea65240 100644 --- a/fastanime/cli/__init__.py +++ b/fastanime/cli/__init__.py @@ -4,7 +4,6 @@ import click from .. import __version__ from ..libs.anime_provider import SERVERS_AVAILABLE, anime_sources -from ..Utility.data import anilist_sort_normalizer from .commands import LazyGroup commands = { @@ -116,9 +115,9 @@ signal.signal(signal.SIGINT, handle_exit) help="Auto select anime title?", ) @click.option( - "-S", - "--sort-by", - type=click.Choice(anilist_sort_normalizer.keys()), # pyright: ignore + "--normalize-titles/--no-normalize-titles", + type=bool, + help="whether to normalize anime and episode titls given by providers", ) @click.option("-d", "--downloads-dir", type=click.Path(), help="Downloads location") @click.option("--fzf", is_flag=True, help="Use fzf for the ui") @@ -165,7 +164,7 @@ def run_cli( quality, auto_next, auto_select, - sort_by, + normalize_titles, downloads_dir, fzf, default, @@ -232,6 +231,11 @@ def run_cli( ctx.obj.continue_from_history = continue_ if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE: ctx.obj.skip = skip + if ( + ctx.get_parameter_source("normalize_titles") + == click.core.ParameterSource.COMMANDLINE + ): + ctx.obj.normalize_titles = normalize_titles if quality: ctx.obj.quality = quality @@ -254,8 +258,6 @@ def run_cli( == click.core.ParameterSource.COMMANDLINE ): ctx.obj.use_mpv_mod = use_mpv_mod - if sort_by: - ctx.obj.sort_by = sort_by if downloads_dir: ctx.obj.downloads_dir = downloads_dir if translation_type: diff --git a/fastanime/cli/commands/download.py b/fastanime/cli/commands/download.py index 29d5273..9f76b15 100644 --- a/fastanime/cli/commands/download.py +++ b/fastanime/cli/commands/download.py @@ -64,7 +64,7 @@ if TYPE_CHECKING: ) @click.option( "--prompt/--no-prompt", - help="Dont prompt for anything instead just do the best thing", + help="Whether to prompt for anything instead just do the best thing", default=True, ) @click.pass_obj @@ -99,6 +99,7 @@ def download( ) anime_provider = AnimeProvider(config.provider) + anilist_anime_info = None translation_type = config.translation_type download_dir = config.downloads_dir @@ -147,16 +148,18 @@ def download( } if config.auto_select: - search_result = max( + selected_anime_title = max( search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title) ) - print("[cyan]Auto selecting:[/] ", search_result) + print("[cyan]Auto selecting:[/] ", selected_anime_title) else: choices = list(search_results_.keys()) if config.use_fzf: - search_result = fzf.run(choices, "Please Select title: ", "FastAnime") + selected_anime_title = fzf.run( + choices, "Please Select title: ", "FastAnime" + ) else: - search_result = fuzzy_inquirer( + selected_anime_title = fuzzy_inquirer( choices, "Please Select title", ) @@ -165,7 +168,7 @@ def download( with Progress() as progress: progress.add_task("Fetching Anime...", total=None) anime: Anime | None = anime_provider.get_anime( - search_results_[search_result]["id"] + search_results_[selected_anime_title]["id"] ) if not anime: print("Sth went wring anime no found") @@ -215,6 +218,11 @@ def download( else: episodes_range = sorted(episodes, key=float) + if config.normalize_titles: + from ...libs.common.mini_anilist import get_basic_anime_info_by_title + + anilist_anime_info = get_basic_anime_info_by_title(anime["title"]) + # lets download em for episode in episodes_range: try: @@ -279,13 +287,26 @@ def download( subtitles = servers[server_name]["subtitles"] episode_title = servers[server_name]["episode_title"] - print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}") + + if anilist_anime_info: + selected_anime_title = ( + anilist_anime_info["title"][config.preferred_language] + or anilist_anime_info["title"]["romaji"] + or anilist_anime_info["title"]["english"] + ) + import re + + for episode_detail in anilist_anime_info["episodes"]: + if re.match(f"Episode {episode}", episode_detail["title"]): + episode_title = episode_detail["title"] + break + print(f"[purple]Now Downloading:[/] {episode_title}") subtitles = move_preferred_subtitle_lang_to_top( subtitles, config.sub_lang ) downloader._download_file( link, - search_result, + selected_anime_title, episode_title, download_dir, silent, diff --git a/fastanime/cli/commands/search.py b/fastanime/cli/commands/search.py index 67dc58b..882ae18 100644 --- a/fastanime/cli/commands/search.py +++ b/fastanime/cli/commands/search.py @@ -42,6 +42,7 @@ def search(config: Config, anime_titles: str, episode_range: str): ) anime_provider = AnimeProvider(config.provider) + anilist_anime_info = None print(f"[green bold]Streaming:[/] {anime_titles}") for anime_title in anime_titles: @@ -123,6 +124,11 @@ def search(config: Config, anime_titles: str, episode_range: str): episodes_range = iter(episodes_range) + if config.normalize_titles: + from ...libs.common.mini_anilist import get_basic_anime_info_by_title + + anilist_anime_info = get_basic_anime_info_by_title(anime["title"]) + def stream_anime(): clear() episode = None @@ -214,8 +220,23 @@ def search(config: Config, anime_titles: str, episode_range: str): stream_headers = servers[server]["headers"] subtitles = servers[server]["subtitles"] episode_title = servers[server]["episode_title"] - print(f"[purple]Now Playing:[/] {search_result} Episode {episode}") + selected_anime_title = search_result + if anilist_anime_info: + selected_anime_title = ( + anilist_anime_info["title"][config.preferred_language] + or anilist_anime_info["title"]["romaji"] + or anilist_anime_info["title"]["english"] + ) + import re + + for episode_detail in anilist_anime_info["episodes"]: + if re.match(f"Episode {episode}", episode_detail["title"]): + episode_title = episode_detail["title"] + break + print( + f"[purple]Now Playing:[/] {selected_anime_title} Episode {episode}" + ) subtitles = move_preferred_subtitle_lang_to_top( subtitles, config.sub_lang ) diff --git a/fastanime/cli/config.py b/fastanime/cli/config.py index 0c03440..31794bd 100644 --- a/fastanime/cli/config.py +++ b/fastanime/cli/config.py @@ -97,6 +97,7 @@ class Config(object): "rofi_theme_confirm": "", "ffmpegthumnailer_seek_time": "-1", "sub_lang": "eng", + "normalize_titles": "true", } ) self.configparser.add_section("stream") @@ -121,6 +122,7 @@ class Config(object): self.sort_by = self.get_sort_by() self.continue_from_history = self.get_continue_from_history() self.auto_next = self.get_auto_next() + self.normalize_titles = self.get_normalize_titles() self.auto_select = self.get_auto_select() self.use_mpv_mod = self.get_use_mpv_mod() self.quality = self.get_quality() @@ -217,6 +219,9 @@ class Config(object): def get_rofi_theme_confirm(self): return self.configparser.get("general", "rofi_theme_confirm") + def get_normalize_titles(self): + return self.configparser.getboolean("general", "normalize_titles") + # --- stream section --- def get_skip(self): return self.configparser.getboolean("stream", "skip") diff --git a/fastanime/cli/interfaces/anilist_interfaces.py b/fastanime/cli/interfaces/anilist_interfaces.py index 28705e1..e7b90e2 100644 --- a/fastanime/cli/interfaces/anilist_interfaces.py +++ b/fastanime/cli/interfaces/anilist_interfaces.py @@ -120,12 +120,24 @@ def media_player_controls( subtitles = move_preferred_subtitle_lang_to_top( selected_server["subtitles"], config.sub_lang ) + episode_title = selected_server["episode_title"] + if config.normalize_titles: + import re + + for episode_detail in fastanime_runtime_state.selected_anime_anilist[ + "streamingEpisodes" + ]: + if re.match( + f"Episode {current_episode_number}", episode_detail["title"] + ): + episode_title = episode_detail["title"] + break if config.sync_play: from ..utils.syncplay import SyncPlayer stop_time, total_time = SyncPlayer( current_episode_stream_link, - selected_server["episode_title"], + episode_title, headers=selected_server["headers"], subtitles=subtitles, ) @@ -137,7 +149,7 @@ def media_player_controls( config.anime_provider, fastanime_runtime_state, config, - selected_server["episode_title"], + episode_title, start_time, headers=selected_server["headers"], subtitles=subtitles, @@ -147,7 +159,7 @@ def media_player_controls( else: stop_time, total_time = run_mpv( current_episode_stream_link, - selected_server["episode_title"], + episode_title, start_time=start_time, custom_args=custom_args, headers=selected_server["headers"], @@ -514,12 +526,23 @@ def provider_anime_episode_servers_menu( subtitles = move_preferred_subtitle_lang_to_top( selected_server["subtitles"], config.sub_lang ) + episode_title = selected_server["episode_title"] + if config.normalize_titles: + import re + + for episode_detail in fastanime_runtime_state.selected_anime_anilist[ + "streamingEpisodes" + ]: + if re.match(f"Episode {current_episode_number}", episode_detail["title"]): + episode_title = episode_detail["title"] + break + if config.sync_play: from ..utils.syncplay import SyncPlayer stop_time, total_time = SyncPlayer( current_stream_link, - selected_server["episode_title"], + episode_title, headers=selected_server["headers"], subtitles=subtitles, ) @@ -533,7 +556,7 @@ def provider_anime_episode_servers_menu( anime_provider, fastanime_runtime_state, config, - selected_server["episode_title"], + episode_title, start_time, headers=selected_server["headers"], subtitles=subtitles, @@ -547,7 +570,7 @@ def provider_anime_episode_servers_menu( start_time = "0" stop_time, total_time = run_mpv( current_stream_link, - selected_server["episode_title"], + episode_title, start_time=start_time, custom_args=custom_args, headers=selected_server["headers"], @@ -1284,9 +1307,9 @@ def anilist_results_menu( choices = [*anime_data.keys(), "Back"] if config.use_fzf: if config.preview: - from .utils import get_fzf_preview + from .utils import get_fzf_anime_preview - preview = get_fzf_preview(search_results, anime_data.keys()) + preview = get_fzf_anime_preview(search_results, anime_data.keys()) selected_anime_title = fzf.run( choices, prompt="Select Anime: ", diff --git a/fastanime/cli/utils/player.py b/fastanime/cli/utils/player.py index e7855e2..9cb1d92 100644 --- a/fastanime/cli/utils/player.py +++ b/fastanime/cli/utils/player.py @@ -134,6 +134,18 @@ class MpvPlayer(object): ) return self.current_media_title = selected_server["episode_title"] + if config.normalize_titles: + import re + + for episode_detail in fastanime_runtime_state.selected_anime_anilist[ + "streamingEpisodes" + ]: + if re.match( + f"Episode {current_episode_number}", episode_detail["title"] + ): + self.current_media_title = episode_detail["title"] + break + links = selected_server["links"] stream_link_ = filter_by_quality(quality, links) diff --git a/fastanime/libs/anilist/queries_graphql.py b/fastanime/libs/anilist/queries_graphql.py index 62d4898..1be8f19 100644 --- a/fastanime/libs/anilist/queries_graphql.py +++ b/fastanime/libs/anilist/queries_graphql.py @@ -147,6 +147,11 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) { id } popularity + streamingEpisodes{ + title + thumbnail + } + favourites averageScore episodes @@ -289,6 +294,11 @@ query($query:String,%s){ progress } popularity + streamingEpisodes{ + title + thumbnail + } + favourites averageScore episodes @@ -346,6 +356,11 @@ query($type:MediaType){ id } popularity + streamingEpisodes{ + title + thumbnail + } + favourites averageScore genres @@ -412,6 +427,15 @@ query($type:MediaType){ progress } popularity + streamingEpisodes{ + title + thumbnail + } + + streamingEpisodes{ + title + thumbnail + } favourites averageScore episodes @@ -472,6 +496,11 @@ query($type:MediaType){ progress } popularity + streamingEpisodes{ + title + thumbnail + } + episodes favourites averageScore @@ -527,6 +556,11 @@ query($type:MediaType){ } popularity + streamingEpisodes{ + title + thumbnail + } + favourites averageScore description @@ -591,6 +625,11 @@ query($type:MediaType){ progress } popularity + streamingEpisodes{ + title + thumbnail + } + favourites averageScore description @@ -659,6 +698,11 @@ query($type:MediaType){ genres averageScore popularity + streamingEpisodes{ + title + thumbnail + } + favourites tags { name @@ -753,6 +797,11 @@ query ($id: Int,$type:MediaType) { genres averageScore popularity + streamingEpisodes{ + title + thumbnail + } + favourites tags { name @@ -827,6 +876,11 @@ query ($page: Int,$type:MediaType) { progress } popularity + streamingEpisodes{ + title + thumbnail + } + favourites averageScore genres @@ -943,6 +997,11 @@ query($id:Int){ countryOfOrigin averageScore popularity + streamingEpisodes{ + title + thumbnail + } + favourites source hashtag diff --git a/fastanime/libs/anilist/types.py b/fastanime/libs/anilist/types.py index 2f50a67..06aef88 100644 --- a/fastanime/libs/anilist/types.py +++ b/fastanime/libs/anilist/types.py @@ -136,6 +136,11 @@ class AnilistMediaListProperties(TypedDict): hiddenFromStatusLists: bool +class StreamingEpisode(TypedDict): + title: str + thumbnail: str + + class AnilistBaseMediaDataSchema(TypedDict): """ This a convenience class is used to type the received Anilist data to enhance dev experience @@ -159,6 +164,7 @@ class AnilistBaseMediaDataSchema(TypedDict): status: str nextAiringEpisode: AnilistMediaNextAiringEpisode season: str + streamingEpisodes: list[StreamingEpisode] seasonYear: int duration: int synonyms: list[str] diff --git a/fastanime/libs/common/mini_anilist.py b/fastanime/libs/common/mini_anilist.py new file mode 100644 index 0000000..bf738ba --- /dev/null +++ b/fastanime/libs/common/mini_anilist.py @@ -0,0 +1,223 @@ +import logging +from typing import TYPE_CHECKING + +from requests import post +from thefuzz import fuzz + +if TYPE_CHECKING: + from ..anilist.types import AnilistDataSchema +logger = logging.getLogger(__name__) + +ANILIST_ENDPOINT = "https://graphql.anilist.co" +""" +query($query:String){ + Page(perPage:50){ + pageInfo{ + total + currentPage + hasNextPage + } + media(search:$query,type:ANIME){ + id + idMal + title{ + romaji + english + } + episodes + status + nextAiringEpisode { + timeUntilAiring + airingAt + episode + } + } + } +} +""" + + +def search_foranime_with_anilist(anime_title: str): + query = """ + query($query:String){ + Page(perPage:50){ + pageInfo{ + total + currentPage + hasNextPage + } + media(search:$query,type:ANIME){ + id + idMal + title{ + romaji + english + } + episodes + status + nextAiringEpisode { + timeUntilAiring + airingAt + episode + } + } + } + } + """ + response = post( + ANILIST_ENDPOINT, + json={"query": query, "variables": {"query": anime_title}}, + timeout=10, + ) + if response.status_code == 200: + anilist_data: "AnilistDataSchema" = response.json() + return { + "pageInfo": anilist_data["data"]["Page"]["pageInfo"], + "results": [ + { + "id": anime_result["id"], + "title": anime_result["title"]["romaji"] + or anime_result["title"]["english"], + "type": "anime", + "availableEpisodes": list( + range( + 1, + ( + anime_result["episodes"] + if not anime_result["status"] == "RELEASING" + and anime_result["episodes"] + else ( + anime_result["nextAiringEpisode"]["episode"] - 1 + if anime_result["nextAiringEpisode"] + else 0 + ) + ), + ) + ), + } + for anime_result in anilist_data["data"]["Page"]["media"] + ], + } + + +def get_mal_id_and_anilist_id(anime_title: str) -> "dict[str,int] | None": + """the abstraction over all none authenticated requests and that returns data of a similar type + + Args: + query: the anilist query + variables: the anilist api variables + + Returns: + a boolean indicating success and none or an anilist object depending on success + """ + query = """ + query($query:String){ + Page(perPage:50){ + pageInfo{ + total + currentPage + hasNextPage + } + media(search:$query,type:ANIME){ + id + idMal + title{ + romaji + english + } + } + } + } + """ + + try: + variables = {"query": anime_title} + response = post( + ANILIST_ENDPOINT, + json={"query": query, "variables": variables}, + timeout=10, + ) + anilist_data: "AnilistDataSchema" = response.json() + if response.status_code == 200: + anime = max( + anilist_data["data"]["Page"]["media"], + key=lambda anime: max( + ( + fuzz.ratio(anime, str(anime["title"]["romaji"])), + fuzz.ratio(anime_title, str(anime["title"]["english"])), + ) + ), + ) + return {"id_anilist": anime["id"], "id_mal": anime["idMal"]} + except Exception as e: + logger.error(f"Something unexpected occured {e}") + + +def get_basic_anime_info_by_title(anime_title: str): + """the abstraction over all none authenticated requests and that returns data of a similar type + + Args: + query: the anilist query + variables: the anilist api variables + + Returns: + a boolean indicating success and none or an anilist object depending on success + """ + query = """ + query($query:String){ + Page(perPage:50){ + pageInfo{ + total + } + media(search:$query,type:ANIME){ + id + idMal + title{ + romaji + english + } + streamingEpisodes{ + title + } + } + } + } + """ + + from ...Utility.data import anime_normalizer + + # normalize the title + anime_title = anime_normalizer.get(anime_title, anime_title) + try: + variables = {"query": anime_title} + response = post( + ANILIST_ENDPOINT, + json={"query": query, "variables": variables}, + timeout=10, + ) + anilist_data: "AnilistDataSchema" = response.json() + if response.status_code == 200: + anime = max( + anilist_data["data"]["Page"]["media"], + key=lambda anime: max( + ( + fuzz.ratio(anime_title, str(anime["title"]["romaji"])), + fuzz.ratio(anime_title, str(anime["title"]["english"])), + ) + ), + ) + return { + "idAilist": anime["id"], + "idMal": anime["idMal"], + "title": { + "english": anime["title"]["english"], + "romaji": anime["title"]["romaji"], + }, + "episodes": [ + {"title": episode["title"]} + for episode in anime["streamingEpisodes"] + if episode + ], + } + except Exception as e: + logger.error(f"Something unexpected occured {e}")