gendesign/backend/app/services/photos/thumbs.py
Light1YT 249182b678
All checks were successful
CI / frontend-tests (pull_request) Successful in 1m2s
CI / openapi-codegen-check (push) Successful in 2m13s
CI / openapi-codegen-check (pull_request) Successful in 1m45s
CI / backend-tests (push) Successful in 9m29s
CI / backend-tests (pull_request) Successful in 9m29s
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (push) Successful in 1m7s
fix(week-review): дожим ревью — #1569, #1590, #1642 + feat #801 MSW-preview
Финиш-волна по cross-file/partial остаткам аудита + a11y-харнесс.

Полностью (4): #1569 alembic регистрирует все ORM-модели (+убран фантомный Parcel) ·
#1590 «Индекс дефицита» больше не лжёт на fallback-горизонте (report_pdf+report_md) ·
#1642 thumbs mtime-freshness (нет устаревшей миниатюры) ·
#801 /__preview/estimate mock-render для axe/lighthouse (env-gated bypass).

Частично (остаток cross-file/домен, открыты): #1593 #1635 #1640.
needs-leha (открыт): #1650 (нужна новая миграция вьюхи supply-слоёв).

Verify: tsc --noEmit 0; py_compile OK.

Closes #1569
Closes #1590
Closes #1642
Closes #801
2026-06-17 11:24:34 +05:00

67 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Thumbnail generation for DOM.РФ photos.
Strategy: original PNG/JPG ~1-3 MB downloaded by scraper to
data/raw/domrf_photos/<obj_id>/<file_id>.{png,jpg}. We additionally write a
WebP cover-thumbnail at 320×240 (~10-30 KB) next to it, suffix `_thumb.webp`.
The frontend grid uses /api/v1/photos/<obj_id>/<file_id>?size=thumb. Lightbox
opens the original DOM.РФ URL (we don't need to mirror originals).
"""
from __future__ import annotations
import logging
from pathlib import Path
from PIL import Image, ImageOps
logger = logging.getLogger(__name__)
THUMB_SIZE: tuple[int, int] = (320, 240)
THUMB_QUALITY: int = 70
THUMB_SUFFIX: str = "_thumb.webp"
def thumb_path_for(src: Path) -> Path:
"""Return canonical thumbnail path next to the source file."""
return src.with_name(src.stem + THUMB_SUFFIX)
def make_thumbnail(
src: Path,
*,
size: tuple[int, int] = THUMB_SIZE,
quality: int = THUMB_QUALITY,
overwrite: bool = False,
) -> Path | None:
"""Generate a WebP cover-thumbnail next to src. Returns thumb path on success.
Skips if thumb already exists, is not stale (dst mtime >= src mtime) and
overwrite=False. A stale thumb (src rewritten after thumb, e.g. original
re-downloaded on size mismatch) is regenerated. Returns None on any error
(logged) so callers can keep going with the next file.
"""
if not src.exists():
return None
dst = thumb_path_for(src)
if dst.exists() and not overwrite:
# Existence alone is not freshness: if src was rewritten after dst was
# generated, the thumb is stale and must be regenerated. Only skip when
# the thumb is at least as new as the source.
try:
if dst.stat().st_mtime >= src.stat().st_mtime:
return dst
except OSError as e:
# stat failed (race / removed) — fall through and try to regenerate.
logger.warning("thumbnail freshness check %s failed: %s", src, e)
try:
with Image.open(src) as im:
im = ImageOps.exif_transpose(im)
im = im.convert("RGB")
im = ImageOps.fit(im, size, method=Image.Resampling.LANCZOS)
dst.parent.mkdir(parents=True, exist_ok=True)
im.save(dst, format="WEBP", quality=quality, method=4)
return dst
except Exception as e:
logger.warning("thumbnail %s failed: %s", src, e)
return None