kmoe-manga-downloader 1.0.0__tar.gz → 1.1.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 (44) hide show
  1. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/PKG-INFO +30 -36
  2. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/README.md +29 -35
  3. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/pyproject.toml +1 -1
  4. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/core/__init__.py +3 -1
  5. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/core/bases.py +17 -5
  6. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/core/defaults.py +23 -0
  7. kmoe_manga_downloader-1.1.0/src/kmdr/core/error.py +15 -0
  8. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/main.py +4 -7
  9. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/authenticator/CookieAuthenticator.py +8 -4
  10. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/authenticator/LoginAuthenticator.py +18 -16
  11. kmoe_manga_downloader-1.1.0/src/kmdr/module/authenticator/utils.py +79 -0
  12. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/configurer/ConfigUnsetter.py +4 -1
  13. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/configurer/option_validate.py +46 -12
  14. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/downloader/DirectDownloader.py +1 -1
  15. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/downloader/ReferViaDownloader.py +3 -3
  16. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/downloader/utils.py +20 -11
  17. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmoe_manga_downloader.egg-info/PKG-INFO +30 -36
  18. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +2 -0
  19. kmoe_manga_downloader-1.1.0/tests/test_cache_by_kwargs.py +87 -0
  20. kmoe_manga_downloader-1.0.0/src/kmdr/module/authenticator/utils.py +0 -25
  21. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/LICENSE +0 -0
  22. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/setup.cfg +0 -0
  23. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/__init__.py +0 -0
  24. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/core/registry.py +0 -0
  25. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/core/structure.py +0 -0
  26. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/core/utils.py +0 -0
  27. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/__init__.py +0 -0
  28. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/configurer/ConfigClearer.py +0 -0
  29. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/configurer/OptionLister.py +0 -0
  30. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/configurer/OptionSetter.py +0 -0
  31. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/lister/BookUrlLister.py +0 -0
  32. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/lister/FollowedBookLister.py +0 -0
  33. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/lister/utils.py +0 -0
  34. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
  35. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/picker/DefaultVolPicker.py +0 -0
  36. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmdr/module/picker/utils.py +0 -0
  37. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
  38. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
  39. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
  40. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
  41. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/tests/test_kmdr_config_option.py +0 -0
  42. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/tests/test_kmdr_download.py +0 -0
  43. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/tests/test_kmdr_login.py +0 -0
  44. {kmoe_manga_downloader-1.0.0 → kmoe_manga_downloader-1.1.0}/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.0.0
3
+ Version: 1.1.0
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -43,28 +43,24 @@ Dynamic: license-file
43
43
 
44
44
  # Kmoe Manga Downloader
45
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)
46
+ [![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)
47
47
 
48
- `kmdr (Kmoe Manga Downloader)` 是一个 Python 脚本,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
48
+ `kmdr (Kmoe Manga Downloader)` 是一个 Python 应用,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
49
49
 
50
50
  ## ✨功能特性
51
51
 
52
- - 以命令行参数登录网站并持久化凭证
53
- - 支持多种方式筛选需要的内容
54
- - 支持网站上提供的不同的下载方式
55
- - 支持多线程下载,失败重试、断点续传
56
- - 提供自定义的下载完成回调命令
57
- - 提供通用配置持久化的实现
52
+ - **凭证管理**: 命令行登录并持久化会话
53
+ - **多种下载方式**: 支持通过书籍 URL 或从收藏列表下载
54
+ - **高效下载**: 支持多线程、失败重试及断点续传
55
+ - **配置持久化**: 保存常用下载目录、代理等设置
56
+ - **回调支持**: 下载完成后自动执行自定义脚本
58
57
 
59
- ## 🛠️安装依赖
58
+ ## 🛠️安装应用
60
59
 
61
- 在使用本脚本之前,请确保你已经安装了项目所需要的依赖:
60
+ 你可以通过 PyPI 使用 `pip` 进行安装:
62
61
 
63
62
  ```bash
64
- git clone https://github.com/chrisis58/kmoe-manga-downloader.git
65
- cd kmoe-manga-downloader
66
-
67
- pip install -r requirements.txt
63
+ pip install kmoe-manga-downloader
68
64
  ```
69
65
 
70
66
  ## 📋使用方法
@@ -74,35 +70,34 @@ pip install -r requirements.txt
74
70
  首先需要登录 `kox.moe` 并保存登录状态(Cookie)。
75
71
 
76
72
  ```bash
77
- python kmdr.py login -u <your_username> -p <your_password>
73
+ kmdr login -u <your_username> -p <your_password>
74
+ # 或者
75
+ kmdr login -u <your_username>
78
76
  ```
79
77
 
80
- 或者:
78
+ 第二种方式会在程序运行时获取登录密码,此时你输入的密码**不会显示**在终端中。
81
79
 
82
- ```bash
83
- python kmdr.py login -u <your_username>
84
- ```
85
-
86
- 第二种方式会在程序运行时获取登录密码。如果登录成功,会同时显示当前登录用户及配额。
80
+ 如果登录成功,会同时显示当前登录用户及配额。
87
81
 
88
82
  ### 2. 下载漫画书籍
89
83
 
90
84
  你可以通过以下命令下载指定书籍或卷:
91
85
 
92
86
  ```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
87
+ # 在当前目录下载第一、二、三卷
88
+ kmdr download --dest . --book-url https://kox.moe/c/50076.htm --volume 1,2,3
89
+ kmdr download -l https://kox.moe/c/50076.htm -v 1-3
96
90
  ```
97
91
 
98
92
  ```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
93
+ # 在目标目录下载全部番外篇
94
+ kmdr download --dest path/to/destination --book-url https://kox.moe/c/50076.htm --vol-type extra -v all
95
+ kmdr download -d path/to/destination -l https://kox.moe/c/50076.htm -t extra -v all
101
96
  ```
102
97
 
103
98
  #### 常用参数说明:
104
99
 
105
- - `-d`, `--dest`: 下载的目标目录,在此基础上会额外添加一个为书籍名称的子目录
100
+ - `-d`, `--dest`: 下载的目标目录(默认为当前目录),在此基础上会额外添加一个为书籍名称的子目录
106
101
  - `-l`, `--book-url`: 指定书籍的主页地址
107
102
  - `-v`, `--volume`: 指定卷的名称,多个名称使用逗号分隔,`all` 表示下载所有卷
108
103
  - `-t`, `--vol-type`: 卷类型,`vol`: 单行本(默认);`extra`: 番外;`seri`: 连载话;`all`: 全部
@@ -115,10 +110,10 @@ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/5007
115
110
 
116
111
  ### 3. 查看账户状态
117
112
 
118
- 查看当前账户信息(例如:账户名和配额等):
113
+ 查看当前账户信息(账户名和配额等):
119
114
 
120
115
  ```bash
121
- python kmdr.py status
116
+ kmdr status
122
117
  ```
123
118
 
124
119
  ### 4. 回调函数
@@ -126,7 +121,7 @@ python kmdr.py status
126
121
  你可以设置一个回调函数,下载完成后执行。回调可以是任何你想要的命令:
127
122
 
128
123
  ```bash
129
- python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
124
+ kmdr download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
130
125
  --callback "echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
131
126
  ```
132
127
 
@@ -146,11 +141,11 @@ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/5007
146
141
 
147
142
  ### 5. 持久化配置
148
143
 
149
- 重复设置下载的代理服务器、目标路径等参数,可能会降低脚本的使用效率。所以脚本也提供了通用配置的持久化命令:
144
+ 重复设置下载的代理服务器、目标路径等参数,可能会降低应用的使用效率。所以应用也提供了通用配置的持久化命令:
150
145
 
151
146
  ```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"
147
+ kmdr config --set proxy=http://localhost:7890 dest=/path/to/destination
148
+ kmdr config -s num_workers=5 "callback=echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
154
149
  ```
155
150
 
156
151
  只需要配置一次即可对之后的所有的下载指令生效。
@@ -175,8 +170,7 @@ python kmdr.py config -s num_workers=5 "callback=echo '{b.name} {v.name} downloa
175
170
  ---
176
171
 
177
172
  <div align=center>
178
- 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 或开 discussion 交流!<br />
173
+ 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 交流!<br />
179
174
  ⭐ 如果这个项目对你有帮助,请给它一个星标!<br /> <br />
180
175
  <img src="https://counter.seku.su/cmoe?name=kmdr&theme=mbs" />
181
176
  </div>
182
-
@@ -1,27 +1,23 @@
1
1
  # Kmoe Manga Downloader
2
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)
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://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
6
6
 
