kmoe-manga-downloader 1.2.6__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.
Files changed (64) hide show
  1. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/PKG-INFO +1 -1
  2. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/_version.py +3 -3
  3. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/ReferViaDownloader.py +5 -1
  4. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/download_utils.py +87 -21
  5. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/PKG-INFO +1 -1
  6. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  7. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  8. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/.github/workflows/release-package.yml +0 -0
  9. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/.github/workflows/unit-test.yml +0 -0
  10. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/.gitignore +0 -0
  11. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/LICENSE +0 -0
  12. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/README.md +0 -0
  13. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/assets/kmdr-demo.gif +0 -0
  14. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/assets/kmdr-log-demo.gif +0 -0
  15. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/mirror/mirrors.json +0 -0
  16. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/pyproject.toml +0 -0
  17. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/setup.cfg +0 -0
  18. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/__init__.py +0 -0
  19. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/__init__.py +0 -0
  20. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/bases.py +0 -0
  21. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/console.py +0 -0
  22. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/constants.py +0 -0
  23. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/context.py +0 -0
  24. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/defaults.py +0 -0
  25. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/error.py +0 -0
  26. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/protocol.py +0 -0
  27. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/registry.py +0 -0
  28. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/session.py +0 -0
  29. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/structure.py +0 -0
  30. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/core/utils.py +0 -0
  31. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/main.py +0 -0
  32. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/__init__.py +0 -0
  33. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/CookieAuthenticator.py +0 -0
  34. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/LoginAuthenticator.py +0 -0
  35. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/__init__.py +0 -0
  36. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/authenticator/utils.py +0 -0
  37. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/BaseUrlUpdator.py +0 -0
  38. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/ConfigClearer.py +0 -0
  39. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/ConfigUnsetter.py +0 -0
  40. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/OptionLister.py +0 -0
  41. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/OptionSetter.py +0 -0
  42. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/__init__.py +0 -0
  43. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/configurer/option_validate.py +0 -0
  44. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/DirectDownloader.py +0 -0
  45. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/__init__.py +0 -0
  46. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/downloader/misc.py +0 -0
  47. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/BookUrlLister.py +0 -0
  48. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/FollowedBookLister.py +0 -0
  49. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/__init__.py +0 -0
  50. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/lister/utils.py +0 -0
  51. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/ArgsFilterPicker.py +0 -0
  52. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/DefaultVolPicker.py +0 -0
  53. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/__init__.py +0 -0
  54. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmdr/module/picker/utils.py +0 -0
  55. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/SOURCES.txt +0 -0
  56. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/dependency_links.txt +0 -0
  57. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/entry_points.txt +0 -0
  58. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/requires.txt +0 -0
  59. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/src/kmoe_manga_downloader.egg-info/top_level.txt +0 -0
  60. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/tests/__init__.py +0 -0
  61. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/tests/test_async_retry_decorator.py +0 -0
  62. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/tests/test_kmdr_config_option.py +0 -0
  63. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/tests/test_kmdr_download.py +0 -0
  64. {kmoe_manga_downloader-1.2.6 → kmoe_manga_downloader-1.2.7}/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.2.6
3
+ Version: 1.2.7
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -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.6'
32
- __version_tuple__ = version_tuple = (1, 2, 6)
31
+ __version__ = version = '1.2.7'
32
+ __version_tuple__ = version_tuple = (1, 2, 7)
33
33
 
34
- __commit_id__ = commit_id = 'ge80e6fa7b'
34
+ __commit_id__ = commit_id = 'g097a6376f'
@@ -38,7 +38,11 @@ class ReferViaDownloader(Downloader):
38
38
  )
39
39
 
40
40
  @alru_cache(maxsize=128)
41
- @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
+ )
42
46
  async def fetch_download_url(self, book_id: str, volume_id: str) -> str:
43
47
 
44
48
  async with self._session.get(
@@ -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 * 1024 * 1024
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(*[_validate_part(part_paths[i], part_expected_sizes[i]) for i in range(num_chunks)])
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 _merge_parts(part_paths, filename_downloading)
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
- async def _validate_part(part_path: str, expected_size: int) -> bool:
347
- if not await aio_os.path.exists(part_path):
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 = await aio_os.path.getsize(part_path)
361
+ actual_size = os.path.getsize(part_path)
350
362
  return actual_size == expected_size
351
363
 
352
- async def _merge_parts(part_paths: list[str], final_path: str):
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
- async with aiofiles.open(final_path, 'wb') as final_file:
355
- try:
374
+ try:
375
+ with open(final_path, 'wb') as final_file:
356
376
  for part_path in part_paths:
357
- async with aiofiles.open(part_path, 'rb') as part_file:
358
- while True:
359
- chunk = await part_file.read(8192)
360
- if not chunk:
361
- break
362
- await final_file.write(chunk)
363
- except Exception as e:
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
  '\\': '\',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kmoe-manga-downloader
3
- Version: 1.2.6
3
+ Version: 1.2.7
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License