kmoe-manga-downloader 1.2.1__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.
Files changed (64) hide show
  1. kmoe_manga_downloader-1.2.3b0/.github/workflows/release-package.yml +45 -0
  2. kmoe_manga_downloader-1.2.3b0/.github/workflows/unit-test.yml +31 -0
  3. kmoe_manga_downloader-1.2.3b0/.gitignore +23 -0
  4. {kmoe_manga_downloader-1.2.1/src/kmoe_manga_downloader.egg-info → kmoe_manga_downloader-1.2.3b0}/PKG-INFO +1 -1
  5. kmoe_manga_downloader-1.2.3b0/assets/kmdr-demo.gif +0 -0
  6. kmoe_manga_downloader-1.2.3b0/mirror/mirrors.json +11 -0
  7. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/pyproject.toml +8 -1
  8. kmoe_manga_downloader-1.2.3b0/src/kmdr/__init__.py +4 -0
  9. kmoe_manga_downloader-1.2.3b0/src/kmdr/_version.py +34 -0
  10. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/__init__.py +6 -4
  11. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/bases.py +24 -19
  12. kmoe_manga_downloader-1.2.3b0/src/kmdr/core/console.py +67 -0
  13. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/constants.py +17 -22
  14. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/context.py +18 -3
  15. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/defaults.py +22 -7
  16. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/error.py +23 -1
  17. kmoe_manga_downloader-1.2.3b0/src/kmdr/core/protocol.py +10 -0
  18. kmoe_manga_downloader-1.2.3b0/src/kmdr/core/session.py +119 -0
  19. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/utils.py +51 -2
  20. kmoe_manga_downloader-1.2.3b0/src/kmdr/main.py +70 -0
  21. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/CookieAuthenticator.py +4 -8
  22. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/LoginAuthenticator.py +8 -21
  23. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/utils.py +16 -19
  24. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/BaseUrlUpdator.py +3 -2
  25. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/ConfigClearer.py +3 -2
  26. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/ConfigUnsetter.py +3 -2
  27. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/OptionLister.py +3 -2
  28. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/OptionSetter.py +3 -2
  29. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/option_validate.py +11 -11
  30. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/DirectDownloader.py +10 -13
  31. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/ReferViaDownloader.py +11 -12
  32. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/download_utils.py +53 -7
  33. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/misc.py +2 -0
  34. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/FollowedBookLister.py +4 -4
  35. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/utils.py +10 -8
  36. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/picker/DefaultVolPicker.py +2 -1
  37. kmoe_manga_downloader-1.2.3b0/src/kmdr/module/picker/utils.py +45 -0
  38. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0/src/kmoe_manga_downloader.egg-info}/PKG-INFO +1 -1
  39. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +9 -0
  40. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/tests/test_kmdr_download.py +11 -8
  41. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/tests/test_utils_resolve_volme.py +3 -3
  42. kmoe_manga_downloader-1.2.1/src/kmdr/core/session.py +0 -16
  43. kmoe_manga_downloader-1.2.1/src/kmdr/main.py +0 -49
  44. kmoe_manga_downloader-1.2.1/src/kmdr/module/picker/utils.py +0 -37
  45. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/LICENSE +0 -0
  46. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/README.md +0 -0
  47. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/setup.cfg +0 -0
  48. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/registry.py +0 -0
  49. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/core/structure.py +0 -0
  50. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/__init__.py +0 -0
  51. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/authenticator/__init__.py +0 -0
  52. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/configurer/__init__.py +0 -0
  53. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/downloader/__init__.py +0 -0
  54. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/BookUrlLister.py +0 -0
  55. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/lister/__init__.py +0 -0
  56. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
  57. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmdr/module/picker/__init__.py +0 -0
  58. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
  59. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
  60. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
  61. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
  62. {kmoe_manga_downloader-1.2.1/src/kmdr → kmoe_manga_downloader-1.2.3b0/tests}/__init__.py +0 -0
  63. {kmoe_manga_downloader-1.2.1 → kmoe_manga_downloader-1.2.3b0}/tests/test_async_retry_decorator.py +0 -0
  64. {kmoe_manga_downloader-1.2.1 → 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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kmoe-manga-downloader
3
- Version: 1.2.1
3
+ Version: 1.2.3b0
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -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.1"
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.3b0'
32
+ __version_tuple__ = version_tuple = (1, 2, 3, 'b0')
33
+
34
+ __commit_id__ = commit_id = 'g28497cb81'
@@ -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, KMDR_SESSION
3
+ from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER, SESSION_MANAGER
4
4
 
5
- from .defaults import argument_parser, session_var
5
+ from .defaults import argument_parser, post_init
6
6
 
7
7
  from .error import KmdrError, LoginError
8
8
 
9
- from .session import KmdrSession
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
- # 这里添加了 base_url_setter,以便在重定向时更新 base_url
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
- self._console.print("[red]认证失败。请检查您的登录凭据或会话 cookie。[/red]")
45
- self._console.print(f"[yellow]详细信息:{e}[/yellow]")
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
- self._console.print("No volumes to download.")
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
- self._console.print("\n操作已取消(KeyboardInterrupt)")
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
- KMDR_SESSION = Registry[ClientSession]('KmdrSession', True)
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 typing_extensions import deprecated
3
+ from typing import Union
4
4
 
5
+ from typing_extensions import deprecated
5
6
 
6
- @dataclass(frozen=True)
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
- @property
15
- def KXX(self) -> str:
16
- return 'https://kxx.moe'
14
+ KXX = 'https://kxx.moe'
17
15
 
18
- @property
19
- def KXO(self) -> str:
20
- return 'https://kxo.moe'
16
+ KXO = 'https://kxo.moe'
21
17
 
22
- @property
23
- def KOZ(self) -> str:
24
- return 'https://koz.moe'
18
+ KOZ = 'https://koz.moe'
25
19
 
26
- @property
27
- def MOX(self) -> str:
28
- return 'https://mox.moe'
20
+ MOX = 'https://mox.moe'
29
21
 
30
- @property
31
- def DEFAULT(self) -> str:
32
- """默认基础 URL"""
33
- return self.KXX
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
- 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
 
@@ -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
- 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)
@@ -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})"
@@ -0,0 +1,10 @@
1
+ from typing import Protocol, TypeVar
2
+
3
+ S = TypeVar('S', covariant=True)
4
+ T = TypeVar('T', contravariant=True)
5
+
6
+ class Supplier(Protocol[S]):
7
+ def __call__(self) -> S: ...
8
+
9
+ class Consumer(Protocol[T]):
10
+ def __call__(self, value: T) -> None: ...