changed to interact with standalone image downloader service

This commit is contained in:
garaev kamil 2025-12-06 06:37:33 +03:00
parent 93f12666cd
commit fc0ddf334d
2 changed files with 46 additions and 69 deletions

View file

@ -1,77 +1,35 @@
# anime_etl/images/downloader.py
from __future__ import annotations
import asyncio
import hashlib
import os
from pathlib import Path
from typing import Tuple
from urllib.parse import urlparse
from typing import Final
import httpx
# Корень хранилища картинок внутри контейнера/процесса
MEDIA_ROOT = Path(os.getenv("NYANIMEDB_MEDIA_ROOT", "media")).resolve()
def _guess_ext_from_url(url: str) -> str:
path = urlparse(url).path
_, ext = os.path.splitext(path)
if ext and len(ext) <= 5:
return ext
return ".jpg"
def _build_rel_path_from_hash(h: str, ext: str, subdir: str = "posters") -> Tuple[str, Path]:
"""
Строим путь вида subdir/ab/cd/<hash>.ext по sha1-хешу содержимого.
"""
level1 = h[:2]
level2 = h[2:4]
rel = f"{subdir}/{level1}/{level2}/{h}{ext}"
abs_path = MEDIA_ROOT / rel
return rel, abs_path
async def _fetch_bytes(url: str) -> bytes:
async with httpx.AsyncClient(timeout=20.0) as client:
r = await client.get(url)
r.raise_for_status()
return r.content
IMAGE_SERVICE_URL: Final[str] = os.getenv(
"NYANIMEDB_IMAGE_SERVICE_URL",
"http://127.0.0.1:8000"
)
async def ensure_image_downloaded(url: str, subdir: str = "posters") -> str:
"""
Гарантирует, что картинка по URL лежит в MEDIA_ROOT/subdir в структуре:
subdir/ab/cd/<sha1(content)>.ext
Просит image-service скачать картинку по URL и сохранить её у себя.
Возвращает относительный путь (для записи в БД).
Один и тот же файл (по содержимому) всегда даёт один и тот же путь,
даже если URL меняется.
Возвращает относительный путь (subdir/ab/cd/<sha1>.ext),
который можно писать в images.image_path.
"""
# Скачиваем данные
data = await _fetch_bytes(url)
async with httpx.AsyncClient(timeout=20.0) as client:
resp = await client.post(
f"{IMAGE_SERVICE_URL}/download-by-url",
json={"url": url, "subdir": subdir},
)
resp.raise_for_status()
data = resp.json()
# Хешируем именно содержимое, а не URL
h = hashlib.sha1(data).hexdigest()
ext = _guess_ext_from_url(url)
# ожидаем {"path": "..."}
path = data["path"]
if not isinstance(path, str):
raise RuntimeError(f"Invalid response from image service: {data!r}")
rel, abs_path = _build_rel_path_from_hash(h, ext, subdir=subdir)
# Если файл уже есть (другой процесс/воркер успел сохранить) — просто возвращаем путь
if abs_path.exists():
return rel
abs_path.parent.mkdir(parents=True, exist_ok=True)
# Пишем во временный файл и затем делаем atomic rename
tmp_path = abs_path.with_suffix(abs_path.suffix + ".tmp")
def _write() -> None:
with open(tmp_path, "wb") as f:
f.write(data)
# os.replace атомарно заменит файл, даже если он уже появился
os.replace(tmp_path, abs_path)
await asyncio.to_thread(_write)
return rel
return path