anipy-cli 2.7.31__py3-none-any.whl → 3.0.0.dev0__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.dev0.dist-info/METADATA +67 -0
  24. anipy_cli-3.0.0.dev0.dist-info/RECORD +26 -0
  25. {anipy_cli-2.7.31.dist-info → anipy_cli-3.0.0.dev0.dist-info}/WHEEL +1 -2
  26. anipy_cli-3.0.0.dev0.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,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
+ )