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