anipy-cli 2.7.30__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.
- anipy_cli/__init__.py +2 -20
- anipy_cli/arg_parser.py +30 -20
- anipy_cli/cli.py +66 -0
- anipy_cli/clis/__init__.py +15 -0
- anipy_cli/clis/base_cli.py +32 -0
- anipy_cli/clis/binge_cli.py +83 -0
- anipy_cli/clis/default_cli.py +104 -0
- anipy_cli/clis/download_cli.py +111 -0
- anipy_cli/clis/history_cli.py +93 -0
- anipy_cli/clis/mal_cli.py +71 -0
- anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
- anipy_cli/colors.py +4 -4
- anipy_cli/config.py +308 -87
- anipy_cli/discord.py +34 -0
- anipy_cli/mal_proxy.py +216 -0
- anipy_cli/menus/__init__.py +5 -0
- anipy_cli/{cli/menus → menus}/base_menu.py +8 -12
- anipy_cli/menus/mal_menu.py +660 -0
- anipy_cli/menus/menu.py +194 -0
- anipy_cli/menus/seasonal_menu.py +263 -0
- anipy_cli/prompts.py +231 -0
- anipy_cli/util.py +262 -0
- anipy_cli-3.0.0.dist-info/METADATA +67 -0
- anipy_cli-3.0.0.dist-info/RECORD +26 -0
- {anipy_cli-2.7.30.dist-info → anipy_cli-3.0.0.dist-info}/WHEEL +1 -2
- anipy_cli-3.0.0.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 -108
- anipy_cli/cli/menus/seasonal_menu.py +0 -177
- anipy_cli/cli/util.py +0 -125
- anipy_cli/download.py +0 -467
- anipy_cli/history.py +0 -83
- anipy_cli/mal.py +0 -651
- anipy_cli/misc.py +0 -227
- anipy_cli/player/__init__.py +0 -1
- anipy_cli/player/player.py +0 -36
- anipy_cli/player/players/__init__.py +0 -3
- anipy_cli/player/players/base.py +0 -107
- 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 -100
- anipy_cli/run_anipy_cli.py +0 -14
- anipy_cli/seasonal.py +0 -112
- anipy_cli/url_handler.py +0 -470
- anipy_cli/version.py +0 -1
- anipy_cli-2.7.30.dist-info/LICENSE +0 -674
- anipy_cli-2.7.30.dist-info/METADATA +0 -162
- anipy_cli-2.7.30.dist-info/RECORD +0 -43
- anipy_cli-2.7.30.dist-info/entry_points.txt +0 -2
- anipy_cli-2.7.30.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
3
|
+
from typing import Dict, List, Tuple
|
|
4
|
+
|
|
5
|
+
from anipy_api.anime import Anime
|
|
6
|
+
from anipy_api.download import Downloader
|
|
7
|
+
from anipy_api.mal import MALAnime, MALMyListStatusEnum, MyAnimeList
|
|
8
|
+
from anipy_api.provider import LanguageTypeEnum
|
|
9
|
+
from anipy_api.provider.base import Episode
|
|
10
|
+
|
|
11
|
+
from anipy_api.locallist import LocalList, LocalListEntry
|
|
12
|
+
from InquirerPy import inquirer
|
|
13
|
+
from InquirerPy.base.control import Choice
|
|
14
|
+
from InquirerPy.utils import get_style
|
|
15
|
+
|
|
16
|
+
from anipy_cli.arg_parser import CliArgs
|
|
17
|
+
from anipy_cli.colors import colors, cprint
|
|
18
|
+
from anipy_cli.config import Config
|
|
19
|
+
from anipy_cli.mal_proxy import MyAnimeListProxy
|
|
20
|
+
from anipy_cli.menus.base_menu import MenuBase, MenuOption
|
|
21
|
+
from anipy_cli.util import (
|
|
22
|
+
DotSpinner,
|
|
23
|
+
error,
|
|
24
|
+
find_closest,
|
|
25
|
+
get_configured_player,
|
|
26
|
+
get_download_path,
|
|
27
|
+
migrate_locallist,
|
|
28
|
+
)
|
|
29
|
+
from anipy_cli.prompts import search_show_prompt
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MALMenu(MenuBase):
|
|
33
|
+
def __init__(self, mal: MyAnimeList, options: CliArgs):
|
|
34
|
+
self.mal = mal
|
|
35
|
+
self.mal_proxy = MyAnimeListProxy(self.mal)
|
|
36
|
+
|
|
37
|
+
with DotSpinner("Fetching your MyAnimeList..."):
|
|
38
|
+
self.mal_proxy.get_list()
|
|
39
|
+
|
|
40
|
+
self.options = options
|
|
41
|
+
self.player = get_configured_player(self.options.optional_player)
|
|
42
|
+
self.seasonals_list = LocalList(
|
|
43
|
+
Config()._seasonal_file_path, migrate_cb=migrate_locallist
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.dl_path = Config().seasonals_dl_path
|
|
47
|
+
if options.location:
|
|
48
|
+
self.dl_path = options.location
|
|
49
|
+
|
|
50
|
+
def print_header(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def menu_options(self) -> List[MenuOption]:
|
|
55
|
+
return [
|
|
56
|
+
MenuOption("Add Anime", self.add_anime, "a"),
|
|
57
|
+
MenuOption("Delete one anime from MyAnimeList", self.del_anime, "e"),
|
|
58
|
+
MenuOption("List anime in MyAnimeList", self.list_anime, "l"),
|
|
59
|
+
MenuOption("Tag anime in MyAnimeList (dub/ignore)", self.tag_anime, "t"),
|
|
60
|
+
MenuOption("Map MyAnimeList anime to providers", self.manual_maps, "m"),
|
|
61
|
+
MenuOption("Sync MyAnimeList into seasonals", self.sync_mal_seasonls, "s"),
|
|
62
|
+
MenuOption("Sync seasonals into MyAnimeList", self.sync_seasonals_mal, "b"),
|
|
63
|
+
MenuOption(
|
|
64
|
+
"Download newest episodes", lambda: self.download(all=False), "d"
|
|
65
|
+
),
|
|
66
|
+
MenuOption("Download all episodes", lambda: self.download(all=True), "x"),
|
|
67
|
+
MenuOption("Binge watch newest episodes", self.binge_latest, "w"),
|
|
68
|
+
MenuOption("Quit", sys.exit, "q"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
def add_anime(self):
|
|
72
|
+
self.print_options()
|
|
73
|
+
|
|
74
|
+
query = inquirer.text( # type: ignore
|
|
75
|
+
"Search Anime:",
|
|
76
|
+
long_instruction="To cancel this prompt press ctrl+z",
|
|
77
|
+
mandatory=False,
|
|
78
|
+
).execute()
|
|
79
|
+
|
|
80
|
+
if query is None:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
with DotSpinner("Searching for ", colors.BLUE, query, "..."):
|
|
84
|
+
results = self.mal.get_search(query)
|
|
85
|
+
|
|
86
|
+
anime = inquirer.fuzzy( # type: ignore
|
|
87
|
+
message="Select Show:",
|
|
88
|
+
choices=[Choice(value=r, name=self._format_mal_anime(r)) for r in results],
|
|
89
|
+
transformer=lambda x: [e.split("|")[-1].strip() for e in x],
|
|
90
|
+
long_instruction="To skip this prompt press crtl+z",
|
|
91
|
+
mandatory=False,
|
|
92
|
+
).execute()
|
|
93
|
+
|
|
94
|
+
if anime is None:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
anime = MALAnime.from_dict(anime)
|
|
98
|
+
with DotSpinner("Adding ", colors.BLUE, anime.title, " to your MAL...") as s:
|
|
99
|
+
self.mal_proxy.update_show(anime, MALMyListStatusEnum.WATCHING)
|
|
100
|
+
s.ok("✔")
|
|
101
|
+
|
|
102
|
+
def del_anime(self):
|
|
103
|
+
self.print_options()
|
|
104
|
+
with DotSpinner("Fetching your MAL..."):
|
|
105
|
+
mylist = self.mal_proxy.get_list()
|
|
106
|
+
|
|
107
|
+
entries = (
|
|
108
|
+
inquirer.fuzzy( # type: ignore
|
|
109
|
+
message="Select Seasonals to delete:",
|
|
110
|
+
choices=[
|
|
111
|
+
Choice(value=e, name=self._format_mal_anime(e)) for e in mylist
|
|
112
|
+
],
|
|
113
|
+
multiselect=True,
|
|
114
|
+
transformer=lambda x: [e.split("|")[-1].strip() for e in x],
|
|
115
|
+
long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
|
|
116
|
+
mandatory=False,
|
|
117
|
+
keybindings={"toggle": [{"key": "c-space"}]},
|
|
118
|
+
style=get_style(
|
|
119
|
+
{"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
|
|
120
|
+
),
|
|
121
|
+
).execute()
|
|
122
|
+
or []
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
with DotSpinner("Deleting anime from your MAL...") as s:
|
|
126
|
+
for e in entries:
|
|
127
|
+
self.mal_proxy.delete_show(MALAnime.from_dict(e))
|
|
128
|
+
s.ok("✔")
|
|
129
|
+
|
|
130
|
+
def list_anime(self):
|
|
131
|
+
with DotSpinner("Fetching your MAL..."):
|
|
132
|
+
mylist = [
|
|
133
|
+
self._format_mal_anime(e)
|
|
134
|
+
for e in self.mal_proxy.get_list(
|
|
135
|
+
status_catagories=set(
|
|
136
|
+
[
|
|
137
|
+
MALMyListStatusEnum.WATCHING,
|
|
138
|
+
MALMyListStatusEnum.COMPLETED,
|
|
139
|
+
MALMyListStatusEnum.ON_HOLD,
|
|
140
|
+
MALMyListStatusEnum.PLAN_TO_WATCH,
|
|
141
|
+
MALMyListStatusEnum.DROPPED,
|
|
142
|
+
]
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
if e.my_list_status
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if not mylist:
|
|
149
|
+
error("your list is empty")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
inquirer.fuzzy( # type: ignore
|
|
153
|
+
message="View your List",
|
|
154
|
+
choices=mylist,
|
|
155
|
+
mandatory=False,
|
|
156
|
+
transformer=lambda x: [e.split("|")[-1].strip() for e in x],
|
|
157
|
+
long_instruction="To skip this prompt press ctrl+z",
|
|
158
|
+
).execute()
|
|
159
|
+
|
|
160
|
+
self.print_options()
|
|
161
|
+
|
|
162
|
+
def tag_anime(self):
|
|
163
|
+
with DotSpinner("Fetching your MAL..."):
|
|
164
|
+
mylist = [
|
|
165
|
+
Choice(value=e, name=self._format_mal_anime(e))
|
|
166
|
+
for e in self.mal_proxy.get_list()
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
entries = (
|
|
170
|
+
inquirer.fuzzy( # type: ignore
|
|
171
|
+
message="Select Anime to change tags of:",
|
|
172
|
+
choices=mylist,
|
|
173
|
+
multiselect=True,
|
|
174
|
+
long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
|
|
175
|
+
transformer=lambda x: [e.split("|")[-1].strip() for e in x],
|
|
176
|
+
mandatory=False,
|
|
177
|
+
keybindings={"toggle": [{"key": "c-space"}]},
|
|
178
|
+
style=get_style(
|
|
179
|
+
{"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
|
|
180
|
+
),
|
|
181
|
+
).execute()
|
|
182
|
+
or []
|
|
183
|
+
)
|
|
184
|
+
entries = [MALAnime.from_dict(e) for e in entries]
|
|
185
|
+
|
|
186
|
+
if not entries:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
config = Config()
|
|
190
|
+
|
|
191
|
+
choices = []
|
|
192
|
+
if config.mal_dub_tag:
|
|
193
|
+
choices.append(
|
|
194
|
+
Choice(
|
|
195
|
+
value=config.mal_dub_tag,
|
|
196
|
+
name=f"{config.mal_dub_tag} (sets wheter you prefer to watch a particular anime in dub)",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if config.mal_ignore_tag:
|
|
201
|
+
choices.append(
|
|
202
|
+
Choice(
|
|
203
|
+
value=config.mal_ignore_tag,
|
|
204
|
+
name=f"{config.mal_ignore_tag} (sets wheter anipy-cli will ignore a particular anime)",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if not choices:
|
|
209
|
+
error("no tags to configure, check your config")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
tags: List[str] = inquirer.select( # type: ignore
|
|
213
|
+
message="Select tags to add/remove:",
|
|
214
|
+
choices=choices,
|
|
215
|
+
multiselect=True,
|
|
216
|
+
long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
|
|
217
|
+
mandatory=False,
|
|
218
|
+
keybindings={"toggle": [{"key": "c-space"}]},
|
|
219
|
+
style=get_style(
|
|
220
|
+
{"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
|
|
221
|
+
),
|
|
222
|
+
).execute()
|
|
223
|
+
|
|
224
|
+
if not tags:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
action: str = inquirer.select( # type: ignore
|
|
228
|
+
message="Choose which Action to apply:",
|
|
229
|
+
choices=["Add", "Remove"],
|
|
230
|
+
long_instruction="To skip this prompt press ctrl+z",
|
|
231
|
+
mandatory=False,
|
|
232
|
+
).execute()
|
|
233
|
+
|
|
234
|
+
if not action:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
for e in entries:
|
|
238
|
+
if action == "Add":
|
|
239
|
+
self.mal_proxy.update_show(e, tags=set(tags))
|
|
240
|
+
else:
|
|
241
|
+
current_tags = e.my_list_status.tags if e.my_list_status else []
|
|
242
|
+
for t in tags:
|
|
243
|
+
try:
|
|
244
|
+
current_tags.remove(t)
|
|
245
|
+
except ValueError:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
self.mal_proxy.update_show(e, tags=set(current_tags))
|
|
249
|
+
|
|
250
|
+
def download(self, all: bool = False):
|
|
251
|
+
picked = self._choose_latest(all=all)
|
|
252
|
+
config = Config()
|
|
253
|
+
total_eps = sum([len(e) for a, m, lang, e in picked])
|
|
254
|
+
if total_eps == 0:
|
|
255
|
+
print("Nothing to download, returning...")
|
|
256
|
+
return
|
|
257
|
+
else:
|
|
258
|
+
print(f"Downloading a total of {total_eps} episode(s)")
|
|
259
|
+
|
|
260
|
+
with DotSpinner("Starting Download...") as s:
|
|
261
|
+
|
|
262
|
+
def progress_indicator(percentage: float):
|
|
263
|
+
s.set_text(f"Progress: {percentage:.1f}%")
|
|
264
|
+
|
|
265
|
+
def info_display(message: str):
|
|
266
|
+
s.write(f"> {message}")
|
|
267
|
+
|
|
268
|
+
downloader = Downloader(progress_indicator, info_display)
|
|
269
|
+
|
|
270
|
+
for anime, mal_anime, lang, eps in picked:
|
|
271
|
+
for ep in eps:
|
|
272
|
+
s.set_text(
|
|
273
|
+
"Extracting streams for ",
|
|
274
|
+
colors.BLUE,
|
|
275
|
+
f"{anime.name} ({lang})",
|
|
276
|
+
colors.END,
|
|
277
|
+
" Episode ",
|
|
278
|
+
ep,
|
|
279
|
+
"...",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
stream = anime.get_video(
|
|
283
|
+
ep, lang, preferred_quality=self.options.quality
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
info_display(
|
|
287
|
+
f"Downloading Episode {stream.episode} of {anime.name} ({lang})"
|
|
288
|
+
)
|
|
289
|
+
s.set_text("Downloading...")
|
|
290
|
+
|
|
291
|
+
downloader.download(
|
|
292
|
+
stream,
|
|
293
|
+
get_download_path(anime, stream, parent_directory=self.dl_path),
|
|
294
|
+
container=config.remux_to,
|
|
295
|
+
ffmpeg=self.options.ffmpeg or config.ffmpeg_hls,
|
|
296
|
+
)
|
|
297
|
+
if not all:
|
|
298
|
+
self.mal_proxy.update_show(
|
|
299
|
+
mal_anime,
|
|
300
|
+
status=MALMyListStatusEnum.WATCHING,
|
|
301
|
+
episode=int(ep),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def binge_latest(self):
|
|
305
|
+
picked = self._choose_latest()
|
|
306
|
+
total_eps = sum([len(e) for a, m, lang, e in picked])
|
|
307
|
+
if total_eps == 0:
|
|
308
|
+
print("Nothing to watch, returning...")
|
|
309
|
+
return
|
|
310
|
+
else:
|
|
311
|
+
print(f"Playing a total of {total_eps} episode(s)")
|
|
312
|
+
|
|
313
|
+
for anime, mal_anime, lang, eps in picked:
|
|
314
|
+
for ep in eps:
|
|
315
|
+
with DotSpinner(
|
|
316
|
+
"Extracting streams for ",
|
|
317
|
+
colors.BLUE,
|
|
318
|
+
f"{anime.name} ({lang})",
|
|
319
|
+
colors.END,
|
|
320
|
+
" Episode ",
|
|
321
|
+
ep,
|
|
322
|
+
"...",
|
|
323
|
+
) as s:
|
|
324
|
+
stream = anime.get_video(
|
|
325
|
+
ep, lang, preferred_quality=self.options.quality
|
|
326
|
+
)
|
|
327
|
+
s.ok("✔")
|
|
328
|
+
|
|
329
|
+
self.player.play_title(anime, stream)
|
|
330
|
+
self.player.wait()
|
|
331
|
+
|
|
332
|
+
self.mal_proxy.update_show(
|
|
333
|
+
mal_anime, status=MALMyListStatusEnum.WATCHING, episode=int(ep)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def manual_maps(self):
|
|
337
|
+
mylist = self.mal_proxy.get_list()
|
|
338
|
+
self._create_maps_mal(mylist)
|
|
339
|
+
|
|
340
|
+
def sync_seasonals_mal(self):
|
|
341
|
+
config = Config()
|
|
342
|
+
seasonals = self.seasonals_list.get_all()
|
|
343
|
+
mappings = self._create_maps_provider(seasonals)
|
|
344
|
+
with DotSpinner("Syncing Seasonals into MyAnimeList") as s:
|
|
345
|
+
for k, v in mappings.items():
|
|
346
|
+
tags = set()
|
|
347
|
+
if config.mal_dub_tag:
|
|
348
|
+
if k.language == LanguageTypeEnum.DUB:
|
|
349
|
+
tags.add(config.mal_dub_tag)
|
|
350
|
+
|
|
351
|
+
if v.my_list_status:
|
|
352
|
+
if config.mal_ignore_tag in v.my_list_status.tags:
|
|
353
|
+
continue
|
|
354
|
+
tags |= set(v.my_list_status.tags)
|
|
355
|
+
|
|
356
|
+
self.mal_proxy.update_show(
|
|
357
|
+
v,
|
|
358
|
+
status=MALMyListStatusEnum.WATCHING,
|
|
359
|
+
episode=int(k.episode),
|
|
360
|
+
tags=tags,
|
|
361
|
+
)
|
|
362
|
+
s.ok("✔")
|
|
363
|
+
|
|
364
|
+
def sync_mal_seasonls(self):
|
|
365
|
+
config = Config()
|
|
366
|
+
mylist = self.mal_proxy.get_list()
|
|
367
|
+
mappings = self._create_maps_mal(mylist)
|
|
368
|
+
with DotSpinner("Syncing MyAnimeList into Seasonals") as s:
|
|
369
|
+
for k, v in mappings.items():
|
|
370
|
+
if config.mal_dub_tag:
|
|
371
|
+
if k.my_list_status and config.mal_dub_tag in k.my_list_status.tags:
|
|
372
|
+
pref_lang = LanguageTypeEnum.DUB
|
|
373
|
+
else:
|
|
374
|
+
pref_lang = LanguageTypeEnum.SUB
|
|
375
|
+
else:
|
|
376
|
+
pref_lang = (
|
|
377
|
+
LanguageTypeEnum.DUB
|
|
378
|
+
if config.preferred_type == "dub"
|
|
379
|
+
else LanguageTypeEnum.SUB
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if pref_lang in v.languages:
|
|
383
|
+
lang = pref_lang
|
|
384
|
+
else:
|
|
385
|
+
lang = next(iter(v.languages))
|
|
386
|
+
|
|
387
|
+
provider_episodes = v.get_episodes(lang)
|
|
388
|
+
episode = (
|
|
389
|
+
k.my_list_status.num_episodes_watched if k.my_list_status else 0
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if episode == 0:
|
|
393
|
+
episode = provider_episodes[0]
|
|
394
|
+
else:
|
|
395
|
+
episode = find_closest(provider_episodes, episode)
|
|
396
|
+
|
|
397
|
+
self.seasonals_list.update(v, episode=episode, language=lang)
|
|
398
|
+
s.ok("✔")
|
|
399
|
+
|
|
400
|
+
def _choose_latest(
|
|
401
|
+
self, all: bool = False
|
|
402
|
+
) -> List[Tuple[Anime, MALAnime, LanguageTypeEnum, List[Episode]]]:
|
|
403
|
+
cprint(
|
|
404
|
+
colors.GREEN,
|
|
405
|
+
"Hint: ",
|
|
406
|
+
colors.END,
|
|
407
|
+
"you can fine-tune mapping behaviour in your settings!",
|
|
408
|
+
)
|
|
409
|
+
with DotSpinner("Fetching your MAL..."):
|
|
410
|
+
mylist = self.mal_proxy.get_list()
|
|
411
|
+
|
|
412
|
+
for i, e in enumerate(mylist):
|
|
413
|
+
if e.my_list_status is None:
|
|
414
|
+
mylist.pop(i)
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
if e.my_list_status.num_episodes_watched == e.num_episodes:
|
|
418
|
+
mylist.pop(i)
|
|
419
|
+
|
|
420
|
+
if not (all or self.options.auto_update):
|
|
421
|
+
choices = inquirer.fuzzy( # type: ignore
|
|
422
|
+
message="Select shows to catch up to:",
|
|
423
|
+
choices=[
|
|
424
|
+
Choice(value=e, name=self._format_mal_anime(e)) for e in mylist
|
|
425
|
+
],
|
|
426
|
+
multiselect=True,
|
|
427
|
+
transformer=lambda x: [e.split("|")[-1].strip() for e in x],
|
|
428
|
+
long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
|
|
429
|
+
mandatory=False,
|
|
430
|
+
keybindings={"toggle": [{"key": "c-space"}]},
|
|
431
|
+
style=get_style(
|
|
432
|
+
{"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
|
|
433
|
+
),
|
|
434
|
+
).execute()
|
|
435
|
+
|
|
436
|
+
if choices is None:
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
mylist = [MALAnime.from_dict(c) for c in choices]
|
|
440
|
+
|
|
441
|
+
config = Config()
|
|
442
|
+
to_watch: List[Tuple[Anime, MALAnime, LanguageTypeEnum, List[Episode]]] = []
|
|
443
|
+
|
|
444
|
+
with DotSpinner("Fetching episodes...") as s:
|
|
445
|
+
for e in mylist:
|
|
446
|
+
s.write(f"> Checking out episodes of {e.title}")
|
|
447
|
+
|
|
448
|
+
episodes_to_watch = list(
|
|
449
|
+
range(e.my_list_status.num_episodes_watched + 1, e.num_episodes + 1) # type: ignore
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
result = self.mal_proxy.map_from_mal(e)
|
|
453
|
+
|
|
454
|
+
if result is None:
|
|
455
|
+
s.write(
|
|
456
|
+
f"> No mapping found for {e.title} please use the `m` option to map it"
|
|
457
|
+
)
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
if config.mal_dub_tag:
|
|
461
|
+
if e.my_list_status and config.mal_dub_tag in e.my_list_status.tags:
|
|
462
|
+
pref_lang = LanguageTypeEnum.DUB
|
|
463
|
+
else:
|
|
464
|
+
pref_lang = LanguageTypeEnum.SUB
|
|
465
|
+
else:
|
|
466
|
+
pref_lang = (
|
|
467
|
+
LanguageTypeEnum.DUB
|
|
468
|
+
if config.preferred_type == "dub"
|
|
469
|
+
else LanguageTypeEnum.SUB
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if pref_lang in result.languages:
|
|
473
|
+
lang = pref_lang
|
|
474
|
+
s.write(
|
|
475
|
+
f"> Looking for {lang} episodes because of config/tag preference"
|
|
476
|
+
)
|
|
477
|
+
else:
|
|
478
|
+
lang = next(iter(result.languages))
|
|
479
|
+
s.write(
|
|
480
|
+
f"> Looking for {lang} episodes because your preferred type is not available"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
episodes = result.get_episodes(lang)
|
|
484
|
+
|
|
485
|
+
will_watch = []
|
|
486
|
+
if all:
|
|
487
|
+
will_watch.extend(episodes)
|
|
488
|
+
else:
|
|
489
|
+
for ep in episodes_to_watch:
|
|
490
|
+
try:
|
|
491
|
+
idx = episodes.index(ep)
|
|
492
|
+
will_watch.append(episodes[idx])
|
|
493
|
+
except ValueError:
|
|
494
|
+
s.write(
|
|
495
|
+
f"> Episode {ep} not found in provider, skipping..."
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
to_watch.append((result, e, lang, will_watch))
|
|
499
|
+
|
|
500
|
+
s.ok("✔")
|
|
501
|
+
|
|
502
|
+
return to_watch
|
|
503
|
+
|
|
504
|
+
def _create_maps_mal(self, to_map: List[MALAnime]) -> Dict[MALAnime, Anime]:
|
|
505
|
+
cprint(
|
|
506
|
+
colors.GREEN,
|
|
507
|
+
"Hint: ",
|
|
508
|
+
colors.END,
|
|
509
|
+
"you can fine-tune mapping behaviour in your settings!",
|
|
510
|
+
)
|
|
511
|
+
with DotSpinner("Starting Automapping...") as s:
|
|
512
|
+
failed: List[MALAnime] = []
|
|
513
|
+
mappings: Dict[MALAnime, Anime] = {}
|
|
514
|
+
counter = 0
|
|
515
|
+
|
|
516
|
+
def do_map(anime: MALAnime, to_map_length: int):
|
|
517
|
+
nonlocal failed, counter
|
|
518
|
+
try:
|
|
519
|
+
result = self.mal_proxy.map_from_mal(anime)
|
|
520
|
+
if result is None:
|
|
521
|
+
failed.append(anime)
|
|
522
|
+
s.write(f"> Failed to map {anime.id} ({anime.title})")
|
|
523
|
+
else:
|
|
524
|
+
mappings.update({anime: result})
|
|
525
|
+
s.write(
|
|
526
|
+
f"> Successfully mapped {anime.id} to {result.identifier}"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
counter += 1
|
|
530
|
+
s.set_text(f"Progress: {counter / to_map_length * 100:.1f}%")
|
|
531
|
+
except:
|
|
532
|
+
failed.append(anime)
|
|
533
|
+
|
|
534
|
+
with ThreadPoolExecutor(max_workers=5) as pool:
|
|
535
|
+
futures = [pool.submit(do_map, a, len(to_map)) for a in to_map]
|
|
536
|
+
try:
|
|
537
|
+
for future in as_completed(futures):
|
|
538
|
+
future.result()
|
|
539
|
+
except KeyboardInterrupt:
|
|
540
|
+
pool.shutdown(wait=False, cancel_futures=True)
|
|
541
|
+
raise
|
|
542
|
+
|
|
543
|
+
if not failed or self.options.auto_update:
|
|
544
|
+
self.print_options()
|
|
545
|
+
print("Everything is mapped")
|
|
546
|
+
return mappings
|
|
547
|
+
|
|
548
|
+
for f in failed:
|
|
549
|
+
cprint("Manually mapping ", colors.BLUE, f.title)
|
|
550
|
+
anime = search_show_prompt("mal")
|
|
551
|
+
if not anime:
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
map = self.mal_proxy.map_from_mal(f, anime)
|
|
555
|
+
if map is not None:
|
|
556
|
+
mappings.update({f: map})
|
|
557
|
+
|
|
558
|
+
self.print_options()
|
|
559
|
+
return mappings
|
|
560
|
+
|
|
561
|
+
def _create_maps_provider(
|
|
562
|
+
self, to_map: List[LocalListEntry]
|
|
563
|
+
) -> Dict[LocalListEntry, MALAnime]:
|
|
564
|
+
with DotSpinner("Starting Automapping...") as s:
|
|
565
|
+
failed: List[LocalListEntry] = []
|
|
566
|
+
mappings: Dict[LocalListEntry, MALAnime] = {}
|
|
567
|
+
counter = 0
|
|
568
|
+
|
|
569
|
+
def do_map(entry: LocalListEntry, to_map_length: int):
|
|
570
|
+
nonlocal failed, counter
|
|
571
|
+
try:
|
|
572
|
+
anime = Anime.from_local_list_entry(entry)
|
|
573
|
+
result = self.mal_proxy.map_from_provider(anime)
|
|
574
|
+
if result is None:
|
|
575
|
+
failed.append(entry)
|
|
576
|
+
s.write(f"> Failed to map {anime.identifier} ({anime.name})")
|
|
577
|
+
else:
|
|
578
|
+
mappings.update({entry: result})
|
|
579
|
+
s.write(
|
|
580
|
+
f"> Successfully mapped {anime.identifier} to {result.id}"
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
counter += 1
|
|
584
|
+
s.set_text(f"Progress: {counter / to_map_length * 100}%")
|
|
585
|
+
except:
|
|
586
|
+
failed.append(entry)
|
|
587
|
+
|
|
588
|
+
with ThreadPoolExecutor(max_workers=5) as pool:
|
|
589
|
+
futures = [pool.submit(do_map, a, len(to_map)) for a in to_map]
|
|
590
|
+
try:
|
|
591
|
+
for future in as_completed(futures):
|
|
592
|
+
future.result()
|
|
593
|
+
except KeyboardInterrupt:
|
|
594
|
+
pool.shutdown(wait=False, cancel_futures=True)
|
|
595
|
+
raise
|
|
596
|
+
|
|
597
|
+
if not failed or self.options.auto_update or self.options.mal_sync_seasonals:
|
|
598
|
+
self.print_options()
|
|
599
|
+
print("Everything is mapped")
|
|
600
|
+
return mappings
|
|
601
|
+
|
|
602
|
+
for f in failed:
|
|
603
|
+
cprint("Manually mapping ", colors.BLUE, f.name)
|
|
604
|
+
query = inquirer.text( # type: ignore
|
|
605
|
+
"Search Anime:",
|
|
606
|
+
long_instruction="To skip this prompt press ctrl+z",
|
|
607
|
+
mandatory=False,
|
|
608
|
+
).execute()
|
|
609
|
+
|
|
610
|
+
if query is None:
|
|
611
|
+
continue
|
|
612
|
+
|
|
613
|
+
with DotSpinner("Searching for ", colors.BLUE, query, "..."):
|
|
614
|
+
results = self.mal.get_search(query)
|
|
615
|
+
|
|
616
|
+
anime = inquirer.fuzzy( # type: ignore
|
|
617
|
+
message="Select Show:",
|
|
618
|
+
choices=[
|
|
619
|
+
Choice(value=r, name=self._format_mal_anime(r)) for r in results
|
|
620
|
+
],
|
|
621
|
+
transformer=lambda x: [e.split("|")[-1].strip() for e in x],
|
|
622
|
+
long_instruction="To skip this prompt press crtl+z",
|
|
623
|
+
mandatory=False,
|
|
624
|
+
).execute()
|
|
625
|
+
|
|
626
|
+
if not anime:
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
anime = MALAnime.from_dict(anime)
|
|
630
|
+
map = self.mal_proxy.map_from_provider(
|
|
631
|
+
Anime.from_local_list_entry(f), anime
|
|
632
|
+
)
|
|
633
|
+
if map is not None:
|
|
634
|
+
mappings.update({f: map})
|
|
635
|
+
|
|
636
|
+
self.print_options()
|
|
637
|
+
return mappings
|
|
638
|
+
|
|
639
|
+
@staticmethod
|
|
640
|
+
def _format_mal_anime(anime: MALAnime) -> str:
|
|
641
|
+
config = Config()
|
|
642
|
+
dub = (
|
|
643
|
+
config.mal_dub_tag in anime.my_list_status.tags
|
|
644
|
+
if anime.my_list_status
|
|
645
|
+
else False
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
return "{:<9} | {:<7} | {}".format(
|
|
649
|
+
(
|
|
650
|
+
(
|
|
651
|
+
anime.my_list_status.status.value.capitalize().replace("_", "")
|
|
652
|
+
if anime.my_list_status.status != MALMyListStatusEnum.PLAN_TO_WATCH
|
|
653
|
+
else "Planning"
|
|
654
|
+
)
|
|
655
|
+
if anime.my_list_status
|
|
656
|
+
else "Not Added"
|
|
657
|
+
),
|
|
658
|
+
f"{anime.my_list_status.num_episodes_watched if anime.my_list_status else 0}/{anime.num_episodes}",
|
|
659
|
+
f"{anime.title} {'(dub)' if dub else ''}",
|
|
660
|
+
)
|