kmoe-manga-downloader 1.2.1__tar.gz → 1.2.3__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.3/.github/workflows/release-package.yml +45 -0
- kmoe_manga_downloader-1.2.3/.github/workflows/unit-test.yml +31 -0
- kmoe_manga_downloader-1.2.3/.gitignore +23 -0
- {kmoe_manga_downloader-1.2.1/src/kmoe_manga_downloader.egg-info → kmoe_manga_downloader-1.2.3}/PKG-INFO +21 -4
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/README.md +20 -3
- kmoe_manga_downloader-1.2.3/assets/kmdr-demo.gif +0 -0
- kmoe_manga_downloader-1.2.3/assets/kmdr-log-demo.gif +0 -0
- kmoe_manga_downloader-1.2.3/mirror/mirrors.json +11 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/pyproject.toml +8 -1
- kmoe_manga_downloader-1.2.3/src/kmdr/__init__.py +4 -0
- kmoe_manga_downloader-1.2.3/src/kmdr/_version.py +34 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/__init__.py +6 -4
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/bases.py +24 -19
- kmoe_manga_downloader-1.2.3/src/kmdr/core/console.py +67 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/constants.py +17 -22
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/context.py +18 -3
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/defaults.py +22 -7
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/error.py +23 -1
- kmoe_manga_downloader-1.2.3/src/kmdr/core/protocol.py +10 -0
- kmoe_manga_downloader-1.2.3/src/kmdr/core/session.py +119 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/utils.py +51 -2
- kmoe_manga_downloader-1.2.3/src/kmdr/main.py +70 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/CookieAuthenticator.py +4 -8
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/LoginAuthenticator.py +8 -21
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/utils.py +16 -19
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/BaseUrlUpdator.py +3 -2
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/ConfigClearer.py +3 -2
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/ConfigUnsetter.py +3 -2
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/OptionLister.py +3 -2
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/OptionSetter.py +3 -2
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/option_validate.py +11 -11
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/DirectDownloader.py +10 -13
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/ReferViaDownloader.py +11 -12
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/download_utils.py +52 -7
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/misc.py +2 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/FollowedBookLister.py +4 -4
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/utils.py +10 -8
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/picker/DefaultVolPicker.py +2 -1
- kmoe_manga_downloader-1.2.3/src/kmdr/module/picker/utils.py +45 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3/src/kmoe_manga_downloader.egg-info}/PKG-INFO +21 -4
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +10 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/tests/test_kmdr_download.py +11 -8
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/tests/test_utils_resolve_volme.py +3 -3
- kmoe_manga_downloader-1.2.1/src/kmdr/core/session.py +0 -16
- kmoe_manga_downloader-1.2.1/src/kmdr/main.py +0 -49
- kmoe_manga_downloader-1.2.1/src/kmdr/module/picker/utils.py +0 -37
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/LICENSE +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/setup.cfg +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/registry.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/structure.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/BookUrlLister.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/picker/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
- {kmoe_manga_downloader-1.2.1/src/kmdr → kmoe_manga_downloader-1.2.3/tests}/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/tests/test_async_retry_decorator.py +0 -0
- {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3}/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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kmoe-manga-downloader
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: A CLI-downloader for site @kox.moe.
|
|
5
5
|
Author-email: Chris Zheng <chrisis58@outlook.com>
|
|
6
6
|
License: MIT License
|
|
@@ -50,9 +50,26 @@ Dynamic: license-file
|
|
|
50
50
|
|
|
51
51
|
`kmdr (Kmoe Manga Downloader)` 是一个 Python 终端应用,用于从 [Kmoe](https://kxx.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定漫画及其卷,并支持回调脚本执行。
|
|
52
52
|
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
53
|
+
<table style="min-width: 600px;">
|
|
54
|
+
<tbody>
|
|
55
|
+
<tr>
|
|
56
|
+
<td style="text-align: center;" width="100">
|
|
57
|
+
交互模式
|
|
58
|
+
</td>
|
|
59
|
+
<td style="text-align: center;">
|
|
60
|
+
<img src="assets/kmdr-demo.gif" alt="kmdr 使用演示" />
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
<tr>
|
|
64
|
+
<td style="text-align: center;" width="100">
|
|
65
|
+
日志模式
|
|
66
|
+
</td>
|
|
67
|
+
<td style="text-align: center;">
|
|
68
|
+
<img src="assets/kmdr-log-demo.gif" alt="kmdr 日志使用演示" />
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
56
73
|
|
|
57
74
|
## ✨功能特性
|
|
58
75
|
|
|
@@ -4,9 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
`kmdr (Kmoe Manga Downloader)` 是一个 Python 终端应用,用于从 [Kmoe](https://kxx.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定漫画及其卷,并支持回调脚本执行。
|
|
6
6
|
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
7
|
+
<table style="min-width: 600px;">
|
|
8
|
+
<tbody>
|
|
9
|
+
<tr>
|
|
10
|
+
<td style="text-align: center;" width="100">
|
|
11
|
+
交互模式
|
|
12
|
+
</td>
|
|
13
|
+
<td style="text-align: center;">
|
|
14
|
+
<img src="assets/kmdr-demo.gif" alt="kmdr 使用演示" />
|
|
15
|
+
</td>
|
|
16
|
+
</tr>
|
|
17
|
+
<tr>
|
|
18
|
+
<td style="text-align: center;" width="100">
|
|
19
|
+
日志模式
|
|
20
|
+
</td>
|
|
21
|
+
<td style="text-align: center;">
|
|
22
|
+
<img src="assets/kmdr-log-demo.gif" alt="kmdr 日志使用演示" />
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
</tbody>
|
|
26
|
+
</table>
|
|
10
27
|
|
|
11
28
|
## ✨功能特性
|
|
12
29
|
|
|
Binary file
|
|
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.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 2, 3)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'g8e5af2188'
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
from .bases import Authenticator, Lister, Picker, Downloader, Configurer
|
|
1
|
+
from .bases import Authenticator, Lister, Picker, Downloader, Configurer, SessionManager
|
|
2
2
|
from .structure import VolInfo, BookInfo, VolumeType
|
|
3
|
-
from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER,
|
|
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
|
|
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
|
|
@@ -19,12 +20,18 @@ class Configurer(ConfigContext, TerminalContext):
|
|
|
19
20
|
@abstractmethod
|
|
20
21
|
def operate(self) -> None: ...
|
|
21
22
|
|
|
23
|
+
class SessionManager(SessionContext, ConfigContext, TerminalContext):
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def session(self) -> ClientSession: ...
|
|
30
|
+
|
|
22
31
|
class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalContext):
|
|
23
32
|
|
|
24
33
|
def __init__(self, *args, **kwargs):
|
|
25
34
|
super().__init__(*args, **kwargs)
|
|
26
|
-
# 这里的 base url 可能会在认证过程中被更新
|
|
27
|
-
self._inner_base_url: Optional[str] = None
|
|
28
35
|
|
|
29
36
|
# 在使用代理登录时,可能会出现问题,但是现在还不清楚是不是代理的问题。
|
|
30
37
|
# 主站正常情况下不使用代理也能登录成功。但是不排除特殊的网络环境下需要代理。
|
|
@@ -33,21 +40,11 @@ class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalC
|
|
|
33
40
|
async def authenticate(self) -> None:
|
|
34
41
|
with self._console.status("认证中..."):
|
|
35
42
|
try:
|
|
36
|
-
|
|
37
|
-
assert await async_retry(
|
|
38
|
-
base_url_setter=self._configurer.set_base_url
|
|
39
|
-
)(self._authenticate)()
|
|
40
|
-
|
|
41
|
-
# 登录成功后,更新 base_url
|
|
42
|
-
self._base_url = self.base_url
|
|
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
|
-
@property
|
|
49
|
-
def base_url(self) -> str:
|
|
50
|
-
return self._inner_base_url or self._configurer.base_url
|
|
51
48
|
|
|
52
49
|
@abstractmethod
|
|
53
50
|
async def _authenticate(self) -> bool: ...
|
|
@@ -86,22 +83,30 @@ class Downloader(SessionContext, UserProfileContext, TerminalContext):
|
|
|
86
83
|
|
|
87
84
|
async def download(self, book: BookInfo, volumes: list[VolInfo]):
|
|
88
85
|
if not volumes:
|
|
89
|
-
|
|
86
|
+
info("没有可下载的卷。", style="blue")
|
|
90
87
|
exit(0)
|
|
91
88
|
|
|
92
89
|
try:
|
|
93
90
|
with self._progress:
|
|
94
91
|
tasks = [self._download(book, volume) for volume in volumes]
|
|
95
|
-
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)
|
|
96
101
|
|
|
97
102
|
except KeyboardInterrupt:
|
|
98
|
-
|
|
103
|
+
info("\n操作已取消(KeyboardInterrupt)")
|
|
99
104
|
exit(130)
|
|
100
105
|
|
|
101
106
|
@abstractmethod
|
|
102
107
|
async def _download(self, book: BookInfo, volume: VolInfo): ...
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
SESSION_MANAGER = Registry[SessionManager]('SessionManager', True)
|
|
105
110
|
AUTHENTICATOR = Registry[Authenticator]('Authenticator')
|
|
106
111
|
LISTERS = Registry[Lister]('Lister')
|
|
107
112
|
PICKERS = Registry[Picker]('Picker')
|
|
@@ -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,37 +1,30 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from
|
|
3
|
+
from typing import Union
|
|
4
4
|
|
|
5
|
+
from typing_extensions import deprecated
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
class _BaseUrl:
|
|
7
|
+
class BASE_URL(Enum):
|
|
8
8
|
|
|
9
9
|
@property
|
|
10
10
|
@deprecated("KOX 已过时,请使用 KXO 或 KOZ。")
|
|
11
11
|
def KOX(self) -> str:
|
|
12
12
|
return 'https://kox.moe'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
def KXX(self) -> str:
|
|
16
|
-
return 'https://kxx.moe'
|
|
14
|
+
KXX = 'https://kxx.moe'
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
def KXO(self) -> str:
|
|
20
|
-
return 'https://kxo.moe'
|
|
16
|
+
KXO = 'https://kxo.moe'
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
def KOZ(self) -> str:
|
|
24
|
-
return 'https://koz.moe'
|
|
18
|
+
KOZ = 'https://koz.moe'
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
def MOX(self) -> str:
|
|
28
|
-
return 'https://mox.moe'
|
|
20
|
+
MOX = 'https://mox.moe'
|
|
29
21
|
|
|
30
|
-
@
|
|
31
|
-
def
|
|
32
|
-
"""
|
|
33
|
-
return
|
|
22
|
+
@classmethod
|
|
23
|
+
def alternatives(cls) -> set[str]:
|
|
24
|
+
"""返回备用的基础 URL 列表"""
|
|
25
|
+
return {cls.KXO.value, cls.KOZ.value, cls.MOX.value}
|
|
34
26
|
|
|
27
|
+
DEFAULT = KXX
|
|
35
28
|
|
|
36
29
|
@dataclass(frozen=True)
|
|
37
30
|
class _ApiRoute():
|
|
@@ -71,9 +64,11 @@ class LoginResponse(Enum):
|
|
|
71
64
|
def from_code(cls, code: str) -> 'LoginResponse':
|
|
72
65
|
return cls.__members__.get(code, cls.unknown)
|
|
73
66
|
|
|
67
|
+
@classmethod
|
|
68
|
+
def ok(cls, code: Union[str, 'LoginResponse']) -> bool:
|
|
69
|
+
if isinstance(code, LoginResponse):
|
|
70
|
+
return code == cls.m100
|
|
71
|
+
return cls.from_code(code) == cls.m100
|
|
74
72
|
|
|
75
73
|
API_ROUTE = _ApiRoute()
|
|
76
74
|
"""API 路由常量实例"""
|
|
77
|
-
|
|
78
|
-
BASE_URL = _BaseUrl()
|
|
79
|
-
"""基础 URL 实例"""
|
|
@@ -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
|
|
|
@@ -30,6 +41,10 @@ class SessionContext:
|
|
|
30
41
|
@property
|
|
31
42
|
def _session(self) -> ClientSession:
|
|
32
43
|
return session_var.get()
|
|
44
|
+
|
|
45
|
+
@_session.setter
|
|
46
|
+
def _session(self, value: ClientSession):
|
|
47
|
+
session_var.set(value)
|
|
33
48
|
|
|
34
49
|
@property
|
|
35
50
|
def _base_url(self) -> str:
|
|
@@ -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)
|
|
@@ -176,7 +179,7 @@ class Configurer:
|
|
|
176
179
|
@property
|
|
177
180
|
def base_url(self) -> str:
|
|
178
181
|
if self._config is None or self._config.base_url is None:
|
|
179
|
-
return BASE_URL.DEFAULT
|
|
182
|
+
return BASE_URL.DEFAULT.value
|
|
180
183
|
return self._config.base_url
|
|
181
184
|
|
|
182
185
|
def set_base_url(self, value: str):
|
|
@@ -185,6 +188,9 @@ class Configurer:
|
|
|
185
188
|
self._config.base_url = value
|
|
186
189
|
self.update()
|
|
187
190
|
|
|
191
|
+
def get_base_url(self) -> Optional[str]:
|
|
192
|
+
return self._config.base_url
|
|
193
|
+
|
|
188
194
|
def update(self):
|
|
189
195
|
with open(os.path.join(os.path.expanduser("~"), self.__filename), 'w') as f:
|
|
190
196
|
json.dump(self._config.__dict__, f, indent=4, ensure_ascii=False)
|
|
@@ -234,3 +240,12 @@ def combine_args(dest: argparse.Namespace) -> argparse.Namespace:
|
|
|
234
240
|
return __combine_args(dest, option)
|
|
235
241
|
|
|
236
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)
|
|
@@ -7,6 +7,20 @@ 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
|
+
class InitializationError(KmdrError):
|
|
11
|
+
def __init__(self, message, solution: Optional[list[str]] = None):
|
|
12
|
+
super().__init__(message, solution)
|
|
13
|
+
|
|
14
|
+
def __str__(self):
|
|
15
|
+
return f"{self.message}\n{self._solution}"
|
|
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
|
+
|
|
10
24
|
class LoginError(KmdrError):
|
|
11
25
|
def __init__(self, message, solution: Optional[list[str]] = None):
|
|
12
26
|
super().__init__(message, solution)
|
|
@@ -20,4 +34,12 @@ class RedirectError(KmdrError):
|
|
|
20
34
|
self.new_base_url = new_base_url
|
|
21
35
|
|
|
22
36
|
def __str__(self):
|
|
23
|
-
return f"{self.message} 新的地址: {self.new_base_url}"
|
|
37
|
+
return f"{self.message} 新的地址: {self.new_base_url}"
|
|
38
|
+
|
|
39
|
+
class ResponseError(KmdrError):
|
|
40
|
+
def __init__(self, message, status_code: int):
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
self.status_code = status_code
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
return f"{self.message} (状态码: {self.status_code})"
|