Files
wordarr/wordarr/covers.py
T

45 lines
1.7 KiB
Python

"""Covers are fetched once and kept under the config dir, so a dead link or a
slow host does not take the picture with it."""
import hashlib
import logging
import mimetypes
import tempfile
from pathlib import Path
import httpx
from . import config
MAX_BYTES = 5 * 1024 * 1024
log = logging.getLogger(__name__)
# shortcut: covers of deleted requests stay on disk, add a sweep if the dir grows
async def cached(url: str) -> Path | None:
"""The file for this url, downloaded on first use; None if it cannot be had."""
if not url.startswith(("http://", "https://")):
return None
folder = config.CONFIG_DIR / "covers"
name = hashlib.sha256(url.encode()).hexdigest()
if hit := next(folder.glob(name + ".*"), None):
return hit
try:
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
resp = await client.get(url)
resp.raise_for_status()
except httpx.HTTPError as exc:
log.warning("cover %s failed: %s", url, exc)
return None
ctype = resp.headers.get("content-type", "").split(";")[0].strip()
if not ctype.startswith("image/") or len(resp.content) > MAX_BYTES:
log.warning("cover %s is no usable image (%s, %d bytes)", url, ctype, len(resp.content))
return None
folder.mkdir(parents=True, exist_ok=True)
# written aside and renamed, so a parallel request never serves half a file
with tempfile.NamedTemporaryFile(dir=folder, suffix=".tmp", delete=False) as tmp:
tmp.write(resp.content)
path = folder / (name + (mimetypes.guess_extension(ctype) or ".img"))
Path(tmp.name).chmod(0o644) # NamedTemporaryFile creates 0600
Path(tmp.name).replace(path)
return path