7
7
  ## ✨功能特性
8
8
 
9
- - 以命令行参数登录网站并持久化凭证
10
- - 支持多种方式筛选需要的内容
11
- - 支持网站上提供的不同的下载方式
12
- - 支持多线程下载,失败重试、断点续传
13
- - 提供自定义的下载完成回调命令
14
- - 提供通用配置持久化的实现
9
+ - **凭证管理**: 命令行登录并持久化会话
10
+ - **多种下载方式**: 支持通过书籍 URL 或从收藏列表下载
11
+ - **高效下载**: 支持多线程、失败重试及断点续传
12
+ - **配置持久化**: 保存常用下载目录、代理等设置
13
+ - **回调支持**: 下载完成后自动执行自定义脚本
15
14
 
16
- ## 🛠️安装依赖
15
+ ## 🛠️安装应用
17
16
 
18
- 在使用本脚本之前,请确保你已经安装了项目所需要的依赖:
17
+ 你可以通过 PyPI 使用 `pip` 进行安装:
19
18
 
20
19
  ```bash
21
- git clone https://github.com/chrisis58/kmoe-manga-downloader.git
22
- cd kmoe-manga-downloader
23
-
24
- pip install -r requirements.txt
20
+ pip install kmoe-manga-downloader
25
21
  ```
26
22
 
27
23
  ## 📋使用方法
@@ -31,35 +27,34 @@ pip install -r requirements.txt
31
27
  首先需要登录 `kox.moe` 并保存登录状态(Cookie)。
32
28
 
33
29
  ```bash
34
- python kmdr.py login -u <your_username> -p <your_password>
30
+ kmdr login -u <your_username> -p <your_password>
31
+ # 或者
32
+ kmdr login -u <your_username>
35
33
  ```
36
34
 
37
- 或者:
35
+ 第二种方式会在程序运行时获取登录密码,此时你输入的密码**不会显示**在终端中。
38
36
 
39
- ```bash
40
- python kmdr.py login -u <your_username>
41
- ```
42
-
43
- 第二种方式会在程序运行时获取登录密码。如果登录成功,会同时显示当前登录用户及配额。
37
+ 如果登录成功,会同时显示当前登录用户及配额。
44
38
 
45
39
  ### 2. 下载漫画书籍
46
40
 
47
41
  你可以通过以下命令下载指定书籍或卷:
48
42
 
49
43
  ```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
44
+ # 在当前目录下载第一、二、三卷
45
+ kmdr download --dest . --book-url https://kox.moe/c/50076.htm --volume 1,2,3
46
+ kmdr download -l https://kox.moe/c/50076.htm -v 1-3
53
47
  ```
54
48
 
55
49
  ```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
50
+ # 在目标目录下载全部番外篇
51
+ kmdr download --dest path/to/destination --book-url https://kox.moe/c/50076.htm --vol-type extra -v all
52
+ kmdr download -d path/to/destination -l https://kox.moe/c/50076.htm -t extra -v all
58
53
  ```
59
54
 
60
55
  #### 常用参数说明:
61
56
 
