From 9ef234ba1cb8c770887050ae8f26fa113e095614 Mon Sep 17 00:00:00 2001 From: Dragon of Shuu <68718280+DragonOfShuu@users.noreply.github.com> Date: Sun, 4 Aug 2024 23:56:39 -0600 Subject: [PATCH 1/4] feat: seasonal search through command args --- api/src/anipy_api/player/player.py | 8 +- cli/src/anipy_cli/arg_parser.py | 10 ++ cli/src/anipy_cli/clis/default_cli.py | 13 ++- cli/src/anipy_cli/config.py | 38 +++++--- cli/src/anipy_cli/prompts.py | 135 +++++++++++++++++++------- cli/src/anipy_cli/util.py | 19 ++++ 6 files changed, 169 insertions(+), 54 deletions(-) diff --git a/api/src/anipy_api/player/player.py b/api/src/anipy_api/player/player.py index 44c073a0..a483c902 100644 --- a/api/src/anipy_api/player/player.py +++ b/api/src/anipy_api/player/player.py @@ -40,7 +40,13 @@ def get_player( if Path(player.name).stem == "mpv-controlled": return MpvControllable(play_callback=play_callback) - player_dict = {"mpv": Mpv, "mpvnet": Mpv, "vlc": Vlc, "syncplay": Syncplay, "iina": Iina} + player_dict = { + "mpv": Mpv, + "mpvnet": Mpv, + "vlc": Vlc, + "syncplay": Syncplay, + "iina": Iina, + } player_class = player_dict.get(Path(player.name).stem, None) diff --git a/cli/src/anipy_cli/arg_parser.py b/cli/src/anipy_cli/arg_parser.py index bafd8659..0eaaa3a1 100644 --- a/cli/src/anipy_cli/arg_parser.py +++ b/cli/src/anipy_cli/arg_parser.py @@ -23,6 +23,7 @@ class CliArgs: location: Optional[Path] mal_password: Optional[str] config: bool + seasonal_search: Optional[str] def parse_args(override_args: Optional[list[str]] = None) -> CliArgs: @@ -107,6 +108,15 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs: help="Provide a search term to Default, Download or Binge mode in this format: {query}:{episode range}:{dub/sub}. Examples: 'frieren:1-10:sub' or 'frieren:1:sub' or 'frieren:1-3 7-12:dub', this argument may be appended to any of the modes mentioned like so: 'anipy-cli (-D/B) -s '", ) + options_group.add_argument( + "-ss", + "--seasonal-search", + required=False, + dest="seasonal_search", + action="store", + help="Provide search parameters for seasons to Default, Download, or Binge mode in this format: {year}:{season}. Examples: '2024:winter' or '2020:fall'", + ) + options_group.add_argument( "-q", "--quality", diff --git a/cli/src/anipy_cli/clis/default_cli.py b/cli/src/anipy_cli/clis/default_cli.py index ada00214..de14ed3f 100644 --- a/cli/src/anipy_cli/clis/default_cli.py +++ b/cli/src/anipy_cli/clis/default_cli.py @@ -11,6 +11,7 @@ search_show_prompt, lang_prompt, parse_auto_search, + parse_seasonal_search, ) from anipy_cli.util import ( DotSpinner, @@ -42,6 +43,15 @@ def __init__(self, options: "CliArgs"): def print_header(self): pass + def _get_anime_from_user(self): + if (ss := self.options.seasonal_search) is not None: + return parse_seasonal_search( + "default", + ss, + ) + + return search_show_prompt("default") + def take_input(self): if self.options.search is not None: self.anime, self.lang, episodes = parse_auto_search( @@ -50,9 +60,10 @@ def take_input(self): self.epsiode = episodes[0] return - anime = search_show_prompt("default") + anime = self._get_anime_from_user() if anime is None: + print("Anime was none") return False self.lang = lang_prompt(anime) diff --git a/cli/src/anipy_cli/config.py b/cli/src/anipy_cli/config.py index 8f00116c..cc77bb13 100644 --- a/cli/src/anipy_cli/config.py +++ b/cli/src/anipy_cli/config.py @@ -358,6 +358,12 @@ def skip_season_search(self) -> bool: """If this is set to true you will not be prompted to search in season.""" return self._get_value("skip_season_search", False, bool) + @property + def assume_season_search(self) -> bool: + """If this is set to true, the system will assume you want to search in season. + If skip_season_search is true, this will be ignored)""" + return self._get_value("assume_season_search", False, bool) + def _get_path_value(self, key: str, fallback: Path) -> Path: path = self._get_value(key, fallback, str) try: @@ -385,21 +391,23 @@ def _create_config(self): if attribute.startswith("_"): continue - if isinstance(value, property): - doc = inspect.getdoc(value) - if doc: - # Add docstrings - doc = Template(doc).safe_substitute(version=__version__) - doc = "\n".join([f"# {line}" for line in doc.split("\n")]) - dump = dump + doc + "\n" - - val = self.__getattribute__(attribute) - val = str(val) if isinstance(val, Path) else val - dump = ( - dump - + yaml.dump({attribute: val}, indent=4, default_flow_style=False) - + "\n" - ) + if not isinstance(value, property): + continue + + doc = inspect.getdoc(value) + if doc: + # Add docstrings + doc = Template(doc).safe_substitute(version=__version__) + doc = "\n".join([f"# {line}" for line in doc.split("\n")]) + dump = dump + doc + "\n" + + val = self.__getattribute__(attribute) + val = str(val) if isinstance(val, Path) else val + dump = ( + dump + + yaml.dump({attribute: val}, indent=4, default_flow_style=False) + + "\n" + ) self._config_file.write_text(dump) diff --git a/cli/src/anipy_cli/prompts.py b/cli/src/anipy_cli/prompts.py index a6be3504..544f2c96 100644 --- a/cli/src/anipy_cli/prompts.py +++ b/cli/src/anipy_cli/prompts.py @@ -17,6 +17,7 @@ get_prefered_providers, error, parse_episode_ranges, + convert_letter_to_season, ) from anipy_cli.colors import colors from anipy_cli.config import Config @@ -30,22 +31,9 @@ def search_show_prompt( mode: str, skip_season_search: bool = False ) -> Optional["Anime"]: if not (Config().skip_season_search or skip_season_search): - season_provider = None - for p in get_prefered_providers(mode): - if p.FILTER_CAPS & ( - FilterCapabilities.SEASON - | FilterCapabilities.YEAR - | FilterCapabilities.NO_QUERY - ): - season_provider = p - if season_provider is not None: - should_search = inquirer.confirm("Do you want to search in season?", default=False).execute() # type: ignore - if not should_search: - print( - "Hint: you can set `skip_season_search` to `true` in the config to skip this prompt!" - ) - else: - return season_search_prompt(season_provider) + anime = season_search_pre_prompt(mode) + if anime is not None: + return anime query = inquirer.text( # type: ignore "Search Anime:", @@ -88,41 +76,83 @@ def search_show_prompt( return anime -def season_search_prompt(provider: "BaseProvider") -> Optional["Anime"]: - year = inquirer.number( # type: ignore - message="Enter year:", - long_instruction="To skip this prompt press ctrl+z", - default=time.localtime().tm_year, - mandatory=False, - ).execute() +def _get_season_provider(mode: str) -> Optional["BaseProvider"]: + season_provider = None + for p in get_prefered_providers(mode): + if p.FILTER_CAPS & ( + FilterCapabilities.SEASON + | FilterCapabilities.YEAR + | FilterCapabilities.NO_QUERY + ): + season_provider = p + return season_provider + + +def season_search_pre_prompt( + mode: str, year: Optional[int] = None, season: Optional[str] = None +) -> Optional["Anime"]: + season_provider = _get_season_provider(mode) + assume_season_search = Config().assume_season_search + + # If there is no proper season provider + if season_provider is None: + if not assume_season_search: + return + # If assume search was on, and there is no proper season provider + print( + f"`assume_season_search` was set to true, but the providers ({", ".join(Config().providers[mode])}) you have selected do not have seasonal capabilities" + ) + return + + if assume_season_search or (year and season): + return season_search_prompt(season_provider, year, season) + + should_search = inquirer.confirm("Do you want to search in season?", default=False).execute() # type: ignore + + if should_search: + return season_search_prompt(season_provider) + + print( + "Hint: you can set `skip_season_search` to `true` in the config to skip this prompt!" + ) + + +def season_search_prompt( + provider: "BaseProvider", year: Optional[int] = None, season: Optional[str] = None +) -> Optional["Anime"]: + if year is None: + year = inquirer.number( # type: ignore + message="Enter year:", + long_instruction="To skip this prompt press ctrl+z", + default=time.localtime().tm_year, + mandatory=False, + ).execute() if year is None: return - season = inquirer.select( # type: ignore - message="Select Season:", - choices=["Winter", "Spring", "Summer", "Fall"], - instruction="The season selected by default is the current season.", - long_instruction="To skip this prompt press ctrl+z", - default=get_anime_season(time.localtime().tm_mon), - mandatory=False, - ).execute() + if season is None: + season = inquirer.select( # type: ignore + message="Select Season:", + choices=["Winter", "Spring", "Summer", "Fall"], + instruction="The season selected by default is the current season.", + long_instruction="To skip this prompt press ctrl+z", + default=get_anime_season(time.localtime().tm_mon), + mandatory=False, + ).execute() if season is None: return season = Season[season.upper()] - filters = Filters(year=year, season=season) - results = [ - Anime.from_search_result(provider, r) - for r in provider.get_search(query="", filters=filters) - ] + discovered_anime = get_anime_by_season(provider, year, season) anime = inquirer.fuzzy( # type: ignore message="Select Show:", choices=[ - Choice(value=r, name=f"{n + 1}. {repr(r)}") for n, r in enumerate(results) + Choice(value=r, name=f"{n + 1}. {repr(r)}") + for n, r in enumerate(discovered_anime) ], long_instruction=( "\nS = Anime is available in sub\n" @@ -136,6 +166,14 @@ def season_search_prompt(provider: "BaseProvider") -> Optional["Anime"]: return anime +def get_anime_by_season(provider: "BaseProvider", year: int, season: str): + filters = Filters(year=year, season=season) + return [ + Anime.from_search_result(provider, r) + for r in provider.get_search(query="", filters=filters) + ] + + def pick_episode_prompt( anime: "Anime", lang: LanguageTypeEnum, instruction: str = "" ) -> Optional["Episode"]: @@ -203,6 +241,29 @@ def lang_prompt(anime: "Anime") -> LanguageTypeEnum: return next(iter(anime.languages)) +def parse_seasonal_search(mode: str, passed: str) -> Optional["Anime"]: + """ + Mode: The provider to use. + Passed: The search terms passed (ex: `year:season`) + """ + options = iter(passed.split(":")) + year = next(options, None) + season = next(options, None) + + if (not year) or not year.isnumeric(): + error("A year was either not provided, or was not a number", fatal=True) + + if not season: + error("A season was not provided", fatal=True) + + season = convert_letter_to_season(season) + + if not season: + error("The given season was not a valid season", fatal=True) + + return season_search_pre_prompt(mode, int(year), season) + + def parse_auto_search( mode: str, passed: str ) -> Tuple["Anime", LanguageTypeEnum, List["Episode"]]: diff --git a/cli/src/anipy_cli/util.py b/cli/src/anipy_cli/util.py index de024f15..a1509d24 100644 --- a/cli/src/anipy_cli/util.py +++ b/cli/src/anipy_cli/util.py @@ -182,6 +182,25 @@ def get_anime_season(month): return "Fall" +def convert_letter_to_season(letter: str) -> Optional[str]: + """ + Converts the beginning of the name of a season to that season name. + + Ex: + ``` + win -> Winter + su -> Summer + sp -> Spring + ``` + + Returns None if the letter does not correspond to a season + """ + for season in ["Winter", "Summer", "Spring", "Fall"]: + if season.startswith(letter.capitalize()): + return season + return + + def migrate_locallist(file: Path) -> LocalListData: import json import re From e632637e839c44c191073e34907cbaabaabaeec1 Mon Sep 17 00:00:00 2001 From: Dragon of Shuu <68718280+DragonOfShuu@users.noreply.github.com> Date: Mon, 5 Aug 2024 01:14:12 -0600 Subject: [PATCH 2/4] feat completion: season search --- cli/src/anipy_cli/arg_parser.py | 6 ++-- cli/src/anipy_cli/clis/binge_cli.py | 12 ++++++- cli/src/anipy_cli/clis/download_cli.py | 12 ++++++- cli/src/anipy_cli/prompts.py | 46 ++++++++++++++++++-------- 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/cli/src/anipy_cli/arg_parser.py b/cli/src/anipy_cli/arg_parser.py index 0eaaa3a1..7fed923e 100644 --- a/cli/src/anipy_cli/arg_parser.py +++ b/cli/src/anipy_cli/arg_parser.py @@ -113,8 +113,10 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs: "--seasonal-search", required=False, dest="seasonal_search", - action="store", - help="Provide search parameters for seasons to Default, Download, or Binge mode in this format: {year}:{season}. Examples: '2024:winter' or '2020:fall'", + nargs="?", # 1 or none possible args + default=None, # Used if flag is not present (added this line for clarity, because default is always None) + const=True, # Used if flag is present, but no value + help="Provide search parameters for seasons to Default, Download, or Binge mode in this format: {year}:{season}. You can only use part of the season name if you wish. Examples: '2024:win' or '2020:fa'", ) options_group.add_argument( diff --git a/cli/src/anipy_cli/clis/binge_cli.py b/cli/src/anipy_cli/clis/binge_cli.py index be6976a3..18a09964 100644 --- a/cli/src/anipy_cli/clis/binge_cli.py +++ b/cli/src/anipy_cli/clis/binge_cli.py @@ -7,6 +7,7 @@ from anipy_cli.colors import colors, cprint from anipy_cli.config import Config from anipy_cli.prompts import ( + parse_seasonal_search, pick_episode_range_prompt, search_show_prompt, lang_prompt, @@ -34,6 +35,15 @@ def __init__(self, options: "CliArgs"): def print_header(self): cprint(colors.GREEN, "***Binge Mode***") + def _get_anime_from_user(self): + if (ss := self.options.seasonal_search) is not None: + return parse_seasonal_search( + "binge", + ss, + ) + + return search_show_prompt("binge") + def take_input(self): if self.options.search is not None: self.anime, self.lang, self.episodes = parse_auto_search( @@ -41,7 +51,7 @@ def take_input(self): ) return - anime = search_show_prompt("binge") + anime = self._get_anime_from_user() if anime is None: sys.exit(0) diff --git a/cli/src/anipy_cli/clis/download_cli.py b/cli/src/anipy_cli/clis/download_cli.py index 84dce41c..657ac39b 100644 --- a/cli/src/anipy_cli/clis/download_cli.py +++ b/cli/src/anipy_cli/clis/download_cli.py @@ -6,6 +6,7 @@ from anipy_cli.colors import colors, cprint from anipy_cli.config import Config from anipy_cli.prompts import ( + parse_seasonal_search, pick_episode_range_prompt, search_show_prompt, lang_prompt, @@ -39,6 +40,15 @@ def print_header(self): cprint(colors.GREEN, "***Download Mode***") cprint(colors.GREEN, "Downloads are stored in: ", colors.END, str(self.dl_path)) + def _get_anime_from_user(self): + if (ss := self.options.seasonal_search) is not None: + return parse_seasonal_search( + "download", + ss, + ) + + return search_show_prompt("download") + def take_input(self): if self.options.search is not None: self.anime, self.lang, self.episodes = parse_auto_search( @@ -46,7 +56,7 @@ def take_input(self): ) return - anime = search_show_prompt("download") + anime = self._get_anime_from_user() if anime is None: return False diff --git a/cli/src/anipy_cli/prompts.py b/cli/src/anipy_cli/prompts.py index 544f2c96..b3441b57 100644 --- a/cli/src/anipy_cli/prompts.py +++ b/cli/src/anipy_cli/prompts.py @@ -121,10 +121,11 @@ def season_search_prompt( provider: "BaseProvider", year: Optional[int] = None, season: Optional[str] = None ) -> Optional["Anime"]: if year is None: + curr_year = time.localtime().tm_year year = inquirer.number( # type: ignore message="Enter year:", long_instruction="To skip this prompt press ctrl+z", - default=time.localtime().tm_year, + default=curr_year, mandatory=False, ).execute() @@ -144,9 +145,7 @@ def season_search_prompt( if season is None: return - season = Season[season.upper()] - - discovered_anime = get_anime_by_season(provider, year, season) + discovered_anime = get_anime_by_season(provider, year, Season[season.upper()]) anime = inquirer.fuzzy( # type: ignore message="Select Show:", @@ -166,12 +165,15 @@ def season_search_prompt( return anime -def get_anime_by_season(provider: "BaseProvider", year: int, season: str): - filters = Filters(year=year, season=season) - return [ - Anime.from_search_result(provider, r) - for r in provider.get_search(query="", filters=filters) - ] +def get_anime_by_season(provider: "BaseProvider", year: int, season: Season): + with DotSpinner( + "Retrieving anime in ", colors.BLUE, f"{season.name} {year}", "..." + ): + filters = Filters(year=year, season=season) + return [ + Anime.from_search_result(provider, r) + for r in provider.get_search(query="", filters=filters) + ] def pick_episode_prompt( @@ -241,11 +243,29 @@ def lang_prompt(anime: "Anime") -> LanguageTypeEnum: return next(iter(anime.languages)) -def parse_seasonal_search(mode: str, passed: str) -> Optional["Anime"]: +def parse_seasonal_search(mode: str, passed: str | bool) -> Optional["Anime"]: """ - Mode: The provider to use. - Passed: The search terms passed (ex: `year:season`) + Takes the mode we are in, as well as the search parameters. + Asks the user to choose an anime. + + `Mode`: The provider to use. + `Passed`: The search terms passed (ex: `year:season`) or True, + if True we'll ask the user for this information """ + if isinstance(passed, bool): + if not passed: + return + + provider = _get_season_provider(mode) + + if not provider: + error( + "No valid provider was found for season search in the current mode", + fatal=True, + ) + + return season_search_prompt(provider) + options = iter(passed.split(":")) year = next(options, None) season = next(options, None) From 0f0ed399cd9a2c20860302bda4c825675f5d0d65 Mon Sep 17 00:00:00 2001 From: Dragon of Shuu <68718280+DragonOfShuu@users.noreply.github.com> Date: Mon, 5 Aug 2024 01:23:05 -0600 Subject: [PATCH 3/4] bug: arranged seasons in order --- cli/src/anipy_cli/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/anipy_cli/util.py b/cli/src/anipy_cli/util.py index a1509d24..790b5483 100644 --- a/cli/src/anipy_cli/util.py +++ b/cli/src/anipy_cli/util.py @@ -195,7 +195,7 @@ def convert_letter_to_season(letter: str) -> Optional[str]: Returns None if the letter does not correspond to a season """ - for season in ["Winter", "Summer", "Spring", "Fall"]: + for season in ["Spring", "Summer", "Fall", "Winter"]: if season.startswith(letter.capitalize()): return season return From 01a38f0fe0a04e76561fe732c3e10e886aa43f8a Mon Sep 17 00:00:00 2001 From: Dragon of Shuu <68718280+DragonOfShuu@users.noreply.github.com> Date: Mon, 5 Aug 2024 01:30:28 -0600 Subject: [PATCH 4/4] bug: removed forgotten print statement --- cli/src/anipy_cli/clis/default_cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/anipy_cli/clis/default_cli.py b/cli/src/anipy_cli/clis/default_cli.py index de14ed3f..a2c6adc6 100644 --- a/cli/src/anipy_cli/clis/default_cli.py +++ b/cli/src/anipy_cli/clis/default_cli.py @@ -63,7 +63,6 @@ def take_input(self): anime = self._get_anime_from_user() if anime is None: - print("Anime was none") return False self.lang = lang_prompt(anime)