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.

@@ -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
 
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.mal_ignore_tag in e.my_list_status.tags:
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.mal_status_categories]
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.mal_ignore_tag not in e.my_list_status.tags
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.mal_tags)
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.mal_mapping_min_similarity,
187
- config.mal_mapping_use_filters,
188
- config.mal_mapping_use_alternatives,
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.mal_mapping_min_similarity,
216
- config.mal_mapping_use_alternatives,
215
+ config.tracker_mapping_min_similarity,
216
+ config.tracker_mapping_use_alternatives,
217
217
  )
218
218
 
219
219
  if result:
@@ -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"]