62
- - `-d`, `--dest`: 下载的目标目录,在此基础上会额外添加一个为书籍名称的子目录
57
+ - `-d`, `--dest`: 下载的目标目录(默认为当前目录),在此基础上会额外添加一个为书籍名称的子目录
63
58
  - `-l`, `--book-url`: 指定书籍的主页地址
64
59
  - `-v`, `--volume`: 指定卷的名称,多个名称使用逗号分隔,`all` 表示下载所有卷
65
60
  - `-t`, `--vol-type`: 卷类型,`vol`: 单行本(默认);`extra`: 番外;`seri`: 连载话;`all`: 全部
@@ -72,10 +67,10 @@ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/5007
72
67
 
73
68
  ### 3. 查看账户状态
74
69
 
75
- 查看当前账户信息(例如:账户名和配额等):
70
+ 查看当前账户信息(账户名和配额等):
76
71
 
77
72
  ```bash
78
- python kmdr.py status
73
+ kmdr status
79
74
  ```
80
75
 
81
76
  ### 4. 回调函数
@@ -83,7 +78,7 @@ python kmdr.py status
83
78
  你可以设置一个回调函数,下载完成后执行。回调可以是任何你想要的命令:
84
79
 
85
80
  ```bash
86
- python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
81
+ kmdr download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
87
82
  --callback "echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
88
83
  ```
89
84
 
@@ -103,11 +98,11 @@ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/5007
103
98
 
104
99
  ### 5. 持久化配置
105
100
 
106
- 重复设置下载的代理服务器、目标路径等参数,可能会降低脚本的使用效率。所以脚本也提供了通用配置的持久化命令:
101
+ 重复设置下载的代理服务器、目标路径等参数,可能会降低应用的使用效率。所以应用也提供了通用配置的持久化命令:
107
102
 
108
103
  ```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"
104
+ kmdr config --set proxy=http://localhost:7890 dest=/path/to/destination
105
+ kmdr config -s num_workers=5 "callback=echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
111
106
  ```
112
107
 
113
108
  只需要配置一次即可对之后的所有的下载指令生效。
@@ -132,8 +127,7 @@ python kmdr.py config -s num_workers=5 "callback=echo '{b.name} {v.name} downloa
132
127
  ---
133
128
 
134
129
  <div align=center>
135
- 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 或开 discussion 交流!<br />
130
+ 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 交流!<br />
136
131
  ⭐ 如果这个项目对你有帮助,请给它一个星标!<br /> <br />
137
132
  <img src="https://counter.seku.su/cmoe?name=kmdr&theme=mbs" />
138
133
  </div>
139
-
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kmoe-manga-downloader"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  authors = [
5
5
  { name="Chris Zheng", email="chrisis58@outlook.com" },
6
6
  ]
@@ -2,4 +2,6 @@ from .bases import Authenticator, Lister, Picker, Downloader, Configurer
2
2
  from .structure import VolInfo, BookInfo, VolumeType
3
3
  from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER
4
4
 
5
- from .defaults import argument_parser
5
+ from .defaults import argument_parser
6
+
7
+ from .error import KmdrError, LoginError
@@ -2,10 +2,11 @@ import os
2
2
 
3
3
  from typing import Callable, Optional
4
4
 
5
+ from .error import LoginError
5
6
  from .registry import Registry
6
7
  from .structure import VolInfo, BookInfo
7
8
  from .utils import get_singleton_session, construct_callback
8
- from .defaults import Configurer as InnerConfigurer
9
+ from .defaults import Configurer as InnerConfigurer, UserProfile
9
10
 
10
11
  class SessionContext:
11
12
 
@@ -13,6 +14,12 @@ class SessionContext:
13
14
  super().__init__()
14
15
  self._session = get_singleton_session()
15
16
 
17
+ class UserProfileContext:
18
+
19
+ def __init__(self, *args, **kwargs):
20
+ super().__init__()
21
+ self._profile = UserProfile()
22
+
16
23
  class ConfigContext:
17
24
 
18
25
  def __init__(self, *args, **kwargs):
@@ -26,7 +33,7 @@ class Configurer(ConfigContext):
26
33
 
27
34
  def operate(self) -> None: ...
28
35
 
29
- class Authenticator(SessionContext, ConfigContext):
36
+ class Authenticator(SessionContext, ConfigContext, UserProfileContext):
30
37
 
31
38
  def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
32
39
  super().__init__(*args, **kwargs)
@@ -41,8 +48,13 @@ class Authenticator(SessionContext, ConfigContext):
41
48
  # 主站正常情况下不使用代理也能登录成功。但是不排除特殊的网络环境下需要代理。
42
49
  # 所以暂时保留代理登录的功能,如果后续确认是代理的问题,可以考虑启用 @no_proxy 装饰器。
43
50
  # @no_proxy
44
- def authenticate(self) -> bool:
45
- return self._authenticate()
51
+ def authenticate(self) -> None:
52
+ try:
53
+ assert self._authenticate()
54
+ except LoginError as e:
55
+ print("Authentication failed. Please check your login credentials or session cookies.")
56
+ print(f"Details: {e}")
57
+ exit(1)
46
58
 
47
59
  def _authenticate(self) -> bool: ...
48
60
 
@@ -60,7 +72,7 @@ class Picker(SessionContext):
60
72
 
61
73
  def pick(self, volumes: list[VolInfo]) -> list[VolInfo]: ...
62
74
 
63
- class Downloader(SessionContext):
75
+ class Downloader(SessionContext, UserProfileContext):
64
76
 
