anipy-cli 3.5.8__py3-none-any.whl → 3.6.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/arg_parser.py +20 -0
- anipy_cli/cli.py +22 -2
- anipy_cli/colors.py +7 -4
- anipy_cli/config.py +1 -1
- anipy_cli/download_component.py +17 -7
- anipy_cli/logger.py +199 -0
- anipy_cli/menus/base_menu.py +4 -5
- anipy_cli/menus/mal_menu.py +1 -1
- 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.8.dist-info → anipy_cli-3.6.0.dist-info}/METADATA +2 -2
- anipy_cli-3.6.0.dist-info/RECORD +28 -0
- anipy_cli-3.5.8.dist-info/RECORD +0 -27
- {anipy_cli-3.5.8.dist-info → anipy_cli-3.6.0.dist-info}/WHEEL +0 -0
- {anipy_cli-3.5.8.dist-info → anipy_cli-3.6.0.dist-info}/entry_points.txt +0 -0
anipy_cli/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__appname__ = "anipy-cli"
|
|
2
|
-
__version__ = "3.
|
|
2
|
+
__version__ = "3.6.0"
|
anipy_cli/arg_parser.py
CHANGED
|
@@ -22,6 +22,8 @@ class CliArgs:
|
|
|
22
22
|
optional_player: Optional[str]
|
|
23
23
|
search: Optional[str]
|
|
24
24
|
location: Optional[Path]
|
|
25
|
+
verbosity: int
|
|
26
|
+
stack_always: bool
|
|
25
27
|
mal_password: Optional[str]
|
|
26
28
|
config: bool
|
|
27
29
|
seasonal_search: Optional[str]
|
|
@@ -176,6 +178,24 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
|
|
|
176
178
|
help="Override all configured download locations",
|
|
177
179
|
)
|
|
178
180
|
|
|
181
|
+
options_group.add_argument(
|
|
182
|
+
"-V",
|
|
183
|
+
"--verbose",
|
|
184
|
+
required=False,
|
|
185
|
+
dest="verbosity",
|
|
186
|
+
action="count",
|
|
187
|
+
default=0,
|
|
188
|
+
help="Verbosity levels in the console: -V = 'fatal' -VV = 'warnings' -VVV = 'info'",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
options_group.add_argument(
|
|
192
|
+
"--stack-always",
|
|
193
|
+
required=False,
|
|
194
|
+
dest="stack_always",
|
|
195
|
+
action="store_true",
|
|
196
|
+
help="Always show the stack trace on any log outputs.",
|
|
197
|
+
)
|
|
198
|
+
|
|
179
199
|
options_group.add_argument(
|
|
180
200
|
"-so",
|
|
181
201
|
"--sub-only",
|
anipy_cli/cli.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from types import TracebackType
|
|
1
3
|
from typing import Optional
|
|
2
4
|
|
|
3
5
|
from pypresence.exceptions import DiscordNotFound
|
|
@@ -5,16 +7,34 @@ from pypresence.exceptions import DiscordNotFound
|
|
|
5
7
|
from anipy_api.locallist import LocalList
|
|
6
8
|
from anipy_cli.prompts import migrate_provider
|
|
7
9
|
|
|
8
|
-
from anipy_cli.arg_parser import parse_args
|
|
10
|
+
from anipy_cli.arg_parser import CliArgs, parse_args
|
|
9
11
|
from anipy_cli.clis import *
|
|
10
|
-
from anipy_cli.colors import colors, cprint
|
|
12
|
+
from anipy_cli.colors import color, colors, cprint
|
|
11
13
|
from anipy_cli.util import error, DotSpinner, migrate_locallist
|
|
12
14
|
from anipy_cli.config import Config
|
|
13
15
|
from anipy_cli.discord import DiscordPresence
|
|
16
|
+
import anipy_cli.logger as logger
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def run_cli(override_args: Optional[list[str]] = None):
|
|
17
20
|
args = parse_args(override_args)
|
|
21
|
+
|
|
22
|
+
logger.set_cli_verbosity(args.verbosity)
|
|
23
|
+
logger.set_stack_always(args.stack_always)
|
|
24
|
+
|
|
25
|
+
def fatal_handler(exc_val: BaseException, exc_tb: TracebackType, logs_location: Path):
|
|
26
|
+
print(
|
|
27
|
+
color(
|
|
28
|
+
colors.RED,
|
|
29
|
+
f'A fatal error of type [{exc_val.__class__.__name__}] has occurred with message "{exc_val.args[0]}". Logs can be found at {logs_location}.',
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
with logger.safe(fatal_handler):
|
|
34
|
+
_safe_cli(args)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _safe_cli(args: CliArgs):
|
|
18
38
|
config = Config()
|
|
19
39
|
# This updates the config, adding new values doc changes and the like.
|
|
20
40
|
config._create_config()
|
anipy_cli/colors.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class colors: # noqa: N801
|
|
2
5
|
"""Just a class for colors."""
|
|
3
6
|
|
|
4
7
|
GREEN = "\033[92m"
|
|
@@ -14,7 +17,7 @@ class colors:
|
|
|
14
17
|
RESET = "\033[0m"
|
|
15
18
|
|
|
16
19
|
|
|
17
|
-
def color(*values, sep: str = "") -> str:
|
|
20
|
+
def color(*values: Any, sep: str = "") -> str:
|
|
18
21
|
"""Decorate a string with color codes.
|
|
19
22
|
|
|
20
23
|
Basically just ensures that the color doesn't "leak"
|
|
@@ -24,13 +27,13 @@ def color(*values, sep: str = "") -> str:
|
|
|
24
27
|
return sep.join(map(str, values)) + colors.END
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
def cinput(*prompt, input_color: str = "") -> str:
|
|
30
|
+
def cinput(*prompt: Any, input_color: str = "") -> str:
|
|
28
31
|
"""An input function that handles coloring input."""
|
|
29
32
|
inp = input(color(*prompt) + input_color)
|
|
30
33
|
print(colors.END, end="")
|
|
31
34
|
return inp
|
|
32
35
|
|
|
33
36
|
|
|
34
|
-
def cprint(*values, sep: str = "", **kwargs) -> None:
|
|
37
|
+
def cprint(*values: Any, sep: str = "", **kwargs: Any) -> None:
|
|
35
38
|
"""Prints colored text."""
|
|
36
39
|
print(color(*values, sep=sep), **kwargs)
|
anipy_cli/config.py
CHANGED
|
@@ -419,7 +419,7 @@ class Config:
|
|
|
419
419
|
except RuntimeError:
|
|
420
420
|
return fallback
|
|
421
421
|
|
|
422
|
-
def _get_value(self, key: str, fallback, _type: Type) -> Any:
|
|
422
|
+
def _get_value(self, key: str, fallback: Any, _type: Type) -> Any:
|
|
423
423
|
value = self._yaml_conf.get(key, fallback)
|
|
424
424
|
if isinstance(value, _type):
|
|
425
425
|
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
|
|
@@ -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/menus/base_menu.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from abc import ABC, abstractmethod
|
|
3
2
|
from dataclasses import dataclass
|
|
4
3
|
from typing import Callable, List
|
|
5
4
|
|
|
6
5
|
from anipy_cli.colors import color, colors
|
|
7
|
-
from anipy_cli.util import error
|
|
6
|
+
from anipy_cli.util import clear_screen, error
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
@dataclass(frozen=True)
|
|
@@ -40,9 +39,9 @@ class MenuBase(ABC):
|
|
|
40
39
|
|
|
41
40
|
op.callback()
|
|
42
41
|
|
|
43
|
-
def print_options(self,
|
|
44
|
-
if
|
|
45
|
-
|
|
42
|
+
def print_options(self, should_clear_screen: bool = True):
|
|
43
|
+
if should_clear_screen:
|
|
44
|
+
clear_screen()
|
|
46
45
|
|
|
47
46
|
self.print_header()
|
|
48
47
|
for op in self.menu_options:
|
anipy_cli/menus/mal_menu.py
CHANGED
|
@@ -275,7 +275,7 @@ class MALMenu(MenuBase):
|
|
|
275
275
|
)
|
|
276
276
|
DownloadComponent.serve_download_errors(errors)
|
|
277
277
|
|
|
278
|
-
self.print_options(
|
|
278
|
+
self.print_options(should_clear_screen=len(errors) == 0)
|
|
279
279
|
|
|
280
280
|
def binge_latest(self):
|
|
281
281
|
picked = self._choose_latest()
|
anipy_cli/menus/menu.py
CHANGED
|
@@ -6,6 +6,7 @@ from InquirerPy.base.control import Choice
|
|
|
6
6
|
from anipy_api.download import Downloader
|
|
7
7
|
from anipy_api.provider import LanguageTypeEnum, ProviderStream
|
|
8
8
|
from anipy_api.locallist import LocalList
|
|
9
|
+
import anipy_cli.logger as logger
|
|
9
10
|
|
|
10
11
|
from anipy_cli.colors import colors, cprint
|
|
11
12
|
from anipy_cli.config import Config
|
|
@@ -221,10 +222,12 @@ class Menu(MenuBase):
|
|
|
221
222
|
def progress_indicator(percentage: float):
|
|
222
223
|
s.set_text(f"Downloading ({percentage:.1f}%)")
|
|
223
224
|
|
|
224
|
-
def info_display(message: str):
|
|
225
|
+
def info_display(message: str, exc_info: BaseException | None = None):
|
|
226
|
+
logger.info(message, exc_info, exc_info is not None)
|
|
225
227
|
s.write(f"> {message}")
|
|
226
228
|
|
|
227
|
-
def error_display(message: str):
|
|
229
|
+
def error_display(message: str, exc_info: BaseException | None = None):
|
|
230
|
+
logger.error(message, exc_info)
|
|
228
231
|
s.write(f"{colors.RED}! {message}{colors.END}")
|
|
229
232
|
|
|
230
233
|
downloader = Downloader(progress_indicator, info_display, error_display)
|
anipy_cli/menus/seasonal_menu.py
CHANGED
|
@@ -8,7 +8,7 @@ from anipy_api.anime import Anime
|
|
|
8
8
|
from anipy_api.provider import LanguageTypeEnum
|
|
9
9
|
from anipy_api.provider.base import Episode
|
|
10
10
|
from anipy_api.locallist import LocalList, LocalListEntry
|
|
11
|
-
from anipy_api.error import
|
|
11
|
+
from anipy_api.error import ProviderNotAvailableError
|
|
12
12
|
from InquirerPy import inquirer
|
|
13
13
|
from InquirerPy.base.control import Choice
|
|
14
14
|
from InquirerPy.utils import get_style
|
|
@@ -67,7 +67,7 @@ class SeasonalMenu(MenuBase):
|
|
|
67
67
|
for s in self.seasonal_list.get_all():
|
|
68
68
|
try:
|
|
69
69
|
anime = Anime.from_local_list_entry(s)
|
|
70
|
-
except
|
|
70
|
+
except ProviderNotAvailableError:
|
|
71
71
|
error(
|
|
72
72
|
f"Can not load '{s.name}' because the configured provider"
|
|
73
73
|
f" '{s.provider}' was not found, maybe try to migrate"
|
|
@@ -212,7 +212,7 @@ class SeasonalMenu(MenuBase):
|
|
|
212
212
|
|
|
213
213
|
def migrate_provider(self):
|
|
214
214
|
migrate_provider("seasonal", self.seasonal_list)
|
|
215
|
-
self.print_options(
|
|
215
|
+
self.print_options(should_clear_screen=True)
|
|
216
216
|
|
|
217
217
|
def download_latest(self):
|
|
218
218
|
picked = self._choose_latest()
|
|
@@ -232,7 +232,7 @@ class SeasonalMenu(MenuBase):
|
|
|
232
232
|
|
|
233
233
|
if not self.options.auto_update:
|
|
234
234
|
# Clear screen only if there were no issues
|
|
235
|
-
self.print_options(
|
|
235
|
+
self.print_options(should_clear_screen=len(failed_series) == 0)
|
|
236
236
|
|
|
237
237
|
def binge_latest(self):
|
|
238
238
|
picked = self._choose_latest()
|
anipy_cli/util.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
1
3
|
import sys
|
|
2
4
|
import subprocess as sp
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import (
|
|
5
7
|
TYPE_CHECKING,
|
|
8
|
+
Any,
|
|
6
9
|
Iterator,
|
|
7
10
|
List,
|
|
8
11
|
Literal,
|
|
@@ -12,6 +15,8 @@ from typing import (
|
|
|
12
15
|
overload,
|
|
13
16
|
)
|
|
14
17
|
|
|
18
|
+
import anipy_cli.logger as logger
|
|
19
|
+
|
|
15
20
|
from anipy_api.anime import Anime
|
|
16
21
|
from anipy_api.download import Downloader, PostDownloadCallback
|
|
17
22
|
from anipy_api.locallist import LocalListData
|
|
@@ -31,7 +36,7 @@ if TYPE_CHECKING:
|
|
|
31
36
|
|
|
32
37
|
|
|
33
38
|
class DotSpinner(Yaspin):
|
|
34
|
-
def __init__(self, *text_and_colors, **spinner_args):
|
|
39
|
+
def __init__(self, *text_and_colors: Any, **spinner_args: Any):
|
|
35
40
|
super().__init__(
|
|
36
41
|
text=color(*text_and_colors),
|
|
37
42
|
color="cyan",
|
|
@@ -43,21 +48,26 @@ class DotSpinner(Yaspin):
|
|
|
43
48
|
self.start()
|
|
44
49
|
return self
|
|
45
50
|
|
|
46
|
-
def set_text(self, *text_and_colors):
|
|
51
|
+
def set_text(self, *text_and_colors: Any):
|
|
47
52
|
self.text = color(*text_and_colors)
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
@overload
|
|
51
56
|
def error(error: str, fatal: Literal[True]) -> NoReturn: ...
|
|
52
57
|
@overload
|
|
53
|
-
def error(
|
|
58
|
+
def error(
|
|
59
|
+
error: str, fatal: Literal[False] = ..., log_level: int = logging.INFO
|
|
60
|
+
) -> None: ...
|
|
54
61
|
|
|
55
62
|
|
|
56
|
-
def error(
|
|
63
|
+
def error(
|
|
64
|
+
error: str, fatal: bool = False, log_level: int = logging.INFO
|
|
65
|
+
) -> Union[NoReturn, None]:
|
|
57
66
|
if not fatal:
|
|
58
67
|
sys.stderr.write(
|
|
59
68
|
color(colors.RED, "anipy-cli: error: ", colors.END, f"{error}\n")
|
|
60
69
|
)
|
|
70
|
+
logger.log(log_level, error)
|
|
61
71
|
return
|
|
62
72
|
|
|
63
73
|
sys.stderr.write(
|
|
@@ -68,9 +78,16 @@ def error(error: str, fatal: bool = False) -> Union[NoReturn, None]:
|
|
|
68
78
|
f"{error}, exiting\n",
|
|
69
79
|
)
|
|
70
80
|
)
|
|
81
|
+
logger.warn(error)
|
|
71
82
|
sys.exit(1)
|
|
72
83
|
|
|
73
84
|
|
|
85
|
+
def clear_screen():
|
|
86
|
+
if logger.get_console_log_level() < 60:
|
|
87
|
+
return
|
|
88
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
89
|
+
|
|
90
|
+
|
|
74
91
|
def get_prefered_providers(mode: str) -> Iterator["BaseProvider"]:
|
|
75
92
|
config = Config()
|
|
76
93
|
preferred_providers = config.providers[mode]
|
|
@@ -206,7 +223,7 @@ def get_configured_player(player_override: Optional[str] = None) -> "PlayerBase"
|
|
|
206
223
|
return get_player(player, args, discord_cb)
|
|
207
224
|
|
|
208
225
|
|
|
209
|
-
def get_anime_season(month):
|
|
226
|
+
def get_anime_season(month: int):
|
|
210
227
|
if 1 <= month <= 3:
|
|
211
228
|
return "Winter"
|
|
212
229
|
elif 4 <= month <= 6:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: anipy-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: Watch and Download anime from the comfort of your Terminal
|
|
5
5
|
License: GPL-3.0
|
|
6
6
|
Keywords: anime,cli
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
-
Requires-Dist: anipy-api (>=3.
|
|
17
|
+
Requires-Dist: anipy-api (>=3.6.0,<4.0.0)
|
|
18
18
|
Requires-Dist: appdirs (>=1.4.4,<2.0.0)
|
|
19
19
|
Requires-Dist: inquirerpy (>=0.3.4,<0.4.0)
|
|
20
20
|
Requires-Dist: pypresence (>=4.3.0,<5.0.0)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
anipy_cli/__init__.py,sha256=nsrqDoMdWxA4pIvlvZQnqkEN5MBQzRO_nHuLtE2x7Us,48
|
|
2
|
+
anipy_cli/arg_parser.py,sha256=zQEUHlj-masxY-_CPeEjR9YDy2-p8R81kf8mIs95hK4,6972
|
|
3
|
+
anipy_cli/cli.py,sha256=HhVt7W_W6pDgFYPDj0hEbjOmqTEsL5_4xy0dBsz61GU,2868
|
|
4
|
+
anipy_cli/clis/__init__.py,sha256=Y00uiPWiMvvRImxJMvfLA55BOkMUOrrx5vJUNvquNsY,411
|
|
5
|
+
anipy_cli/clis/base_cli.py,sha256=JfS7mnxNgTK4_Pqeg4IyjHluhfVyO_YLL_TqdyTtyiQ,803
|
|
6
|
+
anipy_cli/clis/binge_cli.py,sha256=ioZ-V0WfGYBqETFkd8epGrT9dPHwsRJ1qvIdqf4waIs,2551
|
|
7
|
+
anipy_cli/clis/default_cli.py,sha256=aJrJwtwdD7l-Z3dMjSHlvMvgTVnwA3_OXwS-9DZQIy8,3078
|
|
8
|
+
anipy_cli/clis/download_cli.py,sha256=sREoLhgiPk5nQ7eFzbbGznyt80_uaE4VhiOCxrg4ce0,2400
|
|
9
|
+
anipy_cli/clis/history_cli.py,sha256=2ccv6BpQQpUhY4K-KM7lO9qxVLXBrmCY5lec6czipSE,2863
|
|
10
|
+
anipy_cli/clis/mal_cli.py,sha256=_tSLgDUOa6GOZNyCncSSzaVj088y5GAKkHVRSndLLxk,2258
|
|
11
|
+
anipy_cli/clis/seasonal_cli.py,sha256=GV2TQNm9UotG1cxfYbrFFgg7Jmy8SFa7w_GlFtPdRVE,616
|
|
12
|
+
anipy_cli/colors.py,sha256=l4KJoAMnkie6guktKMnYcfAHajPTMamTsPHiUIxF92c,974
|
|
13
|
+
anipy_cli/config.py,sha256=N_hs5bvRZZwx_oVhmAqq1jyAkmJgeP2DKLEpmoyN5DY,17980
|
|
14
|
+
anipy_cli/discord.py,sha256=c6mdqnEdblzZBYs3cGP66oDeS4ySm59OfTRP-R-Duls,1160
|
|
15
|
+
anipy_cli/download_component.py,sha256=8W_AMeT1pIdA18uaMQosf6W7V9QhF-hf5nipZ0UwGvw,6216
|
|
16
|
+
anipy_cli/logger.py,sha256=beLn_fr4iRFU_AuUvhuA6Y25X4kheK7Hr6mQmugvXi0,5194
|
|
17
|
+
anipy_cli/mal_proxy.py,sha256=me2ESB442pYeNEpHY8mqrOEb477UA0uAg2LprKcp8sM,7098
|
|
18
|
+
anipy_cli/menus/__init__.py,sha256=aIzbphxAW-QGfZwR1DIegFZuTJp1O3tSUnai0f0f4lY,185
|
|
19
|
+
anipy_cli/menus/base_menu.py,sha256=vDvPI36grmk0Dklthj3d_3yE_vG9oyHWNrWwVvJLxpg,1132
|
|
20
|
+
anipy_cli/menus/mal_menu.py,sha256=IYblCvY8Qq5X7btdcPawkJ9-DY2SR1p2czjgugqnOzY,23704
|
|
21
|
+
anipy_cli/menus/menu.py,sha256=BasA7VOLS8ajlAJ2pFANiHm9BeBiRDSdyThWBcA4AjE,8645
|
|
22
|
+
anipy_cli/menus/seasonal_menu.py,sha256=CdtK1Z98z31et0q18MeVqdsIrP3IWOxX9CALOyTRCDE,8949
|
|
23
|
+
anipy_cli/prompts.py,sha256=fegNqV7mPxY2bG6OjrB23hjFce9GvOyLisA3eHrJegs,11909
|
|
24
|
+
anipy_cli/util.py,sha256=D-eSkUakEk6WMT-eHfTdzfM8THdIrdQUV8z-wnEMtq8,7102
|
|
25
|
+
anipy_cli-3.6.0.dist-info/METADATA,sha256=-MeFeR-O8JuNZQ9BqdbH5pyEMMHBXINMPgs_oDtue90,3481
|
|
26
|
+
anipy_cli-3.6.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
27
|
+
anipy_cli-3.6.0.dist-info/entry_points.txt,sha256=86iXpcm_ECFndrt0JAI2mqYfXC2Ar7mGi0iOaxCrNP0,51
|
|
28
|
+
anipy_cli-3.6.0.dist-info/RECORD,,
|
anipy_cli-3.5.8.dist-info/RECORD
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
anipy_cli/__init__.py,sha256=Oc_RaEDdTBzwNwjRQpCDNZZbxgPF58GgJK_SjMLV7kU,48
|
|
2
|
-
anipy_cli/arg_parser.py,sha256=efUOHrMcKkEELbkxsudSzHC0FF2Z_tSFDTmDBqirVGY,6459
|
|
3
|
-
anipy_cli/cli.py,sha256=Bk2H15VRF34yKLbGgKVq0A2vdByMJj-JTyx7Ep-JbH0,2238
|
|
4
|
-
anipy_cli/clis/__init__.py,sha256=Y00uiPWiMvvRImxJMvfLA55BOkMUOrrx5vJUNvquNsY,411
|
|
5
|
-
anipy_cli/clis/base_cli.py,sha256=JfS7mnxNgTK4_Pqeg4IyjHluhfVyO_YLL_TqdyTtyiQ,803
|
|
6
|
-
anipy_cli/clis/binge_cli.py,sha256=ioZ-V0WfGYBqETFkd8epGrT9dPHwsRJ1qvIdqf4waIs,2551
|
|
7
|
-
anipy_cli/clis/default_cli.py,sha256=aJrJwtwdD7l-Z3dMjSHlvMvgTVnwA3_OXwS-9DZQIy8,3078
|
|
8
|
-
anipy_cli/clis/download_cli.py,sha256=sREoLhgiPk5nQ7eFzbbGznyt80_uaE4VhiOCxrg4ce0,2400
|
|
9
|
-
anipy_cli/clis/history_cli.py,sha256=2ccv6BpQQpUhY4K-KM7lO9qxVLXBrmCY5lec6czipSE,2863
|
|
10
|
-
anipy_cli/clis/mal_cli.py,sha256=_tSLgDUOa6GOZNyCncSSzaVj088y5GAKkHVRSndLLxk,2258
|
|
11
|
-
anipy_cli/clis/seasonal_cli.py,sha256=GV2TQNm9UotG1cxfYbrFFgg7Jmy8SFa7w_GlFtPdRVE,616
|
|
12
|
-
anipy_cli/colors.py,sha256=voXC7z1Fs9tHg4zzNTNMIrt9k-EVgJ3_xEf5KiW2xgo,916
|
|
13
|
-
anipy_cli/config.py,sha256=Vc2KlfbZddQw7Jb8GNUAiUSAGjKVgdRo4h8icClW75k,17975
|
|
14
|
-
anipy_cli/discord.py,sha256=c6mdqnEdblzZBYs3cGP66oDeS4ySm59OfTRP-R-Duls,1160
|
|
15
|
-
anipy_cli/download_component.py,sha256=5TxlipuaN0tHZkysOmGJB8kd9CxJBXuD_CGnYzU4r-Q,5590
|
|
16
|
-
anipy_cli/mal_proxy.py,sha256=me2ESB442pYeNEpHY8mqrOEb477UA0uAg2LprKcp8sM,7098
|
|
17
|
-
anipy_cli/menus/__init__.py,sha256=aIzbphxAW-QGfZwR1DIegFZuTJp1O3tSUnai0f0f4lY,185
|
|
18
|
-
anipy_cli/menus/base_menu.py,sha256=g5b9Z7SpvCxcq_vqObcPzxLwcXeGPltLgSwa0sEzyfk,1140
|
|
19
|
-
anipy_cli/menus/mal_menu.py,sha256=jAVJh7K5d0BCnoT4qUIZ7CavrnrctXj-eg9k8E90ulE,23697
|
|
20
|
-
anipy_cli/menus/menu.py,sha256=QEZfUr_9Epc9ghuZDZGxJxBkmcQK86ZXJaMChzzygko,8416
|
|
21
|
-
anipy_cli/menus/seasonal_menu.py,sha256=rH_6XRKN_2DvMAo5LHuzoO8aATkHgz66dO3kGATnGLs,8925
|
|
22
|
-
anipy_cli/prompts.py,sha256=fegNqV7mPxY2bG6OjrB23hjFce9GvOyLisA3eHrJegs,11909
|
|
23
|
-
anipy_cli/util.py,sha256=Omoq_OtfyxUYvaWyjsinDv50xNVHtDEDVb2cQAGVwu8,6745
|
|
24
|
-
anipy_cli-3.5.8.dist-info/METADATA,sha256=mTAM7R4gcwJoMvsnnxaTAd8wswKgize1zQIJFP7BN3Y,3481
|
|
25
|
-
anipy_cli-3.5.8.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
26
|
-
anipy_cli-3.5.8.dist-info/entry_points.txt,sha256=86iXpcm_ECFndrt0JAI2mqYfXC2Ar7mGi0iOaxCrNP0,51
|
|
27
|
-
anipy_cli-3.5.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|