feat: create database entry for new video only after successful upload
This commit is contained in:
parent
a2cfe78445
commit
91dc969c6d
7 changed files with 91 additions and 47 deletions
|
|
@ -11,4 +11,4 @@ class VideoResource(resources.ModelResource):
|
||||||
class VideoAdmin(ImportExportModelAdmin):
|
class VideoAdmin(ImportExportModelAdmin):
|
||||||
resource_class = VideoResource
|
resource_class = VideoResource
|
||||||
readonly_fields = ('file_path',)
|
readonly_fields = ('file_path',)
|
||||||
list_display = ('title', 'file_path', 'created_at')
|
list_display = ('title', 'file_path', 'genre', 'created_at')
|
||||||
|
|
@ -2,7 +2,7 @@ from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from .class_assets import VIDEO_GENRES
|
from .class_assets import VIDEO_GENRES
|
||||||
import os
|
import uuid
|
||||||
|
|
||||||
class Video(models.Model):
|
class Video(models.Model):
|
||||||
creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,default=1)
|
creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,default=1)
|
||||||
|
|
@ -14,12 +14,4 @@ class Video(models.Model):
|
||||||
file_name = models.CharField(max_length=50, blank=True, null=True)
|
file_name = models.CharField(max_length=50, blank=True, null=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'({self.id}) {self.title}'
|
return f'({self.id}) {self.title}'
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if self.file_path and not self.file_name:
|
|
||||||
file_path = self.file_path.path
|
|
||||||
base_filename, _ = os.path.splitext(os.path.basename(file_path))
|
|
||||||
self.file_name = base_filename
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
41
backend/video_app/services.py
Normal file
41
backend/video_app/services.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.conf import settings
|
||||||
|
from .models import Video
|
||||||
|
|
||||||
|
def validate_video_file(uploaded_file, max_size_mb=20):
|
||||||
|
"""
|
||||||
|
Validate a video file
|
||||||
|
"""
|
||||||
|
if not uploaded_file:
|
||||||
|
return 'No file uploaded'
|
||||||
|
if not uploaded_file.name.endswith('.mp4'):
|
||||||
|
return 'Invalid file type. Only .mp4 files are allowed.'
|
||||||
|
if uploaded_file.size > max_size_mb * 1024 * 1024:
|
||||||
|
return f'File size exceeds the allowed limit of {max_size_mb}MB'
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_video_file(uploaded_file):
|
||||||
|
"""
|
||||||
|
Save an uploaded video file and return the short name and the file path.
|
||||||
|
"""
|
||||||
|
short_name = uuid.uuid4().hex[:6]
|
||||||
|
ext = os.path.splitext(uploaded_file.name)[1]
|
||||||
|
new_filename = f"{short_name}{ext}"
|
||||||
|
fs = FileSystemStorage(location=settings.VIDEO_DIR)
|
||||||
|
fs.save(new_filename, uploaded_file)
|
||||||
|
return short_name, os.path.join('videos', new_filename)
|
||||||
|
|
||||||
|
def create_video_record(data, user, short_name, file_path):
|
||||||
|
"""
|
||||||
|
Creates a Video record in the database.
|
||||||
|
"""
|
||||||
|
return Video.objects.create(
|
||||||
|
title=data.get('title'),
|
||||||
|
description=data.get('description'),
|
||||||
|
genre=data.get('genre'),
|
||||||
|
creator=user,
|
||||||
|
file_name=short_name,
|
||||||
|
file_path=file_path
|
||||||
|
)
|
||||||
|
|
@ -9,21 +9,28 @@ import shutil
|
||||||
@receiver(post_save, sender=Video)
|
@receiver(post_save, sender=Video)
|
||||||
def video_post_save(sender, instance, created, **kwargs):
|
def video_post_save(sender, instance, created, **kwargs):
|
||||||
"""
|
"""
|
||||||
Generates a thumbnail, enqueues tasks to convert the video to different resolutions, and schedules the deletion of the original video file.
|
Signal handler for processing a video after saving:
|
||||||
|
- Create thumbnails
|
||||||
|
- Convert video to different resolutions (HLS)
|
||||||
|
- Delete original video
|
||||||
"""
|
"""
|
||||||
if created:
|
if not created:
|
||||||
queue = django_rq.get_queue("default", autocommit=True)
|
return
|
||||||
|
|
||||||
#Create thumbnail
|
if instance.file_path and instance.file_name:
|
||||||
create_thumbnails(instance, instance.id)
|
try:
|
||||||
|
create_thumbnails(instance, instance.id)
|
||||||
#Convert video
|
|
||||||
for resolution in ["1280x720", "640x360", "1920x1080"]:
|
queue = django_rq.get_queue("default", autocommit=True)
|
||||||
queue.enqueue(convert_video_to_hls, instance.file_path.path, resolution, instance.id)
|
resolutions = ["1280x720", "640x360", "1920x1080"]
|
||||||
|
for resolution in resolutions:
|
||||||
#Delete the original video file
|
queue.enqueue(convert_video_to_hls, instance.file_path.path, resolution, instance.id)
|
||||||
queue.enqueue(delete_original_video, instance.file_path.path)
|
|
||||||
|
queue.enqueue(delete_original_video, instance.file_path.path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Video ID video processing error {instance.id}: {e}")
|
||||||
|
|
||||||
@receiver(post_delete, sender=Video)
|
@receiver(post_delete, sender=Video)
|
||||||
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -64,4 +71,4 @@ def remove_first_mp4(filename):
|
||||||
"""
|
"""
|
||||||
name_part, ext_part = filename.split('.mp4', 1)
|
name_part, ext_part = filename.split('.mp4', 1)
|
||||||
new_filename = name_part + ext_part
|
new_filename = name_part + ext_part
|
||||||
return new_filename
|
return new_filename
|
||||||
|
|
@ -3,14 +3,14 @@ import subprocess
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
import os
|
import os
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
def convert_video_to_hls(source, resolution, model_id):
|
def convert_video_to_hls(source, resolution, model_id):
|
||||||
"""
|
"""
|
||||||
Converts a video to the HLS format
|
Converts a video to the HLS format
|
||||||
"""
|
"""
|
||||||
resolution_after_x = resolution.split('x')[1] if 'x' in resolution else resolution
|
resolution_after_x = resolution.split('x')[1] if 'x' in resolution else resolution
|
||||||
|
|
||||||
target_dir = os.path.join(os.path.dirname(source), str(model_id))
|
target_dir = os.path.join(settings.VIDEO_DIR, str(model_id))
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
|
||||||
base_filename = os.path.basename(source).split(".")[0]
|
base_filename = os.path.basename(source).split(".")[0]
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,21 @@
|
||||||
|
from django.forms import ValidationError
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from .serializer import VideoSerializer
|
from .serializer import VideoSerializer
|
||||||
from .models import Video
|
from .models import Video
|
||||||
|
from .services import validate_video_file, save_video_file, create_video_record
|
||||||
from .class_assets import VIDEO_GENRES
|
from .class_assets import VIDEO_GENRES
|
||||||
from django.core.cache.backends.base import DEFAULT_TIMEOUT
|
|
||||||
from django.views.decorators.cache import cache_page
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
from django.db import transaction
|
||||||
import os
|
import os
|
||||||
|
|
||||||
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
#@cache_page(CACHE_TTL)
|
|
||||||
def video_list(request):
|
def video_list(request):
|
||||||
"""
|
"""
|
||||||
List all videos.
|
List all videos
|
||||||
"""
|
"""
|
||||||
videos = Video.objects.all()
|
videos = Video.objects.all()
|
||||||
serializer = VideoSerializer(videos, many=True)
|
serializer = VideoSerializer(videos, many=True)
|
||||||
|
|
@ -61,13 +57,21 @@ def check_video_resolutions(request, id):
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def video_upload(request):
|
def video_upload(request):
|
||||||
"""
|
"""
|
||||||
Handle the upload of a video file.
|
Handles the upload of a video file:
|
||||||
"""
|
"""
|
||||||
parser_classes = (MultiPartParser, FormParser)
|
uploaded_file = request.FILES.get('file_path')
|
||||||
if request.method == 'POST':
|
|
||||||
serializer = VideoSerializer(data=request.data)
|
error = validate_video_file(uploaded_file)
|
||||||
if serializer.is_valid():
|
if error:
|
||||||
creator = request.user
|
return Response({'error': error}, status=400)
|
||||||
serializer.save(creator=creator)
|
|
||||||
return Response(serializer.data, status=201)
|
try:
|
||||||
return Response({"error": "Invalid data", "details": serializer.errors}, status=400)
|
short_name, file_path = save_video_file(uploaded_file)
|
||||||
|
with transaction.atomic():
|
||||||
|
video = create_video_record(request.data, request.user, short_name, file_path)
|
||||||
|
return Response(VideoSerializer(video).data, status=201)
|
||||||
|
|
||||||
|
except ValidationError as ve:
|
||||||
|
return Response({'error': f'Validation Error: {str(ve)}'}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return Response({'error': f'Error while saving video file: {str(e)}'}, status=500)
|
||||||
|
|
@ -107,8 +107,6 @@ RQ_QUEUES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CACHE_TTL = 60 * 15
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'videoflix.wsgi.application'
|
WSGI_APPLICATION = 'videoflix.wsgi.application'
|
||||||
|
|
||||||
# Media
|
# Media
|
||||||
|
|
@ -117,7 +115,9 @@ MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
THUMBNAIL_DIR = MEDIA_ROOT / 'thumbnails'
|
THUMBNAIL_DIR = str(MEDIA_ROOT / 'thumbnails')
|
||||||
|
|
||||||
|
VIDEO_DIR = str(MEDIA_ROOT / 'videos')
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue