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