"""基础图片下载器"""
import asyncio
from pathlib import Path
import httpx
from kobo_manga.config import AppConfig
from kobo_manga.models import PageImage
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
}
async def download_image(
client: httpx.AsyncClient,
image: PageImage,
output_dir: Path,
referer: str,
max_retry: int = 3,
) -> PageImage:
"""下载单张图片,返回更新了 local_path 的 PageImage。"""
# 从 URL 推断扩展名
ext = Path(image.url.split("?")[0]).suffix or ".jpg"
filename = f"{image.page_number:03d}{ext}"
filepath = output_dir / filename
if filepath.exists():
image.local_path = str(filepath)
return image
for attempt in range(max_retry):
try:
resp = await client.get(
image.url,
headers={"Referer": referer},
)
resp.raise_for_status()
filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.write_bytes(resp.content)
image.local_path = str(filepath)
return image
except (httpx.HTTPError, OSError) as e:
if attempt == max_retry - 1:
raise RuntimeError(
f"下载失败 (第{image.page_number}页): {e}"
) from e
await asyncio.sleep(1.0 * (attempt + 1))
return image
async def download_chapter(
images: list[PageImage],
output_dir: Path,
referer: str,
config: AppConfig | None = None,
) -> list[PageImage]:
"""下载一个章节的所有图片。
Args:
images: 图片列表(含 URL)
output_dir: 输出目录
referer: Referer header
config: 应用配置(用于并发数、重试数等)
Returns:
更新了 local_path 的图片列表
"""
concurrent = 3
max_retry = 3
delay = 1.0
if config:
concurrent = config.download.concurrent
max_retry = config.download.retry
delay = config.download.delay
output_dir.mkdir(parents=True, exist_ok=True)
semaphore = asyncio.Semaphore(concurrent)
async with httpx.AsyncClient(
headers=HEADERS,
follow_redirects=True,
timeout=60.0,
) as client:
async def _download_with_limit(img: PageImage) -> PageImage:
async with semaphore:
result = await download_image(
client, img, output_dir, referer, max_retry
)
await asyncio.sleep(delay)
return result
tasks = [_download_with_limit(img) for img in images]
results = await asyncio.gather(*tasks, return_exceptions=True)
downloaded = []
errors = []
for r in results:
if isinstance(r, Exception):
errors.append(r)
else:
downloaded.append(r)
if errors:
print(f" ⚠ {len(errors)} 张图片下载失败:")
for e in errors:
print(f" - {e}")
return downloaded