From e2e7463dac54149ccf012837b8b91d0d183f7dcd Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sun, 6 Sep 2026 09:03:11 +0330 Subject: [PATCH] feat(videos):auto thumbnail generator logic added to the backend --- apps/video/models.py | 47 ++++++++++++++++++++++++++++++++++---------- requirements.txt | 1 + utils/media.py | 44 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/apps/video/models.py b/apps/video/models.py index dd7bd79..98c61b1 100644 --- a/apps/video/models.py +++ b/apps/video/models.py @@ -193,21 +193,48 @@ class Video(LowercaseSlugMixin, models.Model): super().save(*args, **kwargs) - if (file_changed or self.video_time == datetime.time(0, 0, 0)) and self.video_file and self.video_type == self.VedioTypeChoices.VIDEO_FILE: - from utils.media import get_media_duration_time - try: - duration_time = get_media_duration_time(self.video_file.path) - if duration_time: - self.video_time = duration_time - Video.objects.filter(pk=self.pk).update(video_time=duration_time) - + if self.video_type == self.VedioTypeChoices.VIDEO_FILE and self.video_file: + from utils.media import get_media_duration_time, extract_video_frame_bytes + from django.core.files.base import ContentFile + import os + + fields_to_update = [] + + if (file_changed or self.video_time == datetime.time(0, 0, 0)): + try: + duration_time = get_media_duration_time(self.video_file.path) + if duration_time: + self.video_time = duration_time + fields_to_update.append('video_time') + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to calculate video duration: {e}") + + if not self.thumbnail: + try: + thumb_bytes = extract_video_frame_bytes(self.video_file.path, timestamp_sec=1.0) + if thumb_bytes: + base_name = os.path.splitext(os.path.basename(self.video_file.name))[0] + filename = f"thumb_{base_name}_{self.pk}.jpg" + self.thumbnail.save(filename, ContentFile(thumb_bytes), save=False) + fields_to_update.append('thumbnail') + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to extract video thumbnail: {e}") + + if fields_to_update: + update_dict = {field: getattr(self, field) for field in fields_to_update} + Video.objects.filter(pk=self.pk).update(**update_dict) + + if 'video_time' in fields_to_update: + try: # Update playlists that contain this video for appearance in self.playlist_appearances.all(): playlist = appearance.playlist playlist.total_time = playlist.calculate_total_time() playlist.save(update_fields=['total_time']) - except Exception as e: - pass + except Exception as e: + pass if self.video_type == self.VedioTypeChoices.YOUTUBE_LINK: try: diff --git a/requirements.txt b/requirements.txt index 74e9b23..282d152 100644 --- a/requirements.txt +++ b/requirements.txt @@ -136,6 +136,7 @@ cryptography>=41.0.0 django-celery-beat==2.5.0 yt-dlp>=2024.3.10 openpyxl==3.1.5 +imageio-ffmpeg>=0.5.1 https://yaghoubi:e07059e0ac6be3b0032ded5f65f03363fbd3811f@git.habibapp.com/NewHorizon/django-limitless-dashboard.git/archive/master.zip diff --git a/utils/media.py b/utils/media.py index a225229..d515b61 100644 --- a/utils/media.py +++ b/utils/media.py @@ -1,5 +1,6 @@ import datetime import logging +import subprocess from tinytag import TinyTag logger = logging.getLogger(__name__) @@ -36,3 +37,46 @@ def get_media_duration_time(file_path): secs = seconds % 60 return datetime.time(hour=hours, minute=minutes, second=secs) + +def extract_video_frame_bytes(file_path, timestamp_sec=1.0): + """ + Extracts a single frame from a video file as JPEG bytes. + First attempts seeking to timestamp_sec (default 1.0s), then falls back to 0.0s. + Returns bytes or None if extraction fails. + """ + try: + import imageio_ffmpeg + ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe() + + # Try seeking to timestamp_sec first + cmd = [ + ffmpeg_exe, + '-ss', str(timestamp_sec), + '-i', str(file_path), + '-vframes', '1', + '-f', 'image2', + '-c:v', 'mjpeg', + 'pipe:1' + ] + res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15) + if res.returncode == 0 and res.stdout: + return res.stdout + + # Fallback to start of video + cmd_fallback = [ + ffmpeg_exe, + '-i', str(file_path), + '-vframes', '1', + '-f', 'image2', + '-c:v', 'mjpeg', + 'pipe:1' + ] + res_fb = subprocess.run(cmd_fallback, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15) + if res_fb.returncode == 0 and res_fb.stdout: + return res_fb.stdout + + logger.warning(f"Failed to extract video frame from {file_path}: {res.stderr.decode('utf-8', errors='ignore')}") + return None + except Exception as e: + logger.error(f"Error extracting video frame from {file_path}: {e}") + return None