116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
# run this after adding images to gallery/
|
|
# makes thumbnails, turns gifs into way smaller mp4s (needs ffmpeg) and updates manifest.json
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
from PIL import Image
|
|
|
|
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif"}
|
|
VIDEO_EXTS = {".mp4"}
|
|
THUMB_SIZE = (400, 400)
|
|
FFMPEG = shutil.which("ffmpeg")
|
|
|
|
|
|
def sort_key(filename):
|
|
# so 10.jpg comes after 9.jpg
|
|
stem = os.path.splitext(filename)[0]
|
|
return (0, int(stem), "") if stem.isdigit() else (1, 0, stem.lower())
|
|
|
|
|
|
def list_files(base_folder, exts):
|
|
return sorted(
|
|
(f for f in os.listdir(base_folder)
|
|
if os.path.isfile(os.path.join(base_folder, f))
|
|
and os.path.splitext(f)[1].lower() in exts),
|
|
key=sort_key,
|
|
)
|
|
|
|
|
|
def convert_gifs(base_folder):
|
|
if not FFMPEG:
|
|
print("ffmpeg not found — skipping GIF -> MP4 conversion")
|
|
return
|
|
for filename in list_files(base_folder, {".gif"}):
|
|
src = os.path.join(base_folder, filename)
|
|
dst = os.path.join(base_folder, os.path.splitext(filename)[0] + ".mp4")
|
|
if os.path.exists(dst):
|
|
continue
|
|
with Image.open(src) as img:
|
|
if getattr(img, "n_frames", 1) < 2:
|
|
continue # not animated
|
|
subprocess.run([
|
|
FFMPEG, "-hide_banner", "-loglevel", "error", "-y", "-i", src,
|
|
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", # mp4 needs even width/height
|
|
"-c:v", "libx264", "-crf", "23", "-pix_fmt", "yuv420p",
|
|
"-an", "-movflags", "+faststart", dst,
|
|
], check=True)
|
|
print(f"Converted {filename} -> {os.path.basename(dst)}")
|
|
|
|
|
|
def make_thumbnail(src, thumb_path):
|
|
if os.path.splitext(src)[1].lower() in VIDEO_EXTS:
|
|
if not FFMPEG:
|
|
raise RuntimeError("ffmpeg needed for video thumbnails")
|
|
subprocess.run([
|
|
FFMPEG, "-hide_banner", "-loglevel", "error", "-y", "-i", src,
|
|
"-frames:v", "1", "-vf", "scale='min(400,iw)':'min(400,ih)':force_original_aspect_ratio=decrease",
|
|
"-q:v", "4", thumb_path,
|
|
], check=True)
|
|
return
|
|
with Image.open(src) as img:
|
|
if img.mode != "RGB":
|
|
img = img.convert("RGB")
|
|
img.thumbnail(THUMB_SIZE)
|
|
img.save(thumb_path, "JPEG", quality=85)
|
|
|
|
|
|
def create_thumbnails(base_folder, files):
|
|
thumb_dir = os.path.join(base_folder, "thumbnails")
|
|
os.makedirs(thumb_dir, exist_ok=True)
|
|
|
|
count = 0
|
|
for filename in files:
|
|
stem = os.path.splitext(filename)[0]
|
|
thumb_path = os.path.join(thumb_dir, f"{stem}.jpg")
|
|
if os.path.exists(thumb_path):
|
|
continue
|
|
try:
|
|
make_thumbnail(os.path.join(base_folder, filename), thumb_path)
|
|
count += 1
|
|
print(f"Created thumbnail for {filename}")
|
|
except Exception as e:
|
|
print(f"Failed to process {filename}: {e}")
|
|
|
|
print(f"Created {count} thumbnails in {base_folder}")
|
|
|
|
|
|
def pick_for_site(files):
|
|
# use the mp4 instead of the gif when both exist
|
|
chosen = {}
|
|
for f in files:
|
|
stem, ext = os.path.splitext(f)
|
|
if stem not in chosen or ext.lower() in VIDEO_EXTS:
|
|
chosen[stem] = f
|
|
return sorted(chosen.values(), key=sort_key)
|
|
|
|
|
|
def write_manifest(base_folder, files):
|
|
path = os.path.join(base_folder, "manifest.json")
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(files, f, indent=2)
|
|
f.write("\n")
|
|
print(f"Wrote {path} ({len(files)} items)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
folder = "gallery"
|
|
if not os.path.isdir(folder):
|
|
print(f"Folder {folder} does not exist.")
|
|
else:
|
|
convert_gifs(folder)
|
|
site_files = pick_for_site(list_files(folder, IMAGE_EXTS | VIDEO_EXTS))
|
|
create_thumbnails(folder, site_files)
|
|
write_manifest(folder, site_files)
|