kmoe-manga-downloader 1.0.0__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 (41) hide show
  1. kmoe_manga_downloader-1.0.0/LICENSE +21 -0
  2. kmoe_manga_downloader-1.0.0/PKG-INFO +182 -0
  3. kmoe_manga_downloader-1.0.0/README.md +139 -0
  4. kmoe_manga_downloader-1.0.0/pyproject.toml +37 -0
  5. kmoe_manga_downloader-1.0.0/setup.cfg +4 -0
  6. kmoe_manga_downloader-1.0.0/src/kmdr/__init__.py +0 -0
  7. kmoe_manga_downloader-1.0.0/src/kmdr/core/__init__.py +5 -0
  8. kmoe_manga_downloader-1.0.0/src/kmdr/core/bases.py +113 -0
  9. kmoe_manga_downloader-1.0.0/src/kmdr/core/defaults.py +154 -0
  10. kmoe_manga_downloader-1.0.0/src/kmdr/core/registry.py +128 -0
  11. kmoe_manga_downloader-1.0.0/src/kmdr/core/structure.py +68 -0
  12. kmoe_manga_downloader-1.0.0/src/kmdr/core/utils.py +77 -0
  13. kmoe_manga_downloader-1.0.0/src/kmdr/main.py +39 -0
  14. kmoe_manga_downloader-1.0.0/src/kmdr/module/__init__.py +5 -0
  15. kmoe_manga_downloader-1.0.0/src/kmdr/module/authenticator/CookieAuthenticator.py +25 -0
  16. kmoe_manga_downloader-1.0.0/src/kmdr/module/authenticator/LoginAuthenticator.py +54 -0
  17. kmoe_manga_downloader-1.0.0/src/kmdr/module/authenticator/utils.py +25 -0
  18. kmoe_manga_downloader-1.0.0/src/kmdr/module/configurer/ConfigClearer.py +11 -0
  19. kmoe_manga_downloader-1.0.0/src/kmdr/module/configurer/ConfigUnsetter.py +15 -0
  20. kmoe_manga_downloader-1.0.0/src/kmdr/module/configurer/OptionLister.py +19 -0
  21. kmoe_manga_downloader-1.0.0/src/kmdr/module/configurer/OptionSetter.py +32 -0
  22. kmoe_manga_downloader-1.0.0/src/kmdr/module/configurer/option_validate.py +76 -0
  23. kmoe_manga_downloader-1.0.0/src/kmdr/module/downloader/DirectDownloader.py +28 -0
  24. kmoe_manga_downloader-1.0.0/src/kmdr/module/downloader/ReferViaDownloader.py +44 -0
  25. kmoe_manga_downloader-1.0.0/src/kmdr/module/downloader/utils.py +118 -0
  26. kmoe_manga_downloader-1.0.0/src/kmdr/module/lister/BookUrlLister.py +15 -0
  27. kmoe_manga_downloader-1.0.0/src/kmdr/module/lister/FollowedBookLister.py +38 -0
  28. kmoe_manga_downloader-1.0.0/src/kmdr/module/lister/utils.py +79 -0
  29. kmoe_manga_downloader-1.0.0/src/kmdr/module/picker/ArgsFilterPicker.py +49 -0
  30. kmoe_manga_downloader-1.0.0/src/kmdr/module/picker/DefaultVolPicker.py +21 -0
  31. kmoe_manga_downloader-1.0.0/src/kmdr/module/picker/utils.py +37 -0
  32. kmoe_manga_downloader-1.0.0/src/kmoe_manga_downloader.egg-info/PKG-INFO +182 -0
  33. kmoe_manga_downloader-1.0.0/src/kmoe_manga_downloader.egg-info/SOURCES.txt +39 -0
  34. kmoe_manga_downloader-1.0.0/src/kmoe_manga_downloader.egg-info/dependency_links.txt +1 -0
  35. kmoe_manga_downloader-1.0.0/src/kmoe_manga_downloader.egg-info/entry_points.txt +2 -0
  36. kmoe_manga_downloader-1.0.0/src/kmoe_manga_downloader.egg-info/requires.txt +4 -0
  37. kmoe_manga_downloader-1.0.0/src/kmoe_manga_downloader.egg-info/top_level.txt +1 -0
  38. kmoe_manga_downloader-1.0.0/tests/test_kmdr_config_option.py +66 -0
  39. kmoe_manga_downloader-1.0.0/tests/test_kmdr_download.py +149 -0
  40. kmoe_manga_downloader-1.0.0/tests/test_kmdr_login.py +29 -0
  41. kmoe_manga_downloader-1.0.0/tests/test_utils_resolve_volme.py +55 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 chris zheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: kmoe-manga-downloader
