anipy-cli 3.4.8__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 +1 -1
- anipy_cli/anilist_proxy.py +229 -0
- anipy_cli/arg_parser.py +57 -0
- anipy_cli/cli.py +25 -2
- anipy_cli/clis/__init__.py +2 -0
- anipy_cli/clis/anilist_cli.py +62 -0
- anipy_cli/clis/base_cli.py +1 -1
- anipy_cli/clis/binge_cli.py +4 -1
- anipy_cli/clis/default_cli.py +10 -8
- anipy_cli/clis/download_cli.py +6 -2
- anipy_cli/clis/mal_cli.py +1 -1
- anipy_cli/colors.py +7 -4
- anipy_cli/config.py +55 -40
- anipy_cli/download_component.py +42 -16
- anipy_cli/logger.py +200 -0
- anipy_cli/mal_proxy.py +15 -9
- anipy_cli/menus/__init__.py +2 -1
- anipy_cli/menus/anilist_menu.py +671 -0
- anipy_cli/menus/base_menu.py +4 -5
- anipy_cli/menus/mal_menu.py +23 -15
- anipy_cli/menus/menu.py +24 -5
- anipy_cli/menus/seasonal_menu.py +15 -11
- anipy_cli/prompts.py +3 -3
- anipy_cli/util.py +37 -18
- {anipy_cli-3.4.8.dist-info → anipy_cli-3.8.2.dist-info}/METADATA +5 -5
- anipy_cli-3.8.2.dist-info/RECORD +31 -0
- {anipy_cli-3.4.8.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -1
- anipy_cli-3.4.8.dist-info/RECORD +0 -27
- {anipy_cli-3.4.8.dist-info → anipy_cli-3.8.2.dist-info}/entry_points.txt +0 -0
anipy_cli/config.py
CHANGED
|
@@ -42,6 +42,10 @@ class Config:
|
|
|
42
42
|
def _mal_local_user_list_path(self) -> Path:
|
|
43
43
|
return self.user_files_path / "mal_list.json"
|
|
44
44
|
|
|
45
|
+
@property
|
|
46
|
+
def _anilist_local_user_list_path(self) -> Path:
|
|
47
|
+
return self.user_files_path / "anilist_list.json"
|
|
48
|
+
|
|
45
49
|
@property
|
|
46
50
|
def download_folder_path(self) -> Path:
|
|
47
51
|
"""Path to your download folder/directory.
|
|
@@ -66,13 +70,13 @@ class Config:
|
|
|
66
70
|
def providers(self) -> Dict[str, List[str]]:
|
|
67
71
|
"""A list of pairs defining which providers will search for anime
|
|
68
72
|
in different parts of the program. Configurable areas are as follows:
|
|
69
|
-
default (and history), download (-D), seasonal (-S), binge (-B)
|
|
70
|
-
(-M) The example will show you how it is done! Please note that for seasonal
|
|
73
|
+
default (and history), download (-D), seasonal (-S), binge (-B), anilist (-A)
|
|
74
|
+
and mal (-M) The example will show you how it is done! Please note that for seasonal
|
|
71
75
|
search always the first provider that supports it is used.
|
|
72
76
|
|
|
73
77
|
For an updated list of providers look here: https://sdaqo.github.io/anipy-cli/availabilty
|
|
74
78
|
|
|
75
|
-
Supported providers (as of $version): animekai
|
|
79
|
+
Supported providers (as of $version): allanime, animekai (animekai is not functional for now), native (filesystem provider, set root in provider_urls config)
|
|
76
80
|
|
|
77
81
|
Examples:
|
|
78
82
|
providers:
|
|
@@ -81,13 +85,15 @@ class Config:
|
|
|
81
85
|
seasonal: ["provider3"]
|
|
82
86
|
binge: ["provider4"]
|
|
83
87
|
mal: ["provider2", "provider3"]
|
|
88
|
+
anilist: ["provider1"]
|
|
84
89
|
"""
|
|
85
90
|
defaults = {
|
|
86
|
-
"default": ["
|
|
87
|
-
"download": ["
|
|
88
|
-
"seasonal": ["
|
|
89
|
-
"binge": ["
|
|
90
|
-
"mal": ["
|
|
91
|
+
"default": ["allanime"],
|
|
92
|
+
"download": ["allanime"],
|
|
93
|
+
"seasonal": ["allanime"],
|
|
94
|
+
"binge": ["allanime"],
|
|
95
|
+
"mal": ["allanime"],
|
|
96
|
+
"anilist": ["allanime"],
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
value = self._get_value("providers", defaults, dict)
|
|
@@ -100,9 +106,12 @@ class Config:
|
|
|
100
106
|
def provider_urls(self) -> Dict[str, str]:
|
|
101
107
|
"""A list of pairs to override the default urls that providers use.
|
|
102
108
|
|
|
109
|
+
Note: the native provider accepts a path instead of a url, it defaults to ~/Videos
|
|
110
|
+
|
|
103
111
|
Examples:
|
|
104
112
|
provider_urls:
|
|
105
113
|
gogoanime: "https://gogoanime3.co"
|
|
114
|
+
native: "~/Videos"
|
|
106
115
|
provider_urls: {} # do not override any urls
|
|
107
116
|
"""
|
|
108
117
|
|
|
@@ -227,9 +236,9 @@ class Config:
|
|
|
227
236
|
"""With this option you can define scripts that run after a file
|
|
228
237
|
has been downloaded. As with the 'providers' option, you can configure
|
|
229
238
|
different behaviour, depending on which part of anipy-cli the download occurs.
|
|
230
|
-
Configurable areas are as follows: default (and history), download (-D), seasonal (-S)
|
|
231
|
-
and mal (-M). The example will show you how it is done! Please note that
|
|
232
|
-
several scripts for one area, they will run in the order you put them in the list.
|
|
239
|
+
Configurable areas are as follows: default (and history), download (-D), seasonal (-S),
|
|
240
|
+
anilist (-A) and mal (-M). The example will show you how it is done! Please note that
|
|
241
|
+
if you define several scripts for one area, they will run in the order you put them in the list.
|
|
233
242
|
You can also define a timeout (in seconds), after which a script will be terminated,
|
|
234
243
|
if set to null there will be no timeout and any script will run forever.
|
|
235
244
|
|
|
@@ -254,7 +263,8 @@ class Config:
|
|
|
254
263
|
"download": [],
|
|
255
264
|
"seasonal": [],
|
|
256
265
|
"mal": [],
|
|
257
|
-
"
|
|
266
|
+
"anilist": [],
|
|
267
|
+
"timeout": None,
|
|
258
268
|
}
|
|
259
269
|
|
|
260
270
|
value = self._get_value("post_download_scripts", defaults, dict)
|
|
@@ -279,6 +289,11 @@ class Config:
|
|
|
279
289
|
"""Your MyAnimeList username for MAL mode."""
|
|
280
290
|
return self._get_value("mal_user", "", str)
|
|
281
291
|
|
|
292
|
+
@property
|
|
293
|
+
def anilist_token(self) -> str:
|
|
294
|
+
"""Your AniList access token for AniList mode."""
|
|
295
|
+
return self._get_value("anilist_token", "", str)
|
|
296
|
+
|
|
282
297
|
@property
|
|
283
298
|
def mal_password(self) -> str:
|
|
284
299
|
"""Your MyAnimeList password for MAL mode.
|
|
@@ -289,55 +304,55 @@ class Config:
|
|
|
289
304
|
return self._get_value("mal_password", "", str)
|
|
290
305
|
|
|
291
306
|
@property
|
|
292
|
-
def
|
|
307
|
+
def tracker_ignore_tag(self) -> str:
|
|
293
308
|
"""All anime in your MyAnimeList with this tag will be ignored by
|
|
294
309
|
anipy-cli.
|
|
295
310
|
|
|
296
311
|
Examples:
|
|
297
|
-
|
|
298
|
-
|
|
312
|
+
tracker_ignore_tag: ignore # all anime with ignore tag will be ignored
|
|
313
|
+
tracker_ignore_tag: "" # no anime will be ignored
|
|
299
314
|
"""
|
|
300
|
-
return self._get_value("
|
|
315
|
+
return self._get_value("tracker_ignore_tag", "ignore", str)
|
|
301
316
|
|
|
302
317
|
@property
|
|
303
|
-
def
|
|
304
|
-
"""All anime in your
|
|
305
|
-
dub in
|
|
306
|
-
anipy-cli will use `preferred_type` to choose dub or sub in
|
|
318
|
+
def tracker_dub_tag(self) -> str:
|
|
319
|
+
"""All anime in your Anime Tracker with this tag will be switched over to
|
|
320
|
+
dub in tracker mode, if the dub is available. If you do not specify a tag,
|
|
321
|
+
anipy-cli will use `preferred_type` to choose dub or sub in tracker mode.
|
|
307
322
|
|
|
308
323
|
Examples:
|
|
309
|
-
|
|
310
|
-
|
|
324
|
+
tracker_dub_tag: dub # all anime with this tag will be switched to dub
|
|
325
|
+
tracker_dub_tag: "" # no anime will be switched to dub, except you have preferred_type on dub
|
|
311
326
|
"""
|
|
312
|
-
return self._get_value("
|
|
327
|
+
return self._get_value("tracker_dub_tag", "dub", str)
|
|
313
328
|
|
|
314
329
|
@property
|
|
315
|
-
def
|
|
316
|
-
"""Custom tags to tag all anime in your
|
|
330
|
+
def tracker_tags(self) -> List[str]:
|
|
331
|
+
"""Custom tags to tag all anime in your Anime Tracker that are
|
|
317
332
|
altered/added by anipy-cli.
|
|
318
333
|
|
|
319
334
|
Examples:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
335
|
+
tracker_tags: ["anipy-cli"] # tag all anime with anipy-cli
|
|
336
|
+
tracker_tags: ["anipy-cli", "important"] # tag all anime with anipy-cli and important
|
|
337
|
+
tracker_tags: null or tracker_tags: [] # Do not tag the anime
|
|
323
338
|
"""
|
|
324
|
-
return self._get_value("
|
|
339
|
+
return self._get_value("tracker_tags", [], list)
|
|
325
340
|
|
|
326
341
|
@property
|
|
327
|
-
def
|
|
328
|
-
"""Status categories of your
|
|
342
|
+
def tracker_status_categories(self) -> List[str]:
|
|
343
|
+
"""Status categories of your Anime Tracker that anipy-cli uses for
|
|
329
344
|
downloading/watching new episodes listing anime in your list and stuff
|
|
330
345
|
like that. Normally the watching catagory should be enough as you would
|
|
331
346
|
normally put anime you currently watch in the watching catagory.
|
|
332
347
|
|
|
333
348
|
Valid values are: watching, completed, on_hold, dropped, plan_to_watch
|
|
334
349
|
"""
|
|
335
|
-
return self._get_value("
|
|
350
|
+
return self._get_value("tracker_status_categories", ["watching"], list)
|
|
336
351
|
|
|
337
352
|
@property
|
|
338
|
-
def
|
|
353
|
+
def tracker_mapping_min_similarity(self) -> float:
|
|
339
354
|
"""
|
|
340
|
-
The minumum similarity between titles when mapping anime in
|
|
355
|
+
The minumum similarity between titles when mapping anime in tracker mode.
|
|
341
356
|
This is a decimal number from 0 - 1, 1 meaning 100% match and 0 meaning all characters are different.
|
|
342
357
|
If the similarity of a map is below the threshold you will be prompted for a manual map.
|
|
343
358
|
|
|
@@ -347,24 +362,24 @@ class Config:
|
|
|
347
362
|
|
|
348
363
|
If you are interested, the algorithm being used here is this: https://en.wikipedia.org/wiki/Levenshtein_distance
|
|
349
364
|
"""
|
|
350
|
-
return self._get_value("
|
|
365
|
+
return self._get_value("tracker_mapping_min_similarity", 0.8, float)
|
|
351
366
|
|
|
352
367
|
@property
|
|
353
|
-
def
|
|
368
|
+
def tracker_mapping_use_alternatives(self) -> bool:
|
|
354
369
|
"""Check alternative names when mapping anime.
|
|
355
370
|
|
|
356
371
|
If turned on this will slow down mapping but provide better
|
|
357
372
|
chances of finding a match.
|
|
358
373
|
"""
|
|
359
|
-
return self._get_value("
|
|
374
|
+
return self._get_value("tracker_mapping_use_alternatives", True, bool)
|
|
360
375
|
|
|
361
376
|
@property
|
|
362
|
-
def
|
|
377
|
+
def tracker_mapping_use_filters(self) -> bool:
|
|
363
378
|
"""Use filters (e.g. year, season etc.) of providers to narrow down the
|
|
364
379
|
results, this will lead to more accurate mapping, but provide wrong
|
|
365
380
|
results if the filters of the provider do not work properly or if anime
|
|
366
381
|
are not correctly marked with the correct data."""
|
|
367
|
-
return self._get_value("
|
|
382
|
+
return self._get_value("tracker_mapping_use_filters", True, bool)
|
|
368
383
|
|
|
369
384
|
@property
|
|
370
385
|
def auto_sync_mal_to_seasonals(self) -> bool:
|
|
@@ -414,7 +429,7 @@ class Config:
|
|
|
414
429
|
except RuntimeError:
|
|
415
430
|
return fallback
|
|
416
431
|
|
|
417
|
-
def _get_value(self, key: str, fallback, _type: Type) -> Any:
|
|
432
|
+
def _get_value(self, key: str, fallback: Any, _type: Type) -> Any:
|
|
418
433
|
value = self._yaml_conf.get(key, fallback)
|
|
419
434
|
if isinstance(value, _type):
|
|
420
435
|
return value
|
anipy_cli/download_component.py
CHANGED
|
@@ -4,6 +4,7 @@ from anipy_cli.arg_parser import CliArgs
|
|
|
4
4
|
from anipy_cli.colors import color, colors
|
|
5
5
|
from anipy_cli.config import Config
|
|
6
6
|
from anipy_cli.util import DotSpinner, get_download_path, get_post_download_scripts_hook
|
|
7
|
+
import anipy_cli.logger as logger
|
|
7
8
|
|
|
8
9
|
from anipy_api.anime import Anime
|
|
9
10
|
from anipy_api.download import Downloader
|
|
@@ -32,7 +33,7 @@ class DownloadComponent:
|
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
35
|
def __init__(self, options: CliArgs, dl_path: Path, mode: str) -> None:
|
|
35
|
-
self.options = options
|
|
36
|
+
self.options = options
|
|
36
37
|
self.dl_path = dl_path
|
|
37
38
|
self.mode = mode
|
|
38
39
|
|
|
@@ -41,6 +42,7 @@ class DownloadComponent:
|
|
|
41
42
|
picked: List[Tuple[Anime, LanguageTypeEnum, List[Episode]]],
|
|
42
43
|
after_success_ep: SuccessfulEpDownload = lambda anime, ep, lang: None,
|
|
43
44
|
only_skip_ep_on_err: bool = False,
|
|
45
|
+
sub_only: bool = False,
|
|
44
46
|
) -> List[Tuple[Anime, Episode]]:
|
|
45
47
|
"""
|
|
46
48
|
Attributes:
|
|
@@ -53,11 +55,13 @@ class DownloadComponent:
|
|
|
53
55
|
def progress_indicator(percentage: float):
|
|
54
56
|
s.set_text(f"Progress: {percentage:.1f}%")
|
|
55
57
|
|
|
56
|
-
def info_display(message: str):
|
|
58
|
+
def info_display(message: str, exc_info: BaseException | None = None):
|
|
59
|
+
logger.info(message, exc_info, exc_info is not None)
|
|
57
60
|
s.write(f"> {message}")
|
|
58
61
|
|
|
59
|
-
def error_display(message: str):
|
|
60
|
-
|
|
62
|
+
def error_display(message: str, exc_info: BaseException | None = None):
|
|
63
|
+
logger.error(message, exc_info)
|
|
64
|
+
s.write(f"{colors.RED}! {message}{colors.END}")
|
|
61
65
|
|
|
62
66
|
downloader = Downloader(progress_indicator, info_display, error_display)
|
|
63
67
|
|
|
@@ -72,6 +76,7 @@ class DownloadComponent:
|
|
|
72
76
|
eps,
|
|
73
77
|
after_success_ep,
|
|
74
78
|
only_skip_ep_on_err,
|
|
79
|
+
sub_only,
|
|
75
80
|
)
|
|
76
81
|
|
|
77
82
|
return failed
|
|
@@ -85,12 +90,18 @@ class DownloadComponent:
|
|
|
85
90
|
eps: List[Episode],
|
|
86
91
|
after_success_ep: SuccessfulEpDownload = lambda anime, ep, lang: None,
|
|
87
92
|
only_skip_ep_on_err: bool = False,
|
|
93
|
+
sub_only: bool = False,
|
|
88
94
|
) -> List[Tuple[Anime, Episode]]:
|
|
89
95
|
fails = []
|
|
90
96
|
for ep in eps:
|
|
91
97
|
try:
|
|
92
|
-
self.download_ep(spinner, downloader, anime, lang, ep)
|
|
93
|
-
except Exception as
|
|
98
|
+
self.download_ep(spinner, downloader, anime, lang, ep, sub_only)
|
|
99
|
+
except Exception as anime_download_error:
|
|
100
|
+
# Log it first so we don't run into another error below
|
|
101
|
+
logger.error(
|
|
102
|
+
f"Error downloading episode {ep} of {anime.name}. Skipped.",
|
|
103
|
+
anime_download_error,
|
|
104
|
+
)
|
|
94
105
|
if only_skip_ep_on_err:
|
|
95
106
|
error_msg = f"! Issues downloading episode {ep} of {anime.name}. Skipping..."
|
|
96
107
|
else:
|
|
@@ -98,7 +109,7 @@ class DownloadComponent:
|
|
|
98
109
|
spinner.write(
|
|
99
110
|
color(
|
|
100
111
|
colors.RED,
|
|
101
|
-
f"! Error: {
|
|
112
|
+
f"! Error: {anime_download_error}\n",
|
|
102
113
|
error_msg,
|
|
103
114
|
)
|
|
104
115
|
)
|
|
@@ -117,6 +128,7 @@ class DownloadComponent:
|
|
|
117
128
|
anime: Anime,
|
|
118
129
|
lang: LanguageTypeEnum,
|
|
119
130
|
ep: Episode,
|
|
131
|
+
sub_only: bool = False,
|
|
120
132
|
):
|
|
121
133
|
config = Config()
|
|
122
134
|
|
|
@@ -132,19 +144,33 @@ class DownloadComponent:
|
|
|
132
144
|
|
|
133
145
|
stream = anime.get_video(ep, lang, preferred_quality=self.options.quality)
|
|
134
146
|
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
if stream is None:
|
|
148
|
+
logger.info("Could not find a stream")
|
|
149
|
+
DownloadComponent.serve_download_errors(
|
|
150
|
+
[(anime, ep)], only_skip_ep_on_err=True
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
download_message_update = (
|
|
155
|
+
f"Downloading Episode {stream.episode} of {anime.name} ({lang})"
|
|
137
156
|
)
|
|
157
|
+
logger.info(download_message_update)
|
|
158
|
+
spinner.write(f"> {download_message_update}")
|
|
138
159
|
|
|
139
160
|
spinner.set_text("Downloading...")
|
|
140
161
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
162
|
+
if not sub_only:
|
|
163
|
+
downloader.download(
|
|
164
|
+
stream,
|
|
165
|
+
get_download_path(anime, stream, parent_directory=self.dl_path),
|
|
166
|
+
container=config.remux_to,
|
|
167
|
+
ffmpeg=self.options.ffmpeg or config.ffmpeg_hls,
|
|
168
|
+
post_dl_cb=get_post_download_scripts_hook(self.mode, anime, spinner),
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
downloader.download_sub(
|
|
172
|
+
stream, get_download_path(anime, stream, parent_directory=self.dl_path)
|
|
173
|
+
)
|
|
148
174
|
|
|
149
175
|
@staticmethod
|
|
150
176
|
def serve_download_errors(
|
anipy_cli/logger.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import logging.handlers
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Protocol
|
|
9
|
+
import datetime
|
|
10
|
+
|
|
11
|
+
from anipy_cli.config import Config
|
|
12
|
+
from anipy_cli import __appname__
|
|
13
|
+
from appdirs import user_data_dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FatalHandler(Protocol):
|
|
17
|
+
def __call__(
|
|
18
|
+
self, exc_val: BaseException, exc_tb: TracebackType, logs_location: Path
|
|
19
|
+
): ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FatalCatcher:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
logs_location: Path,
|
|
26
|
+
fatal_handler: FatalHandler | None = None,
|
|
27
|
+
ignore_system_exit: bool = True,
|
|
28
|
+
):
|
|
29
|
+
self._fatal_handler = fatal_handler
|
|
30
|
+
|
|
31
|
+
self.logs_location = logs_location
|
|
32
|
+
self.ignore_system_exit = ignore_system_exit
|
|
33
|
+
|
|
34
|
+
def __enter__(self):
|
|
35
|
+
info("Initializing program...")
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def __exit__(
|
|
39
|
+
self,
|
|
40
|
+
exc_type: type[BaseException] | None,
|
|
41
|
+
exc_val: BaseException | None,
|
|
42
|
+
exc_tb: TracebackType | None,
|
|
43
|
+
):
|
|
44
|
+
if (not exc_type) or (not exc_val) or (not exc_tb):
|
|
45
|
+
info("Program exited successfully...")
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
if exc_type is SystemExit and self.ignore_system_exit:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Attempt to let a handler know something is up
|
|
53
|
+
# so it can get to the user
|
|
54
|
+
if self._fatal_handler:
|
|
55
|
+
self._fatal_handler(exc_val, exc_tb, self.logs_location)
|
|
56
|
+
except Exception:
|
|
57
|
+
# If that fails, at least get something to the user
|
|
58
|
+
sys.stderr.write("An extra fatal error occurred...")
|
|
59
|
+
|
|
60
|
+
fatal(f"A fatal error has occurred - {','.join(exc_val.args)}", exc_val)
|
|
61
|
+
info("Program exited with fatal errors...")
|
|
62
|
+
|
|
63
|
+
return True # Return true because we have processed the error
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
LOGGER_NAME = "cli_logger"
|
|
67
|
+
MAX_LOGS = 5
|
|
68
|
+
DEFAULT_FILE_LOG_LEVEL = 10
|
|
69
|
+
DEFAULT_CONSOLE_LOG_LEVEL = 60
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_logs_location():
|
|
73
|
+
user_file_path = Path()
|
|
74
|
+
try:
|
|
75
|
+
user_file_path = Config().user_files_path
|
|
76
|
+
except Exception:
|
|
77
|
+
user_file_path = Path(user_data_dir(__appname__, appauthor=False))
|
|
78
|
+
finally:
|
|
79
|
+
return user_file_path / "logs"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_logger = logging.getLogger(LOGGER_NAME)
|
|
83
|
+
|
|
84
|
+
_logger.setLevel(10)
|
|
85
|
+
|
|
86
|
+
file_formatter = logging.Formatter(
|
|
87
|
+
"{asctime} - {levelname} - {message}", style="{", datefmt=r"%Y-%m-%d %H:%M:%S"
|
|
88
|
+
)
|
|
89
|
+
console_formatter = logging.Formatter("{levelname} -> {message}", style="{")
|
|
90
|
+
|
|
91
|
+
console_handler = logging.StreamHandler()
|
|
92
|
+
console_handler.setFormatter(console_formatter)
|
|
93
|
+
console_handler.setLevel(DEFAULT_CONSOLE_LOG_LEVEL)
|
|
94
|
+
_logger.addHandler(console_handler)
|
|
95
|
+
|
|
96
|
+
log_dir = get_logs_location()
|
|
97
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
current_time = datetime.datetime.now()
|
|
100
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
101
|
+
get_logs_location() / f"{current_time.isoformat().replace(':', '.')}.log",
|
|
102
|
+
backupCount=5,
|
|
103
|
+
encoding="utf-8",
|
|
104
|
+
)
|
|
105
|
+
file_handler.setFormatter(file_formatter)
|
|
106
|
+
file_handler.setLevel(DEFAULT_FILE_LOG_LEVEL)
|
|
107
|
+
_logger.addHandler(file_handler)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_console_log_level():
|
|
111
|
+
return console_handler.level
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def set_console_log_level(value: logging._Level):
|
|
115
|
+
console_handler.setLevel(value)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_file_log_level():
|
|
119
|
+
return file_handler.level
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def set_file_log_level(value: logging._Level):
|
|
123
|
+
file_handler.setLevel(value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def set_cli_verbosity(level: int):
|
|
127
|
+
"""
|
|
128
|
+
Set how extreme the error has to
|
|
129
|
+
be for it to be printed in the CLI.
|
|
130
|
+
|
|
131
|
+
Default is 0.
|
|
132
|
+
|
|
133
|
+
0 = No Statements To CLI
|
|
134
|
+
1 = Fatal
|
|
135
|
+
2 = Warnings
|
|
136
|
+
3 = Info
|
|
137
|
+
"""
|
|
138
|
+
level_conversion = {
|
|
139
|
+
0: 60,
|
|
140
|
+
1: 50,
|
|
141
|
+
2: 30,
|
|
142
|
+
3: 20,
|
|
143
|
+
}
|
|
144
|
+
other = 10 # If anything else, default to debug.
|
|
145
|
+
console_handler.setLevel(level_conversion.get(level, other))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def safe(fatal_handler: FatalHandler | None = None):
|
|
149
|
+
return FatalCatcher(get_logs_location(), fatal_handler)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
_stack_always = False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def set_stack_always(value: bool):
|
|
156
|
+
global _stack_always
|
|
157
|
+
|
|
158
|
+
_stack_always = value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_stack_always(passthrough: bool):
|
|
162
|
+
"""
|
|
163
|
+
If _stack_always is true, return true.
|
|
164
|
+
|
|
165
|
+
Otherwise return passthrough.
|
|
166
|
+
"""
|
|
167
|
+
return True if _stack_always else passthrough
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def debug(
|
|
171
|
+
content: str, exc_info: logging._ExcInfoType = None, stack_info: bool = False
|
|
172
|
+
):
|
|
173
|
+
_logger.debug(content, exc_info=exc_info, stack_info=is_stack_always(stack_info))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def info(content: str, exc_info: logging._ExcInfoType = None, stack_info: bool = False):
|
|
177
|
+
_logger.info(content, exc_info=exc_info, stack_info=is_stack_always(stack_info))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def warn(content: str, exc_info: logging._ExcInfoType = None, stack_info: bool = False):
|
|
181
|
+
_logger.warning(content, exc_info=exc_info, stack_info=is_stack_always(stack_info))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def error(content: str, exc_info: logging._ExcInfoType = None):
|
|
185
|
+
_logger.error(content, exc_info=exc_info, stack_info=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def fatal(content: str, exc_info: logging._ExcInfoType = None):
|
|
189
|
+
_logger.critical(content, exc_info=exc_info, stack_info=True)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def log(
|
|
193
|
+
level: int,
|
|
194
|
+
content: str,
|
|
195
|
+
exc_info: logging._ExcInfoType = None,
|
|
196
|
+
stack_info: bool = False,
|
|
197
|
+
):
|
|
198
|
+
_logger.log(
|
|
199
|
+
level, content, exc_info=exc_info, stack_info=is_stack_always(stack_info)
|
|
200
|
+
)
|
anipy_cli/mal_proxy.py
CHANGED
|
@@ -82,7 +82,10 @@ class MyAnimeListProxy:
|
|
|
82
82
|
config = Config()
|
|
83
83
|
for e in mylist:
|
|
84
84
|
if self.local_list.mappings.get(e.id, None):
|
|
85
|
-
if
|
|
85
|
+
if (
|
|
86
|
+
e.my_list_status
|
|
87
|
+
and config.tracker_ignore_tag in e.my_list_status.tags
|
|
88
|
+
):
|
|
86
89
|
self.local_list.mappings.pop(e.id)
|
|
87
90
|
else:
|
|
88
91
|
self.local_list.mappings[e.id].mal_anime = e
|
|
@@ -112,7 +115,10 @@ class MyAnimeListProxy:
|
|
|
112
115
|
status_catagories
|
|
113
116
|
if status_catagories is not None
|
|
114
117
|
else set(
|
|
115
|
-
[
|
|
118
|
+
[
|
|
119
|
+
MALMyListStatusEnum[s.upper()]
|
|
120
|
+
for s in config.tracker_status_categories
|
|
121
|
+
]
|
|
116
122
|
)
|
|
117
123
|
)
|
|
118
124
|
|
|
@@ -120,7 +126,7 @@ class MyAnimeListProxy:
|
|
|
120
126
|
mylist.extend(
|
|
121
127
|
filter(
|
|
122
128
|
lambda e: (
|
|
123
|
-
config.
|
|
129
|
+
config.tracker_ignore_tag not in e.my_list_status.tags
|
|
124
130
|
if e.my_list_status
|
|
125
131
|
else True
|
|
126
132
|
),
|
|
@@ -145,7 +151,7 @@ class MyAnimeListProxy:
|
|
|
145
151
|
tags: Set[str] = set(),
|
|
146
152
|
) -> MALMyListStatus:
|
|
147
153
|
config = Config()
|
|
148
|
-
tags |= set(config.
|
|
154
|
+
tags |= set(config.tracker_tags)
|
|
149
155
|
result = self.mal.update_anime_list(
|
|
150
156
|
anime.id, status=status, watched_episodes=episode, tags=list(tags)
|
|
151
157
|
)
|
|
@@ -183,9 +189,9 @@ class MyAnimeListProxy:
|
|
|
183
189
|
adapter = MyAnimeListAdapter(self.mal, p)
|
|
184
190
|
result = adapter.from_myanimelist(
|
|
185
191
|
anime,
|
|
186
|
-
config.
|
|
187
|
-
config.
|
|
188
|
-
config.
|
|
192
|
+
config.tracker_mapping_min_similarity,
|
|
193
|
+
config.tracker_mapping_use_filters,
|
|
194
|
+
config.tracker_mapping_use_alternatives,
|
|
189
195
|
)
|
|
190
196
|
|
|
191
197
|
if result is not None:
|
|
@@ -212,8 +218,8 @@ class MyAnimeListProxy:
|
|
|
212
218
|
adapter = MyAnimeListAdapter(self.mal, anime.provider)
|
|
213
219
|
result = adapter.from_provider(
|
|
214
220
|
anime,
|
|
215
|
-
config.
|
|
216
|
-
config.
|
|
221
|
+
config.tracker_mapping_min_similarity,
|
|
222
|
+
config.tracker_mapping_use_alternatives,
|
|
217
223
|
)
|
|
218
224
|
|
|
219
225
|
if result:
|
anipy_cli/menus/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from anipy_cli.menus.menu import Menu
|
|
2
2
|
from anipy_cli.menus.mal_menu import MALMenu
|
|
3
|
+
from anipy_cli.menus.anilist_menu import AniListMenu
|
|
3
4
|
from anipy_cli.menus.seasonal_menu import SeasonalMenu
|
|
4
5
|
|
|
5
|
-
__all__ = ["Menu", "MALMenu", "SeasonalMenu"]
|
|
6
|
+
__all__ = ["Menu", "MALMenu", "AniListMenu", "SeasonalMenu"]
|