kmoe-manga-downloader 1.2.2__tar.gz → 1.2.3b0__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.3b0/.github/workflows/release-package.yml +45 -0
- kmoe_manga_downloader-1.2.3b0/.github/workflows/unit-test.yml +31 -0
- kmoe_manga_downloader-1.2.3b0/.gitignore +23 -0
- {kmoe_manga_downloader-1.2.2/src/kmoe_manga_downloader.egg-info → kmoe_manga_downloader-1.2.3b0}/PKG-INFO +1 -1
- kmoe_manga_downloader-1.2.3b0/assets/kmdr-demo.gif +0 -0
- kmoe_manga_downloader-1.2.3b0/mirror/mirrors.json +11 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/pyproject.toml +8 -1
- kmoe_manga_downloader-1.2.3b0/src/kmdr/__init__.py +4 -0
- kmoe_manga_downloader-1.2.3b0/src/kmdr/_version.py +34 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/__init__.py +4 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/bases.py +14 -5
- kmoe_manga_downloader-1.2.3b0/src/kmdr/core/console.py +67 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/context.py +14 -3
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/defaults.py +19 -7
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/error.py +7 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/session.py +6 -2
- kmoe_manga_downloader-1.2.3b0/src/kmdr/main.py +70 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/utils.py +11 -4
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/BaseUrlUpdator.py +3 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/ConfigClearer.py +3 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/ConfigUnsetter.py +3 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/OptionLister.py +3 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/OptionSetter.py +3 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/option_validate.py +11 -11
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/DirectDownloader.py +1 -1
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/ReferViaDownloader.py +2 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/download_utils.py +23 -7
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/FollowedBookLister.py +3 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/utils.py +5 -1
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/picker/DefaultVolPicker.py +2 -1
- kmoe_manga_downloader-1.2.3b0/src/kmdr/module/picker/utils.py +45 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0/src/kmoe_manga_downloader.egg-info}/PKG-INFO +1 -1
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +8 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/tests/test_kmdr_download.py +2 -2
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/tests/test_utils_resolve_volme.py +3 -3
- kmoe_manga_downloader-1.2.2/src/kmdr/main.py +0 -51
- kmoe_manga_downloader-1.2.2/src/kmdr/module/picker/utils.py +0 -37
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/LICENSE +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/README.md +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/setup.cfg +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/constants.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/protocol.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/registry.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/structure.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/utils.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/CookieAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/LoginAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/misc.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/BookUrlLister.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/picker/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
- {kmoe_manga_downloader-1.2.2/src/kmdr → kmoe_manga_downloader-1.2.3b0/tests}/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/tests/test_async_retry_decorator.py +0 -0
- {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/tests/test_kmdr_config_option.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Release Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build-and-release:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
# Ensure full history for versioning tools `setuptools-scm`
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: '3.9'
|
|
26
|
+
|
|
27
|
+
- name: Install build dependencies
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install build
|
|
31
|
+
|
|
32
|
+
- name: Build package
|
|
33
|
+
run: python -m build
|
|
34
|
+
|
|
35
|
+
- name: Publish package to PyPI
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
37
|
+
|
|
38
|
+
- name: Upload assets to GitHub Release
|
|
39
|
+
uses: svenstaro/upload-release-action@v2
|
|
40
|
+
with:
|
|
41
|
+
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
42
|
+
file: dist/*
|
|
43
|
+
tag: ${{ github.ref }}
|
|
44
|
+
overwrite: true
|
|
45
|
+
file_glob: true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Unit Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout code
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: '3.9'
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies and project
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip
|
|
23
|
+
pip install pytest
|
|
24
|
+
pip install -e .
|
|
25
|
+
|
|
26
|
+
- name: Run tests with pytest
|
|
27
|
+
env:
|
|
28
|
+
KMOE_USERNAME: ${{ secrets.KMOE_USERNAME }}
|
|
29
|
+
KMOE_PASSWORD: ${{ secrets.KMOE_PASSWORD }}
|
|
30
|
+
run: |
|
|
31
|
+
pytest
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ---> VisualStudioCode
|
|
2
|
+
.vscode/*
|
|
3
|
+
.vscode/settings.json
|
|
4
|
+
.vscode/tasks.json
|
|
5
|
+
.vscode/launch.json
|
|
6
|
+
.vscode/extensions.json
|
|
7
|
+
.vscode/*.code-snippets
|
|
8
|
+
|
|
9
|
+
# Local History for Visual Studio Code
|
|
10
|
+
.history/
|
|
11
|
+
|
|
12
|
+
# Built Visual Studio Code Extensions
|
|
13
|
+
*.vsix
|
|
14
|
+
|
|
15
|
+
__pycache__/
|
|
16
|
+
*.py[codz]
|
|
17
|
+
*$py.class
|
|
18
|
+
|
|
19
|
+
*.egg-info/
|
|
20
|
+
dist/
|
|
21
|
+
|
|
22
|
+
# version file generated by setuptools-scm
|
|
23
|
+
src/kmdr/_version.py
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kmoe-manga-downloader"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Chris Zheng", email="chrisis58@outlook.com" },
|
|
6
6
|
]
|
|
@@ -36,5 +36,12 @@ kmdr = "kmdr.main:entry_point"
|
|
|
36
36
|
[tool.setuptools]
|
|
37
37
|
package-dir = {"" = "src"}
|
|
38
38
|
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["setuptools>=61.0", "setuptools-scm[toml]>=8.0"]
|
|
41
|
+
build-backend = "setuptools.build_meta"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools_scm]
|
|
44
|
+
write_to = "src/kmdr/_version.py"
|
|
45
|
+
|
|
39
46
|
[tool.pytest.ini_options]
|
|
40
47
|
pythonpath = "src"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.2.3b0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 2, 3, 'b0')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'g28497cb81'
|
|
@@ -2,8 +2,10 @@ from .bases import Authenticator, Lister, Picker, Downloader, Configurer, Sessio
|
|
|
2
2
|
from .structure import VolInfo, BookInfo, VolumeType
|
|
3
3
|
from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER, SESSION_MANAGER
|
|
4
4
|
|
|
5
|
-
from .defaults import argument_parser,
|
|
5
|
+
from .defaults import argument_parser, post_init
|
|
6
6
|
|
|
7
7
|
from .error import KmdrError, LoginError
|
|
8
8
|
|
|
9
|
-
from .session import KmdrSessionManager
|
|
9
|
+
from .session import KmdrSessionManager
|
|
10
|
+
|
|
11
|
+
from .console import info, debug, exception, log
|
|
@@ -4,6 +4,7 @@ from abc import abstractmethod
|
|
|
4
4
|
import asyncio
|
|
5
5
|
from aiohttp import ClientSession
|
|
6
6
|
|
|
7
|
+
from .console import *
|
|
7
8
|
from .error import LoginError
|
|
8
9
|
from .registry import Registry
|
|
9
10
|
from .structure import VolInfo, BookInfo
|
|
@@ -41,8 +42,8 @@ class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalC
|
|
|
41
42
|
try:
|
|
42
43
|
assert await async_retry()(self._authenticate)()
|
|
43
44
|
except LoginError as e:
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
info(f"[yellow]详细信息:{e}[/yellow]")
|
|
46
|
+
info("[red]认证失败。请检查您的登录凭据或会话 cookie。[/red]")
|
|
46
47
|
exit(1)
|
|
47
48
|
|
|
48
49
|
@abstractmethod
|
|
@@ -82,16 +83,24 @@ class Downloader(SessionContext, UserProfileContext, TerminalContext):
|
|
|
82
83
|
|
|
83
84
|
async def download(self, book: BookInfo, volumes: list[VolInfo]):
|
|
84
85
|
if not volumes:
|
|
85
|
-
|
|
86
|
+
info("没有可下载的卷。", style="blue")
|
|
86
87
|
exit(0)
|
|
87
88
|
|
|
88
89
|
try:
|
|
89
90
|
with self._progress:
|
|
90
91
|
tasks = [self._download(book, volume) for volume in volumes]
|
|
91
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
92
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
93
|
+
|
|
94
|
+
exceptions = [res for res in results if isinstance(res, Exception)]
|
|
95
|
+
if exceptions:
|
|
96
|
+
info(f"[red]下载过程中出现 {len(exceptions)} 个错误:[/red]")
|
|
97
|
+
for exc in exceptions:
|
|
98
|
+
info(f"[red]- {exc}[/red]")
|
|
99
|
+
exception(exc)
|
|
100
|
+
exit(1)
|
|
92
101
|
|
|
93
102
|
except KeyboardInterrupt:
|
|
94
|
-
|
|
103
|
+
info("\n操作已取消(KeyboardInterrupt)")
|
|
95
104
|
exit(130)
|
|
96
105
|
|
|
97
106
|
@abstractmethod
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KMDR 用于管理控制台输出的模块。
|
|
3
|
+
|
|
4
|
+
提供信息、调试和日志记录功能,确保在交互式和非交互式环境中均能正确输出。
|
|
5
|
+
"""
|
|
6
|
+
from typing import Any
|
|
7
|
+
import sys
|
|
8
|
+
import io
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.traceback import Traceback
|
|
12
|
+
|
|
13
|
+
from kmdr.core.defaults import is_verbose
|
|
14
|
+
|
|
15
|
+
_console_config = dict[str, Any](
|
|
16
|
+
log_time_format="[%Y-%m-%d %H:%M:%S]",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
utf8_stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='backslashreplace')
|
|
21
|
+
_console_config['file'] = utf8_stdout
|
|
22
|
+
except io.UnsupportedOperation:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
_console = Console(**_console_config)
|
|
26
|
+
|
|
27
|
+
def info(*args, **kwargs):
|
|
28
|
+
"""
|
|
29
|
+
在终端中输出信息
|
|
30
|
+
|
|
31
|
+
会根据终端是否为交互式选择合适的输出方式。
|
|
32
|
+
"""
|
|
33
|
+
if _console.is_interactive:
|
|
34
|
+
_console.print(*args, **kwargs)
|
|
35
|
+
else:
|
|
36
|
+
_console.log(*args, **kwargs, _stack_offset=2)
|
|
37
|
+
|
|
38
|
+
def debug(*args, **kwargs):
|
|
39
|
+
"""
|
|
40
|
+
在终端中输出调试信息
|
|
41
|
+
|
|
42
|
+
`info` 的条件版本,仅当启用详细模式时才会输出。
|
|
43
|
+
"""
|
|
44
|
+
if is_verbose():
|
|
45
|
+
if _console.is_interactive:
|
|
46
|
+
_console.print("[dim]DEBUG:[/]", *args, **kwargs)
|
|
47
|
+
else:
|
|
48
|
+
_console.log("DEBUG:", *args, **kwargs, _stack_offset=2)
|
|
49
|
+
|
|
50
|
+
def log(*args, debug=False, **kwargs):
|
|
51
|
+
"""
|
|
52
|
+
仅在非交互式终端中记录日志信息
|
|
53
|
+
|
|
54
|
+
:warning: 仅在非交互式终端中输出日志信息,避免干扰交互式用户界面。
|
|
55
|
+
"""
|
|
56
|
+
if _console.is_interactive:
|
|
57
|
+
# 如果是交互式终端,则不记录日志
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if debug and is_verbose():
|
|
61
|
+
# 仅在调试模式和启用详细模式时记录调试日志
|
|
62
|
+
_console.log("DEBUG:", *args, **kwargs, _stack_offset=2)
|
|
63
|
+
else:
|
|
64
|
+
_console.log(*args, **kwargs, _stack_offset=2)
|
|
65
|
+
|
|
66
|
+
def exception(exception: Exception):
|
|
67
|
+
_console.print((Traceback.from_exception(type(exception), exception, exception.__traceback__)))
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
1
3
|
from aiohttp import ClientSession
|
|
4
|
+
from rich.progress import Progress
|
|
2
5
|
|
|
6
|
+
from .defaults import Configurer as InnerConfigurer, UserProfile, session_var, base_url_var, progress_definition
|
|
7
|
+
from .console import _console
|
|
3
8
|
|
|
4
|
-
|
|
9
|
+
_lazy_progress: Optional[Progress] = None
|
|
5
10
|
|
|
6
11
|
class TerminalContext:
|
|
7
12
|
|
|
8
13
|
def __init__(self, *args, **kwargs):
|
|
9
14
|
super().__init__()
|
|
10
|
-
self.
|
|
11
|
-
|
|
15
|
+
self._console = _console
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def _progress(self) -> Progress:
|
|
19
|
+
global _lazy_progress
|
|
20
|
+
if _lazy_progress is None:
|
|
21
|
+
_lazy_progress = Progress(*progress_definition, console=self._console)
|
|
22
|
+
return _lazy_progress
|
|
12
23
|
|
|
13
24
|
class UserProfileContext:
|
|
14
25
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
1
3
|
import os
|
|
2
4
|
import json
|
|
3
5
|
from typing import Optional, Any
|
|
@@ -21,12 +23,8 @@ HEADERS = {
|
|
|
21
23
|
'User-Agent': 'kmdr/1.0 (https://github.com/chrisis58/kmoe-manga-downloader)'
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
console = Console()
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
console.print(*args, **kwargs)
|
|
28
|
-
|
|
29
|
-
progress = Progress(
|
|
27
|
+
progress_definition = (
|
|
30
28
|
TextColumn("[blue]{task.fields[filename]}", justify="left"),
|
|
31
29
|
TextColumn("{task.fields[status]}", justify="right"),
|
|
32
30
|
TextColumn("{task.percentage:>3.1f}%"),
|
|
@@ -38,7 +36,6 @@ progress = Progress(
|
|
|
38
36
|
",",
|
|
39
37
|
TimeRemainingColumn(),
|
|
40
38
|
"]",
|
|
41
|
-
console=console,
|
|
42
39
|
)
|
|
43
40
|
|
|
44
41
|
session_var = ContextVar('session')
|
|
@@ -52,8 +49,13 @@ def argument_parser():
|
|
|
52
49
|
return parser
|
|
53
50
|
|
|
54
51
|
parser = argparse.ArgumentParser(description='Kmoe 漫画下载器')
|
|
52
|
+
|
|
53
|
+
parser.add_argument('-v', '--verbose', action='store_true', help='启用详细输出')
|
|
54
|
+
|
|
55
55
|
subparsers = parser.add_subparsers(title='可用的子命令', dest='command')
|
|
56
56
|
|
|
57
|
+
version_parser = subparsers.add_parser('version', help='显示当前版本信息')
|
|
58
|
+
|
|
57
59
|
download_parser = subparsers.add_parser('download', help='下载指定的漫画')
|
|
58
60
|
download_parser.add_argument('-d', '--dest', type=str, help='指定下载文件的保存路径,默认为当前目录', required=False)
|
|
59
61
|
download_parser.add_argument('-l', '--book-url', type=str, help='漫画详情页面的 URL', required=False)
|
|
@@ -65,6 +67,7 @@ def argument_parser():
|
|
|
65
67
|
download_parser.add_argument('-p', '--proxy', type=str, help='设置下载使用的代理服务器', required=False)
|
|
66
68
|
download_parser.add_argument('-r', '--retry', type=int, help='网络请求失败时的重试次数', required=False)
|
|
67
69
|
download_parser.add_argument('-c', '--callback', type=str, help='每个卷下载完成后执行的回调脚本,例如: `echo {v.name} downloaded!`', required=False)
|
|
70
|
+
download_parser.add_argument('-m', '--method', type=int, help='下载方法,对应网站上的不同下载方式', required=False, choices=[1, 2], default=1)
|
|
68
71
|
|
|
69
72
|
login_parser = subparsers.add_parser('login', help='登录到 Kmoe')
|
|
70
73
|
login_parser.add_argument('-u', '--username', type=str, help='用户名', required=True)
|
|
@@ -185,7 +188,7 @@ class Configurer:
|
|
|
185
188
|
self._config.base_url = value
|
|
186
189
|
self.update()
|
|
187
190
|
|
|
188
|
-
def get_base_url(self) -> str:
|
|
191
|
+
def get_base_url(self) -> Optional[str]:
|
|
189
192
|
return self._config.base_url
|
|
190
193
|
|
|
191
194
|
def update(self):
|
|
@@ -237,3 +240,12 @@ def combine_args(dest: argparse.Namespace) -> argparse.Namespace:
|
|
|
237
240
|
return __combine_args(dest, option)
|
|
238
241
|
|
|
239
242
|
base_url_var = ContextVar('base_url', default=Configurer().base_url)
|
|
243
|
+
|
|
244
|
+
_verbose = False
|
|
245
|
+
|
|
246
|
+
def is_verbose() -> bool:
|
|
247
|
+
return _verbose
|
|
248
|
+
|
|
249
|
+
def post_init(args) -> None:
|
|
250
|
+
global _verbose
|
|
251
|
+
_verbose = getattr(args, 'verbose', False)
|
|
@@ -14,6 +14,13 @@ class InitializationError(KmdrError):
|
|
|
14
14
|
def __str__(self):
|
|
15
15
|
return f"{self.message}\n{self._solution}"
|
|
16
16
|
|
|
17
|
+
class ArgsResolveError(KmdrError):
|
|
18
|
+
def __init__(self, message, solution: Optional[list[str]] = None):
|
|
19
|
+
super().__init__(message, solution)
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return f"{self.message}\n{self._solution}"
|
|
23
|
+
|
|
17
24
|
class LoginError(KmdrError):
|
|
18
25
|
def __init__(self, message, solution: Optional[list[str]] = None):
|
|
19
26
|
super().__init__(message, solution)
|
|
@@ -9,6 +9,7 @@ from .bases import SESSION_MANAGER, SessionManager
|
|
|
9
9
|
from .defaults import HEADERS
|
|
10
10
|
from .error import InitializationError, RedirectError
|
|
11
11
|
from .protocol import Supplier
|
|
12
|
+
from .console import *
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
|
|
@@ -32,8 +33,10 @@ class KmdrSessionManager(SessionManager):
|
|
|
32
33
|
if book_url is not None and book_url.strip() != "" :
|
|
33
34
|
splited = urlsplit(book_url)
|
|
34
35
|
primary_base_url = f"{splited.scheme}://{splited.netloc}"
|
|
36
|
+
debug("提升书籍链接所在镜像地址优先级:", primary_base_url)
|
|
35
37
|
|
|
36
38
|
self._sorter.incr(primary_base_url, 10)
|
|
39
|
+
debug("镜像地址优先级排序:", self._sorter)
|
|
37
40
|
|
|
38
41
|
async def session(self) -> ClientSession:
|
|
39
42
|
try:
|
|
@@ -49,6 +52,7 @@ class KmdrSessionManager(SessionManager):
|
|
|
49
52
|
self._base_url = await self._probing_base_url()
|
|
50
53
|
# 持久化配置
|
|
51
54
|
self._configurer.set_base_url(self._base_url)
|
|
55
|
+
debug("使用的基础 URL:", self._base_url)
|
|
52
56
|
|
|
53
57
|
self._session = ClientSession(
|
|
54
58
|
base_url=self._base_url,
|
|
@@ -75,7 +79,7 @@ class KmdrSessionManager(SessionManager):
|
|
|
75
79
|
|
|
76
80
|
return response.status == 200
|
|
77
81
|
except Exception as e:
|
|
78
|
-
|
|
82
|
+
info(f"[yellow]无法连接到镜像: {url_supplier()},错误信息: {e}[/yellow]")
|
|
79
83
|
return False
|
|
80
84
|
|
|
81
85
|
async def _probing_base_url(self) -> str:
|
|
@@ -107,7 +111,7 @@ class KmdrSessionManager(SessionManager):
|
|
|
107
111
|
|
|
108
112
|
if await async_retry(
|
|
109
113
|
base_url_setter=set_base_url,
|
|
110
|
-
on_failure=lambda e:
|
|
114
|
+
on_failure=lambda e: info(f"[yellow]无法连接到镜像: {get_base_url()},错误信息: {e}[/yellow]"),
|
|
111
115
|
)(self.validate_url)(probe_session, get_base_url):
|
|
112
116
|
return get_base_url()
|
|
113
117
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from kmdr import __version__
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from kmdr.core import *
|
|
8
|
+
from kmdr.module import *
|
|
9
|
+
|
|
10
|
+
async def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
|
|
11
|
+
|
|
12
|
+
post_init(args)
|
|
13
|
+
log('[Lifecycle:Start] 启动 kmdr 版本:', __version__)
|
|
14
|
+
debug('[bold green]以调试模式启动[/bold green]')
|
|
15
|
+
debug('接收到的参数:', args)
|
|
16
|
+
|
|
17
|
+
if args.command == 'version':
|
|
18
|
+
info(f"[green]{__version__}[/green]")
|
|
19
|
+
|
|
20
|
+
elif args.command == 'config':
|
|
21
|
+
CONFIGURER.get(args).operate()
|
|
22
|
+
|
|
23
|
+
elif args.command == 'login':
|
|
24
|
+
async with (await SESSION_MANAGER.get(args).session()):
|
|
25
|
+
await AUTHENTICATOR.get(args).authenticate()
|
|
26
|
+
|
|
27
|
+
elif args.command == 'status':
|
|
28
|
+
async with (await SESSION_MANAGER.get(args).session()):
|
|
29
|
+
await AUTHENTICATOR.get(args).authenticate()
|
|
30
|
+
|
|
31
|
+
elif args.command == 'download':
|
|
32
|
+
async with (await SESSION_MANAGER.get(args).session()):
|
|
33
|
+
await AUTHENTICATOR.get(args).authenticate()
|
|
34
|
+
|
|
35
|
+
book, volumes = await LISTERS.get(args).list()
|
|
36
|
+
debug("获取到书籍《", book.name, "》及其", len(volumes), "个章节信息。")
|
|
37
|
+
|
|
38
|
+
volumes = PICKERS.get(args).pick(volumes)
|
|
39
|
+
debug("选择了", len(volumes), "个章节进行下载:", ', '.join(volume.name for volume in volumes))
|
|
40
|
+
|
|
41
|
+
await DOWNLOADER.get(args).download(book, volumes)
|
|
42
|
+
|
|
43
|
+
else:
|
|
44
|
+
fallback()
|
|
45
|
+
|
|
46
|
+
def main_sync(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
|
|
47
|
+
asyncio.run(main(args, fallback))
|
|
48
|
+
|
|
49
|
+
def entry_point():
|
|
50
|
+
try:
|
|
51
|
+
parser = argument_parser()
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
main_coro = main(args, lambda: parser.print_help())
|
|
55
|
+
asyncio.run(main_coro)
|
|
56
|
+
except KmdrError as e:
|
|
57
|
+
info(f"[red]错误: {e}[/red]")
|
|
58
|
+
exit(1)
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
info("\n操作已取消(KeyboardInterrupt)", style="yellow")
|
|
61
|
+
exit(130)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
exception(e)
|
|
64
|
+
exit(1)
|
|
65
|
+
finally:
|
|
66
|
+
log('[Lifecycle:End] 运行结束,kmdr 已退出')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == '__main__':
|
|
70
|
+
entry_point()
|
{kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/utils.py
RENAMED
|
@@ -8,6 +8,7 @@ from yarl import URL
|
|
|
8
8
|
from kmdr.core.error import LoginError
|
|
9
9
|
from kmdr.core.utils import async_retry
|
|
10
10
|
from kmdr.core.constants import API_ROUTE
|
|
11
|
+
from kmdr.core.console import *
|
|
11
12
|
|
|
12
13
|
NICKNAME_ID = 'div_nickname_display'
|
|
13
14
|
|
|
@@ -27,7 +28,7 @@ async def check_status(
|
|
|
27
28
|
try:
|
|
28
29
|
response.raise_for_status()
|
|
29
30
|
except Exception as e:
|
|
30
|
-
|
|
31
|
+
info(f"Error: {type(e).__name__}: {e}")
|
|
31
32
|
return False
|
|
32
33
|
|
|
33
34
|
if response.history and any(resp.status in (301, 302, 307) for resp in response.history) \
|
|
@@ -50,6 +51,8 @@ async def check_status(
|
|
|
50
51
|
is_vip = int(var_define.get('is_vip', '0'))
|
|
51
52
|
user_level = int(var_define.get('user_level', '0'))
|
|
52
53
|
|
|
54
|
+
debug("解析到用户状态: is_vip=", is_vip, ", user_level=", user_level)
|
|
55
|
+
|
|
53
56
|
if is_vip_setter:
|
|
54
57
|
is_vip_setter(is_vip)
|
|
55
58
|
if level_setter:
|
|
@@ -58,10 +61,14 @@ async def check_status(
|
|
|
58
61
|
if not show_quota:
|
|
59
62
|
return True
|
|
60
63
|
|
|
61
|
-
nickname = soup.find('div', id=NICKNAME_ID).text.strip().split(' ')[0]
|
|
62
|
-
quota = soup.find('div', id=__resolve_quota_id(is_vip, user_level)).text.strip()
|
|
64
|
+
nickname = soup.find('div', id=NICKNAME_ID).text.strip().split(' ')[0].replace('\xa0', '')
|
|
65
|
+
quota = soup.find('div', id=__resolve_quota_id(is_vip, user_level)).text.strip().replace('\xa0', '')
|
|
66
|
+
|
|
67
|
+
if console.is_interactive:
|
|
68
|
+
info(f"\n当前登录为 [bold cyan]{nickname}[/bold cyan]\n\n{quota}")
|
|
69
|
+
else:
|
|
70
|
+
info(f"当前登录为 {nickname}")
|
|
63
71
|
|
|
64
|
-
console.print(f"\n当前登录为 [bold cyan]{nickname}[/bold cyan]\n\n{quota}")
|
|
65
72
|
return True
|
|
66
73
|
|
|
67
74
|
def extract_var_define(script_text) -> dict[str, str]:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from kmdr.core import Configurer, CONFIGURER
|
|
2
|
+
from kmdr.core.console import info
|
|
2
3
|
|
|
3
4
|
@CONFIGURER.register()
|
|
4
5
|
class BaseUrlUpdator(Configurer):
|
|
@@ -10,7 +11,7 @@ class BaseUrlUpdator(Configurer):
|
|
|
10
11
|
try:
|
|
11
12
|
self._configurer.set_base_url(self._base_url)
|
|
12
13
|
except KeyError as e:
|
|
13
|
-
|
|
14
|
+
info(f"[red]{e.args[0]}[/red]")
|
|
14
15
|
exit(1)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
info(f"已设置基础 URL: {self._base_url}")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from kmdr.core import Configurer, CONFIGURER
|
|
2
|
+
from kmdr.core.console import info
|
|
2
3
|
|
|
3
4
|
@CONFIGURER.register()
|
|
4
5
|
class ConfigClearer(Configurer):
|
|
@@ -10,7 +11,7 @@ class ConfigClearer(Configurer):
|
|
|
10
11
|
try:
|
|
11
12
|
self._configurer.clear(self._clear)
|
|
12
13
|
except KeyError as e:
|
|
13
|
-
|
|
14
|
+
info(f"[red]{e.args[0]}[/red]")
|
|
14
15
|
exit(1)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
info(f"Cleared configuration: {self._clear}")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from kmdr.core import Configurer, CONFIGURER
|
|
2
|
+
from kmdr.core.console import info
|
|
2
3
|
|
|
3
4
|
from .option_validate import check_key
|
|
4
5
|
|
|
@@ -10,9 +11,9 @@ class ConfigUnsetter(Configurer):
|
|
|
10
11
|
|
|
11
12
|
def operate(self) -> None:
|
|
12
13
|
if not self._unset:
|
|
13
|
-
|
|
14
|
+
info("[yellow]请提供要取消设置的配置项。[/yellow]")
|
|
14
15
|
return
|
|
15
16
|
|
|
16
17
|
check_key(self._unset)
|
|
17
18
|
self._configurer.unset_option(self._unset)
|
|
18
|
-
|
|
19
|
+
info(f"[green]取消配置项: {self._unset}[/green]")
|
|
@@ -2,6 +2,7 @@ from rich.table import Table
|
|
|
2
2
|
from rich.pretty import Pretty
|
|
3
3
|
|
|
4
4
|
from kmdr.core import CONFIGURER, Configurer
|
|
5
|
+
from kmdr.core.console import info
|
|
5
6
|
|
|
6
7
|
@CONFIGURER.register(
|
|
7
8
|
hasvalues={
|
|
@@ -14,7 +15,7 @@ class OptionLister(Configurer):
|
|
|
14
15
|
|
|
15
16
|
def operate(self) -> None:
|
|
16
17
|
if self._configurer.option is None and self._configurer.base_url is None:
|
|
17
|
-
|
|
18
|
+
info("[blue]当前没有任何配置项。[/blue]")
|
|
18
19
|
return
|
|
19
20
|
|
|
20
21
|
table = Table(title="[green]当前 Kmdr 配置项[/green]", show_header=False, header_style="blue")
|
|
@@ -35,4 +36,4 @@ class OptionLister(Configurer):
|
|
|
35
36
|
if self._configurer.base_url is not None:
|
|
36
37
|
table.add_row('应用配置', '镜像地址', self._configurer.base_url or '未设置')
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
info(table)
|