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.
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/PKG-INFO +1 -1
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/_version.py +3 -3
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/bases.py +7 -6
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/console.py +4 -8
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/context.py +1 -1
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/defaults.py +8 -4
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/error.py +0 -15
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/protocol.py +0 -4
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/registry.py +0 -2
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/session.py +3 -26
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/utils.py +2 -24
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/main.py +6 -2
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/BaseUrlUpdator.py +1 -1
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/ConfigClearer.py +2 -2
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/option_validate.py +5 -7
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/download_utils.py +11 -48
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/misc.py +1 -15
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/FollowedBookLister.py +2 -2
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/utils.py +0 -7
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/PKG-INFO +1 -1
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_kmdr_download.py +1 -1
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.github/workflows/release-package.yml +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.github/workflows/unit-test.yml +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.gitignore +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/LICENSE +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/README.md +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/assets/kmdr-demo.gif +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/assets/kmdr-log-demo.gif +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/mirror/mirrors.json +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/pyproject.toml +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/setup.cfg +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/constants.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/core/structure.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/CookieAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/LoginAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/utils.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/ConfigUnsetter.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/OptionLister.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/OptionSetter.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/DirectDownloader.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/ReferViaDownloader.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/BookUrlLister.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/DefaultVolPicker.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/utils.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_async_retry_decorator.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_kmdr_config_option.py +0 -0
- {kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_utils_resolve_volme.py +0 -0
|
@@ -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.
|
|
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 = '
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
247
|
-
|
|
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)
|
|
@@ -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) ->
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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,
|
|
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
|
|
29
|
+
:param exit_if_invalid: 如果键名无效,是否退出程序
|
|
31
30
|
"""
|
|
32
31
|
if key not in __OPTIONS_VALIDATOR:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
174
|
+
async with semaphore:
|
|
188
175
|
# 获取文件信息,请求以获取文件大小
|
|
189
|
-
#
|
|
190
|
-
|
|
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)
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/misc.py
RENAMED
|
@@ -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
|
-
|
|
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")
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/utils.py
RENAMED
|
@@ -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(
|
|
@@ -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')
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.github/workflows/release-package.yml
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/.github/workflows/unit-test.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/authenticator/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/configurer/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/downloader/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/lister/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/__init__.py
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/src/kmdr/module/picker/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_async_retry_decorator.py
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_kmdr_config_option.py
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.2.4 → kmoe_manga_downloader-1.2.4b0}/tests/test_utils_resolve_volme.py
RENAMED
|
File without changes
|