kmoe-manga-downloader 1.2.0__tar.gz → 1.2.1__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 (54) hide show
  1. {kmoe_manga_downloader-1.2.0/src/kmoe_manga_downloader.egg-info → kmoe_manga_downloader-1.2.1}/PKG-INFO +42 -19
  2. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/README.md +39 -17
  3. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/pyproject.toml +4 -3
  4. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/bases.py +14 -2
  5. kmoe_manga_downloader-1.2.1/src/kmdr/core/constants.py +79 -0
  6. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/context.py +19 -7
  7. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/defaults.py +28 -5
  8. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/error.py +9 -1
  9. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/structure.py +2 -0
  10. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/utils.py +11 -3
  11. kmoe_manga_downloader-1.2.1/src/kmdr/module/__init__.py +5 -0
  12. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/authenticator/CookieAuthenticator.py +8 -5
  13. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/authenticator/LoginAuthenticator.py +14 -4
  14. kmoe_manga_downloader-1.2.1/src/kmdr/module/authenticator/__init__.py +2 -0
  15. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/authenticator/utils.py +15 -5
  16. kmoe_manga_downloader-1.2.1/src/kmdr/module/configurer/BaseUrlUpdator.py +16 -0
  17. kmoe_manga_downloader-1.2.1/src/kmdr/module/configurer/OptionLister.py +38 -0
  18. kmoe_manga_downloader-1.2.1/src/kmdr/module/configurer/__init__.py +5 -0
  19. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/downloader/DirectDownloader.py +12 -2
  20. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/downloader/ReferViaDownloader.py +11 -2
  21. kmoe_manga_downloader-1.2.1/src/kmdr/module/downloader/__init__.py +2 -0
  22. kmoe_manga_downloader-1.2.0/src/kmdr/module/downloader/utils.py → kmoe_manga_downloader-1.2.1/src/kmdr/module/downloader/download_utils.py +33 -26
  23. kmoe_manga_downloader-1.2.1/src/kmdr/module/downloader/misc.py +62 -0
  24. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/lister/FollowedBookLister.py +4 -3
  25. kmoe_manga_downloader-1.2.1/src/kmdr/module/lister/__init__.py +2 -0
  26. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/lister/utils.py +13 -4
  27. kmoe_manga_downloader-1.2.1/src/kmdr/module/picker/__init__.py +2 -0
  28. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1/src/kmoe_manga_downloader.egg-info}/PKG-INFO +42 -19
  29. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +9 -1
  30. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmoe_manga_downloader.egg-info/requires.txt +2 -1
  31. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/tests/test_async_retry_decorator.py +23 -0
  32. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/tests/test_kmdr_download.py +59 -4
  33. kmoe_manga_downloader-1.2.0/src/kmdr/module/__init__.py +0 -5
  34. kmoe_manga_downloader-1.2.0/src/kmdr/module/configurer/OptionLister.py +0 -33
  35. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/LICENSE +0 -0
  36. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/setup.cfg +0 -0
  37. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/__init__.py +0 -0
  38. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/__init__.py +0 -0
  39. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/registry.py +0 -0
  40. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/core/session.py +0 -0
  41. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/main.py +0 -0
  42. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/configurer/ConfigClearer.py +0 -0
  43. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/configurer/ConfigUnsetter.py +0 -0
  44. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/configurer/OptionSetter.py +0 -0
  45. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/configurer/option_validate.py +0 -0
  46. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/lister/BookUrlLister.py +0 -0
  47. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
  48. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/picker/DefaultVolPicker.py +0 -0
  49. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmdr/module/picker/utils.py +0 -0
  50. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
  51. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
  52. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
  53. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/tests/test_kmdr_config_option.py +0 -0
  54. {kmoe_manga_downloader-1.2.0 → kmoe_manga_downloader-1.2.1}/tests/test_utils_resolve_volme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kmoe-manga-downloader
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -35,19 +35,20 @@ Classifier: Operating System :: OS Independent
35
35
  Requires-Python: >=3.9
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
- Requires-Dist: deprecation~=2.1.0
39
38
  Requires-Dist: beautifulsoup4~=4.13.4
40
39
  Requires-Dist: aiohttp~=3.12.15
41
40
  Requires-Dist: aiofiles~=24.1.0
42
41
  Requires-Dist: async-lru~=2.0.5
43
42
  Requires-Dist: rich~=13.9.4
43
+ Requires-Dist: yarl~=1.20.1
44
+ Requires-Dist: typing-extensions~=4.15.0
44
45
  Dynamic: license-file
45
46
 
46
47
  # Kmoe Manga Downloader
47
48
 
