Replace image pipeline with profile-driven variants

- add image normalization profiles and thumbnail profiles
- generate source, full-size variant, and thumbnail image artifacts
- rewrite canonical image URLs through the first configured profile
- emit explicit image Media RSS groups with named thumbnails
- preserve legacy image paths when image conversion is disabled
- cover cache-hit source paths, inline image handling, and thumbnail export
This commit is contained in:
Abel Luck 2026-05-27 09:24:22 +02:00
parent 7316d4723f
commit 525393272e
13 changed files with 1299 additions and 124 deletions

View file

@ -43,6 +43,50 @@ def local_audio_path(s: str) -> str:
return local_file_path(s)
def image_guid(source_url: str) -> str:
return hashlib.sha1(to_bytes(source_url)).hexdigest() # nosec
def image_extension(mimetype_or_extension: str | None, source_url: str = "") -> str:
if mimetype_or_extension:
if mimetype_or_extension.startswith("."):
extension = mimetype_or_extension
elif "/" in mimetype_or_extension:
extension = mimetypes.guess_extension(mimetype_or_extension) or ""
else:
extension = f".{mimetype_or_extension.lstrip('.')}"
if extension == ".jpe":
return ".jpg"
return extension
guessed = Path(source_url).suffix
if guessed == ".jpe":
return ".jpg"
if guessed:
return guessed
return ".img"
def source_image_path(source_url: str, mimetype_or_extension: str | None = None) -> str:
extension = image_extension(mimetype_or_extension, source_url)
return f"source/{image_guid(source_url)}{extension}"
def published_image_path(source_url: str, profile: Mapping[str, Any]) -> str:
return variant_media_path(f"full/{image_guid(source_url)}", profile, hashed=True)
def canonical_published_image_path(
source_url: str, profiles: Sequence[Mapping[str, Any]]
) -> str:
if not profiles:
raise ValueError("Missing image normalization profiles")
return published_image_path(source_url, profiles[0])
def thumbnail_image_path(source_url: str, profile: Mapping[str, Any]) -> str:
return variant_media_path(f"thumbs/{image_guid(source_url)}", profile, hashed=True)
def profile_settings_hash(profile: Mapping[str, Any]) -> str:
settings = {
key: value
@ -65,6 +109,8 @@ def variant_media_path(
def published_media_path(
file_type: FileType, source_url: str, profile: Mapping[str, Any]
) -> str:
if file_type == FileType.IMAGE:
return published_image_path(source_url, profile)
if file_type == FileType.AUDIO:
return variant_media_path(local_audio_path(source_url), profile, hashed=True)
if file_type == FileType.VIDEO:
@ -79,6 +125,8 @@ def canonical_published_media_path(
raise ValueError(f"Missing transcode profiles for {file_type.value}")
# The first configured profile is the public URL contract. Reordering profiles
# changes published URLs for already-mirrored media.
if file_type == FileType.IMAGE:
return canonical_published_image_path(source_url, profiles)
return published_media_path(file_type, source_url, profiles[0])