3
+ Version: 1.0.0
4
+ Summary: A CLI-downloader for site @kox.moe.
5
+ Author-email: Chris Zheng <chrisis58@outlook.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 chris zheng
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/chrisis58/kmoe-manga-downloader
29
+ Project-URL: Issues, https://github.com/chrisis58/kmoe-manga-downloader/issues
30
+ Keywords: manga,downloader,kmoe
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Requires-Python: >=3.9
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: beautifulsoup4<5.0.0,>=4.11.1
39
+ Requires-Dist: requests<3.0.0,>=2.32.3
40
+ Requires-Dist: tqdm<5.0.0,>=4.67.1
41
+ Requires-Dist: cloudscraper<2.0.0,>=1.2.71
42
+ Dynamic: license-file
43
+
44
+ # Kmoe Manga Downloader
45
+
46
+ [![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)
47
+
48
+ `kmdr (Kmoe Manga Downloader)` 是一个 Python 脚本,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
49
+
50
+ ## ✨功能特性
51
+
52
+ - 以命令行参数登录网站并持久化凭证
53
+ - 支持多种方式筛选需要的内容
54
+ - 支持网站上提供的不同的下载方式
55
+ - 支持多线程下载,失败重试、断点续传
56
+ - 提供自定义的下载完成回调命令
57
+ - 提供通用配置持久化的实现
58
+
59
+ ## 🛠️安装依赖
60
+
61
+ 在使用本脚本之前,请确保你已经安装了项目所需要的依赖:
62
+
63
+ ```bash
64
+ git clone https://github.com/chrisis58/kmoe-manga-downloader.git
65
+ cd kmoe-manga-downloader
66
+
67
+ pip install -r requirements.txt
68
+ ```
69
+
70
+ ## 📋使用方法
71
+
72
+ ### 1. 登录 `kmoe`
73
+
74
+ 首先需要登录 `kox.moe` 并保存登录状态(Cookie)。
75
+
76
+ ```bash
77
+ python kmdr.py login -u <your_username> -p <your_password>
78
+ ```
79
+
80
+ 或者:
81
+
82
+ ```bash
83
+ python kmdr.py login -u <your_username>
84
+ ```
85
+
86
+ 第二种方式会在程序运行时获取登录密码。如果登录成功,会同时显示当前登录用户及配额。
87
+
88
+ ### 2. 下载漫画书籍
89
+
90
+ 你可以通过以下命令下载指定书籍或卷:
91
+
92
+ ```bash
93
+ # 在 path/to/destination 目录下载第一、二、三卷
94
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm --volume 1,2,3
95
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3
96
+ ```
97
+
98
+ ```bash
99
+ # 在 path/to/download/destination 目录下载全部番外篇
100
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm --vol-type extra -v all
101
+ ```
102
+
103
+ #### 常用参数说明:
104
+
105
+ - `-d`, `--dest`: 下载的目标目录,在此基础上会额外添加一个为书籍名称的子目录
106
+ - `-l`, `--book-url`: 指定书籍的主页地址
107
+ - `-v`, `--volume`: 指定卷的名称,多个名称使用逗号分隔,`all` 表示下载所有卷
108
+ - `-t`, `--vol-type`: 卷类型,`vol`: 单行本(默认);`extra`: 番外;`seri`: 连载话;`all`: 全部
109
+ - `-p`, `--proxy`: 代理服务器地址
110
+ - `-r`, `--retry`: 下载失败时的重试次数
111
+ - `-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))
112
+ - `--num-workers`: 最大同时下载数量,默认为 1
113
+
114
+ > 完整的参数说明可以从 `help` 指令中获取。
115
+
116
+ ### 3. 查看账户状态
117
+
118
+ 查看当前账户信息(例如:账户名和配额等):
119
+
120
+ ```bash
121
+ python kmdr.py status
122
+ ```
123
+
124
+ ### 4. 回调函数
125
+
126
+ 你可以设置一个回调函数,下载完成后执行。回调可以是任何你想要的命令:
127
+
128
+ ```bash
129
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
130
+ --callback "echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
131
+ ```
132
+
133
+ > 字符串模板会直接朴素地替换,卷名或者书名可能会包含空格,推荐使用引号包含避免出现错误。
134
+
135
+ `{b.name}, {v.name}` 会被分别替换为书籍和卷的名称。常用参数:
136
+
137
+ | 变量名 | 描述 |
138
+ | -------- | -------------- |
139
+ | v.name | 卷的名称 |
140
+ | v.page | 卷的页数 |
141
+ | v.size | 卷的文件大小 |
142
+ | b.name | 对应漫画的名字 |
143
+ | b.author | 对应漫画的作者 |
144
+
145
+ > 完整的可用参数请参考 [structure.py](https://github.com/chrisis58/kmdr/blob/main/core/structure.py#L11) 中关于 `VolInfo` 的定义。
146
+
147
+ ### 5. 持久化配置
148
+
149
+ 重复设置下载的代理服务器、目标路径等参数,可能会降低脚本的使用效率。所以脚本也提供了通用配置的持久化命令:
150
+
151
+ ```bash
152
+ python kmdr.py config --set proxy=http://localhost:7890 dest=/path/to/destination
153
+ python kmdr.py config -s num_workers=5 "callback=echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
154
+ ```
155
+
156
+ 只需要配置一次即可对之后的所有的下载指令生效。
157
+
158
+ > 注意:这里的参数名称不可以使用简写,例如 `dest` 不可用使用 `d` 来替换。
159
+
160
+ 同时,你也可以使用以下命令进行持久化配置的管理:
161
+
162
+ - `-l`, `--list-option`: 显示当前存在的各个配置
163
+ - `-s`, `--set`: 设置持久化的配置,键和值通过 `=` 分隔,设置多个配置可以通过空格间隔
164
+ - `-c`, `--clear`: 清除配置,`all`: 清除所有;`cookie`: 退出登录;`option`: 清除持久化的配置
165
+ - `-d`, `--delete`, `--unset`: 清除单项配置
166
+
167
+ > 当前仅支持部分下载参数的持久化:`num_workers`, `dest`, `retry`, `callback`, `proxy`
168
+
169
+ ## ⚠️ 声明
170
+
171
+ - 本工具仅作学习、研究、交流使用,使用本工具的用户应自行承担风险
172
+ - 作者不对使用本工具导致的任何损失、法律纠纷或其他后果负责
173
+ - 作者不对用户使用本工具的行为负责,包括但不限于用户违反法律或任何第三方权益的行为
174
+
175
+ ---
176
+
177
+ <div align=center>
178
+ 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 或开 discussion 交流!<br />
179
+ ⭐ 如果这个项目对你有帮助,请给它一个星标!<br /> <br />
180
+ <img src="https://counter.seku.su/cmoe?name=kmdr&theme=mbs" />
181
+ </div>
182
+
@@ -0,0 +1,139 @@
1
+ # Kmoe Manga Downloader
2
+
3
+ [![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
+
5
+ `kmdr (Kmoe Manga Downloader)` 是一个 Python 脚本,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
6
+
7
+ ## ✨功能特性
8
+
9
+ - 以命令行参数登录网站并持久化凭证
10
+ - 支持多种方式筛选需要的内容
11
+ - 支持网站上提供的不同的下载方式
12
+ - 支持多线程下载,失败重试、断点续传
13
+ - 提供自定义的下载完成回调命令
14
+ - 提供通用配置持久化的实现
15
+
16
+ ## 🛠️安装依赖
17
+
18
+ 在使用本脚本之前,请确保你已经安装了项目所需要的依赖:
19
+
20
+ ```bash
21
+ git clone https://github.com/chrisis58/kmoe-manga-downloader.git
22
+ cd kmoe-manga-downloader
23
+
24
+ pip install -r requirements.txt
25
+ ```
26
+
27
+ ## 📋使用方法
28
+
29
+ ### 1. 登录 `kmoe`
30
+
31
+ 首先需要登录 `kox.moe` 并保存登录状态(Cookie)。
32
+
33
+ ```bash
34
+ python kmdr.py login -u <your_username> -p <your_password>
35
+ ```
36
+
37
+ 或者:
38
+
39
+ ```bash
40
+ python kmdr.py login -u <your_username>
41
+ ```
42
+
43
+ 第二种方式会在程序运行时获取登录密码。如果登录成功,会同时显示当前登录用户及配额。
44
+
45
+ ### 2. 下载漫画书籍
46
+
47
+ 你可以通过以下命令下载指定书籍或卷:
48
+
49
+ ```bash
50
+ # 在 path/to/destination 目录下载第一、二、三卷
51
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm --volume 1,2,3
52
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3
53
+ ```
54
+
55
+ ```bash
56
+ # 在 path/to/download/destination 目录下载全部番外篇
57
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm --vol-type extra -v all
58
+ ```
59
+
60
+ #### 常用参数说明:
61
+
62
+ - `-d`, `--dest`: 下载的目标目录,在此基础上会额外添加一个为书籍名称的子目录
63
+ - `-l`, `--book-url`: 指定书籍的主页地址
64
+ - `-v`, `--volume`: 指定卷的名称,多个名称使用逗号分隔,`all` 表示下载所有卷
65
+ - `-t`, `--vol-type`: 卷类型,`vol`: 单行本(默认);`extra`: 番外;`seri`: 连载话;`all`: 全部
66
+ - `-p`, `--proxy`: 代理服务器地址
67
+ - `-r`, `--retry`: 下载失败时的重试次数
68
+ - `-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))
69
+ - `--num-workers`: 最大同时下载数量,默认为 1
70
+
71
+ > 完整的参数说明可以从 `help` 指令中获取。
72
+
73
+ ### 3. 查看账户状态
74
+
75
+ 查看当前账户信息(例如:账户名和配额等):
76
+
77
+ ```bash
78
+ python kmdr.py status
79
+ ```
80
+
81
+ ### 4. 回调函数
82
+
83
+ 你可以设置一个回调函数,下载完成后执行。回调可以是任何你想要的命令:
84
+
85
+ ```bash
86
+ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
87
+ --callback "echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
88
+ ```
89
+
90
+ > 字符串模板会直接朴素地替换,卷名或者书名可能会包含空格,推荐使用引号包含避免出现错误。
91
+
92
+ `{b.name}, {v.name}` 会被分别替换为书籍和卷的名称。常用参数:
93
+
94
+ | 变量名 | 描述 |
95
+ | -------- | -------------- |
96
+ | v.name | 卷的名称 |
97
+ | v.page | 卷的页数 |
98
+ | v.size | 卷的文件大小 |
99
+ | b.name | 对应漫画的名字 |
100
+ | b.author | 对应漫画的作者 |
101
+
102
+ > 完整的可用参数请参考 [structure.py](https://github.com/chrisis58/kmdr/blob/main/core/structure.py#L11) 中关于 `VolInfo` 的定义。
103
+
104
+ ### 5. 持久化配置
105
+
106
+ 重复设置下载的代理服务器、目标路径等参数,可能会降低脚本的使用效率。所以脚本也提供了通用配置的持久化命令:
107
+
108
+ ```bash
109
+ python kmdr.py config --set proxy=http://localhost:7890 dest=/path/to/destination
110
+ python kmdr.py config -s num_workers=5 "callback=echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
111
+ ```
112
+
113
+ 只需要配置一次即可对之后的所有的下载指令生效。
114
+
115
+ > 注意:这里的参数名称不可以使用简写,例如 `dest` 不可用使用 `d` 来替换。
116
+
117
+ 同时,你也可以使用以下命令进行持久化配置的管理:
118
+
119
+ - `-l`, `--list-option`: 显示当前存在的各个配置
120
+ - `-s`, `--set`: 设置持久化的配置,键和值通过 `=` 分隔,设置多个配置可以通过空格间隔
121
+ - `-c`, `--clear`: 清除配置,`all`: 清除所有;`cookie`: 退出登录;`option`: 清除持久化的配置
122
+ - `-d`, `--delete`, `--unset`: 清除单项配置
123
+
124
+ > 当前仅支持部分下载参数的持久化:`num_workers`, `dest`, `retry`, `callback`, `proxy`
125
+
126
+ ## ⚠️ 声明
127
+
128
+ - 本工具仅作学习、研究、交流使用,使用本工具的用户应自行承担风险
129
+ - 作者不对使用本工具导致的任何损失、法律纠纷或其他后果负责
130
+ - 作者不对用户使用本工具的行为负责,包括但不限于用户违反法律或任何第三方权益的行为
131
+
132
+ ---
133
+
134
+ <div align=center>
135
+ 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 或开 discussion 交流!<br />
136
+ ⭐ 如果这个项目对你有帮助,请给它一个星标!<br /> <br />
137
+ <img src="https://counter.seku.su/cmoe?name=kmdr&theme=mbs" />
138
+ </div>
139
+
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "kmoe-manga-downloader"
3
+ version = "1.0.0"
4
+ authors = [
5
+ { name="Chris Zheng", email="chrisis58@outlook.com" },
6
+ ]
7
+ description = "A CLI-downloader for site @kox.moe."
8
+ readme = "README.md"
9
+ requires-python = ">=3.9"
10
+ license = { file="LICENSE" }
11
+ keywords = ["manga", "downloader", "kmoe"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+
19
+ dependencies = [
20
+ "beautifulsoup4>=4.11.1,<5.0.0",
21
+ "requests>=2.32.3,<3.0.0",
22
+ "tqdm>=4.67.1,<5.0.0",
23
+ "cloudscraper>=1.2.71,<2.0.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/chrisis58/kmoe-manga-downloader"
28
+ Issues = "https://github.com/chrisis58/kmoe-manga-downloader/issues"
29
+
30
+ [project.scripts]
31
+ kmdr = "kmdr.main:entry_point"
32
+
33
+ [tool.setuptools]
34
+ package-dir = {"" = "src"}
35
+
36
+ [tool.pytest.ini_options]
37
+ pythonpath = "src"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,5 @@
1
+ from .bases import Authenticator, Lister, Picker, Downloader, Configurer
2
+ from .structure import VolInfo, BookInfo, VolumeType
3
+ from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER
4
+
5
+ from .defaults import argument_parser
@@ -0,0 +1,113 @@
1
+ import os
2
+
3
+ from typing import Callable, Optional
4
+
5
+ from .registry import Registry
6
+ from .structure import VolInfo, BookInfo
7
+ from .utils import get_singleton_session, construct_callback
8
+ from .defaults import Configurer as InnerConfigurer
9
+
10
+ class SessionContext:
11
+
12
+ def __init__(self, *args, **kwargs):
13
+ super().__init__()
14
+ self._session = get_singleton_session()
15
+
16
+ class ConfigContext:
17
+
18
+ def __init__(self, *args, **kwargs):
19
+ super().__init__()
20
+ self._configurer = InnerConfigurer()
21
+
22
+ class Configurer(ConfigContext):
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ super().__init__(*args, **kwargs)
26
+
27
+ def operate(self) -> None: ...
28
+
29
+ class Authenticator(SessionContext, ConfigContext):
30
+
31
+ def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+
34
+ if proxy:
35
+ self._session.proxies.update({
36
+ 'https': proxy,
37
+ 'http': proxy,
38
+ })
39
+
40
+ # 在使用代理登录时,可能会出现问题,但是现在还不清楚是不是代理的问题。
41
+ # 主站正常情况下不使用代理也能登录成功。但是不排除特殊的网络环境下需要代理。
42
+ # 所以暂时保留代理登录的功能,如果后续确认是代理的问题,可以考虑启用 @no_proxy 装饰器。
43
+ # @no_proxy
44
+ def authenticate(self) -> bool:
45
+ return self._authenticate()
46
+
47
+ def _authenticate(self) -> bool: ...
48
+
49
+ class Lister(SessionContext):
50
+
51
+ def __init__(self, *args, **kwargs):
52
+ super().__init__(*args, **kwargs)
53
+
54
+ def list(self) -> tuple[BookInfo, list[VolInfo]]: ...
55
+
56
+ class Picker(SessionContext):
57
+
58
+ def __init__(self, *args, **kwargs):
59
+ super().__init__(*args, **kwargs)
60
+
61
+ def pick(self, volumes: list[VolInfo]) -> list[VolInfo]: ...
62
+
63
+ class Downloader(SessionContext):
64
+
65
+ def __init__(self,
66
+ dest: str = '.',
67
+ callback: Optional[str] = None,
68
+ retry: int = 3,
69
+ num_workers: int = 1,
70
+ proxy: Optional[str] = None,
71
+ *args, **kwargs
72
+ ):
73
+ super().__init__(*args, **kwargs)
74
+ self._dest: str = dest
75
+ self._callback: Optional[Callable[[BookInfo, VolInfo], int]] = construct_callback(callback)
76
+ self._retry: int = retry
77
+ self._num_workers: int = num_workers
78
+
79
+ if proxy:
80
+ self._session.proxies.update({
81
+ 'https': proxy,
82
+ 'http': proxy,
83
+ })
84
+
85
+ def download(self, book: BookInfo, volumes: list[VolInfo]):
86
+ if volumes is None or not volumes:
87
+ raise ValueError("No volumes to download")
88
+
89
+ if self._num_workers <= 1:
90
+ for volume in volumes:
91
+ self._download(book, volume, self._retry)
92
+ else:
93
+ self._download_with_multiple_workers(book, volumes, self._retry)
94
+
95
+ def _download(self, book: BookInfo, volume: VolInfo, retry: int): ...
96
+
97
+ def _download_with_multiple_workers(self, book: BookInfo, volumes: list[VolInfo], retry: int):
98
+ from concurrent.futures import ThreadPoolExecutor
99
+
100
+ max_workers = min(self._num_workers, len(volumes))
101
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
102
+ futures = [
103
+ executor.submit(self._download, book, volume, retry)
104
+ for volume in volumes
105
+ ]
106
+ for future in futures:
107
+ future.result()
108
+
109
+ AUTHENTICATOR = Registry[Authenticator]('Authenticator')
110
+ LISTERS = Registry[Lister]('Lister')
111
+ PICKERS = Registry[Picker]('Picker')
112
+ DOWNLOADER = Registry[Downloader]('Downloader', True)
113
+ CONFIGURER = Registry[Configurer]('Configurer')
@@ -0,0 +1,154 @@
1
+ import os
2
+ import json
3
+ from typing import Optional
4
+ import argparse
5
+
6
+ from .utils import singleton
7
+ from .structure import Config
8
+
9
+ parser: Optional[argparse.ArgumentParser] = None
10
+ args: Optional[argparse.Namespace] = None
11
+
12
+ def argument_parser():
13
+ global parser
14
+ if parser is not None:
15
+ return parser
16
+
17
+ parser = argparse.ArgumentParser(description='Kox Downloader')
18
+ subparsers = parser.add_subparsers(title='subcommands', dest='command')
19
+
20
+ download_parser = subparsers.add_parser('download', help='Download books')
21
+ download_parser.add_argument('-d', '--dest', type=str, help='Download destination, default to current directory', required=False)
22
+ download_parser.add_argument('-l', '--book-url', type=str, help='Book page\'s url', required=False)
23
+ download_parser.add_argument('-v', '--volume', type=str, help='Volume(s), split using commas, `all` for all', required=False)
24
+ download_parser.add_argument('-t', '--vol-type', type=str, help='Volume type, `vol` for volume, `extra` for extras, `seri` for serialized', required=False, choices=['vol', 'extra', 'seri', 'all'], default='vol')
25
+ download_parser.add_argument('--max-size', type=float, help='Max size of volume in MB', required=False)
26
+ download_parser.add_argument('--limit', type=int, help='Limit number of volumes to download', required=False)
27
+ download_parser.add_argument('--num-workers', type=int, help='Number of workers to use for downloading', required=False)
28
+ download_parser.add_argument('-p', '--proxy', type=str, help='Proxy server', required=False)
29
+ download_parser.add_argument('-r', '--retry', type=int, help='Retry times', required=False)
30
+ download_parser.add_argument('-c', '--callback', type=str, help='Callback script, use as `echo {v.name} downloaded!`', required=False)
31
+
32
+ login_parser = subparsers.add_parser('login', help='Login to kox.moe')
33
+ login_parser.add_argument('-u', '--username', type=str, help='Your username', required=True)
34
+ login_parser.add_argument('-p', '--password', type=str, help='Your password', required=False)
35
+
36
+ status_parser = subparsers.add_parser('status', help='Show status of account and script')
37
+ status_parser.add_argument('-p', '--proxy', type=str, help='Proxy server', required=False)
38
+
39
+ config_parser = subparsers.add_parser('config', help='Configure the downloader')
40
+ config_parser.add_argument('-l', '--list-option', action='store_true', help='List all configurations')
41
+ config_parser.add_argument('-s', '--set', nargs='+', type=str, help='Configuration options to set, e.g. num_workers=3 dest=.')
42
+ config_parser.add_argument('-c', '--clear', type=str, help='Clear configurations, `all`, `cookie`, `option` are available')
43
+ config_parser.add_argument('-d', '--delete', '--unset', dest='unset', type=str, help='Delete a specific configuration option')
44
+
45
+ return parser
46
+
47
+ def parse_args():
48
+ global args
49
+ if args is not None:
50
+ return args
51
+
52
+ parser = argument_parser()
53
+ args = parser.parse_args()
54
+
55
+ if args.command is None:
56
+ parser.print_help()
57
+ exit(1)
58
+
59
+ return args
60
+
61
+ @singleton
62
+ class Configurer:
63
+
64
+ def __init__(self):
65
+ self.__filename = '.kmdr'
66
+
67
+ if not os.path.exists(os.path.join(os.path.expanduser("~"), self.__filename)):
68
+ self._config = Config()
69
+ self.update()
70
+ else:
71
+ with open(os.path.join(os.path.expanduser("~"), self.__filename), 'r') as f:
72
+ config = json.load(f)
73
+
74
+ self._config = Config()
75
+ option = config.get('option', None)
76
+ if option is not None and isinstance(option, dict):
77
+ self._config.option = option
78
+ cookie = config.get('cookie', None)
79
+ if cookie is not None and isinstance(cookie, dict):
80
+ self._config.cookie = cookie
81
+
82
+ @property
83
+ def config(self) -> 'Config':
84
+ return self._config
85
+
86
+ @property
87
+ def cookie(self) -> Optional[dict]:
88
+ if self._config is None:
89
+ return None
90
+ return self._config.cookie
91
+
92
+ @cookie.setter
93
+ def cookie(self, value: Optional[dict[str, str]]):
94
+ if self._config is None:
95
+ self._config = Config()
96
+ self._config.cookie = value
97
+ self.update()
98
+
99
+ @property
100
+ def option(self) -> Optional[dict]:
101
+ if self._config is None:
102
+ return None
103
+ return self._config.option
104
+
105
+ @option.setter
106
+ def option(self, value: Optional[dict[str, any]]):
107
+ if self._config is None:
108
+ self._config = Config()
109
+ self._config.option = value
110
+ self.update()
111
+
112
+ def update(self):
113
+ with open(os.path.join(os.path.expanduser("~"), self.__filename), 'w') as f:
114
+ json.dump(self._config.__dict__, f, indent=4, ensure_ascii=False)
115
+
116
+ def clear(self, key: str):
117
+ if key == 'all':
118
+ self._config = Config()
119
+ elif key == 'cookie':
120
+ self._config.cookie = None
121
+ elif key == 'option':
122
+ self._config.option = None
123
+ else:
124
+ raise ValueError(f"Unsupported clear option: {key}")
125
+
126
+ self.update()
127
+
128
+ def set_option(self, key: str, value: any):
129
+ if self._config.option is None:
130
+ self._config.option = {}
131
+
132
+ self._config.option[key] = value
133
+ self.update()
134
+
135
+ def unset_option(self, key: str):
136
+ if self._config.option is None or key not in self._config.option:
137
+ return
138
+
139
+ del self._config.option[key]
140
+ self.update()
141
+
142
+ def __combine_args(dest: argparse.Namespace, option: dict) -> argparse.Namespace:
143
+ if option is None:
144
+ return dest
145
+
146
+ for key, value in option.items():
147
+ if hasattr(dest, key) and getattr(dest, key) is None:
148
+ setattr(dest, key, value)
149
+ return dest
150
+
151
+ def combine_args(dest: argparse.Namespace) -> argparse.Namespace:
152
+ assert isinstance(dest, argparse.Namespace), "dest must be an argparse.Namespace instance"
153
+ option = Configurer().config.option
154
+ return __combine_args(dest, option)