anipy-cli 2.7.31__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of anipy-cli might be problematic. Click here for more details.

Files changed (62) hide show
  1. anipy_cli/__init__.py +2 -20
  2. anipy_cli/arg_parser.py +30 -20
  3. anipy_cli/cli.py +66 -0
  4. anipy_cli/clis/__init__.py +15 -0
  5. anipy_cli/clis/base_cli.py +32 -0
  6. anipy_cli/clis/binge_cli.py +83 -0
  7. anipy_cli/clis/default_cli.py +104 -0
  8. anipy_cli/clis/download_cli.py +111 -0
  9. anipy_cli/clis/history_cli.py +93 -0
  10. anipy_cli/clis/mal_cli.py +71 -0
  11. anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
  12. anipy_cli/colors.py +4 -4
  13. anipy_cli/config.py +308 -87
  14. anipy_cli/discord.py +34 -0
  15. anipy_cli/mal_proxy.py +216 -0
  16. anipy_cli/menus/__init__.py +5 -0
  17. anipy_cli/{cli/menus → menus}/base_menu.py +8 -12
  18. anipy_cli/menus/mal_menu.py +660 -0
  19. anipy_cli/menus/menu.py +194 -0
  20. anipy_cli/menus/seasonal_menu.py +263 -0
  21. anipy_cli/prompts.py +231 -0
  22. anipy_cli/util.py +262 -0
  23. anipy_cli-3.0.0.dist-info/METADATA +67 -0
  24. anipy_cli-3.0.0.dist-info/RECORD +26 -0
  25. {anipy_cli-2.7.31.dist-info → anipy_cli-3.0.0.dist-info}/WHEEL +1 -2
  26. anipy_cli-3.0.0.dist-info/entry_points.txt +3 -0
  27. anipy_cli/cli/__init__.py +0 -1
  28. anipy_cli/cli/cli.py +0 -37
  29. anipy_cli/cli/clis/__init__.py +0 -6
  30. anipy_cli/cli/clis/base_cli.py +0 -43
  31. anipy_cli/cli/clis/binge_cli.py +0 -54
  32. anipy_cli/cli/clis/default_cli.py +0 -46
  33. anipy_cli/cli/clis/download_cli.py +0 -92
  34. anipy_cli/cli/clis/history_cli.py +0 -64
  35. anipy_cli/cli/clis/mal_cli.py +0 -27
  36. anipy_cli/cli/menus/__init__.py +0 -3
  37. anipy_cli/cli/menus/mal_menu.py +0 -411
  38. anipy_cli/cli/menus/menu.py +0 -108
  39. anipy_cli/cli/menus/seasonal_menu.py +0 -177
  40. anipy_cli/cli/util.py +0 -125
  41. anipy_cli/download.py +0 -467
  42. anipy_cli/history.py +0 -83
  43. anipy_cli/mal.py +0 -651
  44. anipy_cli/misc.py +0 -227
  45. anipy_cli/player/__init__.py +0 -1
  46. anipy_cli/player/player.py +0 -35
  47. anipy_cli/player/players/__init__.py +0 -3
  48. anipy_cli/player/players/base.py +0 -107
  49. anipy_cli/player/players/mpv.py +0 -19
  50. anipy_cli/player/players/mpv_control.py +0 -37
  51. anipy_cli/player/players/syncplay.py +0 -19
  52. anipy_cli/player/players/vlc.py +0 -18
  53. anipy_cli/query.py +0 -100
  54. anipy_cli/run_anipy_cli.py +0 -14
  55. anipy_cli/seasonal.py +0 -112
  56. anipy_cli/url_handler.py +0 -470
  57. anipy_cli/version.py +0 -1
  58. anipy_cli-2.7.31.dist-info/LICENSE +0 -674
  59. anipy_cli-2.7.31.dist-info/METADATA +0 -162
  60. anipy_cli-2.7.31.dist-info/RECORD +0 -43
  61. anipy_cli-2.7.31.dist-info/entry_points.txt +0 -2
  62. anipy_cli-2.7.31.dist-info/top_level.txt +0 -1
