# retoor import io import logging import imagehash from PIL import Image from molodetz import config from molodetz.attachments import blob_path, blob_url, write_blob from molodetz.database import generate_uid, media_by_kind, upsert_media from molodetz.database.media import get_media_by_file logger = logging.getLogger(__name__) THUMB_WIDTH = 720 DUPLICATE_DISTANCE = 2 GALLERY = ( ("flyer-keuzes.jpg", "flyer", "Eenendertig keuzes. Jij hebt er nul."), ("flyer-chat.jpg", "flyer", "Een chat is geen portfolio."), ("flyer-bijna.jpg", "flyer", "Bijna is geen staat."), ("flyer-assistent.jpg", "flyer", "De assistent oordeelt niet."), ("meme-vloer.jpg", "meme", "Een zesje is prima. De vloer, bedoel je."), ("meme-split.jpg", "meme", "Stuur ze naar huis."), ("meme-cursus.jpg", "meme", "Geen ebook. Wel afmaken."), ("take-chatlog.jpg", "meme", "Je portfolio is een chatlog."), ("take-copilot.jpg", "meme", "Copilot is geen senior."), ("take-coping.jpg", "meme", "Een zesje is coping."), ("poster-thuis.jpg", "meme", "Vibe coders, ga naar huis."), ("poster-vloer.jpg", "meme", "Een zesje? Dat is de vloer."), ("poster-cursus.jpg", "meme", "Geen cursus. Wel afmaken."), ("poster-luiheid.jpg", "meme", "AI pakt je baan niet. Luiheid wel."), ("poster-spel.jpg", "meme", "Het spel is om. Jij bent het niet."), ("story.jpg", "meme", "Geen ebook. Geen x10. Afwerken wel."), ("meme-zes.jpg", "meme", "Een zesje. Gefeliciteerd, je bent gemiddeld."), ("meme-gesprek.jpg", "meme", "Mijn portfolio is een gesprek."), ("meme-product.jpg", "meme", "De cursus is het product."), ("meme-bijna.jpg", "meme", "Iedereen staat bij bijna."), ("meme-compiles.jpg", "meme", "It compiles. Ship it, king."), ("meme-senior.jpg", "meme", "My copilot is the senior."), ("meme-trophy.jpg", "meme", "Peak performance, 6 out of 10."), ("meme-todo.jpg", "meme", "Side projects, a love story."), ) def _thumbnail_bytes(image): copy = image.convert("RGB") copy.thumbnail((THUMB_WIDTH, THUMB_WIDTH * 2)) buffer = io.BytesIO() copy.save(buffer, format="WEBP", quality=82, method=4) return buffer.getvalue() def sync_gallery(): seen_hashes = [] synced = 0 for position, (filename, kind, caption) in enumerate(GALLERY): source = config.MEDIA_SOURCE_DIR / filename if not source.exists(): logger.warning("gallery source missing: %s", source) continue existing = get_media_by_file(filename) with Image.open(source) as image: width, height = image.size phash = imagehash.phash(image) if any(phash - other <= DUPLICATE_DISTANCE for other in seen_hashes): logger.info("gallery duplicate skipped: %s", filename) continue seen_hashes.append(phash) thumb_directory = existing.get("thumb_directory") if existing else None thumb_name = existing.get("thumb_name") if existing else None if not thumb_directory or not blob_path(thumb_directory, thumb_name).exists(): thumb_directory, thumb_name = write_blob( existing["uid"] if existing else generate_uid(), "webp", _thumbnail_bytes(image) ) upsert_media(filename, kind, caption, position, width, height, str(phash), thumb_directory, thumb_name) synced += 1 return synced def gallery_items(kind): items = [] for row in media_by_kind(kind): thumb = blob_url(row["thumb_directory"], row["thumb_name"]) if row.get("thumb_directory") else None items.append( { "uid": row["uid"], "filename": row["filename"], "caption": row["caption"], "width": row["width"], "height": row["height"], "source_path": f"/static/media/{row['filename']}", "thumb_url": thumb, } ) return items