kmoe-manga-downloader 1.2.4__tar.gz → 1.2.4b0__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.
Files changed (62) hide show
  1. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/PKG-INFO +1 -1
  2. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/_version.py +3 -3
  3. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/bases.py +7 -6
  4. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/console.py +4 -8
  5. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/context.py +1 -1
  6. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/defaults.py +8 -4
  7. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/error.py +0 -15
  8. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/protocol.py +0 -4
  9. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/registry.py +0 -2
  10. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/session.py +3 -26
  11. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/utils.py +2 -24
  12. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/main.py +6 -2
  13. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/BaseUrlUpdator.py +1 -1
  14. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/ConfigClearer.py +2 -2
  15. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/option_validate.py +5 -7
  16. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/download_utils.py +11 -48
  17. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/misc.py +1 -15
  18. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/FollowedBookLister.py +2 -2
  19. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/utils.py +0 -7
  20. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/PKG-INFO +1 -1
  21. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_kmdr_download.py +1 -1
  22. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.github/workflows/release-package.yml +0 -0
  23. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.github/workflows/unit-test.yml +0 -0
  24. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.gitignore +0 -0
  25. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/LICENSE +0 -0
  26. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/README.md +0 -0
  27. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/assets/kmdr-demo.gif +0 -0
  28. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/assets/kmdr-log-demo.gif +0 -0
  29. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/mirror/mirrors.json +0 -0
  30. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/pyproject.toml +0 -0
  31. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/setup.cfg +0 -0
  32. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/__init__.py +0 -0
  33. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/__init__.py +0 -0
  34. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/constants.py +0 -0
  35. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/structure.py +0 -0
  36. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/__init__.py +0 -0
  37. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/CookieAuthenticator.py +0 -0
  38. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/LoginAuthenticator.py +0 -0
  39. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/__init__.py +0 -0
  40. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/utils.py +0 -0
  41. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/ConfigUnsetter.py +0 -0
  42. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/OptionLister.py +0 -0
  43. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/OptionSetter.py +0 -0
  44. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/__init__.py +0 -0
  45. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/DirectDownloader.py +0 -0
  46. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/ReferViaDownloader.py +0 -0
  47. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/__init__.py +0 -0
  48. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/BookUrlLister.py +0 -0
  49. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/__init__.py +0 -0
  50. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
  51. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/DefaultVolPicker.py +0 -0
  52. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/__init__.py +0 -0
  53. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/utils.py +0 -0
  54. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +0 -0
  55. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
  56. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
  57. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
  58. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
  59. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/__init__.py +0 -0
  60. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_async_retry_decorator.py +0 -0
  61. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_kmdr_config_option.py +0 -0
  62. {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_utils_resolve_volme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kmoe-manga-downloader
3
- Version: 1.2.4
3
+ Version: 1.2.4b0
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.2.4'
32
- __version_tuple__ = version_tuple = (1, 2, 4)
31
+ __version__ = version = '1.2.4b0'
32
+ __version_tuple__ = version_tuple = (1, 2, 4, 'b0')
33
33
 
34
- __commit_id__ = commit_id = 'gb584a1f0c'
34
+ __commit_id__ = commit_id = 'g4623b0002'
@@ -9,7 +9,6 @@ from .error import LoginError
9
9
  from .registry import Registry
10
10
  from .structure import VolInfo, BookInfo
11
11
  from .utils import construct_callback, async_retry
12
- from .protocol import AsyncCtxManager
13
12
 
14
13
  from .context import TerminalContext, SessionContext, UserProfileContext, ConfigContext
15
14
 
@@ -27,7 +26,7 @@ class SessionManager(SessionContext, ConfigContext, TerminalContext):
27
26
  super().__init__(*args, **kwargs)
28
27
 
29
28
  @abstractmethod
30
- async def session(self) -> AsyncCtxManager[ClientSession]: ...
29
+ async def session(self) -> ClientSession: ...
31
30
 
32
31
  class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalContext):
33
32
 
@@ -45,6 +44,7 @@ class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalC
45
44
  except LoginError as e:
46
45
  info(f"[yellow]详细信息:{e}[/yellow]")
47
46
  info("[red]认证失败。请检查您的登录凭据或会话 cookie。[/red]")
47
+ exit(1)
48
48
 
