Initial commit
This commit is contained in:
82
backend/app/routers/images.py
Normal file
82
backend/app/routers/images.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import re
|
||||
import mimetypes
|
||||
from fastapi import APIRouter, HTTPException, Response
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
import httpx
|
||||
|
||||
from ..runtime import get_runtime_settings
|
||||
|
||||
router = APIRouter(prefix="/images", tags=["images"])
|
||||
|
||||
_TMDB_BASE = "https://image.tmdb.org/t/p"
|
||||
_ALLOWED_SIZES = {"w92", "w154", "w185", "w342", "w500", "w780", "original"}
|
||||
|
||||
|
||||
def _safe_filename(path: str) -> str:
|
||||
trimmed = path.strip("/")
|
||||
trimmed = trimmed.replace("/", "_")
|
||||
safe = re.sub(r"[^A-Za-z0-9_.-]", "_", trimmed)
|
||||
return safe or "image"
|
||||
|
||||
|
||||
async def cache_tmdb_image(path: str, size: str = "w342") -> bool:
|
||||
if not path or "://" in path or ".." in path:
|
||||
return False
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
if size not in _ALLOWED_SIZES:
|
||||
return False
|
||||
|
||||
runtime = get_runtime_settings()
|
||||
cache_mode = (runtime.artwork_cache_mode or "remote").lower()
|
||||
if cache_mode != "cache":
|
||||
return False
|
||||
|
||||
cache_dir = os.path.join(os.getcwd(), "data", "artwork", "tmdb", size)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
file_path = os.path.join(cache_dir, _safe_filename(path))
|
||||
if os.path.exists(file_path):
|
||||
return True
|
||||
|
||||
url = f"{_TMDB_BASE}/{size}{path}"
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
content = response.content
|
||||
with open(file_path, "wb") as handle:
|
||||
handle.write(content)
|
||||
return True
|
||||
|
||||
|
||||
@router.get("/tmdb")
|
||||
async def tmdb_image(path: str, size: str = "w342"):
|
||||
if not path or "://" in path or ".." in path:
|
||||
raise HTTPException(status_code=400, detail="Invalid image path")
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
if size not in _ALLOWED_SIZES:
|
||||
raise HTTPException(status_code=400, detail="Invalid size")
|
||||
|
||||
runtime = get_runtime_settings()
|
||||
cache_mode = (runtime.artwork_cache_mode or "remote").lower()
|
||||
url = f"{_TMDB_BASE}/{size}{path}"
|
||||
if cache_mode != "cache":
|
||||
return RedirectResponse(url=url)
|
||||
|
||||
cache_dir = os.path.join(os.getcwd(), "data", "artwork", "tmdb", size)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
file_path = os.path.join(cache_dir, _safe_filename(path))
|
||||
headers = {"Cache-Control": "public, max-age=86400"}
|
||||
if os.path.exists(file_path):
|
||||
media_type = mimetypes.guess_type(file_path)[0] or "image/jpeg"
|
||||
return FileResponse(file_path, media_type=media_type, headers=headers)
|
||||
|
||||
try:
|
||||
await cache_tmdb_image(path, size)
|
||||
if os.path.exists(file_path):
|
||||
media_type = mimetypes.guess_type(file_path)[0] or "image/jpeg"
|
||||
return FileResponse(file_path, media_type=media_type, headers=headers)
|
||||
raise HTTPException(status_code=502, detail="Image cache failed")
|
||||
except httpx.HTTPError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Image fetch failed: {exc}") from exc
|
||||
Reference in New Issue
Block a user