2026-10-05 09:36:20 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
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 = (
|
2026-10-05 11:25:41 +02:00
|
|
|
("flyer-keuzes.jpg", "flyer", "Thirty-one choices. You have zero."),
|
|
|
|
|
("flyer-chat.jpg", "flyer", "A chat is not a portfolio."),
|
|
|
|
|
("flyer-bijna.jpg", "flyer", "Almost is not a state."),
|
|
|
|
|
("flyer-assistent.jpg", "flyer", "The assistant does not judge."),
|
|
|
|
|
("meme-vloer.jpg", "meme", "A pass is fine. The floor, you mean."),
|
|
|
|
|
("meme-split.jpg", "meme", "Send them home."),
|
|
|
|
|
("meme-cursus.jpg", "meme", "No ebook. Just finish it."),
|
|
|
|
|
("take-chatlog.jpg", "meme", "Your portfolio is a chat log."),
|
|
|
|
|
("take-copilot.jpg", "meme", "Copilot is not a senior."),
|
|
|
|
|
("take-coping.jpg", "meme", "A pass is coping."),
|
|
|
|
|
("poster-thuis.jpg", "meme", "Vibe coders, go home."),
|
|
|
|
|
("poster-vloer.jpg", "meme", "A pass? That is the floor."),
|
|
|
|
|
("poster-cursus.jpg", "meme", "No course. Just finish it."),
|
|
|
|
|
("poster-luiheid.jpg", "meme", "AI is not taking your job. Laziness is."),
|
|
|
|
|
("poster-spel.jpg", "meme", "The game has turned. You have not."),
|
|
|
|
|
("story.jpg", "meme", "No ebook. No 10x developer. Just finishing."),
|
|
|
|
|
("meme-zes.jpg", "meme", "A pass. Congratulations, you are average."),
|
|
|
|
|
("meme-gesprek.jpg", "meme", "My portfolio is a conversation."),
|
|
|
|
|
("meme-product.jpg", "meme", "The course is the product. The work is not."),
|
|
|
|
|
("meme-bijna.jpg", "meme", "Everyone lines up at almost."),
|
2026-10-05 09:36:20 +02:00
|
|
|
("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
|