65
77
  def __init__(self,
66
78
  dest: str = '.',
@@ -58,6 +58,29 @@ def parse_args():
58
58
 
59
59
  return args
60
60
 
61
+ @singleton
62
+ class UserProfile:
63
+
64
+ def __init__(self):
65
+ self._is_vip: Optional[int] = None
66
+ self._user_level: Optional[int] = None
67
+
68
+ @property
69
+ def is_vip(self) -> Optional[int]:
70
+ return self._is_vip
71
+
72
+ @property
73
+ def user_level(self) -> Optional[int]:
74
+ return self._user_level
75
+
76
+ @is_vip.setter
77
+ def is_vip(self, value: Optional[int]):
78
+ self._is_vip = value
79
+
80
+ @user_level.setter
81
+ def user_level(self, value: Optional[int]):
82
+ self._user_level = value
83
+
61
84
  @singleton
62
85
  class Configurer:
63
86
 
@@ -0,0 +1,15 @@
1
+ from typing import Optional
2
+
3
+ class KmdrError(RuntimeError):
4
+ def __init__(self, message: str, solution: Optional[list[str]] = None, *args: object, **kwargs: object):
5
+ super().__init__(message, *args, **kwargs)
6
+ self.message = message
7
+
8
+ self._solution = "" if solution is None else "\nSuggested Solution: \n" + "\n".join(f">>> {sol}" for sol in solution)
9
+
10
+ class LoginError(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}"
@@ -7,16 +7,13 @@ from kmdr.module import *
7
7
  def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
8
8
 
9
9
  if args.command == 'login':
10
- if not AUTHENTICATOR.get(args).authenticate():
11
- raise RuntimeError("Authentication failed. Please check your credentials.")
10
+ AUTHENTICATOR.get(args).authenticate()
12
11
 
13
12
  elif args.command == 'status':
14
- if not AUTHENTICATOR.get(args).authenticate():
15
- raise RuntimeError("Authentication failed. Please check your credentials.")
13
+ AUTHENTICATOR.get(args).authenticate()
16
14
 
17
- elif args.command == 'download':
18
- if not AUTHENTICATOR.get(args).authenticate():
19
- raise RuntimeError("Authentication failed. Please check your credentials.")
15
+ elif args.command == 'download':
16
+ AUTHENTICATOR.get(args).authenticate()
20
17
 
21
18
  book, volumes = LISTERS.get(args).list()
22
19
 
@@ -1,6 +1,6 @@
1
1
  from typing import Optional
2
2
 
3
- from kmdr.core import Authenticator, AUTHENTICATOR
3
+ from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
4
4
 
5
5
  from .utils import check_status
6
6
 
@@ -18,8 +18,12 @@ class CookieAuthenticator(Authenticator):
18
18
  cookie = self._configurer.cookie
19
19
 
20
20
  if not cookie:
21
- print("No cookie found. Please login first.")
22
- return False
21
+ raise LoginError("No cookie found, please login first.", ['kmdr login -u <username>'])
23
22
 
24
23
  self._session.cookies.update(cookie)
25
- return check_status(self._session, show_quota=self._show_quota)
24
+ return check_status(
25
+ self._session,
26
+ show_quota=self._show_quota,
27
+ is_vip_setter=lambda value: setattr(self._profile, 'is_vip', value),
28
+ level_setter=lambda value: setattr(self._profile, 'user_level', value),
29
+ )
@@ -1,10 +1,19 @@
1
1
  from typing import Optional
2
2
  import re
3
+ from getpass import getpass
3
4
 
4
- from kmdr.core import Authenticator, AUTHENTICATOR
5
+ from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
5
6
 
6
7
  from .utils import check_status
7
8
 
9
+ CODE_OK = 'm100'
10
+
11
+ CODE_MAPPING = {
12
+ 'e400': "帳號或密碼錯誤。",
13
+ 'e401': "非法訪問,請使用瀏覽器正常打開本站",
14
+ 'e402': "帳號已經註銷。不會解釋原因,無需提問。",
15
+ 'e403': "驗證失效,請刷新頁面重新操作。",
16
+ }
8
17
 
9
18
  @AUTHENTICATOR.register(
10
19
  hasvalues = {'command': 'login'}
@@ -16,7 +25,7 @@ class LoginAuthenticator(Authenticator):
16
25
  self._show_quota = show_quota
17
26
 
18
27
  if password is None:
19
- password = input("please input your password: \n")
28
+ password = getpass("please input your password: ")
20
29
 
21
30
  self._password = password
22
31
 
@@ -31,22 +40,15 @@ class LoginAuthenticator(Authenticator):
31
40
  },
32
41
  )
33
42
  response.raise_for_status()
34
-
35
- match = re.search('"\w+"', response.text)
43
+ match = re.search(r'"\w+"', response.text)
44
+
36
45
  if not match:
37
- raise RuntimeError("Failed to extract authentication code from response.")
38
- code = match.group(0).split('"')[1]
39
- if code != 'm100':
40
- if code == 'e400':
41
- print("帳號或密碼錯誤。")
42
- elif code == 'e401':
43
- print("非法訪問,請使用瀏覽器正常打開本站")
44
- elif code == 'e402':
45
- print("帳號已經註銷。不會解釋原因,無需提問。")
46
- elif code == 'e403':
47
- print("驗證失效,請刷新頁面重新操作。")
48
- raise RuntimeError("Authentication failed with code: " + code)
46
+ raise LoginError("Failed to extract authentication code from response.")
49
47
 
48
+ code = match.group(0).split('"')[1]
49
+ if code != CODE_OK:
50
+ raise LoginError(f"Authentication failed with error code: {code} " + CODE_MAPPING.get(code, "Unknown error."))
51
+
50
52
  if check_status(self._session, show_quota=self._show_quota):
51
53
  self._configurer.cookie = self._session.cookies.get_dict()
52
54
  return True