49
49
  @abstractmethod
50
50
  async def _authenticate(self) -> bool: ...
@@ -84,7 +84,7 @@ class Downloader(SessionContext, UserProfileContext, TerminalContext):
84
84
  async def download(self, book: BookInfo, volumes: list[VolInfo]):
85
85
  if not volumes:
86
86
  info("没有可下载的卷。", style="blue")
87
- return
87
+ exit(0)
88
88
 
89
89
  try:
90
90
  with self._progress:
@@ -97,10 +97,11 @@ class Downloader(SessionContext, UserProfileContext, TerminalContext):
97
97
  for exc in exceptions:
98
98
  info(f"[red]- {exc}[/red]")
99
99
  exception(exc)
100
+ exit(1)
100
101
 
101
- except asyncio.CancelledError:
102
- await asyncio.sleep(0.01)
103
- raise
102
+ except KeyboardInterrupt:
103
+ info("\n操作已取消(KeyboardInterrupt)")
104
+ exit(130)
104
105
 
105
106
  @abstractmethod
106
107
  async def _download(self, book: BookInfo, volume: VolInfo): ...
@@ -10,6 +10,8 @@ import io
10
10
  from rich.console import Console
11
11
  from rich.traceback import Traceback
12
12
 
13
+ from kmdr.core.defaults import is_verbose
14
+
13
15
  _console_config = dict[str, Any](
14
16
  log_time_format="[%Y-%m-%d %H:%M:%S]",
15
17
  )
@@ -22,12 +24,6 @@ except io.UnsupportedOperation:
22
24
 
23
25
  _console = Console(**_console_config)
24
26
 
25
- _is_verbose = False
26
-
27
- def _update_verbose_setting(value: bool):
28
- global _is_verbose
29
- _is_verbose = value
30
-
31
27
  def info(*args, **kwargs):
32
28
  """
33
29
  在终端中输出信息
@@ -45,7 +41,7 @@ def debug(*args, **kwargs):
45
41
 
46
42
  `info` 的条件版本,仅当启用详细模式时才会输出。
47
43
  """
48
- if _is_verbose:
44
+ if is_verbose():
49
45
  if _console.is_interactive:
50
46
  _console.print("[dim]DEBUG:[/]", *args, **kwargs)
51
47
  else:
@@ -61,7 +57,7 @@ def log(*args, debug=False, **kwargs):
61
57
  # 如果是交互式终端,则不记录日志
62
58
  return
63
59
 
64
- if debug and _is_verbose:
60
+ if debug and is_verbose():
65
61
  # 仅在调试模式和启用详细模式时记录调试日志
66
62
  _console.log("DEBUG:", *args, **kwargs, _stack_offset=2)
67
63
  else:
@@ -18,7 +18,7 @@ class TerminalContext:
18
18
  def _progress(self) -> Progress:
19
19
  global _lazy_progress
20
20
  if _lazy_progress is None:
21
- _lazy_progress = Progress(*progress_definition, console=self._console, refresh_per_second=4)
21
+ _lazy_progress = Progress(*progress_definition, console=self._console)
22
22
  return _lazy_progress
23
23
 
24
24
  class UserProfileContext:
@@ -5,7 +5,6 @@ import json
5
5
  from typing import Optional, Any
6
6
  import argparse
7
7
  from contextvars import ContextVar
8
-
9
8
  from rich.console import Console
