kmoe-manga-downloader 1.1.1__py3-none-any.whl → 1.1.2__py3-none-any.whl
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.
- kmdr/core/bases.py +18 -8
- kmdr/main.py +7 -3
- kmdr/module/downloader/utils.py +71 -56
- {kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/METADATA +5 -1
- {kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/RECORD +9 -9
- {kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/WHEEL +0 -0
- {kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/entry_points.txt +0 -0
- {kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/top_level.txt +0 -0
kmdr/core/bases.py
CHANGED
|
@@ -107,16 +107,26 @@ class Downloader(SessionContext, UserProfileContext):
|
|
|
107
107
|
def _download(self, book: BookInfo, volume: VolInfo, retry: int): ...
|
|
108
108
|
|
|
109
109
|
def _download_with_multiple_workers(self, book: BookInfo, volumes: list[VolInfo], retry: int):
|
|
110
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
from concurrent.futures import ThreadPoolExecutor, wait, FIRST_EXCEPTION
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
max_workers = min(self._num_workers, len(volumes))
|
|
114
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
115
|
+
futures = [
|
|
116
|
+
executor.submit(self._download, book, volume, retry)
|
|
117
|
+
for volume in volumes
|
|
118
|
+
]
|
|
119
|
+
wait(futures, return_when=FIRST_EXCEPTION)
|
|
118
120
|
for future in futures:
|
|
119
121
|
future.result()
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
print("\n操作已取消(KeyboardInterrupt)")
|
|
124
|
+
try:
|
|
125
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
126
|
+
except NameError:
|
|
127
|
+
pass
|
|
128
|
+
finally:
|
|
129
|
+
exit(130)
|
|
120
130
|
|
|
121
131
|
AUTHENTICATOR = Registry[Authenticator]('Authenticator')
|
|
122
132
|
LISTERS = Registry[Lister]('Lister')
|
kmdr/main.py
CHANGED
|
@@ -28,9 +28,13 @@ def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPL
|
|
|
28
28
|
fallback()
|
|
29
29
|
|
|
30
30
|
def entry_point():
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
try:
|
|
32
|
+
parser = argument_parser()
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
main(args, lambda: parser.print_help())
|
|
35
|
+
except KeyboardInterrupt:
|
|
36
|
+
print("\n操作已取消(KeyboardInterrupt)")
|
|
37
|
+
exit(130)
|
|
34
38
|
|
|
35
39
|
if __name__ == '__main__':
|
|
36
40
|
entry_point()
|
kmdr/module/downloader/utils.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Callable, Optional, Union
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
|
+
import threading
|
|
4
5
|
from functools import wraps
|
|
5
6
|
|
|
6
7
|
from requests import Session, HTTPError
|
|
@@ -12,15 +13,14 @@ BLOCK_SIZE_REDUCTION_FACTOR = 0.75
|
|
|
12
13
|
MIN_BLOCK_SIZE = 2048
|
|
13
14
|
|
|
14
15
|
def download_file(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
):
|
|
16
|
+
session: Session,
|
|
17
|
+
url: Union[str, Callable[[], str]],
|
|
18
|
+
dest_path: str,
|
|
19
|
+
filename: str,
|
|
20
|
+
retry_times: int = 3,
|
|
21
|
+
headers: Optional[dict] = None,
|
|
22
|
+
callback: Optional[Callable] = None,
|
|
23
|
+
):
|
|
24
24
|
"""
|
|
25
25
|
下载文件
|
|
26
26
|
|
|
@@ -31,67 +31,82 @@ def download_file(
|
|
|
31
31
|
:param retry_times: 重试次数
|
|
32
32
|
:param headers: 请求头
|
|
33
33
|
:param callback: 下载完成后的回调函数
|
|
34
|
-
:param block_size: 块大小
|
|
35
34
|
"""
|
|
36
35
|
if headers is None:
|
|
37
36
|
headers = {}
|
|
38
37
|
filename_downloading = f'{filename}.downloading'
|
|
38
|
+
file_path = os.path.join(dest_path, filename)
|
|
39
|
+
tmp_file_path = os.path.join(dest_path, filename_downloading)
|
|
39
40
|
|
|
40
|
-
file_path = f'{dest_path}/{filename}'
|
|
41
|
-
tmp_file_path = f'{dest_path}/{filename_downloading}'
|
|
42
|
-
|
|
43
41
|
if not os.path.exists(dest_path):
|
|
44
42
|
os.makedirs(dest_path, exist_ok=True)
|
|
45
|
-
|
|
43
|
+
|
|
46
44
|
if os.path.exists(file_path):
|
|
47
|
-
tqdm.write(f"{filename}
|
|
45
|
+
tqdm.write(f"{filename} 已经存在")
|
|
48
46
|
return
|
|
49
|
-
|
|
50
|
-
if callable(url):
|
|
51
|
-
url = url()
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
total_size_in_bytes = 0
|
|
48
|
+
block_size = 8192
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
attempts_left = retry_times + 1
|
|
51
|
+
progress_bar = tqdm(
|
|
52
|
+
total=0, unit='B', unit_scale=True,
|
|
53
|
+
desc=f'{filename} (连接中...)',
|
|
54
|
+
leave=False,
|
|
55
|
+
dynamic_ncols=True
|
|
56
|
+
)
|
|
61
57
|
|
|
62
58
|
try:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
total_size_in_bytes = int(r.headers.get('content-length', 0)) + resume_from
|
|
59
|
+
while attempts_left > 0:
|
|
60
|
+
attempts_left -= 1
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
with tqdm(total=total_size_in_bytes, unit='B', unit_scale=True, desc=f'{filename}', initial=resume_from) as progress_bar:
|
|
70
|
-
for chunk in r.iter_content(chunk_size=block_size):
|
|
71
|
-
if chunk:
|
|
72
|
-
f.write(chunk)
|
|
73
|
-
progress_bar.update(len(chunk))
|
|
62
|
+
resume_from = os.path.getsize(tmp_file_path) if os.path.exists(tmp_file_path) else 0
|
|
74
63
|
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
64
|
+
if resume_from:
|
|
65
|
+
headers['Range'] = f'bytes={resume_from}-'
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
current_url = url() if callable(url) else url
|
|
69
|
+
with session.get(url=current_url, stream=True, headers=headers) as r:
|
|
70
|
+
r.raise_for_status()
|
|
71
|
+
|
|
72
|
+
total_size_in_bytes = int(r.headers.get('content-length', 0)) + resume_from
|
|
73
|
+
|
|
74
|
+
progress_bar.set_description(f'{filename}')
|
|
75
|
+
progress_bar.total = total_size_in_bytes
|
|
76
|
+
progress_bar.n = resume_from
|
|
77
|
+
progress_bar.refresh()
|
|
78
|
+
|
|
79
|
+
with open(tmp_file_path, 'ab') as f:
|
|
80
|
+
for chunk in r.iter_content(chunk_size=block_size):
|
|
81
|
+
if chunk:
|
|
82
|
+
f.write(chunk)
|
|
83
|
+
progress_bar.update(len(chunk))
|
|
84
|
+
|
|
85
|
+
if os.path.getsize(tmp_file_path) >= total_size_in_bytes:
|
|
86
|
+
os.rename(tmp_file_path, file_path)
|
|
87
|
+
if callback:
|
|
88
|
+
callback()
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
if attempts_left > 0:
|
|
93
|
+
progress_bar.set_description(f'{filename} (重试中...)')
|
|
94
|
+
if isinstance(e, ChunkedEncodingError):
|
|
95
|
+
new_block_size = max(int(block_size * BLOCK_SIZE_REDUCTION_FACTOR), MIN_BLOCK_SIZE)
|
|
96
|
+
if new_block_size < block_size:
|
|
97
|
+
block_size = new_block_size
|
|
98
|
+
|
|
99
|
+
# 避免限流
|
|
100
|
+
time.sleep(3)
|
|
101
|
+
else:
|
|
102
|
+
raise e
|
|
103
|
+
finally:
|
|
104
|
+
if progress_bar.total and progress_bar.n >= progress_bar.total:
|
|
105
|
+
tqdm.write(f"{filename} 下载完成")
|
|
106
|
+
elif progress_bar.total is not None:
|
|
107
|
+
tqdm.write(f"{filename} 下载失败")
|
|
108
|
+
progress_bar.close()
|
|
109
|
+
|
|
95
110
|
|
|
96
111
|
def safe_filename(name: str) -> str:
|
|
97
112
|
"""
|
|
@@ -139,4 +154,4 @@ def clear_cache(func):
|
|
|
139
154
|
wrapped = func.__wrapped__
|
|
140
155
|
|
|
141
156
|
if wrapped in function_cache:
|
|
142
|
-
function_cache[wrapped] = {}
|
|
157
|
+
function_cache[wrapped] = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kmoe-manga-downloader
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: A CLI-downloader for site @kox.moe.
|
|
5
5
|
Author-email: Chris Zheng <chrisis58@outlook.com>
|
|
6
6
|
License: MIT License
|
|
@@ -47,6 +47,10 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
`kmdr (Kmoe Manga Downloader)` 是一个 Python 应用,用于从 [Kmoe](https://kox.moe/) 网站下载漫画。它支持在终端环境下的登录、下载指定书籍及其卷,并支持回调脚本执行。
|
|
49
49
|
|
|
50
|
+
<p align="center">
|
|
51
|
+
<img src="assets/kmdr-demo.gif" alt="kmdr 使用演示" width="720">
|
|
52
|
+
</p>
|
|
53
|
+
|
|
50
54
|
## ✨功能特性
|
|
51
55
|
|
|
52
56
|
- **凭证管理**: 命令行登录并持久化会话
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
kmdr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
kmdr/main.py,sha256=
|
|
2
|
+
kmdr/main.py,sha256=F_zXjRr2QIBdNrqvCqpSNkFSxVY6ySfm-mk-Gg9qy7g,1018
|
|
3
3
|
kmdr/core/__init__.py,sha256=WT4MHsww1KOcIlf3z_AK5-4mXNFNBoCE1REQ0dvf9DE,281
|
|
4
|
-
kmdr/core/bases.py,sha256=
|
|
4
|
+
kmdr/core/bases.py,sha256=uDLamjuRfwcVhbGI99aSUZBi-fN32OM2fl91t0bbVog,4484
|
|
5
5
|
kmdr/core/defaults.py,sha256=9vVN0xywLOHm3tcNf1GGJ9RTXB38YLTvBMqxmp0jEAE,6582
|
|
6
6
|
kmdr/core/error.py,sha256=ZXsiD-Ihe0zo5wdNgd7Rg-ISZifsiU5C6FeHesurfy4,597
|
|
7
7
|
kmdr/core/registry.py,sha256=KYjvww5WRuUg5SeIeWZb96803-kgrTIKYvFTj_7bPfo,5560
|
|
@@ -18,16 +18,16 @@ kmdr/module/configurer/OptionSetter.py,sha256=9MIkWZb-aFUTqpVCVrnri3JZTE0zO3CIYu
|
|
|
18
18
|
kmdr/module/configurer/option_validate.py,sha256=DdtUG0E-LgjjHQgPBSyRZ_ID40QRRFSS_FEQHdK94hM,3327
|
|
19
19
|
kmdr/module/downloader/DirectDownloader.py,sha256=5ny3or9fj2rb3qGxEWBmiEs5vlIfvCSGFiMoFISs_J8,1060
|
|
20
20
|
kmdr/module/downloader/ReferViaDownloader.py,sha256=_x-hniGmIqc_7OFY-kQUrOIH0TwDcf-_NCsmVql2pp0,1694
|
|
21
|
-
kmdr/module/downloader/utils.py,sha256=
|
|
21
|
+
kmdr/module/downloader/utils.py,sha256=qGp_c0coVENDKPNUpA7uZoHoDxBIj5jQHlw8qf4NRpQ,4814
|
|
22
22
|
kmdr/module/lister/BookUrlLister.py,sha256=Mq7EBXWSKd-6cygfkTjmOvgcUUaJI4NMQiaEIv9VDSk,470
|
|
23
23
|
kmdr/module/lister/FollowedBookLister.py,sha256=_fShmCAsZQqboeuRtLBjo6d9CkGpat8xNKC3COtKllc,1537
|
|
24
24
|
kmdr/module/lister/utils.py,sha256=0EHQIA05EZ1V0_SfkJ8demjzMRmFh9Q5qPvE6jvBqSU,2560
|
|
25
25
|
kmdr/module/picker/ArgsFilterPicker.py,sha256=f3suMPPeFEtB3u7aUY63k_sGIOP196R-VviQ9RfDBTA,1756
|
|
26
26
|
kmdr/module/picker/DefaultVolPicker.py,sha256=kpG5dvv1UKMhSA01VKGNB59zM5uszspyUVfRlL9aQA0,750
|
|
27
27
|
kmdr/module/picker/utils.py,sha256=lpxM7q9BJeupFQy8glBrHu1o4E38dk7iLexzKytAE6g,1222
|
|
28
|
-
kmoe_manga_downloader-1.1.
|
|
29
|
-
kmoe_manga_downloader-1.1.
|
|
30
|
-
kmoe_manga_downloader-1.1.
|
|
31
|
-
kmoe_manga_downloader-1.1.
|
|
32
|
-
kmoe_manga_downloader-1.1.
|
|
33
|
-
kmoe_manga_downloader-1.1.
|
|
28
|
+
kmoe_manga_downloader-1.1.2.dist-info/licenses/LICENSE,sha256=bKQlsXu8mAYKRZyoZKOEqMcCc8YjT5Q3Hgr21e0yU4E,1068
|
|
29
|
+
kmoe_manga_downloader-1.1.2.dist-info/METADATA,sha256=vPMrYaRtOC0rTenqPS2QWj5Inq1X1gYlMbSZ2HANAPQ,7869
|
|
30
|
+
kmoe_manga_downloader-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
kmoe_manga_downloader-1.1.2.dist-info/entry_points.txt,sha256=DGMytQAhx4uNuKQL7BPkiWESHLXkH-2KSEqwHdygNPA,47
|
|
32
|
+
kmoe_manga_downloader-1.1.2.dist-info/top_level.txt,sha256=e0qxOgWp0tl3GLpmXGjZv3--q_TLoJ7GztM48Ov27wM,5
|
|
33
|
+
kmoe_manga_downloader-1.1.2.dist-info/RECORD,,
|
|
File without changes
|
{kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.1.1.dist-info → kmoe_manga_downloader-1.1.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|