@@ -0,0 +1,79 @@
1
+ from typing import Optional, Callable
2
+
3
+ from requests import Session
4
+
5
+ from kmdr.core.error import LoginError
6
+
7
+ PROFILE_URL = 'https://kox.moe/my.php'
8
+ LOGIN_URL = 'https://kox.moe/login.php'
9
+
10
+ NICKNAME_ID = 'div_nickname_display'
11
+
12
+ VIP_ID = 'div_user_vip'
13
+ NOR_ID = 'div_user_nor'
14
+ LV1_ID = 'div_user_lv1'
15
+
16
+ def check_status(
17
+ session: Session,
18
+ show_quota: bool = False,
19
+ is_vip_setter: Optional[Callable[[int], None]] = None,
20
+ level_setter: Optional[Callable[[int], None]] = None
21
+ ) -> bool:
22
+ response = session.get(url = PROFILE_URL)
23
+
24
+ try:
25
+ response.raise_for_status()
26
+ except Exception as e:
27
+ print(f"Error: {type(e).__name__}: {e}")
28
+ return False
29
+
30
+ if response.history and any(resp.status_code in (301, 302, 307) for resp in response.history) \
31
+ and response.url == LOGIN_URL:
32
+ raise LoginError("Invalid credentials, please login again.", ['kmdr config -c cookie', 'kmdr login -u <username>'])
33
+
34
+ if not is_vip_setter and not level_setter and not show_quota:
35
+ return True
36
+
37
+ from bs4 import BeautifulSoup
38
+
39
+ soup = BeautifulSoup(response.text, 'html.parser')
40
+
41
+ script = soup.find('script', language="javascript")
42
+
43
+ if script:
44
+ var_define = extract_var_define(script.text[:100])
45
+
46
+ is_vip = int(var_define.get('is_vip', '0'))
47
+ user_level = int(var_define.get('user_level', '0'))
48
+
49
+ if is_vip_setter:
50
+ is_vip_setter(is_vip)
51
+ if level_setter:
52
+ level_setter(user_level)
53
+
54
+ if not show_quota:
55
+ return True
56
+
57
+ nickname = soup.find('div', id=NICKNAME_ID).text.strip().split(' ')[0]
58
+ quota = soup.find('div', id=__resolve_quota_id(is_vip, user_level)).text.strip()
59
+
60
+ print(f"\n当前登录为 {nickname}\n\n{quota}")
61
+ return True
62
+
63
+ def extract_var_define(script_text) -> dict[str, str]:
64
+ var_define = {}
65
+ for line in script_text.splitlines():
66
+ line = line.strip()
67
+ if line.startswith("var ") and "=" in line:
68
+ var_name, var_value = line[4:].split("=", 1)
69
+ var_define[var_name.strip()] = var_value.strip().strip(";").strip('"')
70
+ return var_define
71
+
72
+ def __resolve_quota_id(is_vip: Optional[int] = None, user_level: Optional[int] = None):
73
+ if is_vip is not None and is_vip >= 1:
74
+ return VIP_ID
75
+
76
+ if user_level is not None and user_level <= 1:
77
+ return LV1_ID
78
+
79
+ return NOR_ID
@@ -1,5 +1,7 @@
1
1
  from kmdr.core import Configurer, CONFIGURER
2
2
 
3
+ from .option_validate import check_key
4
+
3
5
  @CONFIGURER.register()
4
6
  class ConfigUnsetter(Configurer):
5
7
  def __init__(self, unset: str, *args, **kwargs):
@@ -10,6 +12,7 @@ class ConfigUnsetter(Configurer):
10
12
  if not self._unset:
11
13
  print("No option specified to unset.")
12
14
  return
13
-
15
+
16
+ check_key(self._unset)
14
17
  self._configurer.unset_option(self._unset)
15
18
  print(f"Unset configuration: {self._unset}")
@@ -1,27 +1,61 @@
1
1
  from typing import Optional
2
+ from functools import wraps
2
3
  import os
3
4
 
4
5
  __OPTIONS_VALIDATOR = {}
5
6
 
6
7
  def validate(key: str, value: str) -> Optional[object]:
8
+ """
9
+ 供外部调用的验证函数,根据键名调用相应的验证器。
10
+
11
+ :param key: 配置项的键名
12
+ :param value: 配置项的值
13
+ :return: 验证后的值或 None
14
+ """
7
15
  if key in __OPTIONS_VALIDATOR:
8
16
  return __OPTIONS_VALIDATOR[key](value)
9
17
  else:
10
18
  print(f"Unsupported option: {key}. Supported options are: {', '.join(__OPTIONS_VALIDATOR.keys())}")
11
19
  return None
12
20
 
13
- def _register_validator(func):
14
- global __OPTIONS_VALIDATOR
21
+ def check_key(key: str, exit_if_invalid: bool = True) -> None:
22
+ """
23
+ 供外部调用的验证函数,用于检查配置项的键名是否有效。
24
+ 如果键名无效,函数会打印错误信息并退出程序。
25
+
26
+ :param key: 配置项的键名
27
+ :param exit_if_invalid: 如果键名无效,是否退出程序
28
+ """
29
+ if key not in __OPTIONS_VALIDATOR:
30
+ print(f"Unknown option: {key}. Supported options are: {', '.join(__OPTIONS_VALIDATOR.keys())}")
31
+ if exit_if_invalid:
32
+ exit(1)
33
+
34
+ def register_validator(arg_name):
35
+ """
36
+ 验证函数的注册装饰器,用于将验证函数注册到全局验证器字典中。
37
+
38
+ :param arg_name: 配置项的键名
39
+ """
40
+
41
+ def wrapper(func):
42
+ global __OPTIONS_VALIDATOR
43
+ __OPTIONS_VALIDATOR[arg_name] = func
44
+
45
+ @wraps(func)
46
+ def inner(*args, **kwargs):
47
+ return func(*args, **kwargs)
48
+
49
+ return inner
15
50
 
16
- func_name = func.__name__
51
+ return wrapper
17
52
 
18
- assert func_name.startswith('validate_'), \
19
- f"Validator function name must start with 'validate_', got '{func_name}'"
20
53
 
