Browse Source

feat(videos):auto thumbnail generator logic added to the backend

master
Mohsen Taba 2 weeks ago
parent
commit
e2e7463dac
  1. 47
      apps/video/models.py
  2. 1
      requirements.txt
  3. 44
      utils/media.py

47
apps/video/models.py

@ -193,21 +193,48 @@ class Video(LowercaseSlugMixin, models.Model):
super().save(*args, **kwargs) 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 # Update playlists that contain this video
for appearance in self.playlist_appearances.all(): for appearance in self.playlist_appearances.all():
playlist = appearance.playlist playlist = appearance.playlist
playlist.total_time = playlist.calculate_total_time() playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=['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: if self.video_type == self.VedioTypeChoices.YOUTUBE_LINK:
try: try:

1
requirements.txt

@ -136,6 +136,7 @@ cryptography>=41.0.0
django-celery-beat==2.5.0 django-celery-beat==2.5.0
yt-dlp>=2024.3.10 yt-dlp>=2024.3.10
openpyxl==3.1.5 openpyxl==3.1.5
imageio-ffmpeg>=0.5.1
https://yaghoubi:e07059e0ac6be3b0032ded5f65f03363fbd3811f@git.habibapp.com/NewHorizon/django-limitless-dashboard.git/archive/master.zip https://yaghoubi:e07059e0ac6be3b0032ded5f65f03363fbd3811f@git.habibapp.com/NewHorizon/django-limitless-dashboard.git/archive/master.zip

44
utils/media.py

@ -1,5 +1,6 @@
import datetime import datetime
import logging import logging
import subprocess
from tinytag import TinyTag from tinytag import TinyTag
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,3 +37,46 @@ def get_media_duration_time(file_path):
secs = seconds % 60 secs = seconds % 60
return datetime.time(hour=hours, minute=minutes, second=secs) 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
Loading…
Cancel
Save