@@ -0,0 +1,194 @@
1
+ import sys
2
+ from typing import TYPE_CHECKING, List
3
+
4
+ from anipy_api.download import Downloader
5
+ from anipy_api.provider import LanguageTypeEnum
6
+ from anipy_api.locallist import LocalList
7
+
8
+ from anipy_cli.colors import colors, cprint
9
+ from anipy_cli.config import Config
10
+ from anipy_cli.menus.base_menu import MenuBase, MenuOption
11
+ from anipy_cli.util import DotSpinner, error, get_download_path, migrate_locallist
12
+ from anipy_cli.prompts import pick_episode_prompt, search_show_prompt
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from anipy_api.anime import Anime
17
+ from anipy_api.player import PlayerBase
18
+ from anipy_api.provider import Episode, ProviderStream
19
+
20
+ from anipy_cli.arg_parser import CliArgs
21
+
22
+
23
+ class Menu(MenuBase):
24
+ def __init__(
25
+ self,
26
+ options: "CliArgs",
27
+ anime: "Anime",
28
+ stream: "ProviderStream",
29
+ player: "PlayerBase",
30
+ ):
31
+ self.options = options
32
+ self.anime = anime
33
+ self.stream = stream
34
+ self.player = player
35
+ self.lang = stream.language
36
+ self.history_list = LocalList(
37
+ Config()._history_file_path, migrate_cb=migrate_locallist
38
+ )
39
+
40
+ @property
41
+ def menu_options(self) -> List["MenuOption"]:
42
+ return [
43
+ MenuOption("Next Episode", self.next_ep, "n"),
44
+ MenuOption("Previous Episode", self.prev_ep, "p"),
45
+ MenuOption("Replay Episode", self.repl_ep, "r"),
46
+ MenuOption(
47
+ f"Change to {'sub' if self.lang == LanguageTypeEnum.DUB else 'dub'}",
48
+ self.change_type,
49
+ "c",
50
+ ),
51
+ MenuOption("Select episode", self.selec_ep, "s"),
52
+ MenuOption("Search for Anime", self.search, "a"),
53
+ MenuOption("Print Video Info", self.video_info, "i"),
54
+ MenuOption("Download Episode", self.download_video, "d"),
55
+ MenuOption("Quit", self.quit, "q"),
56
+ ]
57
+
58
+ def print_header(self):
59
+ cprint(
60
+ colors.GREEN,
61
+ "Playing: ",
62
+ colors.BLUE,
63
+ f"{self.anime.name} ({self.lang})",
64
+ colors.GREEN,
65
+ f" | {self.stream.resolution}p | ",
66
+ colors.RED,
67
+ f"{self.stream.episode}/{self.anime.get_episodes(self.lang)[-1]}",
68
+ )
69
+
70
+ def _start_episode(self, episode: "Episode"):
71
+ with DotSpinner(
72
+ "Extracting streams for ",
73
+ colors.BLUE,
74
+ f"{self.anime.name} ({self.lang})",
75
+ " Episode ",
76
+ episode,
77
+ "...",
78
+ ):
79
+ self.stream = self.anime.get_video(
80
+ episode, self.lang, preferred_quality=self.options.quality
81
+ )
82
+
83
+ self.history_list.update(self.anime, episode=episode, language=self.lang)
84
+ self.player.play_title(self.anime, self.stream)
85
+
86
+ def next_ep(self):
87
+ episodes = self.anime.get_episodes(self.lang)
88
+ current_episode = episodes.index(self.stream.episode)
89
+ if len(episodes) <= current_episode + 1:
90
+ error("no episodes after this")
91
+ else:
92
+ next_episode = episodes[current_episode + 1]
93
+ self._start_episode(next_episode)
94
+
95
+ self.print_options()
96
+
97
+ def prev_ep(self):
98
+ episodes = self.anime.get_episodes(self.lang)
99
+ current_episode = episodes.index(self.stream.episode)
100
+ if current_episode - 1 < 0:
101
+ error("no episodes before this")
102
+ else:
103
+ prev_episode = episodes[current_episode - 1]
104
+ self._start_episode(prev_episode)
105
+
106
+ self.print_options()
107
+
108
+ def repl_ep(self):
109
+ self._start_episode(self.stream.episode)
110
+
111
+ def change_type(self):
112
+ to_change = (
113
+ LanguageTypeEnum.DUB
114
+ if self.lang == LanguageTypeEnum.SUB
115
+ else LanguageTypeEnum.SUB
116
+ )
117
+
118
+ if to_change not in self.anime.languages:
119
+ error(f"this anime does not have a {to_change} version")
120
+ return
121
+
122
+ if self.stream.episode not in self.anime.get_episodes(to_change):
123
+ error(
124
+ f"the current episode ({self.stream.episode}) is not available in {to_change}, not switching..."
125
+ )
126
+ return
127
+
128
+ self.lang = to_change
129
+ self.repl_ep()
130
+ self.print_options()
131
+
132
+ def selec_ep(self):
133
+ episode = pick_episode_prompt(self.anime, self.lang)
134
+ if episode is None:
135
+ return
136
+ self._start_episode(episode)
137
+ self.print_options()
138
+
139
+ def search(self):
140
+ search_result = search_show_prompt("default")
141
+ if search_result is None:
142
+ return
143
+ self.anime = search_result
144
+ episode = pick_episode_prompt(self.anime, self.lang)
145
+ if episode is None:
146
+ return
147
+ self._start_episode(episode)
148
+ self.print_options()
149
+
150
+ def video_info(self):
151
+ print(f"Show Name: {self.anime.name}")
152
+ print(f"Provider: {self.anime.provider.NAME}")
153
+ print(f"Stream Url: {self.stream.url}")
154
+ print(f"Quality: {self.stream.resolution}p")
155
+
156
+ def download_video(self):
157
+ config = Config()
158
+ with DotSpinner("Starting Download...") as s:
159
+
160
+ def progress_indicator(percentage: float):
161
+ s.set_text(f"Downloading ({percentage:.1f}%)")
162
+
163
+ def info_display(message: str):
164
+ s.write(f"> {message}")
165
+
166
+ downloader = Downloader(progress_indicator, info_display)
167
+
168
+ s.set_text(
169
+ "Extracting streams for ",
170
+ colors.BLUE,
171
+ f"{self.anime.name} ({self.lang})",
172
+ colors.END,
173
+ " Episode ",
174
+ self.stream.episode,
175
+ "...",
176
+ )
177
+
178
+ s.set_text("Downloading...")
179
+
180
+ path = downloader.download(
181
+ self.stream,
182
+ get_download_path(self.anime, self.stream),
183
+ container=config.remux_to,
184
+ ffmpeg=self.options.ffmpeg or config.ffmpeg_hls,
185
+ )
186
+
187
+ if Config().auto_open_dl_defaultcli:
188
+ self.player.play_file(str(path))
189
+
190
+ self.print_options()
191
+
192
+ def quit(self):
193
+ self.player.kill_player()
194
+ sys.exit(0)
@@ -0,0 +1,263 @@
1
+ import sys
2
+ from typing import TYPE_CHECKING, List, Tuple
3
+
4
+ from anipy_api.anime import Anime
5
+ from anipy_api.download import Downloader
6
+ from anipy_api.provider import LanguageTypeEnum
7
+ from anipy_api.provider.base import Episode
8
+ from anipy_api.locallist import LocalList, LocalListEntry
9
+ from InquirerPy import inquirer
10
+ from InquirerPy.base.control import Choice
11
+ from InquirerPy.utils import get_style
12
+
13
+ from anipy_cli.colors import colors
14
+ from anipy_cli.config import Config
15
+ from anipy_cli.menus.base_menu import MenuBase, MenuOption
16
+ from anipy_cli.util import (
17
+ DotSpinner,
18
+ error,
19
+ get_configured_player,
20
+ get_download_path,
21
+ migrate_locallist,
22
+ )
23
+ from anipy_cli.prompts import pick_episode_prompt, search_show_prompt, lang_prompt
24
+
25
+ if TYPE_CHECKING:
26
+ from anipy_cli.arg_parser import CliArgs
27
+
28
+
29
+ class SeasonalMenu(MenuBase):
30
+ def __init__(self, options: "CliArgs"):
31
+ self.options = options
32
+ self.player = get_configured_player(self.options.optional_player)
33
+
34
+ config = Config()
35
+ self.dl_path = config.seasonals_dl_path
36
+ self.seasonal_list = LocalList(config._seasonal_file_path, migrate_locallist)
37
+ if options.location:
38
+ self.dl_path = options.location
39
+
40
+ @property
41
+ def menu_options(self) -> List[MenuOption]:
42
+ return [
43
+ MenuOption("Add Anime", self.add_anime, "a"),
44
+ MenuOption("Delete one anime from seasonals", self.del_anime, "e"),
45
+ MenuOption("List anime in seasonals", self.list_animes, "l"),
46
+ MenuOption("Change dub/sub of anime in seasonals", self.change_lang, "c"),
47
+ MenuOption("Download newest episodes", self.download_latest, "d"),
48
+ MenuOption("Binge watch newest episodes", self.binge_latest, "w"),
49
+ MenuOption("Quit", self.quit, "q"),
50
+ ]
51
+
52
+ def print_header(self):
53
+ pass
54
+
55
+ def _choose_latest(self) -> List[Tuple["Anime", LanguageTypeEnum, List["Episode"]]]:
56
+ with DotSpinner("Fetching status of shows in seasonals..."):
57
+ choices = []
58
+ for s in self.seasonal_list.get_all():
59
+ anime = Anime.from_local_list_entry(s)
60
+ lang = s.language
61
+ episodes = anime.get_episodes(lang)
62
+ to_watch = episodes[episodes.index(s.episode) + 1 :]
63
+ if len(to_watch) > 0:
64
+ ch = Choice(
65
+ value=(anime, lang, to_watch),
66
+ name=f"{anime.name} (to watch: {len(to_watch)})",
67
+ )
68
+ choices.append(ch)
69
+
70
+ if self.options.auto_update:
71
+ return [ch.value for ch in choices]
72
+
73
+ choices = inquirer.fuzzy( # type: ignore
74
+ message="Select Seasonals to catch up to:",
75
+ choices=choices,
76
+ multiselect=True,
77
+ long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
78
+ mandatory=False,
79
+ keybindings={"toggle": [{"key": "c-space"}]},
80
+ style=get_style(
81
+ {"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
82
+ ),
83
+ ).execute()
84
+ return choices or []
85
+
86
+ def add_anime(self):
87
+ anime = search_show_prompt("default")
88
+
89
+ if anime is None:
90
+ return
91
+
92
+ lang = lang_prompt(anime)
93
+
94
+ episode = pick_episode_prompt(
95
+ anime, lang, instruction="To start from the beginning skip this Prompt"
96
+ )
97
+
98
+ if episode is None:
99
+ episode = anime.get_episodes(lang)[0]
100
+
101
+ self.seasonal_list.update(anime, episode=episode, language=lang)
102
+
103
+ self.print_options()
104
+
105
+ def del_anime(self):
106
+ seasonals = self.seasonal_list.get_all()
107
+
108
+ if len(seasonals) == 0:
109
+ error("No seasonals configured.")
110
+ return
111
+
112
+ entries: List[LocalListEntry] = (
113
+ inquirer.fuzzy( # type: ignore
114
+ message="Select Seasonals to delete:",
115
+ choices=seasonals,
116
+ multiselect=True,
117
+ long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
118
+ mandatory=False,
119
+ keybindings={"toggle": [{"key": "c-space"}]},
120
+ style=get_style(
121
+ {"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
122
+ ),
123
+ ).execute()
124
+ or []
125
+ )
126
+
127
+ for e in entries:
128
+ self.seasonal_list.delete(e)
129
+
130
+ self.print_options()
131
+
132
+ def change_lang(self):
133
+ seasonals = self.seasonal_list.get_all()
134
+
135
+ if len(seasonals) == 0:
136
+ error("No seasonals configured.")
137
+ return
138
+
139
+ entries: List[LocalListEntry] = (
140
+ inquirer.fuzzy( # type: ignore
141
+ message="Select Seasonals to delete:",
142
+ choices=seasonals,
143
+ multiselect=True,
144
+ long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
145
+ mandatory=False,
146
+ keybindings={"toggle": [{"key": "c-space"}]},
147
+ style=get_style(
148
+ {"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
149
+ ),
150
+ ).execute()
151
+ or []
152
+ )
153
+
154
+ if not entries:
155
+ return
156
+
157
+ action: str = inquirer.select( # type: ignore
158
+ message="Switch to:",
159
+ choices=["Sub", "Dub"],
160
+ long_instruction="To skip this prompt press ctrl+z",
161
+ mandatory=False,
162
+ ).execute()
163
+
164
+ if not action:
165
+ return
166
+
167
+ for e in entries:
168
+ if e.language == LanguageTypeEnum.DUB:
169
+ new_lang = LanguageTypeEnum.SUB
170
+ else:
171
+ new_lang = LanguageTypeEnum.DUB
172
+ if new_lang in e.languages:
173
+ self.seasonal_list.update(e, language=new_lang)
174
+ else:
175
+ print(f"> {new_lang} is for {e.name} not available")
176
+
177
+ def list_animes(self):
178
+ for i in self.seasonal_list.get_all():
179
+ print(i)
180
+
181
+ def download_latest(self):
182
+ picked = self._choose_latest()
183
+ config = Config()
184
+ total_eps = sum([len(e) for _a, _d, e in picked])
185
+ if total_eps == 0:
186
+ print("Nothing to download, returning...")
187
+ return
188
+ else:
189
+ print(f"Downloading a total of {total_eps} episode(s)")
190
+ with DotSpinner("Starting Download...") as s:
191
+
192
+ def progress_indicator(percentage: float):
193
+ s.set_text(f"Progress: {percentage:.1f}%")
194
+
195
+ def info_display(message: str):
196
+ s.write(f"> {message}")
197
+
198
+ downloader = Downloader(progress_indicator, info_display)
199
+
200
+ for anime, lang, eps in picked:
201
+ for ep in eps:
202
+ s.set_text(
203
+ "Extracting streams for ",
204
+ colors.BLUE,
205
+ f"{anime.name} ({lang})",
206
+ colors.END,
207
+ " Episode ",
208
+ ep,
209
+ "...",
210
+ )
211
+
212
+ stream = anime.get_video(
213
+ ep, lang, preferred_quality=self.options.quality
214
+ )
215
+
216
+ info_display(
217
+ f"Downloading Episode {stream.episode} of {anime.name}"
218
+ )
219
+ s.set_text("Downloading...")
220
+
221
+ downloader.download(
222
+ stream,
223
+ get_download_path(anime, stream, parent_directory=self.dl_path),
224
+ container=config.remux_to,
225
+ ffmpeg=self.options.ffmpeg or config.ffmpeg_hls,
226
+ )
227
+ self.seasonal_list.update(anime, episode=ep, language=lang)
228
+
229
+ if not self.options.auto_update:
230
+ self.print_options(clear_screen=True)
231
+
232
+ def binge_latest(self):
233
+ picked = self._choose_latest()
234
+ total_eps = sum([len(e) for _a, _d, e in picked])
235
+ if total_eps == 0:
236
+ print("Nothing to watch, returning...")
237
+ return
238
+ else:
239
+ print(f"Playing a total of {total_eps} episode(s)")
240
+ for anime, lang, eps in picked:
241
+ for ep in eps:
242
+ with DotSpinner(
243
+ "Extracting streams for ",
244
+ colors.BLUE,
245
+ f"{anime.name} ({lang})",
246
+ colors.END,
247
+ " Episode ",
248
+ ep,
249
+ "...",
250
+ ) as s:
251
+ stream = anime.get_video(
252
+ ep, lang, preferred_quality=self.options.quality
253
+ )
254
+ s.ok("✔")
255
+
256
+ self.player.play_title(anime, stream)
257
+ self.player.wait()
258
+ self.seasonal_list.update(anime, episode=ep, language=lang)
259
+
260
+ self.print_options()
261
+
262
+ def quit(self):
263
+ sys.exit(0)
anipy_cli/prompts.py ADDED
@@ -0,0 +1,231 @@
1
+ import time
2
+ from typing import TYPE_CHECKING, Optional, List, Tuple
3
+ from InquirerPy import inquirer
4
+ from anipy_api.provider import (
5
+ BaseProvider,
6
+ FilterCapabilities,
7
+ Filters,
8
+ LanguageTypeEnum,
9
+ Season,
10
+ )
11
+ from anipy_api.anime import Anime
12
+
13
+ from anipy_cli.util import (
14
+ DotSpinner,
15
+ get_anime_season,
16
+ get_prefered_providers,
17
+ error,
18
+ parse_episode_ranges,
19
+ )
20
+ from anipy_cli.colors import colors
21
+ from anipy_cli.config import Config
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from anipy_api.provider import Episode
26
+
27
+
28
+ def search_show_prompt(mode: str) -> Optional["Anime"]:
29
+ if not Config().skip_season_search:
30
+ season_provider = None
31
+ for p in get_prefered_providers(mode):
32
+ if p.FILTER_CAPS & (
33
+ FilterCapabilities.SEASON
34
+ | FilterCapabilities.YEAR
35
+ | FilterCapabilities.NO_QUERY
36
+ ):
37
+ season_provider = p
38
+ if season_provider is not None:
39
+ should_search = inquirer.confirm("Do you want to search in season?", default=False).execute() # type: ignore
40
+ if not should_search:
41
+ print(
42
+ "Hint: you can set `skip_season_search` to `true` in the config to skip this prompt!"
43
+ )
44
+ else:
45
+ return season_search_prompt(season_provider)
46
+
47
+ query = inquirer.text( # type: ignore
48
+ "Search Anime:",
49
+ long_instruction="To cancel this prompt press ctrl+z",
50
+ mandatory=False,
51
+ ).execute()
52
+
53
+ if query is None:
54
+ return None
55
+
56
+ with DotSpinner("Searching for ", colors.BLUE, query, "..."):
57
+ results: List[Anime] = []
58
+ for provider in get_prefered_providers(mode):
59
+ results.extend(
60
+ [
61
+ Anime.from_search_result(provider, x)
62
+ for x in provider.get_search(query)
63
+ ]
64
+ )
65
+
66
+ if len(results) == 0:
67
+ error("no search results")
68
+ return search_show_prompt(mode)
69
+
70
+ anime = inquirer.fuzzy( # type: ignore
71
+ message="Select Show:",
72
+ choices=results,
73
+ long_instruction="\nS = Anime is available in sub\nD = Anime is available in dub\nTo skip this prompt press ctrl+z",
74
+ mandatory=False,
75
+ ).execute()
76
+
77
+ return anime
78
+
79
+
80
+ def season_search_prompt(provider: "BaseProvider") -> Optional["Anime"]:
81
+ year = inquirer.number( # type: ignore
82
+ message="Enter year:",
83
+ long_instruction="To skip this prompt press ctrl+z",
84
+ default=time.localtime().tm_year,
85
+ mandatory=False,
86
+ ).execute()
87
+
88
+ if year is None:
89
+ return
90
+
91
+ season = inquirer.select( # type: ignore
92
+ message="Select Season:",
93
+ choices=["Winter", "Spring", "Summer", "Fall"],
94
+ instruction="The season selected by default is the current season.",
95
+ long_instruction="To skip this prompt press ctrl+z",
96
+ default=get_anime_season(time.localtime().tm_mon),
97
+ mandatory=False,
98
+ ).execute()
99
+
100
+ if season is None:
101
+ return
102
+
103
+ season = Season[season.upper()]
104
+
105
+ filters = Filters(year=year, season=season)
106
+ results = [
107
+ Anime.from_search_result(provider, r)
108
+ for r in provider.get_search(query="", filters=filters)
109
+ ]
110
+
111
+ anime = inquirer.fuzzy( # type: ignore
112
+ message="Select Show:",
113
+ choices=results,
114
+ long_instruction="\nS = Anime is available in sub\nD = Anime is available in dub\nTo skip this prompt press ctrl+z",
115
+ mandatory=False,
116
+ ).execute()
117
+
118
+ return anime
119
+
120
+
121
+ def pick_episode_prompt(
122
+ anime: "Anime", lang: LanguageTypeEnum, instruction: str = ""
123
+ ) -> Optional["Episode"]:
124
+ with DotSpinner("Fetching episode list for ", colors.BLUE, anime.name, "..."):
125
+ episodes = anime.get_episodes(lang)
126
+
127
+ if not episodes:
128
+ error(f"No episodes available for {anime.name}")
129
+ return None
130
+
131
+ return inquirer.fuzzy( # type: ignore
132
+ message="Select Episode:",
133
+ instruction=instruction,
134
+ choices=episodes,
135
+ long_instruction="To skip this prompt press ctrl+z",
136
+ mandatory=False,
137
+ ).execute()
138
+
139
+
140
+ def pick_episode_range_prompt(
141
+ anime: "Anime", lang: LanguageTypeEnum
142
+ ) -> List["Episode"]:
143
+ with DotSpinner("Fetching episode list for ", colors.BLUE, anime.name, "..."):
144
+ episodes = anime.get_episodes(lang)
145
+
146
+ if not episodes:
147
+ error(f"No episodes available for {anime.name}")
148
+ return []
149
+
150
+ res = inquirer.text( # type: ignore
151
+ message=f"Input Episode Range(s) from episodes {episodes[0]} to {episodes[-1]}:",
152
+ long_instruction="Type e.g. `1-10 19-20` or `3-4` or `3`\nTo skip this prompt press ctrl+z",
153
+ mandatory=False,
154
+ ).execute()
155
+
156
+ if res is None:
157
+ return []
158
+
159
+ return parse_episode_ranges(res, episodes)
160
+
161
+
162
+ def lang_prompt(anime: "Anime") -> LanguageTypeEnum:
163
+ config = Config()
164
+ preferred = (
165
+ LanguageTypeEnum[config.preferred_type.upper()]
166
+ if config.preferred_type is not None
167
+ else None
168
+ )
169
+
170
+ if preferred in anime.languages:
171
+ return preferred
172
+
173
+ if LanguageTypeEnum.DUB not in anime.languages:
174
+ return LanguageTypeEnum.SUB
175
+
176
+ if len(anime.languages) == 2:
177
+ res = inquirer.confirm("Want to watch in dub?").execute() # type: ignore
178
+ print("Hint: you can set a default in the config with `preferred_type`!")
179
+
180
+ if res:
181
+ return LanguageTypeEnum.DUB
182
+ else:
183
+ return LanguageTypeEnum.SUB
184
+ else:
185
+ return next(iter(anime.languages))
186
+
187
+
188
+ def parse_auto_search(
189
+ mode: str, passed: str
190
+ ) -> Tuple["Anime", LanguageTypeEnum, List["Episode"]]:
191
+ options = iter(passed.split(":"))
192
+ query = next(options, None)
193
+ ranges = next(options, None)
194
+ ltype = next(options, None)
195
+
196
+ if not query:
197
+ error("you provided the search parameter but no query", fatal=True)
198
+
199
+ if not ranges:
200
+ error("you provided the search parameter but no episode ranges", fatal=True)
201
+
202
+ if not (ltype == "sub" or ltype == "dub"):
203
+ ltype = Config().preferred_type
204
+
205
+ with DotSpinner("Searching for ", colors.BLUE, query, "..."):
206
+ results: List[Anime] = []
207
+ for provider in get_prefered_providers(mode):
208
+ results.extend(
209
+ [
210
+ Anime.from_search_result(provider, x)
211
+ for x in provider.get_search(query)
212
+ ]
213
+ )
214
+ if len(results) == 0:
215
+ error(f"no anime found for query {query}", fatal=True)
216
+
217
+ result = results[0]
218
+ if ltype is None:
219
+ lang = lang_prompt(result)
220
+ else:
221
+ lang = LanguageTypeEnum[ltype.upper()]
222
+
223
+ if lang not in result.languages:
224
+ error(f"{lang} is not available for {result.name}", fatal=True)
225
+
226
+ episodes = result.get_episodes(lang)
227
+ chosen = parse_episode_ranges(ranges, episodes)
228
+ if not chosen:
229
+ error("could not determine any epiosdes from search parameter", fatal=True)
230
+
231
+ return result, lang, chosen