photo26/photologue/utils.py
krek0 f4052a3d99
All checks were successful
Docker / build (release) Successful in 9s
Add video support with unified media display.
2026-05-16 15:13:14 +02:00

63 lines
2 KiB
Python

# This file is part of photo21
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import os
from django.core.files.base import ContentFile
from io import BytesIO
from PIL import Image
import av
logger = logging.getLogger("photologue.utils")
def is_photo(file_obj):
"""Return True if file_obj is a valid image readable by Pillow."""
try:
Image.open(file_obj).verify()
file_obj.seek(0)
return True
except Exception:
file_obj.seek(0)
return False
def is_video(file_obj):
"""Return True if file_obj is a valid video container (has at least one video stream)."""
try:
container = av.open(file_obj)
ok = len(container.streams.video) > 0
container.close()
file_obj.seek(0)
return ok
except Exception:
file_obj.seek(0)
return False
def generate_video_thumbnail(video):
"""Extract the first frame of a video file and save it as the video thumbnail."""
try:
container = av.open(video.file.path)
frame = next(container.decode(video=0))
img = frame.to_image()
rotation = frame.rotation
container.close()
if rotation:
img = img.rotate(rotation, expand=True)
except Exception:
logger.error("Failed to extract video frame for thumbnail", exc_info=True)
return
try:
buffer = BytesIO()
img.save(buffer, "JPEG", quality=70, optimize=True)
# Preserve directory structure: strip the "videos/" storage prefix so that
# get_video_storage_path places the thumb alongside the video file.
rel = video.file.name[len("videos/"):] if video.file.name.startswith("videos/") else video.file.name
thumb_name = os.path.splitext(rel)[0] + "_thumb.jpg"
video.thumbnail.save(thumb_name, ContentFile(buffer.getvalue()), save=True)
except Exception:
logger.error("Failed to save video thumbnail", exc_info=True)