21
- __OPTIONS_VALIDATOR[func.__name__[9:]] = func
22
- return func
54
+ #############################################
55
+ ## 以下为各个配置项的验证函数。
56
+ #############################################
23
57
 
24
- @_register_validator
58
+ @register_validator('num_workers')
25
59
  def validate_num_workers(value: str) -> Optional[int]:
26
60
  try:
27
61
  num_workers = int(value)
@@ -32,7 +66,7 @@ def validate_num_workers(value: str) -> Optional[int]:
32
66
  print(f"Invalid value for num_workers: {value}. {str(e)}")
33
67
  return None
34
68
 
35
- @_register_validator
69
+ @register_validator('dest')
36
70
  def validate_dest(value: str) -> Optional[str]:
37
71
  if not value:
38
72
  print("Destination cannot be empty.")
@@ -50,7 +84,7 @@ def validate_dest(value: str) -> Optional[str]:
50
84
 
51
85
  return value
52
86
 
53
- @_register_validator
87
+ @register_validator('retry')
54
88
  def validate_retry(value: str) -> Optional[int]:
55
89
  try:
56
90
  retry = int(value)
@@ -61,14 +95,14 @@ def validate_retry(value: str) -> Optional[int]:
61
95
  print(f"Invalid value for retry: {value}. {str(e)}")
62
96
  return None
63
97
 
64
- @_register_validator
98
+ @register_validator('callback')
65
99
  def validate_callback(value: str) -> Optional[str]:
66
100
  if not value:
67
101
  print("Callback cannot be empty.")
68
102
  return None
69
103
  return value
70
104
 
71
- @_register_validator
105
+ @register_validator('proxy')
72
106
  def validate_proxy(value: str) -> Optional[str]:
73
107
  if not value:
74
108
  print("Proxy cannot be empty.")
@@ -25,4 +25,4 @@ class DirectDownloader(Downloader):
25
25
  )
26
26
 
27
27
  def construct_download_url(self, book: BookInfo, volume: VolInfo) -> str:
28
- return f'https://kox.moe/dl/{book.id}/{volume.id}/1/2/0/'
28
+ return f'https://kox.moe/dl/{book.id}/{volume.id}/1/2/{self._profile.is_vip}/'
@@ -23,7 +23,7 @@ class ReferViaDownloader(Downloader):
23
23
 
24
24
  download_file(
25
25
  self._session if not self._scraper else self._scraper,
26
- self.fetch_download_url(book=book, volume=volume),
26
+ self.fetch_download_url(book_id=book.id, volume_id=volume.id),
27
27
  download_path,
28
28
  f'[Kmoe][{book.name}][{volume.name}].epub',
29
29
  retry,
@@ -34,8 +34,8 @@ class ReferViaDownloader(Downloader):
34
34
  )
35
35
 
36
36
  @cached_by_kwargs
37
- def fetch_download_url(self, book: BookInfo, volume: VolInfo) -> str:
38
- response = self._session.get(f"https://kox.moe/getdownurl.php?b={book.id}&v={volume.id}&mobi=2&vip=0&json=1")
37
+ def fetch_download_url(self, book_id: str, volume_id: str) -> str:
38
+ response = self._session.get(f"https://kox.moe/getdownurl.php?b={book_id}&v={volume_id}&mobi=2&vip={self._profile.is_vip}&json=1")
39
39
  response.raise_for_status()
40
40
  data = response.json()
41
41
  if data.get('code') != 200:
@@ -1,6 +1,7 @@
1
1
  from typing import Callable, Optional
2
2
  import os
3
3
  import time
4
+ from functools import wraps
4
5
 
5
6
  from requests import Session, HTTPError
6
7
  from requests.exceptions import ChunkedEncodingError