48
49
  [![PyPI Downloads](https://static.pepy.tech/badge/kmoe-manga-downloader)](https://pepy.tech/projects/kmoe-manga-downloader) [![PyPI version](https://img.shields.io/pypi/v/kmoe-manga-downloader.svg)](https://pypi.org/project/kmoe-manga-downloader/) [![Unit Tests](https://github.com/chrisis58/kmdr/actions/workflows/unit-test.yml/badge.svg)](https://github.com/chrisis58/kmdr/actions/workflows/unit-test.yml) [![Interpretor](https://img.shields.io/badge/python-3.9+-blue)](https://www.python.org/) [![License](https://img.shields.io/badge/License-MIT-green)](https://github.com/chrisis58/kmdr/blob/main/LICENSE)
49
50
 
50
- `kmdr (Kmoe Manga Downloader)` 是一个 Python 应用,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定漫画及其卷,并支持回调脚本执行。
51
+ `kmdr (Kmoe Manga Downloader)` 是一个 Python 终端应用,用于从 [Kmoe](https://kxx.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定漫画及其卷,并支持回调脚本执行。
51
52
 
52
53
  <p align="center">
53
54
  <img src="assets/kmdr-demo.gif" alt="kmdr 使用演示" width="720">
@@ -55,11 +56,17 @@ Dynamic: license-file
55
56
 
56
57
  ## ✨功能特性
57
58
 
58
- - **现代化终端界面**: 使用 [rich](https://github.com/Textualize/rich) 构建的终端用户界面(TUI),提供进度条和菜单等现代化、美观的交互式终端界面。
59
+ - **现代化终端界面**: 基于 [rich](https://github.com/Textualize/rich) 构建的终端用户界面(TUI),提供进度条和菜单等现代化、美观的交互式终端界面。
59
60
  - **凭证和配置管理**: 应用自动维护登录凭证和下载设置,实现一次配置、持久有效,提升使用效率。
60
- - **高效下载的性能**: 采用 `asyncio` 并发分片下载技术,充分利用网络带宽,极大加速单个大文件的下载速度。
61
- - **强大的高可用性**: 内置强大的自动重试与断点续传机制,无惧网络中断,确保下载任务最终成功。
62
- - **灵活的自动化接口**: 支持下载完成后自动执行自定义回调脚本,轻松集成到您的自动化流程。
61
+ - **高效下载的性能**: 采用 `asyncio` 并发分片下载方式,充分利用网络带宽,显著加速单个大文件的下载速度。
62
+ - **强大的高可用性**: 内置自动重试与断点续传机制,无惧网络中断,确保下载任务在不稳定环境下依然能够成功。
63
+ - **灵活的自动化接口**: 支持在每个文件下载成功后自动执行自定义回调脚本,轻松集成到您的自动化工作流。
64
+
65
+ ## 🖼️ 使用场景
66
+
67
+ - **通用的加速体验**: 采用并发分片下载方式,充分地利用不同类型用户的网络带宽,提升数据传输效率,从而有效缩短下载的等待时间。
68
+ - **灵活部署与远程控制**: 支持在远端服务器或 NAS 上运行,可以在其他设备(PC、平板)上浏览,而通过简单的命令触发远程下载任务,实现浏览与存储的分离。
69
+ - **智能化自动追新**: 应用支持识别重复内容,可配合定时任务实等现无人值守下载最新的内容,轻松打造时刻保持同步的个人资料库。
63
70
 
64
71
  ## 🛠️安装应用
65
72
 
@@ -73,7 +80,7 @@ pip install kmoe-manga-downloader
73
80
 
74
81
  ### 1. 登录 `kmoe`
75
82
 
76
- 首先需要登录 `kox.moe` 并保存登录状态(Cookie)。
83
+ 首先需要登录 `kmoe` 并保存登录状态(Cookie)。
77
84
 
78
85
  ```bash
79
86
  kmdr login -u <your_username> -p <your_password>
@@ -91,26 +98,28 @@ kmdr login -u <your_username>
91
98
 
92
99
  ```bash
93
100
  # 在当前目录下载第一、二、三卷
94
- kmdr download --dest . --book-url https://kox.moe/c/50076.htm --volume 1,2,3
95
- kmdr download -l https://kox.moe/c/50076.htm -v 1-3
101
+ kmdr download --dest . --book-url https://kxx.moe/c/50076.htm --volume 1,2,3
102
+ # 下面命令的功能与上面相同
103
+ kmdr download -l https://kxx.moe/c/50076.htm -v 1-3
96
104
  ```
97
105
 
98
106
  ```bash
99
107
  # 在目标目录下载全部番外篇
100
- kmdr download --dest path/to/destination --book-url https://kox.moe/c/50076.htm --vol-type extra -v all
101
- kmdr download -d path/to/destination -l https://kox.moe/c/50076.htm -t extra -v all
108
+ kmdr download --dest path/to/destination --book-url https://kxx.moe/c/50076.htm --vol-type extra -v all
109
+ # 下面命令的功能与上面相同
110
+ kmdr download -d path/to/destination -l https://kxx.moe/c/50076.htm -t extra -v all
102
111
  ```
103
112
 
104
113
  #### 常用参数说明:
105
114
 
106
- - `-d`, `--dest`: 下载的目标目录(默认为当前目录),在此基础上会额外添加一个为书籍名称的子目录
107
- - `-l`, `--book-url`: 指定书籍的主页地址
108
- - `-v`, `--volume`: 指定卷的名称,多个名称使用逗号分隔,`all` 表示下载所有卷
115
+ - `-d`, `--dest`: 下载的目标目录(默认为当前目录),在此基础上会额外添加一个为漫画名称的子目录
116
+ - `-l`, `--book-url`: 指定漫画的主页地址
117
+ - `-v`, `--volume`: 指定下载的卷,多个用逗号分隔,例如 `1,2,3` 或 `1-5,8`,`all` 表示全部
109
118
  - `-t`, `--vol-type`: 卷类型,`vol`: 单行本(默认);`extra`: 番外;`seri`: 连载话;`all`: 全部
110
119
  - `-p`, `--proxy`: 代理服务器地址
111
- - `-r`, `--retry`: 下载失败时的重试次数
120
+ - `-r`, `--retry`: 下载失败时的重试次数,默认为 3
112
121
  - `-c`, `--callback`: 下载完成后的回调脚本(使用方式详见 [4. 回调函数](https://github.com/chrisis58/kmoe-manga-downlaoder?tab=readme-ov-file#4-%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0))
113
- - `--num-workers`: 最大同时下载数量,默认为 1
122
+ - `--num-workers`: 最大下载并发数量,默认为 8
114
123
 
115
124
  > 完整的参数说明可以从 `help` 指令中获取。
116
125
 
@@ -127,7 +136,7 @@ kmdr status
127
136
  你可以设置一个回调函数,下载完成后执行。回调可以是任何你想要的命令:
128
137
 
129
138
  ```bash
130
- kmdr download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
139
+ kmdr download -d path/to/destination --book-url https://kxx.moe/c/50076.htm -v 1-3 \
131
140
  --callback "echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
132
141
  ```
133
142
 
@@ -143,7 +152,7 @@ kmdr download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1
143
152
  | b.name | 对应漫画的名字 |
144
153
  | b.author | 对应漫画的作者 |
145
154
 
146
- > 完整的可用参数请参考 [structure.py](https://github.com/chrisis58/kmdr/blob/main/core/structure.py#L11) 中关于 `VolInfo` 的定义。
155
+ > 完整的可用参数请参考 [structure.py](https://github.com/chrisis58/kmoe-manga-downloader/blob/main/src/kmdr/core/structure.py#L11) 中关于 `VolInfo` 的定义。
147
156
 
148
157
  ### 5. 持久化配置
149
158
 
@@ -167,6 +176,20 @@ kmdr config -s num_workers=5 "callback=echo '{b.name} {v.name} downloaded!' >> ~
167
176
 
168
177
  > 当前仅支持部分下载参数的持久化:`num_workers`, `dest`, `retry`, `callback`, `proxy`
169
178
 
179
+ ### 6. 镜像源切换
180
+
181
+ 为了保证服务的长期可用性,并让用户能根据自己的网络环境选择最快的服务器,应用支持灵活地切换镜像源。
182
+
183
+ 当您发现默认源(当前为 `kxx.moe`)访问缓慢或失效时,可以通过 `config` 命令轻松切换到其他备用镜像源:
184
+
185
+ ```
186
+ kmdr config --base-url https://mox.moe
187
+ # 或者
188
+ kmdr config -b https://mox.moe
189
+ ```
190
+
191
+ 你可以参考 [镜像目录](./mirror/mirrors.json) 来选择合适的镜像源,如果你发现部分镜像源过时或者有缺失,欢迎贡献你的内容!
192
+
170
193
  ## ⚠️ 声明
171
194
 
172
195
  - 本工具仅作学习、研究、交流使用,使用本工具的用户应自行承担风险
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![PyPI Downloads](https://static.pepy.tech/badge/kmoe-manga-downloader)](https://pepy.tech/projects/kmoe-manga-downloader) [![PyPI version](https://img.shields.io/pypi/v/kmoe-manga-downloader.svg)](https://pypi.org/project/kmoe-manga-downloader/) [![Unit Tests](https://github.com/chrisis58/kmdr/actions/workflows/unit-test.yml/badge.svg)](https://github.com/chrisis58/kmdr/actions/workflows/unit-test.yml) [![Interpretor](https://img.shields.io/badge/python-3.9+-blue)](https://www.python.org/) [![License](https://img.shields.io/badge/License-MIT-green)](https://github.com/chrisis58/kmdr/blob/main/LICENSE)
4
4
 
5
- `kmdr (Kmoe Manga Downloader)` 是一个 Python 应用,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定漫画及其卷,并支持回调脚本执行。
5
+ `kmdr (Kmoe Manga Downloader)` 是一个 Python 终端应用,用于从 [Kmoe](https://kxx.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定漫画及其卷,并支持回调脚本执行。
6
6
 
7
7
  <p align="center">
8
8
  <img src="assets/kmdr-demo.gif" alt="kmdr 使用演示" width="720">
@@ -10,11 +10,17 @@
10
10
 
11
11
  ## ✨功能特性
12
12
 
13
- - **现代化终端界面**: 使用 [rich](https://github.com/Textualize/rich) 构建的终端用户界面(TUI),提供进度条和菜单等现代化、美观的交互式终端界面。
13
+ - **现代化终端界面**: 基于 [rich](https://github.com/Textualize/rich) 构建的终端用户界面(TUI),提供进度条和菜单等现代化、美观的交互式终端界面。
14
14
  - **凭证和配置管理**: 应用自动维护登录凭证和下载设置,实现一次配置、持久有效,提升使用效率。
15
- - **高效下载的性能**: 采用 `asyncio` 并发分片下载技术,充分利用网络带宽,极大加速单个大文件的下载速度。
16
- - **强大的高可用性**: 内置强大的自动重试与断点续传机制,无惧网络中断,确保下载任务最终成功。
17
- - **灵活的自动化接口**: 支持下载完成后自动执行自定义回调脚本,轻松集成到您的自动化流程。
15
+ - **高效下载的性能**: 采用 `asyncio` 并发分片下载方式,充分利用网络带宽,显著加速单个大文件的下载速度。
16
+ - **强大的高可用性**: 内置自动重试与断点续传机制,无惧网络中断,确保下载任务在不稳定环境下依然能够成功。
17
+ - **灵活的自动化接口**: 支持在每个文件下载成功后自动执行自定义回调脚本,轻松集成到您的自动化工作流。
18
+
19
+ ## 🖼️ 使用场景
20
+
21
+ - **通用的加速体验**: 采用并发分片下载方式,充分地利用不同类型用户的网络带宽,提升数据传输效率,从而有效缩短下载的等待时间。
22
+ - **灵活部署与远程控制**: 支持在远端服务器或 NAS 上运行,可以在其他设备(PC、平板)上浏览,而通过简单的命令触发远程下载任务,实现浏览与存储的分离。
23
+ - **智能化自动追新**: 应用支持识别重复内容,可配合定时任务实等现无人值守下载最新的内容,轻松打造时刻保持同步的个人资料库。
18
24
 
19
25
  ## 🛠️安装应用
20
26
 
@@ -28,7 +34,7 @@ pip install kmoe-manga-downloader
28
34
 
29
35
  ### 1. 登录 `kmoe`
30
36
 
31
- 首先需要登录 `kox.moe` 并保存登录状态(Cookie)。
37
+ 首先需要登录 `kmoe` 并保存登录状态(Cookie)。
32
38
 
33
39
  ```bash
34
40
  kmdr login -u <your_username> -p <your_password>
@@ -46,26 +52,28 @@ kmdr login -u <your_username>
46
52
 
47
53
  ```bash
48
54
  # 在当前目录下载第一、二、三卷
49
- kmdr download --dest . --book-url https://kox.moe/c/50076.htm --volume 1,2,3
50
- kmdr download -l https://kox.moe/c/50076.htm -v 1-3
55
+ kmdr download --dest . --book-url https://kxx.moe/c/50076.htm --volume 1,2,3
56
+ # 下面命令的功能与上面相同
57
+ kmdr download -l https://kxx.moe/c/50076.htm -v 1-3
51
58
  ```
52
59
 
53
60
  ```bash
54
61
  # 在目标目录下载全部番外篇
55
- kmdr download --dest path/to/destination --book-url https://kox.moe/c/50076.htm --vol-type extra -v all
56
- kmdr download -d path/to/destination -l https://kox.moe/c/50076.htm -t extra -v all
62
+ kmdr download --dest path/to/destination --book-url https://kxx.moe/c/50076.htm --vol-type extra -v all
63
+ # 下面命令的功能与上面相同
64
+ kmdr download -d path/to/destination -l https://kxx.moe/c/50076.htm -t extra -v all
57
65
  ```
58
66
 
59
67
  #### 常用参数说明:
60
68
 
61
- - `-d`, `--dest`: 下载的目标目录(默认为当前目录),在此基础上会额外添加一个为书籍名称的子目录
62
- - `-l`, `--book-url`: 指定书籍的主页地址
63
- - `-v`, `--volume`: 指定卷的名称,多个名称使用逗号分隔,`all` 表示下载所有卷
69
+ - `-d`, `--dest`: 下载的目标目录(默认为当前目录),在此基础上会额外添加一个为漫画名称的子目录
70
+ - `-l`, `--book-url`: 指定漫画的主页地址
71
+ - `-v`, `--volume`: 指定下载的卷,多个用逗号分隔,例如 `1,2,3` 或 `1-5,8`,`all` 表示全部
64
72
  - `-t`, `--vol-type`: 卷类型,`vol`: 单行本(默认);`extra`: 番外;`seri`: 连载话;`all`: 全部
65
73
  - `-p`, `--proxy`: 代理服务器地址
66
- - `-r`, `--retry`: 下载失败时的重试次数
74
+ - `-r`, `--retry`: 下载失败时的重试次数,默认为 3
67
75
  - `-c`, `--callback`: 下载完成后的回调脚本(使用方式详见 [4. 回调函数](https://github.com/chrisis58/kmoe-manga-downlaoder?tab=readme-ov-file#4-%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0))
68
- - `--num-workers`: 最大同时下载数量,默认为 1
76
+ - `--num-workers`: 最大下载并发数量,默认为 8
69
77
 
70
78
  > 完整的参数说明可以从 `help` 指令中获取。
71
79
 
@@ -82,7 +90,7 @@ kmdr status
82
90
  你可以设置一个回调函数,下载完成后执行。回调可以是任何你想要的命令:
83
91
 
84
92
  ```bash
85
- kmdr download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
93
+ kmdr download -d path/to/destination --book-url https://kxx.moe/c/50076.htm -v 1-3 \
86
94
  --callback "echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
87
95
  ```
88
96
 
@@ -98,7 +106,7 @@ kmdr download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1
98
106
  | b.name | 对应漫画的名字 |
99
107
  | b.author | 对应漫画的作者 |
100
108
 
101
- > 完整的可用参数请参考 [structure.py](https://github.com/chrisis58/kmdr/blob/main/core/structure.py#L11) 中关于 `VolInfo` 的定义。
109
+ > 完整的可用参数请参考 [structure.py](https://github.com/chrisis58/kmoe-manga-downloader/blob/main/src/kmdr/core/structure.py#L11) 中关于 `VolInfo` 的定义。
102
110
 
103
111
  ### 5. 持久化配置
104
112
 
@@ -122,6 +130,20 @@ kmdr config -s num_workers=5 "callback=echo '{b.name} {v.name} downloaded!' >> ~
122
130
 
123
131
  > 当前仅支持部分下载参数的持久化:`num_workers`, `dest`, `retry`, `callback`, `proxy`
124
132
 
133
+ ### 6. 镜像源切换
134
+
135
+ 为了保证服务的长期可用性,并让用户能根据自己的网络环境选择最快的服务器,应用支持灵活地切换镜像源。
136
+
137
+ 当您发现默认源(当前为 `kxx.moe`)访问缓慢或失效时,可以通过 `config` 命令轻松切换到其他备用镜像源:
138
+
139
+ ```
140
+ kmdr config --base-url https://mox.moe
141
+ # 或者
142
+ kmdr config -b https://mox.moe
143
+ ```
144
+
145
+ 你可以参考 [镜像目录](./mirror/mirrors.json) 来选择合适的镜像源,如果你发现部分镜像源过时或者有缺失,欢迎贡献你的内容!
146
+
125
147
  ## ⚠️ 声明
126
148
 
127
149
  - 本工具仅作学习、研究、交流使用,使用本工具的用户应自行承担风险
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kmoe-manga-downloader"
3
- version = "1.2.0"
3
+ version = "1.2.1"
4
4
  authors = [
5
5
  { name="Chris Zheng", email="chrisis58@outlook.com" },
6
6
  ]
@@ -17,12 +17,13 @@ classifiers = [
17
17
  ]
18
18
 
19
19
  dependencies = [
20
- "deprecation~=2.1.0",
21
20
  "beautifulsoup4~=4.13.4",
22
21
  "aiohttp~=3.12.15",
23
22
  "aiofiles~=24.1.0",
24
23
  "async-lru~=2.0.5",
25
- "rich~=13.9.4"
24
+ "rich~=13.9.4",
25
+ "yarl~=1.20.1",
26
+ "typing-extensions~=4.15.0",
26
27
  ]
27
28
 
28
29
  [project.urls]
@@ -7,7 +7,7 @@ from aiohttp import ClientSession
7
7
  from .error import LoginError
8
8
  from .registry import Registry
9
9
  from .structure import VolInfo, BookInfo
10
- from .utils import construct_callback
10
+ from .utils import construct_callback, async_retry
11
11
 
12
12
  from .context import TerminalContext, SessionContext, UserProfileContext, ConfigContext
13
13
 
@@ -23,6 +23,8 @@ class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalC
23
23
 
24
24
  def __init__(self, *args, **kwargs):
25
25
  super().__init__(*args, **kwargs)
26
+ # 这里的 base url 可能会在认证过程中被更新
27
+ self._inner_base_url: Optional[str] = None
26
28
 
27
29
  # 在使用代理登录时,可能会出现问题,但是现在还不清楚是不是代理的问题。
28
30
  # 主站正常情况下不使用代理也能登录成功。但是不排除特殊的网络环境下需要代理。
@@ -31,11 +33,21 @@ class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalC
31
33
  async def authenticate(self) -> None:
32
34
  with self._console.status("认证中..."):
33
35
  try:
34
- assert await self._authenticate()
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
35
43
  except LoginError as e:
36
44
  self._console.print("[red]认证失败。请检查您的登录凭据或会话 cookie。[/red]")
37
45
  self._console.print(f"[yellow]详细信息:{e}[/yellow]")
38
46
  exit(1)
47
+
48
+ @property
49
+ def base_url(self) -> str:
50
+ return self._inner_base_url or self._configurer.base_url
39
51
 
40
52
  @abstractmethod
41
53
  async def _authenticate(self) -> bool: ...
@@ -0,0 +1,79 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing_extensions import deprecated
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class _BaseUrl:
8
+
9
+ @property
10
+ @deprecated("KOX 已过时,请使用 KXO 或 KOZ。")
11
+ def KOX(self) -> str:
12
+ return 'https://kox.moe'
13
+
14
+ @property
15
+ def KXX(self) -> str:
16
+ return 'https://kxx.moe'
17
+
18
+ @property
19
+ def KXO(self) -> str:
20
+ return 'https://kxo.moe'
21
+
22
+ @property
23
+ def KOZ(self) -> str:
24
+ return 'https://koz.moe'
25
+
26
+ @property
27
+ def MOX(self) -> str:
28
+ return 'https://mox.moe'
29
+
30
+ @property
31
+ def DEFAULT(self) -> str:
32
+ """默认基础 URL"""
33
+ return self.KXX
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class _ApiRoute():
38
+ PROFILE: str = '/my.php'
39
+ """用户信息页面"""
40
+
41
+ LOGIN: str = '/login.php'
42
+ """登录页面"""
43
+
44
+ LOGIN_DO: str = '/login_do.php'
45
+ """登录接口"""
46
+
47
+ MY_FOLLOW: str = '/myfollow.php'
48
+ """关注列表页面"""
49
+
50
+ BOOK_DATA: str = '/book_data.php'
51
+ """书籍数据接口"""
52
+
53
+ DOWNLOAD: str = '/dl/{book_id}/{volume_id}/1/2/{is_vip}/'
54
+ """下载接口"""
55
+
56
+ GETDOWNURL: str = '/getdownurl.php?b={book_id}&v={volume_id}&mobi=2&vip={is_vip}&json=1'
57
+ """获取下载链接接口"""
58
+
59
+
60
+ class LoginResponse(Enum):
61
+ m100 = "登录成功。"
62
+
63
+ e400 = "帳號或密碼錯誤。"
64
+ e401 = "非法訪問,請使用瀏覽器正常打開本站。"
65
+ e402 = "帳號已經註銷。不會解釋原因,無需提問。"
66
+ e403 = "驗證失效,請刷新頁面重新操作。"
67
+
68
+ unknown = "未知响应代码。"
69
+
70
+ @classmethod
71
+ def from_code(cls, code: str) -> 'LoginResponse':
72
+ return cls.__members__.get(code, cls.unknown)
73
+
74
+
75
+ API_ROUTE = _ApiRoute()
76
+ """API 路由常量实例"""
77
+
78
+ BASE_URL = _BaseUrl()
79
+ """基础 URL 实例"""
@@ -1,6 +1,7 @@
1
1
  from aiohttp import ClientSession
2
2
 
3
- from .defaults import Configurer as InnerConfigurer, UserProfile, session_var, progress, console
3
+
4
+ from .defaults import Configurer as InnerConfigurer, UserProfile, session_var, progress, console, base_url_var
4
5
 
5
6
  class TerminalContext:
6
7
 
@@ -9,12 +10,6 @@ class TerminalContext:
9
10
  self._progress = progress
10
11
  self._console = console
11
12
 
12
- class SessionContext:
13
-
14
- def __init__(self, *args, **kwargs):
15
- super().__init__()
16
- self._session: ClientSession = session_var.get()
17
-
18
13
  class UserProfileContext:
19
14
 
20
15
  def __init__(self, *args, **kwargs):
@@ -26,3 +21,20 @@ class ConfigContext:
26
21
  def __init__(self, *args, **kwargs):
27
22
  super().__init__()
28
23
  self._configurer = InnerConfigurer()
24
+
25
+ class SessionContext:
26
+
27
+ def __init__(self, *args, **kwargs):
28
+ super().__init__()
29
+
30
+ @property
31
+ def _session(self) -> ClientSession:
32
+ return session_var.get()
33
+
34
+ @property
35
+ def _base_url(self) -> str:
36
+ return base_url_var.get()
37
+
38
+ @_base_url.setter
39
+ def _base_url(self, value: str):
40
+ base_url_var.set(value)
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import json
3
- from typing import Optional
3
+ from typing import Optional, Any
4
4
  import argparse
5
5
  from contextvars import ContextVar
6
6
  from rich.console import Console
@@ -15,6 +15,7 @@ from rich.progress import (
15
15
 
16
16
  from .utils import singleton
17
17
  from .structure import Config
18
+ from .constants import BASE_URL
18
19
 
19
20
  HEADERS = {
20
21
  'User-Agent': 'kmdr/1.0 (https://github.com/chrisis58/kmoe-manga-downloader)'
@@ -75,6 +76,7 @@ def argument_parser():
75
76
  config_parser = subparsers.add_parser('config', help='配置下载器')
76
77
  config_parser.add_argument('-l', '--list-option', action='store_true', help='列出所有配置')
77
78
  config_parser.add_argument('-s', '--set', nargs='+', type=str, help='设置一个或多个配置项,格式为 `key=value`,例如: `num_workers=8`')
79
+ config_parser.add_argument('-b', '--base-url', type=str, help='设置镜像站点的基础 URL, 例如: `https://kxx.moe`')
78
80
  config_parser.add_argument('-c', '--clear', type=str, help='清除指定配置,可选值为 `all`, `cookie`, `option`')
79
81
  config_parser.add_argument('-d', '--delete', '--unset', dest='unset', type=str, help='删除特定的配置选项')
80
82
 
@@ -137,6 +139,9 @@ class Configurer:
137
139
  cookie = config.get('cookie', None)
138
140
  if cookie is not None and isinstance(cookie, dict):
139
141
  self._config.cookie = cookie
142
+ base_url = config.get('base_url', None)
143
+ if base_url is not None and isinstance(base_url, str):
144
+ self._config.base_url = base_url
140
145
 
141
146
  @property
142
147
  def config(self) -> 'Config':
@@ -162,12 +167,24 @@ class Configurer:
162
167
  return self._config.option
163
168
 
164
169
  @option.setter
165
- def option(self, value: Optional[dict[str, any]]):
170
+ def option(self, value: Optional[dict[str, Any]]):
166
171
  if self._config is None:
167
172
  self._config = Config()
168
173
  self._config.option = value
169
174
  self.update()
170
175
 
176
+ @property
177
+ def base_url(self) -> str:
178
+ if self._config is None or self._config.base_url is None:
179
+ return BASE_URL.DEFAULT
180
+ return self._config.base_url
181
+
182
+ def set_base_url(self, value: str):
183
+ if self._config is None:
184
+ self._config = Config()
185
+ self._config.base_url = value
186
+ self.update()
187
+
171
188
  def update(self):
172
189
  with open(os.path.join(os.path.expanduser("~"), self.__filename), 'w') as f:
173
190
  json.dump(self._config.__dict__, f, indent=4, ensure_ascii=False)
@@ -184,7 +201,7 @@ class Configurer:
184
201
 
185
202
  self.update()
186
203
 
187
- def set_option(self, key: str, value: any):
204
+ def set_option(self, key: str, value: Any):
188
205
  if self._config.option is None:
189
206
  self._config.option = {}
190
207
 
@@ -209,5 +226,11 @@ def __combine_args(dest: argparse.Namespace, option: dict) -> argparse.Namespace
209
226
 
210
227
  def combine_args(dest: argparse.Namespace) -> argparse.Namespace:
211
228
  assert isinstance(dest, argparse.Namespace), "dest must be an argparse.Namespace instance"
212
- option = Configurer().config.option
213
- return __combine_args(dest, option)
229
+ option = Configurer().option
230
+
231
+ if option is None:
232
+ return dest
233
+
234
+ return __combine_args(dest, option)
235
+
236
+ base_url_var = ContextVar('base_url', default=Configurer().base_url)
@@ -12,4 +12,12 @@ class LoginError(KmdrError):
12
12
  super().__init__(message, solution)
13
13
 
14
14
  def __str__(self):
15
- return f"{self.message}\n{self._solution}"
15
+ return f"{self.message}\n{self._solution}"
16
+
17
+ class RedirectError(KmdrError):
18
+ def __init__(self, message, new_base_url: str):
19
+ super().__init__(message)
20
+ self.new_base_url = new_base_url
21
+
22
+ def __str__(self):
23
+ return f"{self.message} 新的地址: {self.new_base_url}"
@@ -66,3 +66,5 @@ class Config:
66
66
  """
67
67
 
68
68
  cookie: Optional[dict[str, str]] = None
69
+
70
+ base_url: Optional[str] = None
@@ -4,10 +4,10 @@ import asyncio
4
4
 
5
5
  import aiohttp
6
6
 
7
- from deprecation import deprecated
8
7
  import subprocess
9
8
 
10
9
  from .structure import BookInfo, VolInfo
10
+ from .error import RedirectError
11
11
 
12
12
 
13
13
  def singleton(cls):
@@ -43,7 +43,8 @@ def async_retry(
43
43
  attempts: int = 3,
44
44
  delay: float = 1.0,
45
45
  backoff: float = 2.0,
46
- retry_on_status: set[int] = {500, 502, 503, 504, 429, 408}
46
+ retry_on_status: set[int] = {500, 502, 503, 504, 429, 408},
47
+ base_url_setter: Optional[Callable[[str], None]] = None,
47
48
  ):
48
49
  def decorator(func):
49
50
  @functools.wraps(func)
@@ -62,9 +63,16 @@ def async_retry(
62
63
  # 对于所有其他 aiohttp 客户端异常和超时,进行重试
63
64
  if attempt == attempts - 1:
64
65
  raise
66
+ except RedirectError as e:
67
+ if base_url_setter:
68
+ base_url_setter(e.new_base_url)
69
+ print(f"检测到重定向,已自动更新 base url 为: {e.new_base_url}。立即重试...")
70
+ continue
71
+ else:
72
+ raise
65
73
 
66
74
  await asyncio.sleep(current_delay)
67
75
 
68
76
  current_delay *= backoff
69
77
  return wrapper
70
- return decorator
78
+ return decorator
@@ -0,0 +1,5 @@
1
+ from .authenticator import *
2
+ from .lister import *
3
+ from .picker import *
4
+ from .downloader import *
5
+ from .configurer import *
@@ -4,12 +4,11 @@ from yarl import URL
4
4
 
5
5
  from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
6
6
 
7
- from .utils import check_status
8
- from .utils import PROFILE_URL
7
+ from .utils import check_status, extract_base_url
9
8
 
10
9
  @AUTHENTICATOR.register()
11
10
  class CookieAuthenticator(Authenticator):
12
- def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
11
+ def __init__(self, proxy: Optional[str] = None, book_url: Optional[str] = None, *args, **kwargs):
13
12
  super().__init__(proxy, *args, **kwargs)
14
13
 
15
14
  if 'command' in kwargs and kwargs['command'] == 'status':
@@ -17,17 +16,21 @@ class CookieAuthenticator(Authenticator):
17
16
  else:
18
17
  self._show_quota = False
19
18
 
19
+ # 根据用户提供的 book_url 来决定访问的镜像站
20
+ self._inner_base_url = extract_base_url(book_url, default=self._base_url)
21
+
20
22
  async def _authenticate(self) -> bool:
21
23
  cookie = self._configurer.cookie
22
24
 
23
25
  if not cookie:
24
26
  raise LoginError("无法找到 Cookie,请先完成登录。", ['kmdr login -u <username>'])
25
27
 
26
- self._session.cookie_jar.update_cookies(cookie, response_url=URL(PROFILE_URL))
28
+ self._session.cookie_jar.update_cookies(cookie, response_url=URL(self.base_url))
27
29
  return await check_status(
28
30
  self._session,
29
31
  self._console,
32
+ base_url=self.base_url,
30
33
  show_quota=self._show_quota,
31
34
  is_vip_setter=lambda value: setattr(self._profile, 'is_vip', value),
32
35
  level_setter=lambda value: setattr(self._profile, 'user_level', value),
33
- )
36
+ )