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.
- anipy_cli/__init__.py +2 -20
- anipy_cli/anilist_proxy.py +229 -0
- anipy_cli/arg_parser.py +109 -21
- anipy_cli/cli.py +98 -0
- anipy_cli/clis/__init__.py +17 -0
- anipy_cli/clis/anilist_cli.py +62 -0
- anipy_cli/clis/base_cli.py +34 -0
- anipy_cli/clis/binge_cli.py +96 -0
- anipy_cli/clis/default_cli.py +115 -0
- anipy_cli/clis/download_cli.py +85 -0
- anipy_cli/clis/history_cli.py +96 -0
- anipy_cli/clis/mal_cli.py +71 -0
- anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
- anipy_cli/colors.py +14 -8
- anipy_cli/config.py +387 -90
- anipy_cli/discord.py +34 -0
- anipy_cli/download_component.py +194 -0
- anipy_cli/logger.py +200 -0
- anipy_cli/mal_proxy.py +228 -0
- anipy_cli/menus/__init__.py +6 -0
- anipy_cli/menus/anilist_menu.py +671 -0
- anipy_cli/{cli/menus → menus}/base_menu.py +9 -14
- anipy_cli/menus/mal_menu.py +657 -0
- anipy_cli/menus/menu.py +265 -0
- anipy_cli/menus/seasonal_menu.py +270 -0
- anipy_cli/prompts.py +387 -0
- anipy_cli/util.py +268 -0
- anipy_cli-3.8.2.dist-info/METADATA +71 -0
- anipy_cli-3.8.2.dist-info/RECORD +31 -0
- {anipy_cli-2.7.17.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -2
- anipy_cli-3.8.2.dist-info/entry_points.txt +3 -0
- anipy_cli/cli/__init__.py +0 -1
- anipy_cli/cli/cli.py +0 -37
- anipy_cli/cli/clis/__init__.py +0 -6
- anipy_cli/cli/clis/base_cli.py +0 -43
- anipy_cli/cli/clis/binge_cli.py +0 -54
- anipy_cli/cli/clis/default_cli.py +0 -46
- anipy_cli/cli/clis/download_cli.py +0 -92
- anipy_cli/cli/clis/history_cli.py +0 -64
- anipy_cli/cli/clis/mal_cli.py +0 -27
- anipy_cli/cli/menus/__init__.py +0 -3
- anipy_cli/cli/menus/mal_menu.py +0 -411
- anipy_cli/cli/menus/menu.py +0 -102
- anipy_cli/cli/menus/seasonal_menu.py +0 -174
- anipy_cli/cli/util.py +0 -118
- anipy_cli/download.py +0 -454
- anipy_cli/history.py +0 -83
- anipy_cli/mal.py +0 -645
- anipy_cli/misc.py +0 -227
- anipy_cli/player/__init__.py +0 -1
- anipy_cli/player/player.py +0 -33
- anipy_cli/player/players/__init__.py +0 -3
- anipy_cli/player/players/base.py +0 -106
- anipy_cli/player/players/mpv.py +0 -19
- anipy_cli/player/players/mpv_contrl.py +0 -37
- anipy_cli/player/players/syncplay.py +0 -19
- anipy_cli/player/players/vlc.py +0 -18
- anipy_cli/query.py +0 -92
- anipy_cli/run_anipy_cli.py +0 -14
- anipy_cli/seasonal.py +0 -106
- anipy_cli/url_handler.py +0 -442
- anipy_cli/version.py +0 -1
- anipy_cli-2.7.17.dist-info/LICENSE +0 -674
- anipy_cli-2.7.17.dist-info/METADATA +0 -159
- anipy_cli-2.7.17.dist-info/RECORD +0 -43
- anipy_cli-2.7.17.dist-info/entry_points.txt +0 -2
- anipy_cli-2.7.17.dist-info/top_level.txt +0 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
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.
|
|
7
|
-
from anipy_cli.
|
|
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,
|
|
46
|
-
if
|
|
47
|
-
|
|
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
|
+
)
|