@@ -64,11 +65,6 @@ def download_file(
64
65
  except Exception as e:
65
66
  prefix = f"{type(e).__name__} occurred while downloading {filename}. "
66
67
 
67
- if isinstance(e, HTTPError):
68
- e.request.headers['Cookie'] = '***MASKED***'
69
- tqdm.write(f"Request Headers: {e.request.headers}")
70
- tqdm.write(f"Response Headers: {e.response.headers}")
71
-
72
68
  new_block_size = block_size
73
69
  if isinstance(e, ChunkedEncodingError):
74
70
  new_block_size = max(int(block_size * BLOCK_SIZE_REDUCTION_FACTOR), MIN_BLOCK_SIZE)
@@ -89,6 +85,8 @@ def safe_filename(name: str) -> str:
89
85
  return re.sub(r'[\\/:*?"<>|]', '_', name)
90
86
 
91
87
 
88
+ function_cache = {}
89
+
92
90
  def cached_by_kwargs(func):
93
91
  """
94
92
  根据关键字参数缓存函数结果的装饰器。
@@ -101,18 +99,29 @@ def cached_by_kwargs(func):
101
99
  >>> result2 = add(3, 2, c=3) # Uses cached result
102
100
  >>> assert result1 == result2 # Both results are the same
103
101
  """
104
- cache = {}
105
102
 
103
+ global function_cache
104
+ if func not in function_cache:
105
+ function_cache[func] = {}
106
+
107
+ @wraps(func)
106
108
  def wrapper(*args, **kwargs):
107
109
  if not kwargs:
108
110
  return func(*args, **kwargs)
109
-
110
- nonlocal cache
111
111
 
112
112
  key = frozenset(kwargs.items())
113
113
 
114
- if key not in cache:
115
- cache[key] = func(*args, **kwargs)
116
- return cache[key]
114
+ if key not in function_cache[func]:
115
+ function_cache[func][key] = func(*args, **kwargs)
116
+ return function_cache[func][key]
117
117
 
118
118
  return wrapper
119
+
120
+ def clear_cache(func):
121
+ assert hasattr(func, "__wrapped__"), "Function is not wrapped"
122
+ global function_cache
123
+
124
+ wrapped = func.__wrapped__
125
+
126
+ if wrapped in function_cache:
127
+ function_cache[wrapped] = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kmoe-manga-downloader
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -43,28 +43,24 @@ Dynamic: license-file
43
43
 
44
44
  # Kmoe Manga Downloader
45
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)
46
+ [![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)
47
47
 
48
- `kmdr (Kmoe Manga Downloader)` 是一个 Python 脚本,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
48
+ `kmdr (Kmoe Manga Downloader)` 是一个 Python 应用,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
49
49
 
50
50
  ## ✨功能特性
51
51
 
52
- - 以命令行参数登录网站并持久化凭证
53
- - 支持多种方式筛选需要的内容
54
- - 支持网站上提供的不同的下载方式
55
- - 支持多线程下载,失败重试、断点续传
56
- - 提供自定义的下载完成回调命令
57
- - 提供通用配置持久化的实现
52
+ - **凭证管理**: 命令行登录并持久化会话
53
+ - **多种下载方式**: 支持通过书籍 URL 或从收藏列表下载
54
+ - **高效下载**: 支持多线程、失败重试及断点续传
55
+ - **配置持久化**: 保存常用下载目录、代理等设置
56
+ - **回调支持**: 下载完成后自动执行自定义脚本
58
57
 
59
- ## 🛠️安装依赖
58
+ ## 🛠️安装应用
60
59
 
61
- 在使用本脚本之前,请确保你已经安装了项目所需要的依赖:
60
+ 你可以通过 PyPI 使用 `pip` 进行安装:
62
61
 
63
62
  ```bash
64
- git clone https://github.com/chrisis58/kmoe-manga-downloader.git
65
- cd kmoe-manga-downloader
66
-
67
- pip install -r requirements.txt
63
+ pip install kmoe-manga-downloader
68
64
  ```
69
65
 
70
66
  ## 📋使用方法
@@ -74,35 +70,34 @@ pip install -r requirements.txt
74
70
  首先需要登录 `kox.moe` 并保存登录状态(Cookie)。
75
71
 
76
72
  ```bash
77
- python kmdr.py login -u <your_username> -p <your_password>
73
+ kmdr login -u <your_username> -p <your_password>
74
+ # 或者
75
+ kmdr login -u <your_username>
78
76
  ```
79
77
 
80
- 或者:
78
+ 第二种方式会在程序运行时获取登录密码,此时你输入的密码**不会显示**在终端中。
81
79
 
82
- ```bash
83
- python kmdr.py login -u <your_username>
84
- ```
85
-
86
- 第二种方式会在程序运行时获取登录密码。如果登录成功,会同时显示当前登录用户及配额。
80
+ 如果登录成功,会同时显示当前登录用户及配额。
87
81
 
88
82
  ### 2. 下载漫画书籍
89
83
 
90
84
  你可以通过以下命令下载指定书籍或卷:
91
85
 
92
86
  ```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
87
+ # 在当前目录下载第一、二、三卷
88
+ kmdr download --dest . --book-url https://kox.moe/c/50076.htm --volume 1,2,3
89
+ kmdr download -l https://kox.moe/c/50076.htm -v 1-3
96
90
  ```
97
91
 
98
92
  ```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
93
+ # 在目标目录下载全部番外篇
94
+ kmdr download --dest path/to/destination --book-url https://kox.moe/c/50076.htm --vol-type extra -v all
95
+ kmdr download -d path/to/destination -l https://kox.moe/c/50076.htm -t extra -v all
101
96
  ```
102
97
 
103
98
  #### 常用参数说明:
104
99
 
105
- - `-d`, `--dest`: 下载的目标目录,在此基础上会额外添加一个为书籍名称的子目录
100
+ - `-d`, `--dest`: 下载的目标目录(默认为当前目录),在此基础上会额外添加一个为书籍名称的子目录
106
101
  - `-l`, `--book-url`: 指定书籍的主页地址
107
102
  - `-v`, `--volume`: 指定卷的名称,多个名称使用逗号分隔,`all` 表示下载所有卷
108
103
  - `-t`, `--vol-type`: 卷类型,`vol`: 单行本(默认);`extra`: 番外;`seri`: 连载话;`all`: 全部
@@ -115,10 +110,10 @@ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/5007
115
110
 
116
111
  ### 3. 查看账户状态
117
112
 
118
- 查看当前账户信息(例如:账户名和配额等):
113
+ 查看当前账户信息(账户名和配额等):
119
114
 
120
115
  ```bash
121
- python kmdr.py status
116
+ kmdr status
122
117
  ```
123
118
 
124
119
  ### 4. 回调函数
@@ -126,7 +121,7 @@ python kmdr.py status
126
121
  你可以设置一个回调函数,下载完成后执行。回调可以是任何你想要的命令:
127
122
 
128
123
  ```bash
129
- python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
124
+ kmdr download -d path/to/destination --book-url https://kox.moe/c/50076.htm -v 1-3 \
130
125
  --callback "echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
131
126
  ```
132
127
 
@@ -146,11 +141,11 @@ python kmdr.py download -d path/to/destination --book-url https://kox.moe/c/5007
146
141
 
147
142
  ### 5. 持久化配置
148
143
 
149
- 重复设置下载的代理服务器、目标路径等参数,可能会降低脚本的使用效率。所以脚本也提供了通用配置的持久化命令:
144
+ 重复设置下载的代理服务器、目标路径等参数,可能会降低应用的使用效率。所以应用也提供了通用配置的持久化命令:
150
145
 
151
146
  ```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"
147
+ kmdr config --set proxy=http://localhost:7890 dest=/path/to/destination
148
+ kmdr config -s num_workers=5 "callback=echo '{b.name} {v.name} downloaded!' >> ~/kmdr.log"
154
149
  ```
155
150
 
156
151
  只需要配置一次即可对之后的所有的下载指令生效。
@@ -175,8 +170,7 @@ python kmdr.py config -s num_workers=5 "callback=echo '{b.name} {v.name} downloa
175
170
  ---
176
171
 
177
172
  <div align=center>
178
- 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 或开 discussion 交流!<br />
173
+ 💬任何使用中遇到的问题、希望添加的功能,都欢迎提交 issue 交流!<br />
179
174
  ⭐ 如果这个项目对你有帮助,请给它一个星标!<br /> <br />
180
175
  <img src="https://counter.seku.su/cmoe?name=kmdr&theme=mbs" />
181
176
  </div>
182
-
@@ -6,6 +6,7 @@ src/kmdr/main.py
6
6
  src/kmdr/core/__init__.py
7
7
  src/kmdr/core/bases.py
8
8
  src/kmdr/core/defaults.py
9
+ src/kmdr/core/error.py
9
10
  src/kmdr/core/registry.py
10
11
  src/kmdr/core/structure.py
11
12
  src/kmdr/core/utils.py
@@ -33,6 +34,7 @@ src/kmoe_manga_downloader.egg-info/dependency_links.txt
33
34
  src/kmoe_manga_downloader.egg-info/entry_points.txt
34
35
  src/kmoe_manga_downloader.egg-info/requires.txt
35
36
  src/kmoe_manga_downloader.egg-info/top_level.txt
37
+ tests/test_cache_by_kwargs.py
36
38
  tests/test_kmdr_config_option.py
37
39
  tests/test_kmdr_download.py
38
40
  tests/test_kmdr_login.py
@@ -0,0 +1,87 @@
1
+ import unittest
2
+
3
+ from kmdr.module.downloader.utils import cached_by_kwargs, clear_cache
4
+
5
+ @cached_by_kwargs
6
+ def add(a, b, c):
7
+ return a + b + c
8
+
9
+ class TestAdder(object):
10
+
11
+ @cached_by_kwargs
12
+ def add(self, a, b, c):
13
+ return a + b + c
14
+
15
+ class AddUtil:
16
+
17
+ @staticmethod
18
+ @cached_by_kwargs
19
+ def add(a, b, c):
20
+ return a + b + c
21
+
22
+ @classmethod
23
+ @cached_by_kwargs
24
+ def mul(cls, a, b, c):
25
+ return a * b * c
26
+
27
+ class TestCacheByKwargs(unittest.TestCase):
28
+
29
+ def tearDown(self):
30
+ clear_cache(add)
31
+ clear_cache(TestAdder.add)
32
+ clear_cache(AddUtil.add)
33
+ clear_cache(AddUtil.mul)
34
+
35
+ def test_function_cache(self):
36
+ result1 = add(1, 2, c=3)
37
+ result2 = add(3, 2, c=3)
38
+ self.assertEqual(result1, result2)
39
+
40
+ def test_classmethod_cache(self):
41
+ result1 = AddUtil.add(1, 2, c=6)
42
+ result2 = AddUtil.add(3, 2, c=6)
43
+ self.assertEqual(result1, result2)
44
+
45
+ def test_instance_method_cache(self):
46
+ instance = TestAdder()
47
+ result1 = instance.add(1, 2, c=9)
48
+ result2 = instance.add(3, 2, c=9)
49
+ self.assertEqual(result1, result2)
50
+
51
+ def test_different_definitions(self):
52
+ result1 = add(1, b=2, c=3)
53
+ result2 = AddUtil.add(2, b=2, c=3)
54
+ result3 = TestAdder().add(3, b=2, c=3)
55
+
56
+ self.assertNotEqual(result1, result2)
57
+ self.assertNotEqual(result1, result3)
58
+ self.assertNotEqual(result2, result3)
59
+
60
+ def test_classmethod_cache(self):
61
+ result1 = AddUtil.mul(1, 2, c=3)
62
+ result2 = AddUtil.mul(2, 2, c=3)
63
+ self.assertEqual(result1, result2)
64
+
65
+ def test_different_instance(self):
66
+ instance1 = TestAdder()
67
+ instance2 = TestAdder()
68
+ result1 = instance1.add(1, 2, c=12)
69
+ result2 = instance2.add(2, 3, c=12)
70
+
71
+ self.assertEqual(result1, result2)
72
+
73
+ def test_clear_cache(self):
74
+ result1 = add(1, 2, c=0)
75
+ clear_cache(add)
76
+ result2 = add(2, 3, c=0)
77
+ self.assertNotEqual(result1, result2)
78
+
79
+ result1 = AddUtil.add(1, 2, c=0)
80
+ clear_cache(AddUtil.add)
81
+ result2 = AddUtil.add(2, 3, c=0)
82
+ self.assertNotEqual(result1, result2)
83
+
84
+ result1 = TestAdder().add(1, 2, c=0)
85
+ clear_cache(TestAdder.add)
86
+ result2 = TestAdder().add(2, 3, c=0)
87
+ self.assertNotEqual(result1, result2)
@@ -1,25 +0,0 @@
1
- from requests import Session
2
-
3
- def check_status(session: Session, show_quota: bool = False) -> bool:
4
- response = session.get(url = 'https://kox.moe/my.php')
5
-
6
- try:
7
- response.raise_for_status()
8
- except Exception as e:
9
- print(f"Error: {type(e).__name__}: {e}")
10
- return False
11
-
12
- if not show_quota:
13
- return True
14
-
15
- from bs4 import BeautifulSoup
16
-
17
- soup = BeautifulSoup(response.text, 'html.parser')
18
-
19
- nickname = soup.find('div', id='div_nickname_display').text.strip().split(' ')[0]
20
- print(f"=========================\n\nLogged in as {nickname}\n\n=========================\n")
21
-
22
- quota = soup.find('div', id='div_user_vip').text.strip()
23
- print(f"=========================\n\n{quota}\n\n=========================\n")
24
- return True
25
-