anipy-cli 3.5.9__py3-none-any.whl → 3.7.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 +39 -0
- anipy_cli/cli.py +23 -2
- anipy_cli/clis/__init__.py +2 -0
- anipy_cli/clis/anilist_cli.py +62 -0
- anipy_cli/colors.py +7 -4
- anipy_cli/config.py +47 -43
- anipy_cli/download_component.py +17 -7
- anipy_cli/logger.py +199 -0
- 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/base_menu.py +4 -5
- anipy_cli/menus/mal_menu.py +15 -15
- anipy_cli/menus/menu.py +5 -2
- anipy_cli/menus/seasonal_menu.py +4 -4
- anipy_cli/util.py +22 -5
- {anipy_cli-3.5.9.dist-info → anipy_cli-3.7.0.dist-info}/METADATA +2 -2
- anipy_cli-3.7.0.dist-info/RECORD +31 -0
- anipy_cli-3.5.9.dist-info/RECORD +0 -27
- {anipy_cli-3.5.9.dist-info → anipy_cli-3.7.0.dist-info}/WHEEL +0 -0
- {anipy_cli-3.5.9.dist-info → anipy_cli-3.7.0.dist-info}/entry_points.txt +0 -0
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
|
|
@@ -54,11 +55,13 @@ class DownloadComponent:
|
|
|
54
55
|
def progress_indicator(percentage: float):
|
|
55
56
|
s.set_text(f"Progress: {percentage:.1f}%")
|
|
56
57
|
|
|
57
|
-
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)
|
|
58
60
|
s.write(f"> {message}")
|
|
59
61
|
|
|
60
|
-
def error_display(message: str):
|
|
61
|
-
|
|
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}")
|
|
62
65
|
|
|
63
66
|
downloader = Downloader(progress_indicator, info_display, error_display)
|
|
64
67
|
|
|
@@ -93,7 +96,12 @@ class DownloadComponent:
|
|
|
93
96
|
for ep in eps:
|
|
94
97
|
try:
|
|
95
98
|
self.download_ep(spinner, downloader, anime, lang, ep, sub_only)
|
|
96
|
-
except Exception as
|
|
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
|
+
)
|
|
97
105
|
if only_skip_ep_on_err:
|
|
98
106
|
error_msg = f"! Issues downloading episode {ep} of {anime.name}. Skipping..."
|
|
99
107
|
else:
|
|
@@ -101,7 +109,7 @@ class DownloadComponent:
|
|
|
101
109
|
spinner.write(
|
|
102
110
|
color(
|
|
103
111
|
colors.RED,
|
|
104
|
-
f"! Error: {
|
|
112
|
+
f"! Error: {anime_download_error}\n",
|
|
105
113
|
error_msg,
|
|
106
114
|
)
|
|
107
115
|
)
|
|
@@ -136,9 +144,11 @@ class DownloadComponent:
|
|
|
136
144
|
|
|
137
145
|
stream = anime.get_video(ep, lang, preferred_quality=self.options.quality)
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
f"
|
|
147
|
+
download_message_update = (
|
|
148
|
+
f"Downloading Episode {stream.episode} of {anime.name} ({lang})"
|
|
141
149
|
)
|
|
150
|
+
logger.info(download_message_update)
|
|
151
|
+
spinner.write(f"> {download_message_update}")
|
|
142
152
|
|
|
143
153
|
spinner.set_text("Downloading...")
|
|
144
154
|
|
anipy_cli/logger.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
_logger = logging.getLogger(LOGGER_NAME)
|
|
82
|
+
|
|
83
|
+
_logger.setLevel(10)
|
|
84
|
+
|
|
85
|
+
file_formatter = logging.Formatter(
|
|
86
|
+
"{asctime} - {levelname} - {message}", style="{", datefmt=r"%Y-%m-%d %H:%M:%S"
|
|
87
|
+
)
|
|
88
|
+
console_formatter = logging.Formatter("{levelname} -> {message}", style="{")
|
|
89
|
+
|
|
90
|
+
console_handler = logging.StreamHandler()
|
|
91
|
+
console_handler.setFormatter(console_formatter)
|
|
92
|
+
console_handler.setLevel(DEFAULT_CONSOLE_LOG_LEVEL)
|
|
93
|
+
_logger.addHandler(console_handler)
|
|
94
|
+
|
|
95
|
+
log_dir = get_logs_location()
|
|
96
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
current_time = datetime.datetime.now()
|
|
99
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
100
|
+
get_logs_location() / f"{current_time.isoformat().replace(':', '.')}.log",
|
|
101
|
+
backupCount=5,
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
)
|
|
104
|
+
file_handler.setFormatter(file_formatter)
|
|
105
|
+
file_handler.setLevel(DEFAULT_FILE_LOG_LEVEL)
|
|
106
|
+
_logger.addHandler(file_handler)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_console_log_level():
|
|
110
|
+
return console_handler.level
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def set_console_log_level(value: logging._Level):
|
|
114
|
+
console_handler.setLevel(value)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_file_log_level():
|
|
118
|
+
return file_handler.level
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def set_file_log_level(value: logging._Level):
|
|
122
|
+
file_handler.setLevel(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def set_cli_verbosity(level: int):
|
|
126
|
+
"""
|
|
127
|
+
Set how extreme the error has to
|
|
128
|
+
be for it to be printed in the CLI.
|
|
129
|
+
|
|
130
|
+
Default is 0.
|
|
131
|
+
|
|
132
|
+
0 = No Statements To CLI
|
|
133
|
+
1 = Fatal
|
|
134
|
+
2 = Warnings
|
|
135
|
+
3 = Info
|
|
136
|
+
"""
|
|
137
|
+
level_conversion = {
|
|
138
|
+
0: 60,
|
|
139
|
+
1: 50,
|
|
140
|
+
2: 30,
|
|
141
|
+
3: 20,
|
|
142
|
+
}
|
|
143
|
+
other = 10 # If anything else, default to debug.
|
|
144
|
+
console_handler.setLevel(level_conversion.get(level, other))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def safe(fatal_handler: FatalHandler | None = None):
|
|
148
|
+
return FatalCatcher(get_logs_location(), fatal_handler)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
_stack_always = False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def set_stack_always(value: bool):
|
|
155
|
+
global _stack_always
|
|
156
|
+
|
|
157
|
+
_stack_always = value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def is_stack_always(passthrough: bool):
|
|
161
|
+
"""
|
|
162
|
+
If _stack_always is true, return true.
|
|
163
|
+
|
|
164
|
+
Otherwise return passthrough.
|
|
165
|
+
"""
|
|
166
|
+
return True if _stack_always else passthrough
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def debug(
|
|
170
|
+
content: str, exc_info: logging._ExcInfoType = None, stack_info: bool = False
|
|
171
|
+
):
|
|
172
|
+
_logger.debug(content, exc_info=exc_info, stack_info=is_stack_always(stack_info))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def info(content: str, exc_info: logging._ExcInfoType = None, stack_info: bool = False):
|
|
176
|
+
_logger.info(content, exc_info=exc_info, stack_info=is_stack_always(stack_info))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def warn(content: str, exc_info: logging._ExcInfoType = None, stack_info: bool = False):
|
|
180
|
+
_logger.warning(content, exc_info=exc_info, stack_info=is_stack_always(stack_info))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def error(content: str, exc_info: logging._ExcInfoType = None):
|
|
184
|
+
_logger.error(content, exc_info=exc_info, stack_info=True)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def fatal(content: str, exc_info: logging._ExcInfoType = None):
|
|
188
|
+
_logger.critical(content, exc_info=exc_info, stack_info=True)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def log(
|
|
192
|
+
level: int,
|
|
193
|
+
content: str,
|
|
194
|
+
exc_info: logging._ExcInfoType = None,
|
|
195
|
+
stack_info: bool = False,
|
|
196
|
+
):
|
|
197
|
+
_logger.log(
|
|
198
|
+
level, content, exc_info=exc_info, stack_info=is_stack_always(stack_info)
|
|
199
|
+
)
|
anipy_cli/mal_proxy.py
CHANGED
|
@@ -82,7 +82,7 @@ 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 e.my_list_status and config.
|
|
85
|
+
if e.my_list_status and config.tracker_ignore_tag in e.my_list_status.tags:
|
|
86
86
|
self.local_list.mappings.pop(e.id)
|
|
87
87
|
else:
|
|
88
88
|
self.local_list.mappings[e.id].mal_anime = e
|
|
@@ -112,7 +112,7 @@ class MyAnimeListProxy:
|
|
|
112
112
|
status_catagories
|
|
113
113
|
if status_catagories is not None
|
|
114
114
|
else set(
|
|
115
|
-
[MALMyListStatusEnum[s.upper()] for s in config.
|
|
115
|
+
[MALMyListStatusEnum[s.upper()] for s in config.tracker_status_categories]
|
|
116
116
|
)
|
|
117
117
|
)
|
|
118
118
|
|
|
@@ -120,7 +120,7 @@ class MyAnimeListProxy:
|
|
|
120
120
|
mylist.extend(
|
|
121
121
|
filter(
|
|
122
122
|
lambda e: (
|
|
123
|
-
config.
|
|
123
|
+
config.tracker_ignore_tag not in e.my_list_status.tags
|
|
124
124
|
if e.my_list_status
|
|
125
125
|
else True
|
|
126
126
|
),
|
|
@@ -145,7 +145,7 @@ class MyAnimeListProxy:
|
|
|
145
145
|
tags: Set[str] = set(),
|
|
146
146
|
) -> MALMyListStatus:
|
|
147
147
|
config = Config()
|
|
148
|
-
tags |= set(config.
|
|
148
|
+
tags |= set(config.tracker_tags)
|
|
149
149
|
result = self.mal.update_anime_list(
|
|
150
150
|
anime.id, status=status, watched_episodes=episode, tags=list(tags)
|
|
151
151
|
)
|
|
@@ -183,9 +183,9 @@ class MyAnimeListProxy:
|
|
|
183
183
|
adapter = MyAnimeListAdapter(self.mal, p)
|
|
184
184
|
result = adapter.from_myanimelist(
|
|
185
185
|
anime,
|
|
186
|
-
config.
|
|
187
|
-
config.
|
|
188
|
-
config.
|
|
186
|
+
config.tracker_mapping_min_similarity,
|
|
187
|
+
config.tracker_mapping_use_filters,
|
|
188
|
+
config.tracker_mapping_use_alternatives,
|
|
189
189
|
)
|
|
190
190
|
|
|
191
191
|
if result is not None:
|
|
@@ -212,8 +212,8 @@ class MyAnimeListProxy:
|
|
|
212
212
|
adapter = MyAnimeListAdapter(self.mal, anime.provider)
|
|
213
213
|
result = adapter.from_provider(
|
|
214
214
|
anime,
|
|
215
|
-
config.
|
|
216
|
-
config.
|
|
215
|
+
config.tracker_mapping_min_similarity,
|
|
216
|
+
config.tracker_mapping_use_alternatives,
|
|
217
217
|
)
|
|
218
218
|
|
|
219
219
|
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"]
|