kmoe-manga-downloader 1.2.5__tar.gz → 1.2.7__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.2.5 → kmoe_manga_downloader-1.2.7}/PKG-INFO +1 -1
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/_version.py +3 -3
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/DirectDownloader.py +1 -1
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/ReferViaDownloader.py +17 -3
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/download_utils.py +87 -21
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/PKG-INFO +1 -1
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/.github/workflows/release-package.yml +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/.github/workflows/unit-test.yml +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/.gitignore +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/LICENSE +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/README.md +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/assets/kmdr-demo.gif +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/assets/kmdr-log-demo.gif +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/mirror/mirrors.json +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/pyproject.toml +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/setup.cfg +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/bases.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/console.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/constants.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/context.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/defaults.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/error.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/protocol.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/registry.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/session.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/structure.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/utils.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/main.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/CookieAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/LoginAuthenticator.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/utils.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/BaseUrlUpdator.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/ConfigClearer.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/ConfigUnsetter.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/OptionLister.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/OptionSetter.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/option_validate.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/misc.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/BookUrlLister.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/FollowedBookLister.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/utils.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/DefaultVolPicker.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/utils.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/__init__.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/test_async_retry_decorator.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/test_kmdr_config_option.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/test_kmdr_download.py +0 -0
- {kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/test_utils_resolve_volme.py +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 2,
|
|
31
|
+
__version__ = version = '1.2.7'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 2, 7)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g097a6376f'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from functools import partial
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import aiohttp
|
|
4
5
|
from async_lru import alru_cache
|
|
5
6
|
|
|
6
7
|
from kmdr.core import Downloader, VolInfo, DOWNLOADER, BookInfo
|
|
@@ -37,14 +38,18 @@ class ReferViaDownloader(Downloader):
|
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
@alru_cache(maxsize=128)
|
|
40
|
-
@async_retry(
|
|
41
|
+
@async_retry(
|
|
42
|
+
delay=3,
|
|
43
|
+
backoff=1.5,
|
|
44
|
+
retry_on_status={500, 502, 503, 504, 429, 408, 403} # 这里加入 403 重试
|
|
45
|
+
)
|
|
41
46
|
async def fetch_download_url(self, book_id: str, volume_id: str) -> str:
|
|
42
47
|
|
|
43
48
|
async with self._session.get(
|
|
44
49
|
API_ROUTE.GETDOWNURL.format(
|
|
45
50
|
book_id=book_id,
|
|
46
51
|
volume_id=volume_id,
|
|
47
|
-
is_vip=self._profile.is_vip if
|
|
52
|
+
is_vip=self._profile.is_vip if self._use_vip else 0
|
|
48
53
|
)
|
|
49
54
|
) as response:
|
|
50
55
|
response.raise_for_status()
|
|
@@ -52,6 +57,15 @@ class ReferViaDownloader(Downloader):
|
|
|
52
57
|
data = json.loads(data)
|
|
53
58
|
debug("获取下载链接响应数据:", data)
|
|
54
59
|
if (code := data.get('code')) != 200:
|
|
55
|
-
|
|
60
|
+
|
|
61
|
+
if code in {401, 403, 404}:
|
|
62
|
+
raise ResponseError("无法获取下载链接" + data.get('msg', 'Unknown error'), code)
|
|
63
|
+
|
|
64
|
+
raise aiohttp.ClientResponseError(
|
|
65
|
+
response.request_info,
|
|
66
|
+
history=response.history,
|
|
67
|
+
status=code,
|
|
68
|
+
message=data.get('msg', 'Unknown error')
|
|
69
|
+
)
|
|
56
70
|
|
|
57
71
|
return data['url']
|
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import re
|
|
4
4
|
import math
|
|
5
5
|
from typing import Callable, Optional, Union, Awaitable
|
|
6
|
+
import shutil
|
|
6
7
|
|
|
7
8
|
from typing_extensions import deprecated
|
|
8
9
|
|
|
@@ -182,14 +183,13 @@ async def download_file_multipart(
|
|
|
182
183
|
|
|
183
184
|
state_manager: Optional[StateManager] = None
|
|
184
185
|
try:
|
|
185
|
-
current_url = await fetch_url(url)
|
|
186
|
-
|
|
187
186
|
async with _get_head_request_semaphore():
|
|
188
187
|
# 获取文件信息,请求以获取文件大小
|
|
189
188
|
# 控制并发,避免过多并发请求触发服务器限流
|
|
189
|
+
current_url = await fetch_url(url)
|
|
190
190
|
total_size = await _fetch_content_length(session, current_url, headers=headers)
|
|
191
191
|
|
|
192
|
-
chunk_size = chunk_size_mb
|
|
192
|
+
chunk_size = determine_chunk_size(file_size=total_size, base_chunk_mb=chunk_size_mb)
|
|
193
193
|
num_chunks = math.ceil(total_size / chunk_size)
|
|
194
194
|
|
|
195
195
|
tasks = []
|
|
@@ -224,10 +224,13 @@ async def download_file_multipart(
|
|
|
224
224
|
await asyncio.gather(*tasks)
|
|
225
225
|
|
|
226
226
|
assert len(part_paths) == len(part_expected_sizes)
|
|
227
|
-
results = await asyncio.gather(*[
|
|
227
|
+
results = await asyncio.gather(*[
|
|
228
|
+
asyncio.to_thread(_sync_validate_part, part_paths[i], part_expected_sizes[i])
|
|
229
|
+
for i in range(num_chunks)
|
|
230
|
+
])
|
|
228
231
|
if all(results):
|
|
229
232
|
await state_manager.request_status_update(part_id=StateManager.PARENT_ID, status=STATUS.MERGING)
|
|
230
|
-
await
|
|
233
|
+
await asyncio.to_thread(_sync_merge_parts, part_paths, filename_downloading)
|
|
231
234
|
await aio_os.rename(filename_downloading, file_path)
|
|
232
235
|
else:
|
|
233
236
|
# 如果有任何一个分片校验失败,则视为下载失败
|
|
@@ -343,28 +346,91 @@ async def _download_part(
|
|
|
343
346
|
debug("分片", os.path.basename(part_path), "下载失败:", e)
|
|
344
347
|
await state_manager.request_status_update(part_id=start, status=STATUS.PARTIALLY_FAILED)
|
|
345
348
|
|
|
346
|
-
|
|
347
|
-
|
|
349
|
+
def _sync_validate_part(part_path: str, expected_size: int) -> bool:
|
|
350
|
+
"""
|
|
351
|
+
使用同步的 IO 来验证分片文件的完整性。
|
|
352
|
+
|
|
353
|
+
:param part_path: 分片文件路径
|
|
354
|
+
:param expected_size: 预期的文件大小
|
|
355
|
+
:return: 如果文件大小匹配则返回 True,否则返回 False
|
|
356
|
+
:note: 这个函数应该在线程池中运行。
|
|
357
|
+
:usage: await asyncio.to_thread(_sync_validate_part, part_path, expected_size)
|
|
358
|
+
"""
|
|
359
|
+
if not os.path.exists(part_path):
|
|
348
360
|
return False
|
|
349
|
-
actual_size =
|
|
361
|
+
actual_size = os.path.getsize(part_path)
|
|
350
362
|
return actual_size == expected_size
|
|
351
363
|
|
|
352
|
-
|
|
364
|
+
def _sync_merge_parts(part_paths: list[str], final_path: str):
|
|
365
|
+
"""
|
|
366
|
+
使用同步的 IO 来合并文件。
|
|
367
|
+
|
|
368
|
+
:param part_paths: 分片文件路径列表
|
|
369
|
+
:param final_path: 最终合并后的文件路径
|
|
370
|
+
:note: 这个函数应该在线程池中运行。
|
|
371
|
+
:usage: await asyncio.to_thread(_sync_merge_parts, part_paths, final_path)
|
|
372
|
+
"""
|
|
353
373
|
debug("合并分片到最终文件:", final_path)
|
|
354
|
-
|
|
355
|
-
|
|
374
|
+
try:
|
|
375
|
+
with open(final_path, 'wb') as final_file:
|
|
356
376
|
for part_path in part_paths:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if aio_os.path.exists(final_path):
|
|
365
|
-
await aio_os.remove(final_path)
|
|
366
|
-
raise e
|
|
377
|
+
with open(part_path, 'rb') as part_file:
|
|
378
|
+
shutil.copyfileobj(part_file, final_file)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
if os.path.exists(final_path):
|
|
381
|
+
os.remove(final_path)
|
|
382
|
+
raise e
|
|
383
|
+
|
|
367
384
|
|
|
385
|
+
def determine_chunk_size(
|
|
386
|
+
file_size: int,
|
|
387
|
+
base_chunk_mb: int = 10,
|
|
388
|
+
max_chunks_limit: int = 100,
|
|
389
|
+
min_chunk_threshold_factor: float = 0.2
|
|
390
|
+
) -> int:
|
|
391
|
+
"""
|
|
392
|
+
计算合适的分片大小以优化下载性能。
|
|
393
|
+
|
|
394
|
+
TODO: 这个算法可以进一步优化,例如考虑网络状况、服务器限制等因素。10 这个魔法数字也许可以调整。
|
|
395
|
+
需要收集更多的信息:
|
|
396
|
+
- 文件大小的分布
|
|
397
|
+
- 用户的下载速度分布
|
|
398
|
+
- 连接开销的大致值
|
|
399
|
+
|
|
400
|
+
:param file_size: 文件总大小(字节)
|
|
401
|
+
:param base_chunk_mb: 基础分片大小 (MB)
|
|
402
|
+
:param max_chunks_limit: 限制的最大分片数
|
|
403
|
+
:param min_chunk_threshold_factor: 最小分片的体积因子
|
|
404
|
+
:return: 最佳的每个分片的大小(字节)
|
|
405
|
+
"""
|
|
406
|
+
base_chunk = int(base_chunk_mb * 1024 * 1024)
|
|
407
|
+
|
|
408
|
+
if not isinstance(file_size, int) or file_size <= 0:
|
|
409
|
+
# 如果文件大小不正常,返回基础分片大小
|
|
410
|
+
return base_chunk
|
|
411
|
+
|
|
412
|
+
if file_size <= base_chunk * (1 + min_chunk_threshold_factor):
|
|
413
|
+
# 如果文件较小,直接使用单分片下载,避免无谓的分片开销
|
|
414
|
+
# 例如 10.2MB 会被视为单分片下载
|
|
415
|
+
return file_size
|
|
416
|
+
|
|
417
|
+
num_chunks = math.ceil(file_size / base_chunk)
|
|
418
|
+
|
|
419
|
+
if num_chunks > max_chunks_limit:
|
|
420
|
+
# 如果文件太大导致分片数量过多 (2GB -> 200 块)
|
|
421
|
+
# 增加分片大小,将分片数限制在 100
|
|
422
|
+
return int(math.ceil(file_size / max_chunks_limit))
|
|
423
|
+
|
|
424
|
+
# 计算最后一个分片的大小,如果过小则调整分片大小
|
|
425
|
+
# 例如 250.2MB - (10MB * (26 - 1)) = 0.2MB
|
|
426
|
+
last_chunk_size = file_size - (base_chunk * (num_chunks - 1))
|
|
427
|
+
|
|
428
|
+
if last_chunk_size < base_chunk * min_chunk_threshold_factor:
|
|
429
|
+
# 如果最后一个分片过小,则均摊到前面的分片中
|
|
430
|
+
# 例如 250.2 / 25 = 10.008MB
|
|
431
|
+
return int(math.ceil(file_size / (num_chunks - 1)))
|
|
432
|
+
else:
|
|
433
|
+
return base_chunk
|
|
368
434
|
|
|
369
435
|
CHAR_MAPPING = {
|
|
370
436
|
'\\': '\',
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/.github/workflows/release-package.yml
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/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.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/__init__.py
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/misc.py
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/BookUrlLister.py
RENAMED
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/__init__.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
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/test_async_retry_decorator.py
RENAMED
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/test_kmdr_config_option.py
RENAMED
|
File without changes
|
|
File without changes
|
{kmoe_manga_downloader-1.2.5 → kmoe_manga_downloader-1.2.7}/tests/test_utils_resolve_volme.py
RENAMED
|
File without changes
|