kmoe-manga-downloader 1.1.0__tar.gz → 1.1.2__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.
- {kmoe_manga_downloader-1.1.0/src/kmoe_manga_downloader.egg-info → kmoe_manga_downloader-1.1.2}/PKG-INFO +5 -1
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/README.md +4 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/pyproject.toml +1 -1
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/core/bases.py +18 -8
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/main.py +7 -3
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/downloader/DirectDownloader.py +2 -2
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/downloader/ReferViaDownloader.py +2 -2
- kmoe_manga_downloader-1.1.2/src/kmdr/module/downloader/utils.py +157 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2/src/kmoe_manga_downloader.egg-info}/PKG-INFO +5 -1
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +0 -1
- kmoe_manga_downloader-1.1.0/src/kmdr/module/downloader/utils.py +0 -127
- kmoe_manga_downloader-1.1.0/tests/test_kmdr_login.py +0 -29
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/LICENSE +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/setup.cfg +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/__init__.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/core/__init__.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/core/defaults.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/core/error.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/core/registry.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/core/structure.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/core/utils.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/__init__.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/authenticator/CookieAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/authenticator/LoginAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/authenticator/utils.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/configurer/ConfigClearer.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/configurer/ConfigUnsetter.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/configurer/OptionLister.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/configurer/OptionSetter.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/configurer/option_validate.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/lister/BookUrlLister.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/lister/FollowedBookLister.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/lister/utils.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/picker/DefaultVolPicker.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/picker/utils.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/tests/test_cache_by_kwargs.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/tests/test_kmdr_config_option.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/tests/test_kmdr_download.py +0 -0
- {kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/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.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
|
- **凭证管理**: 命令行登录并持久化会话
|
|
@@ -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')
|
|
@@ -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()
|
|
@@ -17,9 +17,9 @@ class DirectDownloader(Downloader):
|
|
|
17
17
|
|
|
18
18
|
download_file(
|
|
19
19
|
self._session,
|
|
20
|
-
self.construct_download_url(book, volume),
|
|
20
|
+
lambda: self.construct_download_url(book, volume),
|
|
21
21
|
download_path,
|
|
22
|
-
f'[Kmoe][{book.name}][{volume.name}].epub',
|
|
22
|
+
safe_filename(f'[Kmoe][{book.name}][{volume.name}].epub'),
|
|
23
23
|
retry,
|
|
24
24
|
callback=lambda: self._callback(book, volume) if self._callback else None
|
|
25
25
|
)
|
|
@@ -23,9 +23,9 @@ 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_id=book.id, volume_id=volume.id),
|
|
26
|
+
lambda: self.fetch_download_url(book_id=book.id, volume_id=volume.id),
|
|
27
27
|
download_path,
|
|
28
|
-
f'[Kmoe][{book.name}][{volume.name}].epub',
|
|
28
|
+
safe_filename(f'[Kmoe][{book.name}][{volume.name}].epub'),
|
|
29
29
|
retry,
|
|
30
30
|
headers={
|
|
31
31
|
"X-Km-From": "kb_http_down"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from typing import Callable, Optional, Union
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
from functools import wraps
|
|
6
|
+
|
|
7
|
+
from requests import Session, HTTPError
|
|
8
|
+
from requests.exceptions import ChunkedEncodingError
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
BLOCK_SIZE_REDUCTION_FACTOR = 0.75
|
|
13
|
+
MIN_BLOCK_SIZE = 2048
|
|
14
|
+
|
|
15
|
+
def download_file(
|
|
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
|
+
"""
|
|
25
|
+
下载文件
|
|
26
|
+
|
|
27
|
+
:param session: requests.Session 对象
|
|
28
|
+
:param url: 下载链接或者其 Supplier
|
|
29
|
+
:param dest_path: 目标路径
|
|
30
|
+
:param filename: 文件名
|
|
31
|
+
:param retry_times: 重试次数
|
|
32
|
+
:param headers: 请求头
|
|
33
|
+
:param callback: 下载完成后的回调函数
|
|
34
|
+
"""
|
|
35
|
+
if headers is None:
|
|
36
|
+
headers = {}
|
|
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)
|
|
40
|
+
|
|
41
|
+
if not os.path.exists(dest_path):
|
|
42
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
if os.path.exists(file_path):
|
|
45
|
+
tqdm.write(f"{filename} 已经存在")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
block_size = 8192
|
|
49
|
+
|
|
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
|
+
)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
while attempts_left > 0:
|
|
60
|
+
attempts_left -= 1
|
|
61
|
+
|
|
62
|
+
resume_from = os.path.getsize(tmp_file_path) if os.path.exists(tmp_file_path) else 0
|
|
63
|
+
|
|
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
|
+
|
|
110
|
+
|
|
111
|
+
def safe_filename(name: str) -> str:
|
|
112
|
+
"""
|
|
113
|
+
替换非法文件名字符为下划线
|
|
114
|
+
"""
|
|
115
|
+
return re.sub(r'[\\/:*?"<>|]', '_', name)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
function_cache = {}
|
|
119
|
+
|
|
120
|
+
def cached_by_kwargs(func):
|
|
121
|
+
"""
|
|
122
|
+
根据关键字参数缓存函数结果的装饰器。
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> @kwargs_cached
|
|
126
|
+
>>> def add(a, b, c):
|
|
127
|
+
>>> return a + b + c
|
|
128
|
+
>>> result1 = add(1, 2, c=3) # Calls the function
|
|
129
|
+
>>> result2 = add(3, 2, c=3) # Uses cached result
|
|
130
|
+
>>> assert result1 == result2 # Both results are the same
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
global function_cache
|
|
134
|
+
if func not in function_cache:
|
|
135
|
+
function_cache[func] = {}
|
|
136
|
+
|
|
137
|
+
@wraps(func)
|
|
138
|
+
def wrapper(*args, **kwargs):
|
|
139
|
+
if not kwargs:
|
|
140
|
+
return func(*args, **kwargs)
|
|
141
|
+
|
|
142
|
+
key = frozenset(kwargs.items())
|
|
143
|
+
|
|
144
|
+
if key not in function_cache[func]:
|
|
145
|
+
function_cache[func][key] = func(*args, **kwargs)
|
|
146
|
+
return function_cache[func][key]
|
|
147
|
+
|
|
148
|
+
return wrapper
|
|
149
|
+
|
|
150
|
+
def clear_cache(func):
|
|
151
|
+
assert hasattr(func, "__wrapped__"), "Function is not wrapped"
|
|
152
|
+
global function_cache
|
|
153
|
+
|
|
154
|
+
wrapped = func.__wrapped__
|
|
155
|
+
|
|
156
|
+
if wrapped in function_cache:
|
|
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,127 +0,0 @@
|
|
|
1
|
-
from typing import Callable, Optional
|
|
2
|
-
import os
|
|
3
|
-
import time
|
|
4
|
-
from functools import wraps
|
|
5
|
-
|
|
6
|
-
from requests import Session, HTTPError
|
|
7
|
-
from requests.exceptions import ChunkedEncodingError
|
|
8
|
-
from tqdm import tqdm
|
|
9
|
-
import re
|
|
10
|
-
|
|
11
|
-
BLOCK_SIZE_REDUCTION_FACTOR = 0.75
|
|
12
|
-
MIN_BLOCK_SIZE = 2048
|
|
13
|
-
|
|
14
|
-
def download_file(
|
|
15
|
-
session: Session,
|
|
16
|
-
url: str,
|
|
17
|
-
dest_path: str,
|
|
18
|
-
filename: str,
|
|
19
|
-
retry_times: int = 0,
|
|
20
|
-
headers: Optional[dict] = None,
|
|
21
|
-
callback: Optional[Callable] = None,
|
|
22
|
-
block_size: int = 8192
|
|
23
|
-
):
|
|
24
|
-
if headers is None:
|
|
25
|
-
headers = {}
|
|
26
|
-
filename_downloading = f'{filename}.downloading'
|
|
27
|
-
|
|
28
|
-
file_path = f'{dest_path}/{filename}'
|
|
29
|
-
tmp_file_path = f'{dest_path}/{filename_downloading}'
|
|
30
|
-
|
|
31
|
-
if not os.path.exists(dest_path):
|
|
32
|
-
os.makedirs(dest_path, exist_ok=True)
|
|
33
|
-
|
|
34
|
-
if os.path.exists(file_path):
|
|
35
|
-
tqdm.write(f"{filename} already exists.")
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
resume_from = 0
|
|
39
|
-
total_size_in_bytes = 0
|
|
40
|
-
|
|
41
|
-
if os.path.exists(tmp_file_path):
|
|
42
|
-
resume_from = os.path.getsize(tmp_file_path)
|
|
43
|
-
|
|
44
|
-
if resume_from:
|
|
45
|
-
headers['Range'] = f'bytes={resume_from}-'
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
with session.get(url = url, stream=True, headers=headers) as r:
|
|
49
|
-
r.raise_for_status()
|
|
50
|
-
|
|
51
|
-
total_size_in_bytes = int(r.headers.get('content-length', 0)) + resume_from
|
|
52
|
-
|
|
53
|
-
with open(tmp_file_path, 'ab') as f:
|
|
54
|
-
with tqdm(total=total_size_in_bytes, unit='B', unit_scale=True, desc=f'{filename}', initial=resume_from) as progress_bar:
|
|
55
|
-
for chunk in r.iter_content(chunk_size=block_size):
|
|
56
|
-
if chunk:
|
|
57
|
-
f.write(chunk)
|
|
58
|
-
progress_bar.update(len(chunk))
|
|
59
|
-
|
|
60
|
-
if (os.path.getsize(tmp_file_path) == total_size_in_bytes):
|
|
61
|
-
os.rename(tmp_file_path, file_path)
|
|
62
|
-
|
|
63
|
-
if callback:
|
|
64
|
-
callback()
|
|
65
|
-
except Exception as e:
|
|
66
|
-
prefix = f"{type(e).__name__} occurred while downloading {filename}. "
|
|
67
|
-
|
|
68
|
-
new_block_size = block_size
|
|
69
|
-
if isinstance(e, ChunkedEncodingError):
|
|
70
|
-
new_block_size = max(int(block_size * BLOCK_SIZE_REDUCTION_FACTOR), MIN_BLOCK_SIZE)
|
|
71
|
-
|
|
72
|
-
if retry_times > 0:
|
|
73
|
-
# 重试下载
|
|
74
|
-
tqdm.write(f"{prefix} Retry after 3 seconds...")
|
|
75
|
-
time.sleep(3) # 等待3秒后重试,避免触发限流
|
|
76
|
-
download_file(session, url, dest_path, filename, retry_times - 1, headers, callback, new_block_size)
|
|
77
|
-
else:
|
|
78
|
-
tqdm.write(f"{prefix} Meet max retry times, download failed.")
|
|
79
|
-
raise e
|
|
80
|
-
|
|
81
|
-
def safe_filename(name: str) -> str:
|
|
82
|
-
"""
|
|
83
|
-
替换非法文件名字符为下划线
|
|
84
|
-
"""
|
|
85
|
-
return re.sub(r'[\\/:*?"<>|]', '_', name)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function_cache = {}
|
|
89
|
-
|
|
90
|
-
def cached_by_kwargs(func):
|
|
91
|
-
"""
|
|
92
|
-
根据关键字参数缓存函数结果的装饰器。
|
|
93
|
-
|
|
94
|
-
Example:
|
|
95
|
-
>>> @kwargs_cached
|
|
96
|
-
>>> def add(a, b, c):
|
|
97
|
-
>>> return a + b + c
|
|
98
|
-
>>> result1 = add(1, 2, c=3) # Calls the function
|
|
99
|
-
>>> result2 = add(3, 2, c=3) # Uses cached result
|
|
100
|
-
>>> assert result1 == result2 # Both results are the same
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
global function_cache
|
|
104
|
-
if func not in function_cache:
|
|
105
|
-
function_cache[func] = {}
|
|
106
|
-
|
|
107
|
-
@wraps(func)
|
|
108
|
-
def wrapper(*args, **kwargs):
|
|
109
|
-
if not kwargs:
|
|
110
|
-
return func(*args, **kwargs)
|
|
111
|
-
|
|
112
|
-
key = frozenset(kwargs.items())
|
|
113
|
-
|
|
114
|
-
if key not in function_cache[func]:
|
|
115
|
-
function_cache[func][key] = func(*args, **kwargs)
|
|
116
|
-
return function_cache[func][key]
|
|
117
|
-
|
|
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,29 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
import unittest
|
|
4
|
-
from argparse import Namespace
|
|
5
|
-
|
|
6
|
-
from kmdr.core.utils import clear_session_context
|
|
7
|
-
from kmdr.main import main as kmdr_main
|
|
8
|
-
|
|
9
|
-
KMOE_USERNAME = os.environ.get('KMOE_USERNAME')
|
|
10
|
-
KMOE_PASSWORD = os.environ.get('KMOE_PASSWORD')
|
|
11
|
-
|
|
12
|
-
@unittest.skipUnless(KMOE_USERNAME and KMOE_PASSWORD, "KMOE_USERNAME and KMOE_PASSWORD must be set in environment variables")
|
|
13
|
-
class TestKmdrLogin(unittest.TestCase):
|
|
14
|
-
|
|
15
|
-
@classmethod
|
|
16
|
-
def tearDownClass(cls):
|
|
17
|
-
clear_session_context()
|
|
18
|
-
|
|
19
|
-
def test_login_with_username_and_password(self):
|
|
20
|
-
assert KMOE_USERNAME and KMOE_PASSWORD, "KMOE_USERNAME and KMOE_PASSWORD must be set in environment variables"
|
|
21
|
-
|
|
22
|
-
kmdr_main(
|
|
23
|
-
Namespace(
|
|
24
|
-
command='login',
|
|
25
|
-
username=KMOE_USERNAME,
|
|
26
|
-
password=KMOE_PASSWORD,
|
|
27
|
-
show_quota=False
|
|
28
|
-
)
|
|
29
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/authenticator/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/src/kmdr/module/lister/BookUrlLister.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/tests/test_kmdr_config_option.py
RENAMED
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.1.0 → kmoe_manga_downloader-1.1.2}/tests/test_utils_resolve_volme.py
RENAMED
|
File without changes
|