anipy-cli 3.5.9__tar.gz → 3.6.0__tar.gz

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.

Files changed (27) hide show
  1. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/PKG-INFO +2 -2
  2. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/pyproject.toml +2 -2
  3. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/__init__.py +1 -1
  4. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/arg_parser.py +20 -0
  5. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/cli.py +22 -2
  6. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/colors.py +7 -4
  7. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/config.py +1 -1
  8. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/download_component.py +17 -7
  9. anipy_cli-3.6.0/src/anipy_cli/logger.py +199 -0
  10. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/menus/base_menu.py +4 -5
  11. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/menus/mal_menu.py +1 -1
  12. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/menus/menu.py +5 -2
  13. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/menus/seasonal_menu.py +4 -4
  14. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/util.py +22 -5
  15. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/README.md +0 -0
  16. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/__init__.py +0 -0
  17. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/base_cli.py +0 -0
  18. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/binge_cli.py +0 -0
  19. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/default_cli.py +0 -0
  20. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/download_cli.py +0 -0
  21. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/history_cli.py +0 -0
  22. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/mal_cli.py +0 -0
  23. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/clis/seasonal_cli.py +0 -0
  24. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/discord.py +0 -0
  25. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/mal_proxy.py +0 -0
  26. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/menus/__init__.py +0 -0
  27. {anipy_cli-3.5.9 → anipy_cli-3.6.0}/src/anipy_cli/prompts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: anipy-cli
3
- Version: 3.5.9
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.5.9,<4.0.0)
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)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "anipy-cli"
3
- version = "3.5.9"
3
+ version = "3.6.0"
4
4
  description = "Watch and Download anime from the comfort of your Terminal"
5
5
  authors = ["sdaqo <sdaqo.dev@protonmail.com>"]
6
6
  license = "GPL-3.0"
@@ -20,7 +20,7 @@ yaspin = "^3.0.2"
20
20
  inquirerpy = "^0.3.4"
21
21
  appdirs = "^1.4.4"
22
22
  pypresence = "^4.3.0"
23
- anipy-api = "^3.5.9"
23
+ anipy-api = "^3.6.0"
24
24
 
25
25
  [tool.poetry.scripts]
26
26
  anipy-cli = "anipy_cli.cli:run_cli"
@@ -1,2 +1,2 @@
1
1
  __appname__ = "anipy-cli"
2
- __version__ = "3.5.9"
2
+ __version__ = "3.6.0"
@@ -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",
@@ -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()
@@ -1,4 +1,7 @@
1
- class colors:
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)
@@ -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
@@ -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
- s.write(color(colors.RED, "! ", message))
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 e:
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: {e}\n",
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
- spinner.write(
140
- f"> Downloading Episode {stream.episode} of {anime.name} ({lang})"
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
 
@@ -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
+ )
@@ -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, clear_screen=True):
44
- if clear_screen:
45
- os.system("cls" if os.name == "nt" else "clear")
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:
@@ -275,7 +275,7 @@ class MALMenu(MenuBase):
275
275
  )
276
276
  DownloadComponent.serve_download_errors(errors)
277
277
 
278
- self.print_options(clear_screen=len(errors) == 0)
278
+ self.print_options(should_clear_screen=len(errors) == 0)
279
279
 
280
280
  def binge_latest(self):
281
281
  picked = self._choose_latest()
@@ -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)
@@ -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 ProviderNotAvailable
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 ProviderNotAvailable:
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(clear_screen=True)
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(clear_screen=len(failed_series) == 0)
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()
@@ -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(error: str, fatal: Literal[False] = ...) -> None: ...
58
+ def error(
59
+ error: str, fatal: Literal[False] = ..., log_level: int = logging.INFO
60
+ ) -> None: ...
54
61
 
55
62
 
56
- def error(error: str, fatal: bool = False) -> Union[NoReturn, None]:
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:
File without changes