anipy-cli 2.7.17__py3-none-any.whl → 3.8.2__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.
- anipy_cli/__init__.py +2 -20
- anipy_cli/anilist_proxy.py +229 -0
- anipy_cli/arg_parser.py +109 -21
- anipy_cli/cli.py +98 -0
- anipy_cli/clis/__init__.py +17 -0
- anipy_cli/clis/anilist_cli.py +62 -0
- anipy_cli/clis/base_cli.py +34 -0
- anipy_cli/clis/binge_cli.py +96 -0
- anipy_cli/clis/default_cli.py +115 -0
- anipy_cli/clis/download_cli.py +85 -0
- anipy_cli/clis/history_cli.py +96 -0
- anipy_cli/clis/mal_cli.py +71 -0
- anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
- anipy_cli/colors.py +14 -8
- anipy_cli/config.py +387 -90
- anipy_cli/discord.py +34 -0
- anipy_cli/download_component.py +194 -0
- anipy_cli/logger.py +200 -0
- anipy_cli/mal_proxy.py +228 -0
- anipy_cli/menus/__init__.py +6 -0
- anipy_cli/menus/anilist_menu.py +671 -0
- anipy_cli/{cli/menus → menus}/base_menu.py +9 -14
- anipy_cli/menus/mal_menu.py +657 -0
- anipy_cli/menus/menu.py +265 -0
- anipy_cli/menus/seasonal_menu.py +270 -0
- anipy_cli/prompts.py +387 -0
- anipy_cli/util.py +268 -0
- anipy_cli-3.8.2.dist-info/METADATA +71 -0
- anipy_cli-3.8.2.dist-info/RECORD +31 -0
- {anipy_cli-2.7.17.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -2
- anipy_cli-3.8.2.dist-info/entry_points.txt +3 -0
- anipy_cli/cli/__init__.py +0 -1
- anipy_cli/cli/cli.py +0 -37
- anipy_cli/cli/clis/__init__.py +0 -6
- anipy_cli/cli/clis/base_cli.py +0 -43
- anipy_cli/cli/clis/binge_cli.py +0 -54
- anipy_cli/cli/clis/default_cli.py +0 -46
- anipy_cli/cli/clis/download_cli.py +0 -92
- anipy_cli/cli/clis/history_cli.py +0 -64
- anipy_cli/cli/clis/mal_cli.py +0 -27
- anipy_cli/cli/menus/__init__.py +0 -3
- anipy_cli/cli/menus/mal_menu.py +0 -411
- anipy_cli/cli/menus/menu.py +0 -102
- anipy_cli/cli/menus/seasonal_menu.py +0 -174
- anipy_cli/cli/util.py +0 -118
- anipy_cli/download.py +0 -454
- anipy_cli/history.py +0 -83
- anipy_cli/mal.py +0 -645
- anipy_cli/misc.py +0 -227
- anipy_cli/player/__init__.py +0 -1
- anipy_cli/player/player.py +0 -33
- anipy_cli/player/players/__init__.py +0 -3
- anipy_cli/player/players/base.py +0 -106
- anipy_cli/player/players/mpv.py +0 -19
- anipy_cli/player/players/mpv_contrl.py +0 -37
- anipy_cli/player/players/syncplay.py +0 -19
- anipy_cli/player/players/vlc.py +0 -18
- anipy_cli/query.py +0 -92
- anipy_cli/run_anipy_cli.py +0 -14
- anipy_cli/seasonal.py +0 -106
- anipy_cli/url_handler.py +0 -442
- anipy_cli/version.py +0 -1
- anipy_cli-2.7.17.dist-info/LICENSE +0 -674
- anipy_cli-2.7.17.dist-info/METADATA +0 -159
- anipy_cli-2.7.17.dist-info/RECORD +0 -43
- anipy_cli-2.7.17.dist-info/entry_points.txt +0 -2
- anipy_cli-2.7.17.dist-info/top_level.txt +0 -1
anipy_cli/menus/menu.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import TYPE_CHECKING, List
|
|
3
|
+
|
|
4
|
+
from InquirerPy import inquirer
|
|
5
|
+
from InquirerPy.base.control import Choice
|
|
6
|
+
from anipy_api.download import Downloader
|
|
7
|
+
from anipy_api.provider import LanguageTypeEnum, ProviderStream
|
|
8
|
+
from anipy_api.locallist import LocalList
|
|
9
|
+
import anipy_cli.logger as logger
|
|
10
|
+
|
|
11
|
+
from anipy_cli.colors import colors, cprint
|
|
12
|
+
from anipy_cli.config import Config
|
|
13
|
+
from anipy_cli.menus.base_menu import MenuBase, MenuOption
|
|
14
|
+
from anipy_cli.util import (
|
|
15
|
+
DotSpinner,
|
|
16
|
+
error,
|
|
17
|
+
get_download_path,
|
|
18
|
+
get_post_download_scripts_hook,
|
|
19
|
+
migrate_locallist,
|
|
20
|
+
)
|
|
21
|
+
from anipy_cli.prompts import pick_episode_prompt, search_show_prompt
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from anipy_api.anime import Anime
|
|
26
|
+
from anipy_api.player import PlayerBase
|
|
27
|
+
from anipy_api.provider import Episode
|
|
28
|
+
|
|
29
|
+
from anipy_cli.arg_parser import CliArgs
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Menu(MenuBase):
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
options: "CliArgs",
|
|
36
|
+
anime: "Anime",
|
|
37
|
+
stream: "ProviderStream",
|
|
38
|
+
player: "PlayerBase",
|
|
39
|
+
):
|
|
40
|
+
self.options = options
|
|
41
|
+
self.anime = anime
|
|
42
|
+
self.stream = stream
|
|
43
|
+
self.player = player
|
|
44
|
+
self.lang = stream.language
|
|
45
|
+
self.history_list = LocalList(
|
|
46
|
+
Config()._history_file_path, migrate_cb=migrate_locallist
|
|
47
|
+
)
|
|
48
|
+
self.seasonal_list = LocalList(Config()._seasonal_file_path, migrate_locallist)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def menu_options(self) -> List["MenuOption"]:
|
|
52
|
+
return [
|
|
53
|
+
MenuOption("Next Episode", self.next_ep, "n"),
|
|
54
|
+
MenuOption("Previous Episode", self.prev_ep, "p"),
|
|
55
|
+
MenuOption("Replay Episode", self.repl_ep, "r"),
|
|
56
|
+
MenuOption(
|
|
57
|
+
f"Change to {'sub' if self.lang == LanguageTypeEnum.DUB else 'dub'}",
|
|
58
|
+
self.change_type,
|
|
59
|
+
"c",
|
|
60
|
+
),
|
|
61
|
+
MenuOption("Select episode", self.selec_ep, "s"),
|
|
62
|
+
MenuOption("Select from history", self.selec_hist, "h"),
|
|
63
|
+
MenuOption("Search for Anime", self.search, "a"),
|
|
64
|
+
MenuOption("Add to seasonals", self.add_seasonal, "t"),
|
|
65
|
+
MenuOption("Change video quality", self.change_quality, "v"),
|
|
66
|
+
MenuOption("Print Video Info", self.video_info, "i"),
|
|
67
|
+
MenuOption("Download Episode", self.download_video, "d"),
|
|
68
|
+
MenuOption("Quit", self.quit, "q"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
def print_header(self):
|
|
72
|
+
cprint(
|
|
73
|
+
colors.GREEN,
|
|
74
|
+
"Playing: ",
|
|
75
|
+
colors.BLUE,
|
|
76
|
+
f"{self.anime.name} ({self.lang})",
|
|
77
|
+
colors.GREEN,
|
|
78
|
+
f" | {self.stream.resolution}p | ",
|
|
79
|
+
colors.RED,
|
|
80
|
+
f"{self.stream.episode}/{self.anime.get_episodes(self.lang)[-1]}",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _start_episode(self, episode: "Episode"):
|
|
84
|
+
with DotSpinner(
|
|
85
|
+
"Extracting streams for ",
|
|
86
|
+
colors.BLUE,
|
|
87
|
+
f"{self.anime.name} ({self.lang})",
|
|
88
|
+
" Episode ",
|
|
89
|
+
episode,
|
|
90
|
+
"...",
|
|
91
|
+
):
|
|
92
|
+
self.stream = self.anime.get_video(
|
|
93
|
+
episode, self.lang, preferred_quality=self.options.quality
|
|
94
|
+
)
|
|
95
|
+
if self.stream is None:
|
|
96
|
+
error("Could not find stream for requested Episode!")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
self.history_list.update(self.anime, episode=episode, language=self.lang)
|
|
100
|
+
self.player.play_title(self.anime, self.stream)
|
|
101
|
+
|
|
102
|
+
def next_ep(self):
|
|
103
|
+
episodes = self.anime.get_episodes(self.lang)
|
|
104
|
+
current_episode = episodes.index(self.stream.episode)
|
|
105
|
+
if len(episodes) <= current_episode + 1:
|
|
106
|
+
error("no episodes after this")
|
|
107
|
+
else:
|
|
108
|
+
next_episode = episodes[current_episode + 1]
|
|
109
|
+
self._start_episode(next_episode)
|
|
110
|
+
|
|
111
|
+
self.print_options()
|
|
112
|
+
|
|
113
|
+
def prev_ep(self):
|
|
114
|
+
episodes = self.anime.get_episodes(self.lang)
|
|
115
|
+
current_episode = episodes.index(self.stream.episode)
|
|
116
|
+
if current_episode - 1 < 0:
|
|
117
|
+
error("no episodes before this")
|
|
118
|
+
else:
|
|
119
|
+
prev_episode = episodes[current_episode - 1]
|
|
120
|
+
self._start_episode(prev_episode)
|
|
121
|
+
|
|
122
|
+
self.print_options()
|
|
123
|
+
|
|
124
|
+
def repl_ep(self):
|
|
125
|
+
self._start_episode(self.stream.episode)
|
|
126
|
+
|
|
127
|
+
def change_type(self):
|
|
128
|
+
to_change = (
|
|
129
|
+
LanguageTypeEnum.DUB
|
|
130
|
+
if self.lang == LanguageTypeEnum.SUB
|
|
131
|
+
else LanguageTypeEnum.SUB
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if to_change not in self.anime.languages:
|
|
135
|
+
error(f"this anime does not have a {to_change} version")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if self.stream.episode not in self.anime.get_episodes(to_change):
|
|
139
|
+
error(
|
|
140
|
+
f"the current episode ({self.stream.episode}) is not available in {to_change}, not switching..."
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
self.lang = to_change
|
|
145
|
+
self.repl_ep()
|
|
146
|
+
self.print_options()
|
|
147
|
+
|
|
148
|
+
def selec_ep(self):
|
|
149
|
+
episode = pick_episode_prompt(self.anime, self.lang)
|
|
150
|
+
if episode is None:
|
|
151
|
+
return
|
|
152
|
+
self._start_episode(episode)
|
|
153
|
+
self.print_options()
|
|
154
|
+
|
|
155
|
+
def selec_hist(self):
|
|
156
|
+
from anipy_cli.clis.history_cli import HistoryCli
|
|
157
|
+
|
|
158
|
+
hist_cli = HistoryCli(self.options)
|
|
159
|
+
hist_cli.run()
|
|
160
|
+
|
|
161
|
+
def search(self):
|
|
162
|
+
search_result = search_show_prompt("default")
|
|
163
|
+
if search_result is None:
|
|
164
|
+
return
|
|
165
|
+
self.anime = search_result
|
|
166
|
+
episode = pick_episode_prompt(self.anime, self.lang)
|
|
167
|
+
if episode is None:
|
|
168
|
+
return
|
|
169
|
+
self._start_episode(episode)
|
|
170
|
+
self.print_options()
|
|
171
|
+
|
|
172
|
+
def video_info(self):
|
|
173
|
+
print(f"Show Name: {self.anime.name}")
|
|
174
|
+
print(f"Provider: {self.anime.provider.NAME}")
|
|
175
|
+
print(f"Stream Url: {self.stream.url}")
|
|
176
|
+
print(f"Quality: {self.stream.resolution}p")
|
|
177
|
+
if self.stream.referrer:
|
|
178
|
+
print(f"Referer: {self.stream.referrer}")
|
|
179
|
+
|
|
180
|
+
if self.stream.subtitle:
|
|
181
|
+
print("Subtitles:")
|
|
182
|
+
for name, sub in self.stream.subtitle.items():
|
|
183
|
+
print(f" {name} - {sub.url}")
|
|
184
|
+
|
|
185
|
+
def add_seasonal(self):
|
|
186
|
+
self.seasonal_list.update(
|
|
187
|
+
self.anime, episode=self.stream.episode, language=self.stream.language
|
|
188
|
+
)
|
|
189
|
+
cprint(colors.GREEN, "Anime added to seasonals!")
|
|
190
|
+
|
|
191
|
+
def change_quality(self):
|
|
192
|
+
with DotSpinner(
|
|
193
|
+
"Extracting streams for ",
|
|
194
|
+
colors.BLUE,
|
|
195
|
+
f"{self.anime.name} ({self.lang})",
|
|
196
|
+
" Episode ",
|
|
197
|
+
self.stream.episode,
|
|
198
|
+
"...",
|
|
199
|
+
):
|
|
200
|
+
streams = self.anime.get_videos(self.stream.episode, self.lang)
|
|
201
|
+
streams.reverse()
|
|
202
|
+
|
|
203
|
+
stream = inquirer.select( # type: ignore
|
|
204
|
+
message="Select Stream:",
|
|
205
|
+
choices=[
|
|
206
|
+
Choice(value=s, name=f"{s.resolution}p - {s.url}") for s in streams
|
|
207
|
+
],
|
|
208
|
+
long_instruction="To skip this prompt press ctrl+z",
|
|
209
|
+
mandatory=False,
|
|
210
|
+
).execute()
|
|
211
|
+
|
|
212
|
+
if stream is None:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
stream = next(filter(lambda x: x.url == stream["url"], streams))
|
|
216
|
+
self.options.quality = stream.resolution
|
|
217
|
+
self.stream = stream
|
|
218
|
+
self.player.play_title(self.anime, self.stream)
|
|
219
|
+
self.print_options()
|
|
220
|
+
|
|
221
|
+
def download_video(self):
|
|
222
|
+
config = Config()
|
|
223
|
+
with DotSpinner("Starting Download...") as s:
|
|
224
|
+
|
|
225
|
+
def progress_indicator(percentage: float):
|
|
226
|
+
s.set_text(f"Downloading ({percentage:.1f}%)")
|
|
227
|
+
|
|
228
|
+
def info_display(message: str, exc_info: BaseException | None = None):
|
|
229
|
+
logger.info(message, exc_info, exc_info is not None)
|
|
230
|
+
s.write(f"> {message}")
|
|
231
|
+
|
|
232
|
+
def error_display(message: str, exc_info: BaseException | None = None):
|
|
233
|
+
logger.error(message, exc_info)
|
|
234
|
+
s.write(f"{colors.RED}! {message}{colors.END}")
|
|
235
|
+
|
|
236
|
+
downloader = Downloader(progress_indicator, info_display, error_display)
|
|
237
|
+
|
|
238
|
+
s.set_text(
|
|
239
|
+
"Extracting streams for ",
|
|
240
|
+
colors.BLUE,
|
|
241
|
+
f"{self.anime.name} ({self.lang})",
|
|
242
|
+
colors.END,
|
|
243
|
+
" Episode ",
|
|
244
|
+
self.stream.episode,
|
|
245
|
+
"...",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
s.set_text("Downloading...")
|
|
249
|
+
|
|
250
|
+
path = downloader.download(
|
|
251
|
+
self.stream,
|
|
252
|
+
get_download_path(self.anime, self.stream),
|
|
253
|
+
container=config.remux_to,
|
|
254
|
+
ffmpeg=self.options.ffmpeg or config.ffmpeg_hls,
|
|
255
|
+
post_dl_cb=get_post_download_scripts_hook("default", self.anime, s),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if Config().auto_open_dl_defaultcli:
|
|
259
|
+
self.player.play_file(str(path))
|
|
260
|
+
|
|
261
|
+
self.print_options()
|
|
262
|
+
|
|
263
|
+
def quit(self):
|
|
264
|
+
self.player.kill_player()
|
|
265
|
+
sys.exit(0)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import TYPE_CHECKING, List, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from anipy_cli.download_component import DownloadComponent
|
|
6
|
+
|
|
7
|
+
from anipy_api.anime import Anime
|
|
8
|
+
from anipy_api.provider import LanguageTypeEnum
|
|
9
|
+
from anipy_api.provider.base import Episode
|
|
10
|
+
from anipy_api.locallist import LocalList, LocalListEntry
|
|
11
|
+
from anipy_api.error import ProviderNotAvailableError
|
|
12
|
+
from InquirerPy import inquirer
|
|
13
|
+
from InquirerPy.base.control import Choice
|
|
14
|
+
from InquirerPy.utils import get_style
|
|
15
|
+
|
|
16
|
+
from anipy_cli.colors import colors
|
|
17
|
+
from anipy_cli.config import Config
|
|
18
|
+
from anipy_cli.menus.base_menu import MenuBase, MenuOption
|
|
19
|
+
from anipy_cli.util import (
|
|
20
|
+
DotSpinner,
|
|
21
|
+
error,
|
|
22
|
+
get_configured_player,
|
|
23
|
+
migrate_locallist,
|
|
24
|
+
)
|
|
25
|
+
from anipy_cli.prompts import (
|
|
26
|
+
pick_episode_prompt,
|
|
27
|
+
search_show_prompt,
|
|
28
|
+
lang_prompt,
|
|
29
|
+
migrate_provider,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from anipy_cli.arg_parser import CliArgs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SeasonalMenu(MenuBase):
|
|
38
|
+
def __init__(self, options: "CliArgs"):
|
|
39
|
+
self.options = options
|
|
40
|
+
self.player = get_configured_player(self.options.optional_player)
|
|
41
|
+
|
|
42
|
+
config = Config()
|
|
43
|
+
self.dl_path = config.seasonals_dl_path
|
|
44
|
+
self.seasonal_list = LocalList(config._seasonal_file_path, migrate_locallist)
|
|
45
|
+
if options.location:
|
|
46
|
+
self.dl_path = options.location
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def menu_options(self) -> List[MenuOption]:
|
|
50
|
+
return [
|
|
51
|
+
MenuOption("Add Anime", self.add_anime, "a"),
|
|
52
|
+
MenuOption("Delete one anime from seasonals", self.del_anime, "e"),
|
|
53
|
+
MenuOption("List anime in seasonals", self.list_animes, "l"),
|
|
54
|
+
MenuOption("Migrate to current provider", self.migrate_provider, "m"),
|
|
55
|
+
MenuOption("Change dub/sub of anime in seasonals", self.change_lang, "c"),
|
|
56
|
+
MenuOption("Download newest episodes", self.download_latest, "d"),
|
|
57
|
+
MenuOption("Binge watch newest episodes", self.binge_latest, "w"),
|
|
58
|
+
MenuOption("Quit", self.quit, "q"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def print_header(self):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def _choose_latest(self) -> List[Tuple["Anime", LanguageTypeEnum, List["Episode"]]]:
|
|
65
|
+
with DotSpinner("Fetching status of shows in seasonals..."):
|
|
66
|
+
choices = []
|
|
67
|
+
for s in self.seasonal_list.get_all():
|
|
68
|
+
try:
|
|
69
|
+
anime = Anime.from_local_list_entry(s)
|
|
70
|
+
except ProviderNotAvailableError:
|
|
71
|
+
error(
|
|
72
|
+
f"Can not load '{s.name}' because the configured provider"
|
|
73
|
+
f" '{s.provider}' was not found, maybe try to migrate"
|
|
74
|
+
" providers with 'm'."
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
lang = s.language
|
|
79
|
+
episodes = anime.get_episodes(lang)
|
|
80
|
+
|
|
81
|
+
if s.episode == -1:
|
|
82
|
+
to_watch = episodes
|
|
83
|
+
else:
|
|
84
|
+
to_watch = episodes[episodes.index(s.episode) + 1 :]
|
|
85
|
+
|
|
86
|
+
if len(to_watch) > 0:
|
|
87
|
+
ch = Choice(
|
|
88
|
+
value=(anime, lang, to_watch),
|
|
89
|
+
name=f"{anime.name} (to watch: {len(to_watch)})",
|
|
90
|
+
)
|
|
91
|
+
choices.append(ch)
|
|
92
|
+
|
|
93
|
+
if self.options.auto_update:
|
|
94
|
+
return [ch.value for ch in choices]
|
|
95
|
+
|
|
96
|
+
if not choices:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
choices = inquirer.fuzzy( # type: ignore
|
|
100
|
+
message="Select Seasonals to catch up to:",
|
|
101
|
+
choices=choices,
|
|
102
|
+
multiselect=True,
|
|
103
|
+
long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
|
|
104
|
+
mandatory=False,
|
|
105
|
+
keybindings={"toggle": [{"key": "c-space"}]},
|
|
106
|
+
style=get_style(
|
|
107
|
+
{"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
|
|
108
|
+
),
|
|
109
|
+
).execute()
|
|
110
|
+
|
|
111
|
+
return choices or []
|
|
112
|
+
|
|
113
|
+
def add_anime(self):
|
|
114
|
+
anime = search_show_prompt("seasonal")
|
|
115
|
+
|
|
116
|
+
if anime is None:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
lang = lang_prompt(anime)
|
|
120
|
+
|
|
121
|
+
episode = pick_episode_prompt(
|
|
122
|
+
anime, lang, instruction="To start from the beginning skip this Prompt"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if episode is None:
|
|
126
|
+
episode = -1
|
|
127
|
+
|
|
128
|
+
self.seasonal_list.update(anime, episode=episode, language=lang)
|
|
129
|
+
|
|
130
|
+
self.print_options()
|
|
131
|
+
|
|
132
|
+
def del_anime(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
|
+
for e in entries:
|
|
155
|
+
self.seasonal_list.delete(e)
|
|
156
|
+
|
|
157
|
+
self.print_options()
|
|
158
|
+
|
|
159
|
+
def change_lang(self):
|
|
160
|
+
seasonals = self.seasonal_list.get_all()
|
|
161
|
+
|
|
162
|
+
if len(seasonals) == 0:
|
|
163
|
+
error("No seasonals configured.")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
entries: List[LocalListEntry] = (
|
|
167
|
+
inquirer.fuzzy( # type: ignore
|
|
168
|
+
message="Select Seasonals to delete:",
|
|
169
|
+
choices=seasonals,
|
|
170
|
+
multiselect=True,
|
|
171
|
+
long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
|
|
172
|
+
mandatory=False,
|
|
173
|
+
keybindings={"toggle": [{"key": "c-space"}]},
|
|
174
|
+
style=get_style(
|
|
175
|
+
{"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
|
|
176
|
+
),
|
|
177
|
+
).execute()
|
|
178
|
+
or []
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not entries:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
action: str = inquirer.select( # type: ignore
|
|
185
|
+
message="Switch to:",
|
|
186
|
+
choices=["Sub", "Dub"],
|
|
187
|
+
long_instruction="To skip this prompt press ctrl+z",
|
|
188
|
+
mandatory=False,
|
|
189
|
+
).execute()
|
|
190
|
+
|
|
191
|
+
if not action:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
for e in entries:
|
|
195
|
+
if e.language == LanguageTypeEnum.DUB:
|
|
196
|
+
new_lang = LanguageTypeEnum.SUB
|
|
197
|
+
else:
|
|
198
|
+
new_lang = LanguageTypeEnum.DUB
|
|
199
|
+
if new_lang in e.languages:
|
|
200
|
+
self.seasonal_list.update(e, language=new_lang)
|
|
201
|
+
else:
|
|
202
|
+
print(f"> {new_lang} is for {e.name} not available")
|
|
203
|
+
|
|
204
|
+
def list_animes(self):
|
|
205
|
+
all_seasonals = self.seasonal_list.get_all()
|
|
206
|
+
if not all_seasonals:
|
|
207
|
+
error("No seasonals configured.")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
for i in all_seasonals:
|
|
211
|
+
print(i)
|
|
212
|
+
|
|
213
|
+
def migrate_provider(self):
|
|
214
|
+
migrate_provider("seasonal", self.seasonal_list)
|
|
215
|
+
self.print_options(should_clear_screen=True)
|
|
216
|
+
|
|
217
|
+
def download_latest(self):
|
|
218
|
+
picked = self._choose_latest()
|
|
219
|
+
total_eps = sum([len(e) for _a, _d, e in picked])
|
|
220
|
+
if total_eps == 0:
|
|
221
|
+
print("All up to date on downloads.")
|
|
222
|
+
return
|
|
223
|
+
else:
|
|
224
|
+
print(f"Downloading a total of {total_eps} episode(s)")
|
|
225
|
+
|
|
226
|
+
def on_successful_download(anime: Anime, ep: Episode, lang: LanguageTypeEnum):
|
|
227
|
+
self.seasonal_list.update(anime, episode=ep, language=lang)
|
|
228
|
+
|
|
229
|
+
failed_series = DownloadComponent(
|
|
230
|
+
self.options, self.dl_path, "seasonal"
|
|
231
|
+
).download_anime(picked, on_successful_download)
|
|
232
|
+
|
|
233
|
+
if not self.options.auto_update:
|
|
234
|
+
# Clear screen only if there were no issues
|
|
235
|
+
self.print_options(should_clear_screen=len(failed_series) == 0)
|
|
236
|
+
|
|
237
|
+
def binge_latest(self):
|
|
238
|
+
picked = self._choose_latest()
|
|
239
|
+
total_eps = sum([len(e) for _a, _d, e in picked])
|
|
240
|
+
if total_eps == 0:
|
|
241
|
+
print("Up to date on binge.")
|
|
242
|
+
return
|
|
243
|
+
else:
|
|
244
|
+
print(f"Playing a total of {total_eps} episode(s)")
|
|
245
|
+
for anime, lang, eps in picked:
|
|
246
|
+
for ep in eps:
|
|
247
|
+
with DotSpinner(
|
|
248
|
+
"Extracting streams for ",
|
|
249
|
+
colors.BLUE,
|
|
250
|
+
f"{anime.name} ({lang})",
|
|
251
|
+
colors.END,
|
|
252
|
+
" Episode ",
|
|
253
|
+
ep,
|
|
254
|
+
"...",
|
|
255
|
+
) as s:
|
|
256
|
+
stream = anime.get_video(
|
|
257
|
+
ep, lang, preferred_quality=self.options.quality
|
|
258
|
+
)
|
|
259
|
+
if stream is None:
|
|
260
|
+
error("Could not find stream for requested episode, skipping")
|
|
261
|
+
s.ok("✔")
|
|
262
|
+
|
|
263
|
+
self.player.play_title(anime, stream)
|
|
264
|
+
self.player.wait()
|
|
265
|
+
self.seasonal_list.update(anime, episode=ep, language=lang)
|
|
266
|
+
|
|
267
|
+
self.print_options()
|
|
268
|
+
|
|
269
|
+
def quit(self):
|
|
270
|
+
sys.exit(0)
|