diff --git a/backend/video_app/admin.py b/backend/video_app/admin.py index 5774075..d40b5f9 100644 --- a/backend/video_app/admin.py +++ b/backend/video_app/admin.py @@ -11,4 +11,4 @@ class VideoResource(resources.ModelResource): class VideoAdmin(ImportExportModelAdmin): resource_class = VideoResource readonly_fields = ('file_path',) - list_display = ('title', 'file_path', 'created_at') \ No newline at end of file + list_display = ('title', 'file_path', 'genre', 'created_at') \ No newline at end of file diff --git a/backend/video_app/models.py b/backend/video_app/models.py index e378463..d5a6111 100644 --- a/backend/video_app/models.py +++ b/backend/video_app/models.py @@ -2,7 +2,7 @@ from django.db import models from django.conf import settings from datetime import date from .class_assets import VIDEO_GENRES -import os +import uuid class Video(models.Model): 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) def __str__(self): - 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) - \ No newline at end of file + return f'({self.id}) {self.title}' \ No newline at end of file diff --git a/backend/video_app/services.py b/backend/video_app/services.py new file mode 100644 index 0000000..67eafe3 --- /dev/null +++ b/backend/video_app/services.py @@ -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 + ) \ No newline at end of file diff --git a/backend/video_app/signals.py b/backend/video_app/signals.py index 899dcfe..732c393 100644 --- a/backend/video_app/signals.py +++ b/backend/video_app/signals.py @@ -9,21 +9,28 @@ import shutil @receiver(post_save, sender=Video) 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: - queue = django_rq.get_queue("default", autocommit=True) - - #Create thumbnail - create_thumbnails(instance, instance.id) - - #Convert video - for resolution in ["1280x720", "640x360", "1920x1080"]: - queue.enqueue(convert_video_to_hls, instance.file_path.path, resolution, instance.id) - - #Delete the original video file - queue.enqueue(delete_original_video, instance.file_path.path) + if not created: + return + + if instance.file_path and instance.file_name: + try: + create_thumbnails(instance, instance.id) + + queue = django_rq.get_queue("default", autocommit=True) + resolutions = ["1280x720", "640x360", "1920x1080"] + for resolution in resolutions: + queue.enqueue(convert_video_to_hls, instance.file_path.path, resolution, instance.id) + + 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) 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) new_filename = name_part + ext_part - return new_filename + return new_filename \ No newline at end of file diff --git a/backend/video_app/tasks.py b/backend/video_app/tasks.py index a5b1f2b..4d2fe77 100644 --- a/backend/video_app/tasks.py +++ b/backend/video_app/tasks.py @@ -3,14 +3,14 @@ import subprocess import ffmpeg import os from django.conf import settings - + def convert_video_to_hls(source, resolution, model_id): """ Converts a video to the HLS format """ 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) base_filename = os.path.basename(source).split(".")[0] diff --git a/backend/video_app/views.py b/backend/video_app/views.py index 7b0b1fb..de7263f 100644 --- a/backend/video_app/views.py +++ b/backend/video_app/views.py @@ -1,25 +1,21 @@ +from django.forms import ValidationError from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from .serializer import VideoSerializer from .models import Video +from .services import validate_video_file, save_video_file, create_video_record 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 rest_framework.parsers import MultiPartParser, FormParser from django.http import JsonResponse +from django.db import transaction import os -CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT) - -# Create your views here. @api_view(['GET']) @permission_classes([IsAuthenticated]) -#@cache_page(CACHE_TTL) def video_list(request): """ - List all videos. + List all videos """ videos = Video.objects.all() serializer = VideoSerializer(videos, many=True) @@ -61,13 +57,21 @@ def check_video_resolutions(request, id): @permission_classes([IsAuthenticated]) def video_upload(request): """ - Handle the upload of a video file. + Handles the upload of a video file: """ - parser_classes = (MultiPartParser, FormParser) - if request.method == 'POST': - serializer = VideoSerializer(data=request.data) - if serializer.is_valid(): - creator = request.user - serializer.save(creator=creator) - return Response(serializer.data, status=201) - return Response({"error": "Invalid data", "details": serializer.errors}, status=400) \ No newline at end of file + uploaded_file = request.FILES.get('file_path') + + error = validate_video_file(uploaded_file) + if error: + return Response({'error': error}, status=400) + + try: + 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) \ No newline at end of file diff --git a/backend/videoflix/settings.py b/backend/videoflix/settings.py index 61e6d09..0410638 100644 --- a/backend/videoflix/settings.py +++ b/backend/videoflix/settings.py @@ -107,8 +107,6 @@ RQ_QUEUES = { } } -CACHE_TTL = 60 * 15 - WSGI_APPLICATION = 'videoflix.wsgi.application' # Media @@ -117,7 +115,9 @@ MEDIA_ROOT = BASE_DIR / 'media' MEDIA_URL = '/media/' -THUMBNAIL_DIR = MEDIA_ROOT / 'thumbnails' +THUMBNAIL_DIR = str(MEDIA_ROOT / 'thumbnails') + +VIDEO_DIR = str(MEDIA_ROOT / 'videos') # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases