diff --git a/fa b/fa index 46ef129..84e1aa8 100755 --- a/fa +++ b/fa @@ -1,6 +1,2 @@ #! /usr/bin/bash -cd $HOME/code/python/kivy_apps/FastAnime -poetry install -clear poetry run fastanime $* - diff --git a/fastanime/Utility/user_data_helper.py b/fastanime/Utility/user_data_helper.py index f70fed7..2c66c5a 100644 --- a/fastanime/Utility/user_data_helper.py +++ b/fastanime/Utility/user_data_helper.py @@ -16,7 +16,6 @@ def __init__(self): with open(USER_DATA_PATH, "r") as f: user_data = json.load(f) self.user_data.update(user_data) - print(user_data, self.user_data) except Exception as e: logger.error(e) diff --git a/fastanime/Utility/utils.py b/fastanime/Utility/utils.py index c9d8434..1d69481 100644 --- a/fastanime/Utility/utils.py +++ b/fastanime/Utility/utils.py @@ -2,12 +2,14 @@ import re import shutil from datetime import datetime +from functools import lru_cache # TODO: make it use color_text instead of fixed vals # from .kivy_markup_helper import color_text -def remove_html_tags(text): +@lru_cache() +def remove_html_tags(text: str): clean = re.compile("<.*?>") return re.sub(clean, "", text) @@ -42,6 +44,7 @@ def move_file(source_path, dest_path): return (0, e) +@lru_cache() def sanitize_filename(filename: str): """ Sanitize a string to be safe for use as a file name. diff --git a/fastanime/__init__.py b/fastanime/__init__.py index 2b35f14..ade47f3 100644 --- a/fastanime/__init__.py +++ b/fastanime/__init__.py @@ -41,10 +41,7 @@ APP_CACHE_DIR = dirs.user_cache_dir # video dir -USER_DOWNLOADS_DIR = dirs.user_videos_dir - -print(f"USER_DOWNLOADS_DIR: {USER_DOWNLOADS_DIR}") - +USER_VIDEOS_DIR = dirs.user_videos_dir # web dirs @@ -82,9 +79,9 @@ def FastAnime(gui=False, web=False): run_gui() elif web: - from .web import run_web + from .api import run_api - run_web() + run_api() else: from .cli import run_cli diff --git a/fastanime/cli/config.py b/fastanime/cli/config.py index d92f809..3778e00 100644 --- a/fastanime/cli/config.py +++ b/fastanime/cli/config.py @@ -1,12 +1,19 @@ import os from configparser import ConfigParser +from rich import print -from .. import USER_CONFIG_PATH, USER_DOWNLOADS_DIR +from .. import USER_CONFIG_PATH, USER_VIDEOS_DIR from ..Utility.user_data_helper import user_data_helper class Config(object): + anime_list: list + watch_history: dict + def __init__(self) -> None: + self.load_config() + + def load_config(self): self.configparser = ConfigParser( { "server": "", @@ -14,7 +21,7 @@ def __init__(self) -> None: "quality": "0", "auto_next": "True", "sort_by": "search match", - "downloads_dir": USER_DOWNLOADS_DIR, + "downloads_dir": USER_VIDEOS_DIR, "translation_type": "sub", "preferred_language": "romaji", } @@ -38,16 +45,25 @@ def __init__(self) -> None: self.preferred_language = self.get_preferred_language() # ---- setup user data ------ - self.watch_history = user_data_helper.user_data.get("watch_history", {}) - self.anime_list = user_data_helper.user_data.get("animelist", []) + self.watch_history: dict = user_data_helper.user_data.get("watch_history", {}) + self.anime_list: list = user_data_helper.user_data.get("animelist", []) 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) - def update_anime_list(self, anime_id: int): - self.anime_list.append(anime_id) - user_data_helper.update_animelist(self.anime_list) + def update_anime_list(self, anime_id: int, remove=False): + if remove: + try: + self.anime_list.remove(anime_id) + print("Succesfully removed :cry:") + except Exception: + print(anime_id, "Nothing to remove :confused:") + else: + self.anime_list.append(anime_id) + user_data_helper.update_animelist(self.anime_list) + print("Succesfully added :smile:") + input("Enter to continue...") def get_downloads_dir(self): return self.configparser.get("general", "downloads_dir") diff --git a/fastanime/cli/interfaces/anilist_interfaces.py b/fastanime/cli/interfaces/anilist_interfaces.py index a6992d2..239c447 100644 --- a/fastanime/cli/interfaces/anilist_interfaces.py +++ b/fastanime/cli/interfaces/anilist_interfaces.py @@ -1,16 +1,17 @@ from __future__ import annotations import os +import shutil import sys -from InquirerPy import inquirer from rich import print +from rich.prompt import Prompt -from ... import APP_CACHE_DIR +from ... import APP_CACHE_DIR, USER_CONFIG_PATH, USER_NAME from ...libs.anilist.anilist import AniList from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema from ...libs.anime_provider.allanime.api import anime_provider -from ...libs.anime_provider.allanime.data_types import AllAnimeEpisode, AllAnimeShow +from ...libs.anime_provider.allanime.types import AllAnimeEpisode, AllAnimeShow from ...libs.fzf import fzf from ...Utility import anilist_data_helper from ...Utility.data import anime_normalizer @@ -44,7 +45,7 @@ def write_search_results( f"{ANIME_CACHE}/image", "wb", ) as f: - image = requests.get(anime["coverImage"]["medium"]) + image = requests.get(anime["coverImage"]["large"]) f.write(image.content) with open(f"{ANIME_CACHE}/data", "w") as f: @@ -82,11 +83,12 @@ def get_preview(search_results: list[AnilistBaseMediaDataSchema], config: Config background_worker.daemon = True background_worker.start() - preview = """\ - if [ -f %s/{}/image ]; then fzf-preview %s/{}/image + os.environ["SHELL"] = shutil.which("bash") or "bash" + preview = """ + if [ -s %s/{}/image ]; then fzf-preview %s/{}/image else echo Loading... fi - if [ -f %s/{}/data ]; then cat %s/{}/data + if [ -s %s/{}/data ]; then cat %s/{}/data else echo Loading... fi """ % ( @@ -95,13 +97,18 @@ def get_preview(search_results: list[AnilistBaseMediaDataSchema], config: Config SEARCH_RESULTS_CACHE, SEARCH_RESULTS_CACHE, ) - preview.replace("\n", ";") + # preview.replace("\n", ";") return preview +def exit_app(*args): + print("Have a good day :smile:", USER_NAME) + sys.exit(0) + + def player_controls(config: Config, anilist_config: QueryDict): # user config - translation_type: str = config.translation_type + translation_type: str = config.translation_type.lower() # internal config _anime: AllAnimeShow = anilist_config._anime @@ -112,7 +119,9 @@ def player_controls(config: Config, anilist_config: QueryDict): anime_title: str = anilist_config.anime_title anime_id: int = anilist_config.anime_id - def _back(): + def _servers(): + config.server = "" + fetch_streams(config, anilist_config) def _replay(): @@ -157,7 +166,7 @@ def _previous_episode(): if prev_episode <= 0: prev_episode = 0 episode = anime_provider.get_anime_episode( - _anime["_id"], episodes[prev_episode], config.translation_type + _anime["_id"], episodes[prev_episode], config.translation_type.lower() ) # update internal config @@ -184,11 +193,11 @@ def _change_translation_type(): # prompt for new translation type options = ["sub", "dub"] translation_type = fzf.run( - options, prompt="Select Translation Type: ", header="Language Options" - ) + options, prompt="Select Translation Type: ", header="Lang Options" + ).lower() # update internal config - config.translation_type = translation_type + config.translation_type = translation_type.lower() # reload to controls player_controls(config, anilist_config) @@ -200,11 +209,11 @@ def _change_translation_type(): "Episodes": _episodes, "Change Quality": _change_quality, "Change Translation Type": _change_translation_type, - "Back to servers": _back, + "Servers": _servers, "Main Menu": lambda: anilist(config, anilist_config), "Anime Options Menu": lambda: anilist_options(config, anilist_config), "Search Results": lambda: select_anime(config, anilist_config), - "exit": sys.exit, + "Exit": sys.exit, } if config.auto_next: @@ -241,11 +250,11 @@ def fetch_streams(config: Config, anilist_config: QueryDict): server = list(episode_streams.keys())[0] if not server: server = fzf.run( - [*episode_streams.keys(), "back"], + [*episode_streams.keys(), "Back"], prompt="Select Server: ", header="Servers", ) - if server == "back": + if server == "Back": # reset watch_history config.update_watch_history(anime_id, None) @@ -287,7 +296,7 @@ def fetch_streams(config: Config, anilist_config: QueryDict): def fetch_episode(config: Config, anilist_config: QueryDict): # user config - translation_type: str = config.translation_type + translation_type: str = config.translation_type.lower() continue_from_history: bool = config.continue_from_history user_watch_history: dict = config.watch_history anime_id: int = anilist_config.anime_id @@ -297,18 +306,18 @@ def fetch_episode(config: Config, anilist_config: QueryDict): _anime: AllAnimeShow = anilist_config._anime # prompt for episode number - episodes = anime["show"]["availableEpisodesDetail"][translation_type] + episodes = anime["availableEpisodesDetail"][translation_type] if continue_from_history and user_watch_history.get(str(anime_id)) in episodes: episode_number = user_watch_history[str(anime_id)] print(f"[bold cyan]Continuing from Episode:[/] [bold]{episode_number}[/]") else: episode_number = fzf.run( - [*episodes, "back"], + [*episodes, "Back"], prompt="Select Episode:", header="Episodes", ) - if episode_number == "back": + if episode_number == "Back": provide_anime(config, anilist_config) return config.update_watch_history(anime_id, episode_number) @@ -328,7 +337,7 @@ def fetch_episode(config: Config, anilist_config: QueryDict): fetch_streams(config, anilist_config) -def fetch_anime_epiosode(config, anilist_config: QueryDict): +def fetch_anime_episode(config, anilist_config: QueryDict): selected_anime: AllAnimeShow = anilist_config._anime anilist_config.anime = anime_provider.get_anime(selected_anime["_id"]) @@ -337,7 +346,7 @@ def fetch_anime_epiosode(config, anilist_config: QueryDict): def provide_anime(config: Config, anilist_config: QueryDict): # user config - translation_type = config.translation_type + translation_type = config.translation_type.lower() # internal config selected_anime_title = anilist_config.selected_anime_title @@ -362,50 +371,52 @@ def provide_anime(config: Config, anilist_config: QueryDict): _title = _title anime_title = fzf.run( - [*search_results.keys(), "back"], + [*search_results.keys(), "Back"], prompt="Select Search Result:", header="Anime Search Results", ) - if anime_title == "back": + if anime_title == "Back": anilist_options(config, anilist_config) return anilist_config.anime_title = anime_normalizer.get(anime_title) or anime_title anilist_config._anime = search_results[anime_title] - fetch_anime_epiosode(config, anilist_config) + fetch_anime_episode(config, anilist_config) def anilist_options(config, anilist_config: QueryDict): selected_anime: AnilistBaseMediaDataSchema = anilist_config.selected_anime_anilist selected_anime_title: str = anilist_config.selected_anime_title - def _watch_trailer(config, anilist_config): + def _watch_trailer(config: Config, anilist_config: QueryDict): if trailer := selected_anime.get("trailer"): trailer_url = "https://youtube.com/watch?v=" + trailer["id"] print("[bold magenta]Watching Trailer of:[/]", selected_anime_title) mpv(trailer_url) anilist_options(config, anilist_config) - def _add_to_list(config, anilist_config): - pass + def _add_to_list(config: Config, anilist_config: QueryDict): + config.update_anime_list(anilist_config.anime_id) + anilist_options(config, anilist_config) - def _remove_from_list(): - pass + def _remove_from_list(config: Config, anilist_config: QueryDict): + config.update_anime_list(anilist_config.anime_id, True) + anilist_options(config, anilist_config) - def _change_translation_type(config, anilist_config): + def _change_translation_type(config: Config, anilist_config: QueryDict): # prompt for new translation type - options = ["sub", "dub"] + options = ["Sub", "Dub"] translation_type = fzf.run( options, prompt="Select Translation Type:", header="Language Options" ) # update internal config - config.translation_type = translation_type + config.translation_type = translation_type.lower() anilist_options(config, anilist_config) def _view_info(config, anilist_config): - from InquirerPy import inquirer + from rich.prompt import Confirm from rich.console import Console from ...Utility import anilist_data_helper @@ -415,7 +426,7 @@ def _view_info(config, anilist_config): clear() console = Console() - print_img(selected_anime["coverImage"]["medium"]) + print_img(selected_anime["coverImage"]["large"]) console.print("[bold cyan]Title(jp): ", selected_anime["title"]["romaji"]) console.print("[bold cyan]Title(eng): ", selected_anime["title"]["english"]) console.print("[bold cyan]Popularity: ", selected_anime["popularity"]) @@ -453,19 +464,19 @@ def _view_info(config, anilist_config): "[bold underline cyan]Description\n[/]", remove_html_tags(str(selected_anime["description"])), ) - if inquirer.confirm("Enter to continue", default=True).execute(): + if Confirm.ask("Enter to continue...", default=True): anilist_options(config, anilist_config) return options = { - "stream": provide_anime, - "watch trailer": _watch_trailer, - "add to list": _add_to_list, - "remove from list": _remove_from_list, - "view info": _view_info, + "Stream": provide_anime, + "Watch Trailer": _watch_trailer, + "Add to List": _add_to_list, + "Remove from List": _remove_from_list, + "View Info": _view_info, "Change Translation Type": _change_translation_type, - "back": select_anime, - "exit": sys.exit, + "Back": select_anime, + "Exit": exit_app, } action = fzf.run(list(options.keys()), prompt="Select Action:", header="Anime Menu") options[action](config, anilist_config) @@ -485,14 +496,14 @@ def select_anime(config: Config, anilist_config: QueryDict): selected_anime_title = fzf.run( [ *anime_data.keys(), - "back", + "Back", ], prompt="Select Anime: ", header="Search Results", preview=preview, ) # "bat %s/{}" % SEARCH_RESULTS_CACHE - if selected_anime_title == "back": + if selected_anime_title == "Back": anilist(config, anilist_config) return @@ -508,36 +519,50 @@ def select_anime(config: Config, anilist_config: QueryDict): def anilist(config: Config, anilist_config: QueryDict): def _anilist_search(): - search_term = inquirer.text( - "Search:", instruction="Enter anime to search for" - ).execute() + search_term = Prompt.ask("[cyan]Search for[/]") return AniList.search(query=search_term) def _watch_history(): watch_history = list(map(int, config.watch_history.keys())) - return AniList.search(id_in=watch_history) + return AniList.search(id_in=watch_history, sort="TRENDING_DESC") def _anime_list(): anime_list = config.anime_list return AniList.search(id_in=anime_list) + def edit_config(): + import subprocess + + process = subprocess.run([os.environ.get("EDITOR", "open"), USER_CONFIG_PATH]) + config.load_config() + + anilist(config, anilist_config) + options = { - "trending": AniList.get_trending, - "recently updated anime": AniList.get_most_recently_updated, - "search": _anilist_search, + "Trending": AniList.get_trending, + "Recently Updated Anime": AniList.get_most_recently_updated, + "Search": _anilist_search, "Watch History": _watch_history, "AnimeList": _anime_list, - "most popular anime": AniList.get_most_popular, - "most favourite anime": AniList.get_most_favourite, - "most scored anime": AniList.get_most_scored, - "upcoming anime": AniList.get_upcoming_anime, - "exit": sys.exit, + "Most Popular Anime": AniList.get_most_popular, + "Most Favourite Anime": AniList.get_most_favourite, + "Most Scored Anime": AniList.get_most_scored, + "Upcoming Anime": AniList.get_upcoming_anime, + "Edit Config": edit_config, + "Exit": sys.exit, } action = fzf.run( - list(options.keys()), prompt="Select Action: ", header="Anilist Menu" + list(options.keys()), + prompt="Select Action: ", + header="Anilist Menu", ) anilist_data = options[action]() if anilist_data[0]: anilist_config.data = anilist_data[1] select_anime(config, anilist_config) + + else: + print(anilist_data[1]) + input("Enter to continue...") + anilist(config, anilist_config) diff --git a/fastanime/cli/utils/tools.py b/fastanime/cli/utils/tools.py index 6e1b0a7..c04f7eb 100644 --- a/fastanime/cli/utils/tools.py +++ b/fastanime/cli/utils/tools.py @@ -1,3 +1,6 @@ +from rich.text import Text + + class QueryDict(dict): """dot.notation access to dictionary attributes""" @@ -11,3 +14,14 @@ def __getattr__(self, attr): def __setattr__(self, attr, value): self.__setitem__(attr, value) + + +def get_formatted_str(text: str, style): + # Create a Text object with desired style + text = Text("Hello, World!", style="bold red") + + # Convert the Text object to an ANSI string + ansi_output = text.__rich_console__(None, None) + + # Join the ANSI strings to form the final output + ansi_string = "".join(segment.text for segment in ansi_output) diff --git a/fastanime/libs/anilist/anilist.py b/fastanime/libs/anilist/anilist.py index 4fd0dda..383d83a 100644 --- a/fastanime/libs/anilist/anilist.py +++ b/fastanime/libs/anilist/anilist.py @@ -47,7 +47,7 @@ def get_data( try: # TODO: check if data is as expected response = requests.post( - url, json={"query": query, "variables": variables}, timeout=5 + url, 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 678b8e1..6ecb502 100644 --- a/fastanime/libs/anilist/queries_graphql.py +++ b/fastanime/libs/anilist/queries_graphql.py @@ -63,6 +63,7 @@ } coverImage{ medium + large } trailer { site @@ -119,6 +120,7 @@ } coverImage{ medium + large } trailer { site @@ -172,6 +174,7 @@ } coverImage{ medium + large } trailer { site @@ -219,12 +222,14 @@ Page(perPage:15){ media(sort:SCORE_DESC,type:ANIME,genre_not_in:["hentai"]){ id + large title{ romaji english } coverImage{ medium + large } trailer { site @@ -278,6 +283,7 @@ } coverImage{ medium + large } trailer { site @@ -323,7 +329,7 @@ most_recently_updated_query = """ query{ Page(perPage:15){ - media(sort:UPDATED_AT_DESC,type:ANIME,averageScore_greater:60,genre_not_in:["hentai"]){ + media(sort:UPDATED_AT_DESC,type:ANIME,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){ id title{ romaji @@ -331,6 +337,7 @@ } coverImage{ medium + large } trailer { site @@ -387,6 +394,7 @@ } coverImage{ medium + large } description episodes @@ -441,6 +449,7 @@ } image { medium + large } description gender @@ -474,6 +483,7 @@ } coverImage { medium + large } description episodes @@ -545,6 +555,7 @@ } coverImage { medium + large } trailer { site @@ -619,6 +630,7 @@ age image { medium + large } description } @@ -628,6 +640,7 @@ } image { medium + large } } } @@ -674,6 +687,7 @@ name avatar { medium + large } } } diff --git a/fastanime/libs/anime_provider/allanime/api.py b/fastanime/libs/anime_provider/allanime/api.py index 06bb7a0..e33e7aa 100644 --- a/fastanime/libs/anime_provider/allanime/api.py +++ b/fastanime/libs/anime_provider/allanime/api.py @@ -1,9 +1,20 @@ import json import logging +from typing import Generator import requests from rich import print from rich.progress import Progress +from requests.exceptions import Timeout + +from ....libs.anime_provider.allanime.types import ( + AllAnimeEpisode, + AllAnimeSearchResults, + AllAnimeShow, + AllAnimeSources, + AllAnimeStreams, + Server, +) from .constants import ( ALLANIME_API_ENDPOINT, @@ -38,12 +49,21 @@ def _fetch_gql(self, query: str, variables: dict): timeout=10, ) return response.json()["data"] + except Timeout as e: + print( + "Timeout has been exceeded :cry:. This could mean allanime is down or your internet is down" + ) + Logger.error(f"allanime(Error): {e}") + return {} except Exception as e: + print("sth went wrong :confused:") Logger.error(f"allanime:Error: {e}") return {} - def search_for_anime(self, user_query: str, translation_type: str = "sub"): - search = {"allowAdult": False, "allowUnknown": False, "query": user_query} + def search_for_anime( + self, user_query: str, translation_type: str = "sub", nsfw=True, unknown=True + ) -> AllAnimeSearchResults: + search = {"allowAdult": nsfw, "allowUnknown": unknown, "query": user_query} limit = 40 translationtype = translation_type countryorigin = "all" @@ -59,18 +79,18 @@ def search_for_anime(self, user_query: str, translation_type: str = "sub"): progress.add_task("[cyan]searching..", start=False, total=None) search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables) - return search_results + return search_results # pyright:ignore - def get_anime(self, allanime_show_id: str): + def get_anime(self, allanime_show_id: str) -> AllAnimeShow: variables = {"showId": allanime_show_id} with Progress() as progress: progress.add_task("[cyan]fetching anime..", start=False, total=None) anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables) - return anime + return anime["show"] # pyright:ignore def get_anime_episode( self, allanime_show_id: str, episode_string: str, translation_type: str = "sub" - ): + ) -> AllAnimeEpisode: variables = { "showId": allanime_show_id, "translationType": translation_type, @@ -79,9 +99,18 @@ def get_anime_episode( with Progress() as progress: progress.add_task("[cyan]fetching episode..", start=False, total=None) episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables) - return episode - - def get_episode_streams(self, allanime_episode_embeds_data): + return episode # pyright: ignore + + def get_episode_streams( + self, allanime_episode_embeds_data + ) -> ( + Generator[ + tuple[Server, AllAnimeStreams], + tuple[Server, AllAnimeStreams], + tuple[Server, AllAnimeStreams], + ] + | dict + ): if ( not allanime_episode_embeds_data or allanime_episode_embeds_data.get("episode") is None @@ -138,8 +167,6 @@ def get_episode_streams(self, allanime_episode_embeds_data): Logger.debug("allanime:Found streams from dropbox") print("[yellow]Dropbox Fetched") yield "dropbox", resp.json() - case _: - yield "Unknown", resp.json() else: return {} @@ -175,7 +202,7 @@ def get_episode_streams(self, allanime_episode_embeds_data): anime_data = anime_provider.get_anime(anime_result["_id"]) if anime_data is None: raise Exception("Anime not found") - availableEpisodesDetail = anime_data["show"]["availableEpisodesDetail"] + availableEpisodesDetail = anime_data["availableEpisodesDetail"] if not availableEpisodesDetail.get(translation.strip()): raise Exception("No episodes found") @@ -200,6 +227,7 @@ def get_episode_streams(self, allanime_episode_embeds_data): episode_streams = list(episode_streams) stream_links = [] for server in episode_streams: + # FIXME: stream_links = [ *stream_links, *[stream["link"] for stream in server[1]["links"]], diff --git a/fastanime/libs/anime_provider/allanime/data_types.py b/fastanime/libs/anime_provider/allanime/data_types.py deleted file mode 100644 index 5e83e6f..0000000 --- a/fastanime/libs/anime_provider/allanime/data_types.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import TypedDict - - -class AllAnimeEpisodesInfo(TypedDict): - dub: int - sub: int - raw: int - - -class AllAnimePageInfo(TypedDict): - total: int - - -class AllAnimeShow(TypedDict): - _id: str - name: str - availableEpisodes: AllAnimeEpisodesInfo - __typename: str - - -class AllAnimeShows(TypedDict): - pageInfo: AllAnimePageInfo - edges: list[AllAnimeShow] - - -class AllAnimeSearchResults(TypedDict): - shows: AllAnimeShows - - -class AllAnimeSourcesDownloads(TypedDict): - sourceName: str - dowloadUrl: str - - -class AllAnimeSources(TypedDict): - sourceUrl: str - priority: float - sandbox: str - sourceName: str - type: str - className: str - streamerId: str - downloads: AllAnimeSourcesDownloads - - -class AllAnimeEpisode(TypedDict): - episodeString: str - sourceUrls: list[AllAnimeSources] - notes: str | None diff --git a/fastanime/libs/anime_provider/allanime/types.py b/fastanime/libs/anime_provider/allanime/types.py index e986952..904857c 100644 --- a/fastanime/libs/anime_provider/allanime/types.py +++ b/fastanime/libs/anime_provider/allanime/types.py @@ -57,3 +57,17 @@ class AllAnimeEpisode(TypedDict): episodeString: str sourceUrls: list[AllAnimeSources] notes: str | None + + +class AllAnimeStream: + link: str + mp4: bool + hls: bool | None + resolutionStr: str + fromCache: str + priority: int + headers: dict | None + + +class AllAnimeStreams: + links: list[AllAnimeStream] diff --git a/fastanime/libs/fzf/__init__.py b/fastanime/libs/fzf/__init__.py index 02b038f..c2079e7 100644 --- a/fastanime/libs/fzf/__init__.py +++ b/fastanime/libs/fzf/__init__.py @@ -50,10 +50,9 @@ class FZF: "--no-margin", "+m", "-i", - "--expect=shift-left,shift-right", "--exact", "--tabstop=1", - "--preview-window=left,35%,wrap", + "--preview-window=left,35%", "--wrap", ] @@ -116,6 +115,8 @@ def run( prompt: str, header: str, preview: str | None = None, + expect: str | None = None, + validator: Callable | None = None, ) -> str: _commands = [ *self.default_options, @@ -128,7 +129,21 @@ def run( if preview: _commands.append(f"--preview={preview}") - return self._run_fzf(_commands, fzf_input) # pyright:ignore + if expect: + _commands.append(f"--expect={expect}") + + result = self._run_fzf(_commands, fzf_input) # pyright:ignore + if not result: + print("Please enter a value") + input("Enter to do it right") + return self.run(fzf_input, prompt, header, preview, expect, validator) + elif validator: + success, info = validator(result) + if not success: + print(info) + input("Enter to try again") + return self.run(fzf_input, prompt, header, preview, expect, validator) + return result fzf = FZF()