10
9
  from rich.progress import (
11
10
  Progress,
@@ -19,7 +18,6 @@ from rich.progress import (
19
18
  from .utils import singleton
20
19
  from .structure import Config
21
20
  from .constants import BASE_URL
22
- from .console import _update_verbose_setting
23
21
 
24
22
  HEADERS = {
25
23
  'User-Agent': 'kmdr/1.0 (https://github.com/chrisis58/kmoe-manga-downloader)'
@@ -97,6 +95,7 @@ def parse_args():
97
95
 
98
96
  if args.command is None:
99
97
  parser.print_help()
98
+ exit(1)
100
99
 
101
100
  return args
102
101
 
@@ -242,6 +241,11 @@ def combine_args(dest: argparse.Namespace) -> argparse.Namespace:
242
241
 
243
242
  base_url_var = ContextVar('base_url', default=Configurer().base_url)
244
243
 
244
+ _verbose = False
245
+
246
+ def is_verbose() -> bool:
247
+ return _verbose
248
+
245
249
  def post_init(args) -> None:
246
- _verbose = getattr(args, 'verbose', False)
247
- _update_verbose_setting(_verbose)
250
+ global _verbose
251
+ _verbose = getattr(args, 'verbose', False)
@@ -7,9 +7,6 @@ class KmdrError(RuntimeError):
7
7
 
8
8
  self._solution = "" if solution is None else "\n[bold cyan]推荐解决方法:[/bold cyan] \n" + "\n".join(f"[cyan]>>> {sol}[/cyan]" for sol in solution)
9
9
 
10
- def __str__(self):
11
- return f"{self.message}\n{self._solution}"
12
-
13
10
  class InitializationError(KmdrError):
14
11
  def __init__(self, message, solution: Optional[list[str]] = None):
15
12
  super().__init__(message, solution)
@@ -39,18 +36,6 @@ class RedirectError(KmdrError):
39
36
  def __str__(self):
40
37
  return f"{self.message} 新的地址: {self.new_base_url}"
41
38
 
42
- class ValidationError(KmdrError):
43
- def __init__(self, message, field: str):
44
- super().__init__(message)
45
- self.field = field
46
-
47
- def __str__(self):
48
- return f"{self.message} (字段: {self.field})"
49
-
50
- class EmptyResultError(KmdrError):
51
- def __init__(self, message):
52
- super().__init__(message)
53
-
54
39
  class ResponseError(KmdrError):
55
40
  def __init__(self, message, status_code: int):
56
41
  super().__init__(message)
@@ -8,7 +8,3 @@ class Supplier(Protocol[S]):
8
8
 
9
9
  class Consumer(Protocol[T]):
10
10
  def __call__(self, value: T) -> None: ...
11
-
12
- class AsyncCtxManager(Protocol[S]):
13
- async def __aenter__(self) -> S: ...
14
- async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
@@ -3,7 +3,6 @@ from dataclasses import dataclass, field
3
3
  from argparse import Namespace
4
4
 
5
5
  from .defaults import combine_args
6
- from .console import debug
7
6
 
8
7
  T = TypeVar('T')
9
8
 
@@ -77,7 +76,6 @@ class Registry(Generic[T]):
77
76
  def get(self, condition: Namespace) -> T:
78
77
  if self._combine_args:
79
78
  condition = combine_args(condition)
80
- debug("合并默认参数后,条件为:", condition)
81
79
  return self._get(condition)
82
80
 
83
81
  def _get(self, condition: Namespace) -> T:
@@ -1,9 +1,6 @@
1
1
  from typing import Optional
2
2
  from urllib.parse import urlsplit, urljoin
3
- from typing import Type
4
- from types import TracebackType
5
3
 
6
- import asyncio
7
4
  from aiohttp import ClientSession
8
5
 
9
6
  from .constants import BASE_URL, API_ROUTE
@@ -13,7 +10,6 @@ from .defaults import HEADERS
13
10
  from .error import InitializationError, RedirectError
14
11
  from .protocol import Supplier
15
12
  from .console import *
16
- from .protocol import AsyncCtxManager
17
13
 
18
14
 
19
15
 
@@ -42,11 +38,11 @@ class KmdrSessionManager(SessionManager):
42
38
  self._sorter.incr(primary_base_url, 10)
43
39
  debug("镜像地址优先级排序:", self._sorter)
44
40
 
45
- async def session(self) -> AsyncCtxManager[ClientSession]:
41
+ async def session(self) -> ClientSession:
46
42
  try:
47
43
  if self._session is not None and not self._session.closed:
48
44
  # 幂等性检查:如果 session 已经存在且未关闭,直接返回
49
- return SessionCtxManager(self._session)
45
+ return self._session
50
46
  except LookupError:
51
47
  # session_var 尚未设置
52
48
  pass
@@ -66,7 +62,7 @@ class KmdrSessionManager(SessionManager):
66
62
  headers=HEADERS,
67
63
  )
68
64
 
69
- return SessionCtxManager(self._session)
65
+ return self._session
70
66
 
71
67
  async def validate_url(self, session: ClientSession, url_supplier: Supplier[str]) -> bool:
72
68
  try:
@@ -122,22 +118,3 @@ class KmdrSessionManager(SessionManager):
122
118
 
123
119
  raise InitializationError(f"所有镜像均不可用,请检查您的网络连接或使用其他镜像。\n详情参考:https://github.com/chrisis58/kmoe-manga-downloader/blob/main/mirror/mirrors.json")
124
120
 
125
- class SessionCtxManager:
126
- def __init__(self, session: ClientSession):
127
- self._session = session
128
-
129
- async def __aenter__(self) -> ClientSession:
130
- await self._session.__aenter__()
131
- return self._session
132
-
133
- async def __aexit__(
134
- self,
135
- exc_type: Optional[Type[BaseException]],
136
- exc_value: Optional[BaseException],
137
- traceback: Optional[TracebackType]
138
- ):
139
- await self._session.__aexit__(exc_type, exc_value, traceback)
140
-
141
- if exc_type in (KeyboardInterrupt, asyncio.CancelledError):
142
- debug("任务被取消,正在清理资源")
143
- await asyncio.sleep(0.01)
@@ -1,7 +1,6 @@
1
1
  import functools
2
2
  from typing import Optional, Callable, TypeVar, Hashable, Generic
3
3
  import asyncio
4
- from asyncio.proactor_events import _ProactorBasePipeTransport
5
4
 
6
5
  import aiohttp
7
6
 
@@ -10,7 +9,6 @@ import subprocess
10
9
  from .structure import BookInfo, VolInfo
11
10
  from .error import RedirectError
12
11
  from .protocol import Consumer
13
- from .console import debug
14
12
 
15
13
 
16
14
  def singleton(cls):
@@ -70,7 +68,7 @@ def async_retry(
70
68
  except RedirectError as e:
71
69
  if base_url_setter:
72
70
  base_url_setter(e.new_base_url)
73
- debug("检测到重定向,已自动更新 base url 为", e.new_base_url)
71
+ print(f"检测到重定向,已自动更新 base url 为: {e.new_base_url}。立即重试...")
74
72
  continue
75
73
  else:
76
74
  raise
@@ -126,24 +124,4 @@ class PrioritySorter(Generic[H]):
126
124
 
127
125
  def sort(self) -> list[H]:
128
126
  """返回根据优先级排序后的元素列表,优先级高的元素排在前面"""
129
- return [k for k, v in sorted(self._items.items(), key=lambda item: item[1], reverse=True)]
130
-
131
- def _silence_event_loop_closed(func):
132
- """
133
- 用于静默处理 'Event loop is closed' 异常的装饰器。
134
- 该异常在某些情况下(如 Windows 平台使用 Proactor 事件循环)会在对象销毁时抛出,
135
- 导致程序输出不必要的错误信息。此装饰器捕获该异常并忽略它。
136
-
137
- @see https://github.com/aio-libs/aiohttp/issues/4324
138
- """
139
- @functools.wraps(func)
140
- def wrapper(self, *args, **kwargs):
141
- try:
142
- return func(self, *args, **kwargs)
143
- except RuntimeError as e:
144
- if str(e) != 'Event loop is closed':
145
- raise
146
-
147
- return wrapper
148
-
149
- _ProactorBasePipeTransport.__del__ = _silence_event_loop_closed(_ProactorBasePipeTransport.__del__)
127
+ return [k for k, v in sorted(self._items.items(), key=lambda item: item[1], reverse=True)]
@@ -1,8 +1,9 @@
1
+ from kmdr import __version__
2
+
1
3
  from typing import Callable
2
4
  from argparse import Namespace
3
5
  import asyncio
4
6
 
5
- from kmdr import __version__
6
7
  from kmdr.core import *
7
8
  from kmdr.module import *
8
9
 
@@ -50,14 +51,17 @@ def entry_point():
50
51
  parser = argument_parser()
51
52
  args = parser.parse_args()
52
53
 
53
- main_coro = main(args, parser.print_help)
54
+ main_coro = main(args, lambda: parser.print_help())
54
55
  asyncio.run(main_coro)
55
56
  except KmdrError as e:
56
57
  info(f"[red]错误: {e}[/red]")
58
+ exit(1)
57
59
  except KeyboardInterrupt:
58
60
  info("\n操作已取消(KeyboardInterrupt)", style="yellow")
61
+ exit(130)
59
62
  except Exception as e:
60
63
  exception(e)
64
+ exit(1)
61
65
  finally:
62
66
  log('[Lifecycle:End] 运行结束,kmdr 已退出')
63
67
 
@@ -12,6 +12,6 @@ class BaseUrlUpdator(Configurer):
12
12
  self._configurer.set_base_url(self._base_url)
13
13
  except KeyError as e:
14
14
  info(f"[red]{e.args[0]}[/red]")
15
- return
15
+ exit(1)
16
16
 
17
17
  info(f"已设置基础 URL: {self._base_url}")
@@ -12,6 +12,6 @@ class ConfigClearer(Configurer):
12
12
  self._configurer.clear(self._clear)
13
13
  except KeyError as e:
14
14
  info(f"[red]{e.args[0]}[/red]")
15
- return
15
+ exit(1)
16
16
 
17
- info(f"[green]已清除: {self._clear}[/green]")
17
+ info(f"Cleared configuration: {self._clear}")
@@ -3,7 +3,6 @@ from functools import wraps
3
3
  import os
4
4
 
5
5
  from kmdr.core.console import info
6
- from kmdr.core.error import ValidationError
7
6
 
8
7
  __OPTIONS_VALIDATOR = {}
9
8
 
@@ -21,19 +20,18 @@ def validate(key: str, value: str) -> Optional[object]:
21
20
  info(f"[red]不支持的配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}[/red]")
22
21
  return None
23
22
 
24
- def check_key(key: str, raise_if_invalid: bool = True) -> None:
23
+ def check_key(key: str, exit_if_invalid: bool = True) -> None:
25
24
  """
26
25
  供外部调用的验证函数,用于检查配置项的键名是否有效。
27
26
  如果键名无效,函数会打印错误信息并退出程序。
28
27
 
29
28
  :param key: 配置项的键名
30
- :param raise_if_invalid: 如果键名无效,是否抛出异常
29
+ :param exit_if_invalid: 如果键名无效,是否退出程序
31
30
  """
32
31
  if key not in __OPTIONS_VALIDATOR:
33
- if raise_if_invalid:
34
- raise ValidationError(f"未知配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}", field=key)
35
- else:
36
- info(f"[red]未知配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}[/red]")
32
+ info(f"[red]未知配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}[/red]")
33
+ if exit_if_invalid:
34
+ exit(1)
37
35
 
38
36
  def register_validator(arg_name):
39
37
  """
@@ -14,25 +14,12 @@ from aiohttp.client_exceptions import ClientPayloadError
14
14
 
15
15
  from kmdr.core.console import info, log, debug
16
16
  from kmdr.core.error import ResponseError
17
- from kmdr.core.utils import async_retry
18
17
 
19
18
  from .misc import STATUS, StateManager
20
19
 
21
20
  BLOCK_SIZE_REDUCTION_FACTOR = 0.75
22
21
  MIN_BLOCK_SIZE = 2048
23
22
 
24
- _HEAD_SEMAPHORE_VALUE = 3
25
- _HEAD_SEMAPHORE: Optional[asyncio.Semaphore] = None
26
- """定义的用于 HEAD 请求的信号量,限制并发数量以避免触发服务器限流。"""
27
-
28
- def _get_head_request_semaphore() -> asyncio.Semaphore:
29
- """惰性初始化 HEAD 请求信号量。"""
30
- global _HEAD_SEMAPHORE
31
-
32
- if _HEAD_SEMAPHORE is None:
33
- _HEAD_SEMAPHORE = asyncio.Semaphore(_HEAD_SEMAPHORE_VALUE)
34
- return _HEAD_SEMAPHORE
35
-
36
23
 
37
24
  @deprecated("本函数可能不会积极维护,请改用 'download_file_multipart'")
38
25
  async def download_file(
@@ -184,10 +171,17 @@ async def download_file_multipart(
184
171
  try:
185
172
  current_url = await fetch_url(url)
186
173
 
187
- async with _get_head_request_semaphore():
174
+ async with semaphore:
188
175
  # 获取文件信息,请求以获取文件大小
189
- # 控制并发,避免过多并发请求触发服务器限流
190
- total_size = await _fetch_content_length(session, current_url, headers=headers)
176
+ # 复用 semaphore 以控制并发,避免过多并发请求触发服务器限流
177
+ async with session.head(current_url, headers=headers, allow_redirects=True) as response:
178
+ # 注意:这个请求完成后,服务器就会记录这次下载,并消耗对应的流量配额,详细的规则请参考网站说明:
179
+ # 注 1 : 訂閱連載中的漫畫,有更新時自動推送的卷(冊),暫不計算在使用額度中,不扣減使用額度。
180
+ # 注 2 : 對同一卷(冊)書在 12 小時內重複*下載*,不會重複扣減額度。但重復推送是會扣減的。
181
+ response.raise_for_status()
182
+ if 'Content-Length' not in response.headers:
183
+ raise ResponseError("无法从服务器获取文件大小,请检查网络连接或稍后重试。", status_code=response.status)
184
+ total_size = int(response.headers['Content-Length'])
191
185
 
192
186
  chunk_size = chunk_size_mb * 1024 * 1024
193
187
  num_chunks = math.ceil(total_size / chunk_size)
@@ -247,36 +241,6 @@ async def download_file_multipart(
247
241
  if task_id is not None and state_manager is not None:
248
242
  await state_manager.request_status_update(part_id=StateManager.PARENT_ID, status=STATUS.FAILED)
249
243
 
250
- @async_retry()
251
- async def _fetch_content_length(
252
- session: aiohttp.ClientSession,
253
- url: str,
254
- headers: Optional[dict] = None,
255
- ) -> int:
256
- """
257
- 获取文件的内容长度(字节数)
258
-
259
- :note: 这个请求完成后,服务器就会记录这次下载,并消耗对应的流量配额,详细的规则请参考网站说明:
260
- - 注 1 : 訂閱連載中的漫畫,有更新時自動推送的卷(冊),暫不計算在使用額度中,不扣減使用額度。
261
- - 注 2 : 對同一卷(冊)書在 12 小時內重複*下載*,不會重複扣減額度。但重復推送是會扣減的。
262
-
263
- :param session: aiohttp.ClientSession 对象
264
- :param url: 文件 URL
265
- :param headers: 请求头
266
- :return: 文件大小(字节数)
267
- """
268
- if headers is None:
269
- headers = {}
270
-
271
- async with session.head(url, headers=headers, allow_redirects=True) as response:
272
- # 注意:这个请求完成后,服务器就会记录这次下载,并消耗对应的流量配额,详细的规则请参考网站说明:
273
- # 注 1 : 訂閱連載中的漫畫,有更新時自動推送的卷(冊),暫不計算在使用額度中,不扣減使用額度。
274
- # 注 2 : 對同一卷(冊)書在 12 小時內重複*下載*,不會重複扣減額度。但重復推送是會扣減的。
275
- response.raise_for_status()
276
- if 'Content-Length' not in response.headers:
277
- raise ResponseError("无法从服务器获取文件大小,请检查网络连接或稍后重试。", status_code=response.status)
278
- return int(response.headers['Content-Length'])
279
-
280
244
  async def _download_part(
281
245
  session: aiohttp.ClientSession,
282
246
  semaphore: asyncio.Semaphore,
@@ -319,7 +283,6 @@ async def _download_part(
319
283
  if chunk:
320
284
  await f.write(chunk)
321
285
  state_manager.advance(len(chunk))
322
- await state_manager.pop_part(part_id=start)
323
286
  log("分片", os.path.basename(part_path), "下载完成。")
324
287
  return
325
288
 
@@ -331,8 +294,8 @@ async def _download_part(
331
294
  except Exception as e:
332
295
  if attempts_left > 0:
333
296
  debug("分片", os.path.basename(part_path), "下载出错:", e, ",正在重试... 剩余重试次数:", attempts_left)
334
- await state_manager.request_status_update(part_id=start, status=STATUS.WAITING)
335
297
  await asyncio.sleep(3)
298
+ await state_manager.request_status_update(part_id=start, status=STATUS.WAITING)
336
299
  else:
337
300
  # console.print(f"[red]分片 {os.path.basename(part_path)} 下载失败: {e}[/red]")
338
301
  debug("分片", os.path.basename(part_path), "下载失败:", e)
@@ -3,8 +3,6 @@ import asyncio
3
3
 
4
4
  from rich.progress import Progress, TaskID
5
5
 
6
- from kmdr.core.console import debug
7
-
8
6
 
9
7
  class STATUS(Enum):
10
8
  WAITING='[blue]等待中[/blue]'
@@ -55,24 +53,12 @@ class StateManager:
55
53
  if not self._part_states:
56
54
  return
57
55
 
58
- debug("当前状态:", self._part_states)
59
56
  highest_status = max(self._part_states.values())
60
57
  if highest_status != self._current_status:
61
58
  self._current_status = highest_status
62
- self._progress.update(self._task_id, status=highest_status.value)
63
-
64
- async def pop_part(self, part_id: int):
65
- """
66
- 下载完成后移除分片状态记录,不再参与状态计算
67
-
68
- :note: 为避免状态闪烁,调用后不会更新状态
69
- """
70
- async with self._lock:
71
- if part_id in self._part_states:
72
- self._part_states.pop(part_id)
59
+ self._progress.update(self._task_id, status=highest_status.value, refresh=True)
73
60
 
74
61
  async def request_status_update(self, part_id: int, status: STATUS):
75
62
  async with self._lock:
76
- debug("分片", part_id, "请求状态更新为", status)
77
63
  self._part_states[part_id] = status
78
64
  self._update_status()
@@ -8,7 +8,6 @@ from kmdr.core import Lister, LISTERS, BookInfo, VolInfo
8
8
  from kmdr.core.utils import async_retry
9
9
  from kmdr.core.constants import API_ROUTE
10
10
  from kmdr.core.console import info
11
- from kmdr.core.error import EmptyResultError
12
11
 
13
12
  from .utils import extract_book_info_and_volumes
14
13
 
@@ -25,7 +24,8 @@ class FollowedBookLister(Lister):
25
24
  books = await self._list_followed_books()
26
25
 
27
26
  if not books:
28
- raise EmptyResultError("关注列表为空。")
27
+ info("[yellow]关注列表为空。[/yellow]")
28
+ exit(0)
29
29
 
30
30
  table = Table(title="关注的书籍列表", show_header=True, header_style="bold blue")
31
31
  table.add_column("序号", style="dim", width=4, justify="center")
@@ -8,7 +8,6 @@ from aiohttp import ClientSession as Session
8
8
  from kmdr.core import BookInfo, VolInfo, VolumeType
9
9
  from kmdr.core.utils import async_retry
10
10
  from kmdr.core.console import debug
11
- from kmdr.core.error import KmdrError
12
11
 
13
12
  @async_retry()
14
13
  async def extract_book_info_and_volumes(session: Session, url: str, book_info: Optional[BookInfo] = None) -> tuple[BookInfo, list[VolInfo]]:
@@ -42,12 +41,6 @@ async def extract_book_info_and_volumes(session: Session, url: str, book_info: O
42
41
  def __extract_book_info(url: str, book_page: BeautifulSoup, book_info: Optional[BookInfo]) -> BookInfo:
43
42
  book_name = book_page.find('font', class_='text_bglight_big').text
44
43
 
45
- if '為符合要求,此書內容已屏蔽' in book_name:
46
- raise KmdrError(
47
- "[yellow]该书籍内容已被屏蔽,请检查代理配置。[/yellow]",
48
- solution=["kmdr config -s proxy=<your_proxy> # 设置可用的代理地址"]
49
- )
50
-
51
44
  id = book_page.find('input', attrs={'name': 'bookid'})['value']
52
45
 
53
46
  return BookInfo(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kmoe-manga-downloader
3
- Version: 1.2.4
3
+ Version: 1.2.4b0
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -175,7 +175,7 @@ class TestKmdrDownload(unittest.TestCase):
175
175
 
176
176
  assert len(files := os.listdir(dest)) == 2, "Expected one subdirectory and one callback log file in the destination"
177
177
  assert 'callback.log' in files, "Expected callback log file to be present"
178
- with open(os.path.join(dest, 'callback.log'), 'r') as f:
178
+ with open(os.path.join(dest, 'callback.log'), 'r', encoding='utf-8') as f:
179
179
  log_content = f.read()
180
180
  assert "CALLBACK:" in log_content, "Expected callback log to contain the correct message"
181
181
  files.remove('callback.log')