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.
Files changed (67) hide show
  1. anipy_cli/__init__.py +2 -20
  2. anipy_cli/anilist_proxy.py +229 -0
  3. anipy_cli/arg_parser.py +109 -21
  4. anipy_cli/cli.py +98 -0
  5. anipy_cli/clis/__init__.py +17 -0
  6. anipy_cli/clis/anilist_cli.py +62 -0
  7. anipy_cli/clis/base_cli.py +34 -0
  8. anipy_cli/clis/binge_cli.py +96 -0
  9. anipy_cli/clis/default_cli.py +115 -0
  10. anipy_cli/clis/download_cli.py +85 -0
  11. anipy_cli/clis/history_cli.py +96 -0
  12. anipy_cli/clis/mal_cli.py +71 -0
  13. anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
  14. anipy_cli/colors.py +14 -8
  15. anipy_cli/config.py +387 -90
  16. anipy_cli/discord.py +34 -0
  17. anipy_cli/download_component.py +194 -0
  18. anipy_cli/logger.py +200 -0
  19. anipy_cli/mal_proxy.py +228 -0
  20. anipy_cli/menus/__init__.py +6 -0
  21. anipy_cli/menus/anilist_menu.py +671 -0
  22. anipy_cli/{cli/menus → menus}/base_menu.py +9 -14
  23. anipy_cli/menus/mal_menu.py +657 -0
  24. anipy_cli/menus/menu.py +265 -0
  25. anipy_cli/menus/seasonal_menu.py +270 -0
  26. anipy_cli/prompts.py +387 -0
  27. anipy_cli/util.py +268 -0
  28. anipy_cli-3.8.2.dist-info/METADATA +71 -0
  29. anipy_cli-3.8.2.dist-info/RECORD +31 -0
  30. {anipy_cli-2.7.17.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -2
  31. anipy_cli-3.8.2.dist-info/entry_points.txt +3 -0
  32. anipy_cli/cli/__init__.py +0 -1
  33. anipy_cli/cli/cli.py +0 -37
  34. anipy_cli/cli/clis/__init__.py +0 -6
  35. anipy_cli/cli/clis/base_cli.py +0 -43
  36. anipy_cli/cli/clis/binge_cli.py +0 -54
  37. anipy_cli/cli/clis/default_cli.py +0 -46
  38. anipy_cli/cli/clis/download_cli.py +0 -92
  39. anipy_cli/cli/clis/history_cli.py +0 -64
  40. anipy_cli/cli/clis/mal_cli.py +0 -27
  41. anipy_cli/cli/menus/__init__.py +0 -3
  42. anipy_cli/cli/menus/mal_menu.py +0 -411
  43. anipy_cli/cli/menus/menu.py +0 -102
  44. anipy_cli/cli/menus/seasonal_menu.py +0 -174
  45. anipy_cli/cli/util.py +0 -118
  46. anipy_cli/download.py +0 -454
  47. anipy_cli/history.py +0 -83
  48. anipy_cli/mal.py +0 -645
  49. anipy_cli/misc.py +0 -227
  50. anipy_cli/player/__init__.py +0 -1
  51. anipy_cli/player/player.py +0 -33
  52. anipy_cli/player/players/__init__.py +0 -3
  53. anipy_cli/player/players/base.py +0 -106
  54. anipy_cli/player/players/mpv.py +0 -19
  55. anipy_cli/player/players/mpv_contrl.py +0 -37
  56. anipy_cli/player/players/syncplay.py +0 -19
  57. anipy_cli/player/players/vlc.py +0 -18
  58. anipy_cli/query.py +0 -92
  59. anipy_cli/run_anipy_cli.py +0 -14
  60. anipy_cli/seasonal.py +0 -106
  61. anipy_cli/url_handler.py +0 -442
  62. anipy_cli/version.py +0 -1
  63. anipy_cli-2.7.17.dist-info/LICENSE +0 -674
  64. anipy_cli-2.7.17.dist-info/METADATA +0 -159
  65. anipy_cli-2.7.17.dist-info/RECORD +0 -43
  66. anipy_cli-2.7.17.dist-info/entry_points.txt +0 -2
  67. anipy_cli-2.7.17.dist-info/top_level.txt +0 -1
@@ -1,10 +1,9 @@
1
- import sys
1
+ from abc import ABC, abstractmethod
2
2
  from dataclasses import dataclass
3
3
  from typing import Callable, List
4
- from abc import ABC, abstractmethod
5
4
 
6
- from anipy_cli.misc import error, clear_console
7
- from anipy_cli.colors import colors, color
5
+ from anipy_cli.colors import color, colors
6
+ from anipy_cli.util import clear_screen, error
8
7
 
9
8
 
10
9
  @dataclass(frozen=True)
@@ -20,12 +19,10 @@ class MenuOption:
20
19
  class MenuBase(ABC):
21
20
  @property
22
21
  @abstractmethod
23
- def menu_options(self) -> List[MenuOption]:
24
- pass
22
+ def menu_options(self) -> List[MenuOption]: ...
25
23
 
26
24
  @abstractmethod
27
- def print_header(self):
28
- pass
25
+ def print_header(self): ...
29
26
 
30
27
  def run(self):
31
28
  self.print_options()
@@ -42,12 +39,10 @@ class MenuBase(ABC):
42
39
 
43
40
  op.callback()
44
41
 
45
- def print_options(self, clear_screen=True):
46
- if clear_screen:
47
- clear_console()
42
+ def print_options(self, should_clear_screen: bool = True):
43
+ if should_clear_screen:
44
+ clear_screen()
45
+
48
46
  self.print_header()
49
47
  for op in self.menu_options:
50
48
  print(op)
51
-
52
- def quit(self):
53
- sys.exit()
@@ -0,0 +1,657 @@
1
+ import sys
2
+ from concurrent.futures import ThreadPoolExecutor, as_completed
3
+ from typing import Dict, List, Tuple
4
+
5
+ from anipy_cli.download_component import DownloadComponent
6
+
7
+ from anipy_api.anime import Anime
8
+ from anipy_api.mal import MALAnime, MALMyListStatusEnum, MyAnimeList
9
+ from anipy_api.provider import LanguageTypeEnum
10
+ from anipy_api.provider.base import Episode
11
+
12
+ from anipy_api.locallist import LocalList, LocalListEntry
13
+ from InquirerPy import inquirer
14
+ from InquirerPy.base.control import Choice
15
+ from InquirerPy.utils import get_style
16
+
17
+ from anipy_cli.arg_parser import CliArgs
18
+ from anipy_cli.colors import colors, cprint
19
+ from anipy_cli.config import Config
20
+ from anipy_cli.mal_proxy import MyAnimeListProxy
21
+ from anipy_cli.menus.base_menu import MenuBase, MenuOption
22
+ from anipy_cli.util import (
23
+ DotSpinner,
24
+ error,
25
+ find_closest,
26
+ get_configured_player,
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: x.split("|")[-1].strip(),
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: x.split("|")[-1].strip(),
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.tracker_dub_tag:
193
+ choices.append(
194
+ Choice(
195
+ value=config.tracker_dub_tag,
196
+ name=f"{config.tracker_dub_tag} (sets wheter you prefer to watch a particular anime in dub)",
197
+ )
198
+ )
199
+
200
+ if config.tracker_ignore_tag:
201
+ choices.append(
202
+ Choice(
203
+ value=config.tracker_ignore_tag,
204
+ name=f"{config.tracker_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
+ total_eps = sum([len(e) for a, m, lang, e in picked])
253
+ if total_eps == 0:
254
+ print("Nothing to download, returning...")
255
+ return
256
+ else:
257
+ print(f"Downloading a total of {total_eps} episode(s)")
258
+
259
+ convert: Dict[Anime, MALAnime] = {d[0]: d[1] for d in picked}
260
+ new_picked = [
261
+ (anime_info[0], anime_info[2], anime_info[3]) for anime_info in picked
262
+ ]
263
+
264
+ def on_successful_download(anime: Anime, ep: Episode, lang: LanguageTypeEnum):
265
+ if all:
266
+ return
267
+ self.mal_proxy.update_show(
268
+ convert[anime],
269
+ status=MALMyListStatusEnum.WATCHING,
270
+ episode=int(ep),
271
+ )
272
+
273
+ errors = DownloadComponent(self.options, self.dl_path, "mal").download_anime(
274
+ new_picked, on_successful_download
275
+ )
276
+ DownloadComponent.serve_download_errors(errors)
277
+
278
+ self.print_options(should_clear_screen=len(errors) == 0)
279
+
280
+ def binge_latest(self):
281
+ picked = self._choose_latest()
282
+ total_eps = sum([len(e) for a, m, lang, e in picked])
283
+ if total_eps == 0:
284
+ print("Nothing to watch, returning...")
285
+ return
286
+ else:
287
+ print(f"Playing a total of {total_eps} episode(s)")
288
+
289
+ for anime, mal_anime, lang, eps in picked:
290
+ for ep in eps:
291
+ with DotSpinner(
292
+ "Extracting streams for ",
293
+ colors.BLUE,
294
+ f"{anime.name} ({lang})",
295
+ colors.END,
296
+ " Episode ",
297
+ ep,
298
+ "...",
299
+ ) as s:
300
+ stream = anime.get_video(
301
+ ep, lang, preferred_quality=self.options.quality
302
+ )
303
+ if stream is None:
304
+ error("Could not find stream for requested episode, skipping")
305
+ s.ok("✔")
306
+
307
+ self.player.play_title(anime, stream)
308
+ self.player.wait()
309
+
310
+ self.mal_proxy.update_show(
311
+ mal_anime, status=MALMyListStatusEnum.WATCHING, episode=int(ep)
312
+ )
313
+
314
+ def manual_maps(self):
315
+ mylist = self.mal_proxy.get_list()
316
+ self._create_maps_mal(mylist)
317
+
318
+ def sync_seasonals_mal(self):
319
+ config = Config()
320
+ seasonals = self.seasonals_list.get_all()
321
+ mappings = self._create_maps_provider(seasonals)
322
+ with DotSpinner("Syncing Seasonals into MyAnimeList") as s:
323
+ for k, v in mappings.items():
324
+ tags = set()
325
+ if config.tracker_dub_tag:
326
+ if k.language == LanguageTypeEnum.DUB:
327
+ tags.add(config.tracker_dub_tag)
328
+
329
+ if v.my_list_status:
330
+ if config.tracker_ignore_tag in v.my_list_status.tags:
331
+ continue
332
+ tags |= set(v.my_list_status.tags)
333
+
334
+ self.mal_proxy.update_show(
335
+ v,
336
+ status=MALMyListStatusEnum.WATCHING,
337
+ episode=int(k.episode),
338
+ tags=tags,
339
+ )
340
+ s.ok("✔")
341
+
342
+ def sync_mal_seasonls(self):
343
+ config = Config()
344
+ mylist = self.mal_proxy.get_list()
345
+ mappings = self._create_maps_mal(mylist)
346
+ with DotSpinner("Syncing MyAnimeList into Seasonals") as s:
347
+ for k, v in mappings.items():
348
+ if config.tracker_dub_tag:
349
+ if (
350
+ k.my_list_status
351
+ and config.tracker_dub_tag in k.my_list_status.tags
352
+ ):
353
+ pref_lang = LanguageTypeEnum.DUB
354
+ else:
355
+ pref_lang = LanguageTypeEnum.SUB
356
+ else:
357
+ pref_lang = (
358
+ LanguageTypeEnum.DUB
359
+ if config.preferred_type == "dub"
360
+ else LanguageTypeEnum.SUB
361
+ )
362
+
363
+ if pref_lang in v.languages:
364
+ lang = pref_lang
365
+ else:
366
+ lang = next(iter(v.languages))
367
+
368
+ provider_episodes = v.get_episodes(lang)
369
+ episode = (
370
+ k.my_list_status.num_episodes_watched if k.my_list_status else 0
371
+ )
372
+
373
+ if episode == 0:
374
+ episode = provider_episodes[0]
375
+ else:
376
+ episode = find_closest(provider_episodes, episode)
377
+
378
+ self.seasonals_list.update(v, episode=episode, language=lang)
379
+ s.ok("✔")
380
+
381
+ def _choose_latest(
382
+ self, all: bool = False
383
+ ) -> List[Tuple[Anime, MALAnime, LanguageTypeEnum, List[Episode]]]:
384
+ cprint(
385
+ colors.GREEN,
386
+ "Hint: ",
387
+ colors.END,
388
+ "you can fine-tune mapping behaviour in your settings!",
389
+ )
390
+ with DotSpinner("Fetching your MAL..."):
391
+ mylist = self.mal_proxy.get_list()
392
+
393
+ for i, e in enumerate(mylist):
394
+ if e.my_list_status is None:
395
+ mylist.pop(i)
396
+ continue
397
+
398
+ if (e.my_list_status.num_episodes_watched == e.num_episodes) and (
399
+ e.my_list_status.num_episodes_watched != 0
400
+ ):
401
+ mylist.pop(i)
402
+
403
+ if not mylist:
404
+ error("MAL is empty", False)
405
+ return []
406
+
407
+ if not (all or self.options.auto_update):
408
+ choices = inquirer.fuzzy( # type: ignore
409
+ message="Select shows to catch up to:",
410
+ choices=[
411
+ Choice(value=e, name=self._format_mal_anime(e)) for e in mylist
412
+ ],
413
+ multiselect=True,
414
+ transformer=lambda x: [e.split("|")[-1].strip() for e in x],
415
+ long_instruction="| skip prompt: ctrl+z | toggle: ctrl+space | toggle all: ctrl+a | continue: enter |",
416
+ mandatory=False,
417
+ keybindings={"toggle": [{"key": "c-space"}]},
418
+ style=get_style(
419
+ {"long_instruction": "fg:#5FAFFF bg:#222"}, style_override=False
420
+ ),
421
+ ).execute()
422
+
423
+ if choices is None:
424
+ return []
425
+
426
+ mylist = [MALAnime.from_dict(c) for c in choices]
427
+
428
+ config = Config()
429
+ to_watch: List[Tuple[Anime, MALAnime, LanguageTypeEnum, List[Episode]]] = []
430
+
431
+ with DotSpinner("Fetching episodes...") as s:
432
+ for e in mylist:
433
+ s.write(f"> Checking out episodes of {e.title}")
434
+
435
+ if e.num_episodes != 0:
436
+ episodes_to_watch = list(
437
+ range(e.my_list_status.num_episodes_watched + 1, e.num_episodes + 1) # type: ignore
438
+ )
439
+ elif e.num_episodes == 0:
440
+ episodes_to_watch = list(
441
+ range(e.my_list_status.num_episodes_watched + 1, 10000) # type: ignore
442
+ )
443
+
444
+ result = self.mal_proxy.map_from_mal(e)
445
+
446
+ if result is None:
447
+ s.write(
448
+ f"> No mapping found for {e.title} please use the `m` option to map it"
449
+ )
450
+ continue
451
+
452
+ if config.tracker_dub_tag:
453
+ if (
454
+ e.my_list_status
455
+ and config.tracker_dub_tag in e.my_list_status.tags
456
+ ):
457
+ pref_lang = LanguageTypeEnum.DUB
458
+ else:
459
+ pref_lang = LanguageTypeEnum.SUB
460
+ else:
461
+ pref_lang = (
462
+ LanguageTypeEnum.DUB
463
+ if config.preferred_type == "dub"
464
+ else LanguageTypeEnum.SUB
465
+ )
466
+
467
+ if pref_lang in result.languages:
468
+ lang = pref_lang
469
+ s.write(
470
+ f"> Looking for {lang} episodes because of config/tag preference"
471
+ )
472
+ else:
473
+ lang = next(iter(result.languages))
474
+ s.write(
475
+ f"> Looking for {lang} episodes because your preferred type is not available"
476
+ )
477
+
478
+ episodes = result.get_episodes(lang)
479
+
480
+ will_watch = []
481
+ if all:
482
+ will_watch.extend(episodes)
483
+ else:
484
+ for ep in episodes_to_watch:
485
+ try:
486
+ idx = episodes.index(ep)
487
+ will_watch.append(episodes[idx])
488
+ except ValueError:
489
+ s.write(
490
+ f"> Episode {ep} not found in provider, skipping..."
491
+ )
492
+ if e.num_episodes == 0:
493
+ break
494
+
495
+ to_watch.append((result, e, lang, will_watch))
496
+
497
+ s.ok("✔")
498
+
499
+ return to_watch
500
+
501
+ def _create_maps_mal(self, to_map: List[MALAnime]) -> Dict[MALAnime, Anime]:
502
+ cprint(
503
+ colors.GREEN,
504
+ "Hint: ",
505
+ colors.END,
506
+ "you can fine-tune mapping behaviour in your settings!",
507
+ )
508
+ with DotSpinner("Starting Automapping...") as s:
509
+ failed: List[MALAnime] = []
510
+ mappings: Dict[MALAnime, Anime] = {}
511
+ counter = 0
512
+
513
+ def do_map(anime: MALAnime, to_map_length: int):
514
+ nonlocal failed, counter
515
+ try:
516
+ result = self.mal_proxy.map_from_mal(anime)
517
+ if result is None:
518
+ failed.append(anime)
519
+ s.write(f"> Failed to map {anime.id} ({anime.title})")
520
+ else:
521
+ mappings.update({anime: result})
522
+ s.write(
523
+ f"> Successfully mapped {anime.id} to {result.identifier}"
524
+ )
525
+
526
+ counter += 1
527
+ s.set_text(f"Progress: {counter / to_map_length * 100:.1f}%")
528
+ except: # noqa: E722
529
+ failed.append(anime)
530
+
531
+ with ThreadPoolExecutor(max_workers=5) as pool:
532
+ futures = [pool.submit(do_map, a, len(to_map)) for a in to_map]
533
+ try:
534
+ for future in as_completed(futures):
535
+ future.result()
536
+ except KeyboardInterrupt:
537
+ pool.shutdown(wait=False, cancel_futures=True)
538
+ raise
539
+
540
+ if not failed or self.options.auto_update:
541
+ self.print_options()
542
+ print("Everything is mapped")
543
+ return mappings
544
+
545
+ for f in failed:
546
+ cprint("Manually mapping ", colors.BLUE, f.title)
547
+ anime = search_show_prompt("mal", skip_season_search=True)
548
+ if not anime:
549
+ continue
550
+
551
+ map = self.mal_proxy.map_from_mal(f, anime)
552
+ if map is not None:
553
+ mappings.update({f: map})
554
+
555
+ self.print_options()
556
+ return mappings
557
+
558
+ def _create_maps_provider(
559
+ self, to_map: List[LocalListEntry]
560
+ ) -> Dict[LocalListEntry, MALAnime]:
561
+ with DotSpinner("Starting Automapping...") as s:
562
+ failed: List[LocalListEntry] = []
563
+ mappings: Dict[LocalListEntry, MALAnime] = {}
564
+ counter = 0
565
+
566
+ def do_map(entry: LocalListEntry, to_map_length: int):
567
+ nonlocal failed, counter
568
+ try:
569
+ anime = Anime.from_local_list_entry(entry)
570
+ result = self.mal_proxy.map_from_provider(anime)
571
+ if result is None:
572
+ failed.append(entry)
573
+ s.write(f"> Failed to map {anime.identifier} ({anime.name})")
574
+ else:
575
+ mappings.update({entry: result})
576
+ s.write(
577
+ f"> Successfully mapped {anime.identifier} to {result.id}"
578
+ )
579
+
580
+ counter += 1
581
+ s.set_text(f"Progress: {counter / to_map_length * 100}%")
582
+ except: # noqa: E722
583
+ failed.append(entry)
584
+
585
+ with ThreadPoolExecutor(max_workers=5) as pool:
586
+ futures = [pool.submit(do_map, a, len(to_map)) for a in to_map]
587
+ try:
588
+ for future in as_completed(futures):
589
+ future.result()
590
+ except KeyboardInterrupt:
591
+ pool.shutdown(wait=False, cancel_futures=True)
592
+ raise
593
+
594
+ if not failed or self.options.auto_update or self.options.mal_sync_seasonals:
595
+ self.print_options()
596
+ print("Everything is mapped")
597
+ return mappings
598
+
599
+ for f in failed:
600
+ cprint("Manually mapping ", colors.BLUE, f.name)
601
+ query = inquirer.text( # type: ignore
602
+ "Search Anime:",
603
+ long_instruction="To skip this prompt press ctrl+z",
604
+ mandatory=False,
605
+ ).execute()
606
+
607
+ if query is None:
608
+ continue
609
+
610
+ with DotSpinner("Searching for ", colors.BLUE, query, "..."):
611
+ results = self.mal.get_search(query)
612
+
613
+ anime = inquirer.fuzzy( # type: ignore
614
+ message="Select Show:",
615
+ choices=[
616
+ Choice(value=r, name=self._format_mal_anime(r)) for r in results
617
+ ],
618
+ transformer=lambda x: x.split("|")[-1].strip(),
619
+ long_instruction="To skip this prompt press crtl+z",
620
+ mandatory=False,
621
+ ).execute()
622
+
623
+ if not anime:
624
+ continue
625
+
626
+ anime = MALAnime.from_dict(anime)
627
+ map = self.mal_proxy.map_from_provider(
628
+ Anime.from_local_list_entry(f), anime
629
+ )
630
+ if map is not None:
631
+ mappings.update({f: map})
632
+
633
+ self.print_options()
634
+ return mappings
635
+
636
+ @staticmethod
637
+ def _format_mal_anime(anime: MALAnime) -> str:
638
+ config = Config()
639
+ dub = (
640
+ config.tracker_dub_tag in anime.my_list_status.tags
641
+ if anime.my_list_status
642
+ else False
643
+ )
644
+
645
+ return "{:<9} | {:<7} | {}".format(
646
+ (
647
+ (
648
+ anime.my_list_status.status.value.capitalize().replace("_", "")
649
+ if anime.my_list_status.status != MALMyListStatusEnum.PLAN_TO_WATCH
650
+ else "Planning"
651
+ )
652
+ if anime.my_list_status
653
+ else "Not Added"
654
+ ),
655
+ f"{anime.my_list_status.num_episodes_watched if anime.my_list_status else 0}/{anime.num_episodes}",
656
+ f"{anime.title} {'(dub)' if dub else ''}",
657
+ )