Skip to content

Commit cf5c12c

Browse files
authored
Merge pull request #187 from Ljzd-PRO/devel
Bump to v0.11.0
2 parents 3f439ef + fc37cf0 commit cf5c12c

File tree

12 files changed

+167
-161
lines changed

12 files changed

+167
-161
lines changed

CHANGELOG.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,21 @@
22

33
### 💡 Feature
44

5-
- Added a **graphical configuration editor**, making configuration editing simple and convenient.
6-
- Run `ktoolbox config-editor`
7-
- Added a command to generate a complete sample configuration file.
8-
- Run `ktoolbox example-env`
9-
- **Python** versions below 3.8.1 (**<3.8.1**) are no longer supported
5+
- The `search-creator` command will include search results with similar names.
6+
- For example, the search parameter `--name abc` might return author information such as: `abc, abcd, hi-abc`
7+
- Share an HTTPX client to reuse underlying TCP connections through an HTTP connection pool when calling APIs and downloading,
8+
**significantly improving query and download speeds as well as connection stability**
109

1110
[//]: # (### 🪲 Fix)
1211

1312
- - -
1413

1514
### 💡 新特性
1615

17-
- 新增 **图形化配置编辑器**,配置编辑将变得简单方便
18-
- 运行 `ktoolbox config-editor`
19-
- 新增命令可生成完整的配置文件样例
20-
- 运行 `ktoolbox example-env`
21-
- **Python** 3.8.1 以下 (**<3.8.1**) 的版本不再受支持
16+
- search-creator 搜索作者的命令将包含那些名字相近的搜索结果
17+
- 如搜索参数 `--name abc` 可能得到如下作者信息:`abc, abcd, hi-abc`
18+
- 共享 HTTPX 客户端,调用 API 和下载时将通过 HTTP 连接池重用底层 TCP 连接,**显著提升查询、下载速度和连接稳定性**
2219

2320
[//]: # (### 🪲 修复)
2421

25-
**Full Changelog**: https://github.com/Ljzd-PRO/KToolBox/compare/v0.9.0...v0.10.0
22+
**Full Changelog**: https://github.com/Ljzd-PRO/KToolBox/compare/v0.10.0...v0.11.0

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<p align="center">
1010
KToolBox is a useful CLI tool for downloading posts content in
11-
<a href="https://kemono.su/">Kemono.party / Kemono.su</a>
11+
<a href="https://kemono.su/">Kemono.su / Kemono.party</a>
1212
</p>
1313

1414
<p align="center">
@@ -51,19 +51,20 @@
5151

5252
## Features
5353

54-
- Support for **multi-thread** downloads (technically, coroutine)
55-
- **Retry** after download failed
56-
- Ability to download individual post as well as **all post** by a specified creator/artist
57-
- **Update downloaded** creator/artist directories to the latest status
58-
- Customize the **structure** of downloaded post/creator **directories**
59-
- Search for creators/artists and posts, and **export the results**
60-
- **Cross-platform** support & **iOS shortcuts** available
61-
- For Coomer.su / Coomer.party support, check document [Coomer](https://ktoolbox.readthedocs.io/latest/coomer/) for more.
54+
- Supports concurrent downloads
55+
- Automatically retries API calls and downloads after failures
56+
- Allows downloading individual posts or **all posts** of a specified artist
57+
- Can **update downloaded** artist directories to the latest state
58+
- Supports customizable **file and directory naming formats** and **directory structures** for downloaded posts/artists
59+
- Enables excluding **specified file formats** or downloading only specified formats
60+
- Allows searching for artists and posts, with options to export results
61+
- Compatible with all platforms, with iOS shortcuts provided
62+
- For support related to _Coomer.su / Coomer.party_, please refer to the documentation: [Coomer](https://ktoolbox.readthedocs.io/latest/coomer/)
6263

6364
## Dev Plan
6465

6566
- [ ] GUI
66-
- [x] Add uvloop support for Unix platform
67+
- [ ] Discord support
6768

6869
## Tutorial
6970

README_zh-CN.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<p align="center">
1010
KToolBox 是一个用于下载
11-
<a href="https://kemono.su/">Kemono.party / Kemono.su</a>
11+
<a href="https://kemono.su/">Kemono.su / Kemono.party</a>
1212
中作品内容的实用命令行工具
1313
</p>
1414

@@ -52,19 +52,20 @@
5252

5353
## 功能
5454

55-
- 支持 **多线程** 下载(技术上是协程)
56-
- 下载失败后进行 **重试**
55+
- 支持并发下载
56+
- API 调用和下载失败后 **自动重试**
5757
- 支持下载单个作品以及指定的画师的 **所有作品**
5858
-**更新已下载** 的画师目录至最新状态
59-
- 可自定义下载的作品/画师 **目录结构**
60-
- 可搜索画师和作品,并 **导出结果**
61-
- 支持全平台,并提供 **iOS 快捷指令**
62-
- 对于 Coomer.su / Coomer.party 的支持,请查看文档 [Coomer](https://ktoolbox.readthedocs.io/latest/zh/coomer/)
59+
- 支持自定义下载的作品/画师 **文件和目录名格式****目录结构**
60+
- 支持排除 **指定格式** 的文件或仅下载指定格式的文件
61+
- 可搜索画师和作品,并导出结果
62+
- 支持全平台,并提供 iOS 快捷指令
63+
- 对于 Coomer.su / Coomer.party 的支持,请查看文档 [Coomer](https://ktoolbox.readthedocs.io/latest/zh/coomer/)
6364

6465
## 开发计划
6566

6667
- [ ] GUI
67-
- [x] 对 Unix 平台增加 uvloop 支持
68+
- [ ] Discord 下载支持
6869

6970
## 使用方法
7071

docs/en/index.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939

4040
## Features
4141

42-
- Support for **multi-thread** downloads (technically, coroutine)
43-
- **Retry** after download failed
44-
- Ability to download individual post as well as **all post** by a specified creator/artist
45-
- **Update downloaded** creator/artist directories to the latest status
46-
- Customize the **structure** of downloaded post/creator **directories**
47-
- Search for creators/artists and posts, and **export the results**
48-
- **Cross-platform** support & **iOS shortcuts** available
42+
- Supports concurrent downloads
43+
- Automatically retries API calls and downloads after failures
44+
- Allows downloading individual posts or **all posts** of a specified artist
45+
- Can **update downloaded** artist directories to the latest state
46+
- Supports customizable **file and directory naming formats** and **directory structures** for downloaded posts/artists
47+
- Enables excluding **specified file formats** or downloading only specified formats
48+
- Allows searching for artists and posts, with options to export results
49+
- Compatible with all platforms, with iOS shortcuts provided
50+
- For support related to _Coomer.su / Coomer.party_, please refer to the documentation: [Coomer](https://ktoolbox.readthedocs.io/latest/coomer/)
4951

5052
## Tutorial
5153

docs/zh/index.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<p style="text-align: center">
1010
KToolBox 是一个用于下载
11-
<a href="https://kemono.su/">Kemono.party / Kemono.su</a>
11+
<a href="https://kemono.su/">Kemono.su / Kemono.party</a>
1212
中作品内容的实用命令行工具
1313
</p>
1414

@@ -40,13 +40,15 @@
4040

4141
## 功能
4242

43-
- 支持 **多线程** 下载(技术上是协程)
44-
- 下载失败后进行 **重试**
43+
- 支持并发下载
44+
- API 调用和下载失败后 **自动重试**
4545
- 支持下载单个作品以及指定的画师的 **所有作品**
4646
-**更新已下载** 的画师目录至最新状态
47-
- 可自定义下载的作品/画师 **目录结构**
48-
- 可搜索画师和作品,并 **导出结果**
49-
- 支持全平台,并提供 **iOS 快捷指令**
47+
- 支持自定义下载的作品/画师 **文件和目录名格式****目录结构**
48+
- 支持排除 **指定格式** 的文件或仅下载指定格式的文件
49+
- 可搜索画师和作品,并导出结果
50+
- 支持全平台,并提供 iOS 快捷指令
51+
- 对于 Coomer.su / Coomer.party 的支持,请查看文档 [Coomer](https://ktoolbox.readthedocs.io/latest/zh/coomer/)
5052

5153
## 使用方法
5254

ktoolbox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
__title__ = "KToolBox"
22
# noinspection SpellCheckingInspection
33
__description__ = "A useful CLI tool for downloading posts in Kemono.party / .su"
4-
__version__ = "v0.10.0"
4+
__version__ = "v0.11.0"

ktoolbox/action/search.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,23 @@ async def search_creator(id: str = None, name: str = None, service: str = None)
1919
:param service: The service for the creator
2020
"""
2121

22-
async def inner(**kwargs):
23-
def filter_func(creator: Creator):
24-
"""Filter creators with attributes"""
25-
for key, value in kwargs.items():
26-
if value is None:
27-
continue
28-
elif creator.__getattribute__(key) != value:
29-
return False
30-
return True
31-
32-
ret = await get_creators()
33-
if not ret:
34-
return ret
35-
creators = ret.data
36-
return ActionRet(data=iter(filter(filter_func, creators)))
22+
def filter_func(creator: Creator):
23+
"""Filter creators with attributes"""
24+
if id is not None and creator.id != id:
25+
return False
26+
if name is not None and name not in creator.name:
27+
return False
28+
if service is not None and creator.service != service:
29+
return False
30+
return True
3731

38-
return await inner(id=id, name=name, service=service)
32+
ret = await get_creators()
33+
if not ret:
34+
base_ret = BaseRet.model_validate(ret.model_dump())
35+
base_ret.data = iter([])
36+
return base_ret
37+
creators = ret.data
38+
return ActionRet(data=iter(filter(filter_func, creators)))
3939

4040

4141
# noinspection PyShadowingBuiltins

ktoolbox/api/base.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class BaseAPI(ABC, Generic[_T]):
6868
path: str = "/"
6969
method: Literal["get", "post"]
7070
extra_validator: Optional[Callable[[str], BaseModel]] = None
71+
client = httpx.AsyncClient(verify=config.ssl_verify)
7172

7273
Response = BaseModel
7374
"""API response model"""
@@ -110,14 +111,13 @@ async def request(cls, path: str = None, **kwargs) -> APIRet[_T]:
110111
url_parts = [config.api.scheme, config.api.netloc, f"{config.api.path}{path}", '', '', '']
111112
url = str(urlunparse(url_parts))
112113
try:
113-
async with httpx.AsyncClient(verify=config.ssl_verify) as client:
114-
res = await client.request(
115-
method=cls.method,
116-
url=url,
117-
timeout=config.api.timeout,
118-
follow_redirects=True,
119-
**kwargs
120-
)
114+
res = await cls.client.request(
115+
method=cls.method,
116+
url=url,
117+
timeout=config.api.timeout,
118+
follow_redirects=True,
119+
**kwargs
120+
)
121121
except Exception as e:
122122
return APIRet(
123123
code=RetCodeEnum.NetWorkError,

ktoolbox/api/model/creator.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import datetime
2+
13
from pydantic import BaseModel
24

35
__all__ = ["Creator"]
@@ -10,11 +12,11 @@ class Creator(BaseModel):
1012
"""The number of times this creator has been favorited"""
1113
id: str
1214
"""The ID of the creator"""
13-
indexed: float
15+
indexed: datetime
1416
"""Timestamp when the creator was indexed, Unix time as integer"""
1517
name: str
1618
"""The name of the creator"""
1719
service: str
1820
"""The service for the creator"""
19-
updated: float
21+
updated: datetime
2022
"""Timestamp when the creator was last updated, Unix time as integer"""

ktoolbox/downloader/downloader.py

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Downloader:
2929
"""
3030
:ivar _save_filename: The actual filename for saving.
3131
"""
32+
client = httpx.AsyncClient(verify=config.ssl_verify)
3233

3334
def __init__(
3435
self,
@@ -172,56 +173,55 @@ async def run(
172173

173174
tqdm_class: Type[std_tqdm] = tqdm_class or tqdm.asyncio.tqdm
174175
async with self._lock:
175-
async with httpx.AsyncClient(verify=config.ssl_verify) as client:
176-
async with client.stream(
177-
method="GET",
178-
url=self._url,
179-
follow_redirects=True,
180-
timeout=config.downloader.timeout
181-
) as res: # type: httpx.Response
182-
if res.status_code != httpx.codes.OK:
183-
return DownloaderRet(
184-
code=RetCodeEnum.GeneralFailure,
185-
message=generate_msg(
186-
"Download failed",
187-
status_code=res.status_code,
188-
filename=save_filepath
189-
)
176+
async with self.client.stream(
177+
method="GET",
178+
url=self._url,
179+
follow_redirects=True,
180+
timeout=config.downloader.timeout
181+
) as res: # type: httpx.Response
182+
if res.status_code != httpx.codes.OK:
183+
return DownloaderRet(
184+
code=RetCodeEnum.GeneralFailure,
185+
message=generate_msg(
186+
"Download failed",
187+
status_code=res.status_code,
188+
filename=save_filepath
190189
)
191-
192-
# Get filename for saving and check if file exists (Second-time duplicate file check)
193-
# Priority order can be referenced from the constructor's documentation
194-
self._save_filename = self._designated_filename or sanitize_filename(
195-
filename_from_headers(res.headers)
196-
) or server_path_filename
197-
save_filepath = self._path / self._save_filename
198-
file_existed, ret_msg = duplicate_file_check(save_filepath, bucket_file_path)
199-
if file_existed:
200-
return DownloaderRet(
201-
code=RetCodeEnum.FileExisted,
202-
message=generate_msg(
203-
ret_msg,
204-
path=save_filepath
205-
)
206-
)
207-
208-
# Download
209-
temp_filepath = Path(f"{save_filepath}.{config.downloader.temp_suffix}")
210-
total_size = int(length_str) if (length_str := res.headers.get("Content-Length")) else None
211-
async with aiofiles.open(str(temp_filepath), "wb", self._buffer_size) as f:
212-
chunk_iterator = res.aiter_bytes(self._chunk_size)
213-
t = tqdm_class(
214-
desc=self._save_filename,
215-
total=total_size,
216-
disable=not progress,
217-
unit="B",
218-
unit_scale=True
190+
)
191+
192+
# Get filename for saving and check if file exists (Second-time duplicate file check)
193+
# Priority order can be referenced from the constructor's documentation
194+
self._save_filename = self._designated_filename or sanitize_filename(
195+
filename_from_headers(res.headers)
196+
) or server_path_filename
197+
save_filepath = self._path / self._save_filename
198+
file_existed, ret_msg = duplicate_file_check(save_filepath, bucket_file_path)
199+
if file_existed:
200+
return DownloaderRet(
201+
code=RetCodeEnum.FileExisted,
202+
message=generate_msg(
203+
ret_msg,
204+
path=save_filepath
219205
)
220-
async for chunk in chunk_iterator:
221-
if self._stop:
222-
raise CancelledError
223-
await f.write(chunk)
224-
t.update(len(chunk)) # Update progress bar
206+
)
207+
208+
# Download
209+
temp_filepath = Path(f"{save_filepath}.{config.downloader.temp_suffix}")
210+
total_size = int(length_str) if (length_str := res.headers.get("Content-Length")) else None
211+
async with aiofiles.open(str(temp_filepath), "wb", self._buffer_size) as f:
212+
chunk_iterator = res.aiter_bytes(self._chunk_size)
213+
t = tqdm_class(
214+
desc=self._save_filename,
215+
total=total_size,
216+
disable=not progress,
217+
unit="B",
218+
unit_scale=True
219+
)
220+
async for chunk in chunk_iterator:
221+
if self._stop:
222+
raise CancelledError
223+
await f.write(chunk)
224+
t.update(len(chunk)) # Update progress bar
225225

226226
# Download finished
227227
if config.downloader.use_bucket:

0 commit comments

Comments
 (0)