kmoe-manga-downloader 1.2.2__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.
Files changed (64) hide show
  1. kmoe_manga_downloader-1.2.3/.github/workflows/release-package.yml +45 -0
  2. kmoe_manga_downloader-1.2.3/.github/workflows/unit-test.yml +31 -0
  3. kmoe_manga_downloader-1.2.3/.gitignore +23 -0
  4. {kmoe_manga_downloader-1.2.2/src/kmoe_manga_downloader.egg-info → kmoe_manga_downloader-1.2.3}/PKG-INFO +21 -4
  5. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/README.md +20 -3
  6. kmoe_manga_downloader-1.2.3/assets/kmdr-demo.gif +0 -0
  7. kmoe_manga_downloader-1.2.3/assets/kmdr-log-demo.gif +0 -0
  8. kmoe_manga_downloader-1.2.3/mirror/mirrors.json +11 -0
  9. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/pyproject.toml +8 -1
  10. kmoe_manga_downloader-1.2.3/src/kmdr/__init__.py +4 -0
  11. kmoe_manga_downloader-1.2.3/src/kmdr/_version.py +34 -0
  12. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/__init__.py +4 -2
  13. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/bases.py +14 -5
  14. kmoe_manga_downloader-1.2.3/src/kmdr/core/console.py +67 -0
  15. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/context.py +14 -3
  16. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/defaults.py +19 -7
  17. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/error.py +7 -0
  18. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/session.py +6 -2
  19. kmoe_manga_downloader-1.2.3/src/kmdr/main.py +70 -0
  20. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/utils.py +11 -4
  21. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/BaseUrlUpdator.py +3 -2
  22. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/ConfigClearer.py +3 -2
  23. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/ConfigUnsetter.py +3 -2
  24. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/OptionLister.py +3 -2
  25. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/OptionSetter.py +3 -2
  26. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/option_validate.py +11 -11
  27. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/DirectDownloader.py +1 -1
  28. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/ReferViaDownloader.py +2 -0
  29. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/download_utils.py +22 -7
  30. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/FollowedBookLister.py +3 -2
  31. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/utils.py +5 -1
  32. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/picker/DefaultVolPicker.py +2 -1
  33. kmoe_manga_downloader-1.2.3/src/kmdr/module/picker/utils.py +45 -0
  34. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3/src/kmoe_manga_downloader.egg-info}/PKG-INFO +21 -4
  35. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +9 -0
  36. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/tests/test_kmdr_download.py +2 -2
  37. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/tests/test_utils_resolve_volme.py +3 -3
  38. kmoe_manga_downloader-1.2.2/src/kmdr/main.py +0 -51
  39. kmoe_manga_downloader-1.2.2/src/kmdr/module/picker/utils.py +0 -37
  40. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/LICENSE +0 -0
  41. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/setup.cfg +0 -0
  42. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/constants.py +0 -0
  43. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/protocol.py +0 -0
  44. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/registry.py +0 -0
  45. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/structure.py +0 -0
  46. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/core/utils.py +0 -0
  47. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/__init__.py +0 -0
  48. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/CookieAuthenticator.py +0 -0
  49. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/LoginAuthenticator.py +0 -0
  50. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/authenticator/__init__.py +0 -0
  51. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/configurer/__init__.py +0 -0
  52. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/__init__.py +0 -0
  53. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/downloader/misc.py +0 -0
  54. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/BookUrlLister.py +0 -0
  55. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/lister/__init__.py +0 -0
  56. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
  57. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmdr/module/picker/__init__.py +0 -0
  58. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
  59. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
  60. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
  61. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
  62. {kmoe_manga_downloader-1.2.2/src/kmdr → kmoe_manga_downloader-1.2.3/tests}/__init__.py +0 -0
  63. {kmoe_manga_downloader-1.2.2 → kmoe_manga_downloader-1.2.3}/tests/test_async_retry_decorator.py +0 -0
  64. {kmoe_manga_downloader-1.2.2 → 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.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
- <p align="center">
54
- <img src="assets/kmdr-demo.gif" alt="kmdr 使用演示" width="720">
55
- </p>
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
- <p align="center">
8
- <img src="assets/kmdr-demo.gif" alt="kmdr 使用演示" width="720">
9
- </p>
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
 
@@ -0,0 +1,11 @@
1
+ {
2
+ "default": "https://kxx.moe",
3
+ "alternatives": [
4
+ "https://kxo.moe",
5
+ "https://mox.moe",
6
+ "https://koz.moe"
7
+ ],
8
+ "deprecated": [
9
+ "https://kox.moe"
10
+ ]
11
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kmoe-manga-downloader"
3
- version = "1.2.2"
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,4 @@
1
+ try:
2
+ from ._version import __version__
3
+ except ImportError:
4
+ __version__ = "unknown"
@@ -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'
@@ -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, console
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
- self._console.print(f"[yellow]详细信息:{e}[/yellow]")
45
- self._console.print("[red]认证失败。请检查您的登录凭据或会话 cookie。[/red]")
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
- self._console.print("没有可下载的卷。", style="blue")
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
- self._console.print("\n操作已取消(KeyboardInterrupt)")
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
- from .defaults import Configurer as InnerConfigurer, UserProfile, session_var, progress, console, base_url_var
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._progress = progress
11
- self._console = console
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
- def console_print(*args, **kwargs):
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
- self._console.print(f"[yellow]无法连接到镜像: {url_supplier()},错误信息: {e}[/yellow]")
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: self._console.print(f"[yellow]无法连接到镜像: {get_base_url()},错误信息: {e}[/yellow]"),
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()
@@ -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
- console.print(f"Error: {type(e).__name__}: {e}")
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
- self._console.print(e.args[0])
14
+ info(f"[red]{e.args[0]}[/red]")
14
15
  exit(1)
15
16
 
16
- self._console.print(f"已设置基础 URL: {self._base_url}")
17
+ info(f"已设置基础 URL: {self._base_url}")