feat: settings panel + thumbnail pipeline fixes

- photos.py: stop crashing in FileResponse when a thumb hasn't been
  generated; return a clean 404 with Retry-After so the frontend can
  back off.
- thumbs.py: fix process_video_thumbnail (overwrite_output, robust
  duration probe across stream/format, eager frame load + temp cleanup)
  so videos stop ending up as the gray placeholder.
- library.py: new /maintenance/* endpoints — thumbnail-stats,
  regenerate-thumbnails (with media_type / only_failed filters), and a
  manual data-integrity cleanup trigger.
- Frontend Settings panel (gear in TopBar) surfacing those endpoints
  plus a re-scan button and live thumbnail status counts.
- PhotoThumbnail: stretch the auto-retry schedule for slow RAW jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 09:24:08 +02:00
parent 6fa00f4b37
commit 9ed577f40c
8 changed files with 719 additions and 33 deletions

View File

@@ -322,12 +322,14 @@ async def get_thumbnail(
thumb_path = f"{thumb_dir}/{size}.webp"
if not os.path.exists(thumb_path):
# Generate thumbnail on demand
# Queue background generation (handles RAW/HEIC/video properly)
from app.tasks.thumbs import generate_thumbnails
generate_thumbnails.delay(photo_id)
# For now, return a placeholder or the original with reduced quality
if os.path.exists(photo.filepath):
# Best-effort inline fallback for standard images so the first
# request doesn't have to wait for the worker. RAW/HEIC/video
# and missing source files fall through to a clean 404 below.
if photo.filepath and os.path.exists(photo.filepath):
from PIL import Image
try:
os.makedirs(thumb_dir, exist_ok=True)
@@ -359,10 +361,23 @@ async def get_thumbnail(
# Save as WebP
img.save(thumb_path, 'WEBP', quality=85, optimize=True)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error generating thumbnail: {e}")
raise HTTPException(status_code=404, detail="Could not generate thumbnail")
logger.warning(
f"Inline thumbnail fallback failed for {photo_id} ({size}); "
f"waiting on worker: {e}"
)
# If the inline fallback didn't (or couldn't) produce the file,
# tell the client to retry instead of crashing in FileResponse/nginx.
if not os.path.exists(thumb_path):
raise HTTPException(
status_code=404,
detail="Thumbnail not ready",
headers={"Retry-After": "2"},
)
# Check if we're behind Nginx
if os.environ.get('USE_X_ACCEL_REDIRECT'):
# Use Nginx X-Accel-Redirect for better performance