diff --git a/backend/requirements.txt b/backend/requirements.txt index 5ef2a8f..1696bee 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,18 @@ asgiref==3.8.1 +click==8.1.7 Django==5.0.7 +django-cors-headers==4.4.0 +django-debug-toolbar==4.4.6 +django-redis==5.4.0 +django-rq==2.10.2 djangorestframework==3.15.2 +ffmpeg-python==0.2.0 +future==1.0.0 +imageio-ffmpeg==0.5.1 +numpy==2.0.1 +pillow==10.4.0 +psutil==6.0.0 +redis==5.0.8 +rq==1.16.2 +setuptools==72.1.0 sqlparse==0.5.1 \ No newline at end of file diff --git a/backend/videoflix/content/models.py b/backend/videoflix/content/models.py index 52ea286..03d51da 100644 --- a/backend/videoflix/content/models.py +++ b/backend/videoflix/content/models.py @@ -6,8 +6,8 @@ class Video(models.Model): title = models.CharField(max_length=80) description = models.CharField(max_length=500) film_genre = models.CharField(max_length=50, blank=True, null=True) - video_file = models.FileField(upload_to='videos', blank=True, null=True) - thumbnail = models.FileField(upload_to='img', null=True, blank=True) + video_file = models.FileField(upload_to='videos/', blank=True, null=True) + thumbnail = models.ImageField(upload_to='thumbnails/', blank=True, null=True) def __str__(self): return self.title \ No newline at end of file diff --git a/backend/videoflix/content/signals.py b/backend/videoflix/content/signals.py index aa710af..4eff43d 100644 --- a/backend/videoflix/content/signals.py +++ b/backend/videoflix/content/signals.py @@ -1,8 +1,10 @@ from .models import Video -from content.tasks import convert_video, create_thumbnail +from content.tasks import convert_video, create_thumbnails from django.dispatch import receiver from django.db.models.signals import post_save, post_delete +from django.conf import settings import os +import django_rq @receiver(post_save, sender=Video) def video_post_save(sender, instance, created, **kwargs): @@ -10,20 +12,58 @@ def video_post_save(sender, instance, created, **kwargs): Generates a thumbnail for a newly created `Video` instance. """ if created: + queue = django_rq.get_queue("default", autocommit=True) + #Convert Video - convert_video(instance.video_file.path, "480") - convert_video(instance.video_file.path, "720") - convert_video(instance.video_file.path, "1080") - + queue.enqueue(convert_video, instance.video_file.path, "480") + queue.enqueue(convert_video, instance.video_file.path, "720") + queue.enqueue(convert_video, instance.video_file.path, "1080") + #Create Thumbnail - create_thumbnail(instance) - + queue.enqueue(create_thumbnails, instance) + @receiver(post_delete, sender=Video) def auto_delete_file_on_delete(sender, instance, **kwargs): """ - Deletes file from filesystem + Deletes video file and its converted files from filesystem when corresponding `Video` object is deleted. """ + # Delete the original video file if instance.video_file: if os.path.isfile(instance.video_file.path): - os.remove(instance.video_file.path) \ No newline at end of file + os.remove(instance.video_file.path) + + # Delete the converted videos + if instance.video_file: + delete_converted_files(instance.video_file.path) + + # Delete the thumbnails + if instance.thumbnail: + thumbnail_dir = settings.THUMBNAIL_DIR + base_filename = os.path.splitext(os.path.basename(instance.video_file.path))[0] + thumbnail_1080p_path = os.path.join(thumbnail_dir, base_filename + '_1080p.jpg') + thumbnail_480_path = os.path.join(thumbnail_dir, base_filename + '_480p.jpg') + + if os.path.isfile(thumbnail_1080p_path): + os.remove(thumbnail_1080p_path) + if os.path.isfile(thumbnail_480_path): + os.remove(thumbnail_480_path) + +def remove_first_mp4(filename): + """ + Remove the first .mp4 extension from the filename. + """ + name_part, ext_part = filename.split('.mp4', 1) + new_filename = name_part + ext_part + return new_filename + +def delete_converted_files(video_file_path): + """ + Delete all converted video files related to the original video file. + """ + resolutions = ["480", "720", "1080"] + for resolution in resolutions: + converted_video_path = video_file_path + f'_{resolution}p.mp4' + converted_video_path = remove_first_mp4(converted_video_path) + if os.path.isfile(converted_video_path): + os.remove(converted_video_path) diff --git a/backend/videoflix/content/tasks.py b/backend/videoflix/content/tasks.py index 71340e6..139b8a2 100644 --- a/backend/videoflix/content/tasks.py +++ b/backend/videoflix/content/tasks.py @@ -15,27 +15,27 @@ def remove_first_mp4(filename): new_filename = name_part + ext_part return new_filename -def create_thumbnail(source): - video_file_path = source.video_file.path +def create_thumbnails(self): + video_file_path = self.video_file.path thumbnail_dir = settings.THUMBNAIL_DIR base_filename = os.path.splitext(os.path.basename(video_file_path))[0] - thumbnail_1920_filename = base_filename + '_1920p.jpg' - thumbnail_1920_path = os.path.join(thumbnail_dir, thumbnail_1920_filename) + thumbnail_1080p_filename = base_filename + '_1080p.jpg' + thumbnail_1080p_path = os.path.join(thumbnail_dir, thumbnail_1080p_filename) thumbnail_480_filename = base_filename + '_480p.jpg' thumbnail_480_path = os.path.join(thumbnail_dir, thumbnail_480_filename) - + thumbnail_path = os.path.join(thumbnail_dir, base_filename + ".jpg") + if not os.path.exists(thumbnail_dir): os.makedirs(thumbnail_dir) try: - ffmpeg.input(video_file_path, ss=1).output(thumbnail_1920_path, vf='scale=1920:-1', vframes=1).run(overwrite_output=True) - ffmpeg.input(video_file_path, ss=1).output(thumbnail_480_path, vf='scale=480:-1', vframes=1).run(overwrite_output=True) + ffmpeg.input(video_file_path, ss=1).output(thumbnail_1080p_path, vf='scale=1920:-1', vframes=1).run(overwrite_output=True) + ffmpeg.input(video_file_path, ss=1).output(thumbnail_480_path, vf='scale=720:-1', vframes=1).run(overwrite_output=True) - source.thumbnail_1920 = os.path.relpath(thumbnail_1920_path, start=settings.MEDIA_ROOT) - source.thumbnail_480 = os.path.relpath(thumbnail_480_path, start=settings.MEDIA_ROOT) - - source.save() + # Hier speichern wir nur das Thumbnail für die Vorschau im Modell + self.thumbnail = os.path.relpath(thumbnail_path, start=settings.MEDIA_ROOT) + self.save() except ffmpeg._run.Error as e: print(f"Ein Fehler ist aufgetreten: {e.stderr.decode()}") \ No newline at end of file diff --git a/backend/videoflix/videoflix/settings.py b/backend/videoflix/videoflix/settings.py index 0bfcb90..b409e61 100644 --- a/backend/videoflix/videoflix/settings.py +++ b/backend/videoflix/videoflix/settings.py @@ -52,6 +52,7 @@ INSTALLED_APPS = [ 'rest_framework', 'corsheaders', 'debug_toolbar', + 'django_rq', 'content.apps.ContentConfig', 'users', ] @@ -102,10 +103,20 @@ INTERNAL_IPS = [ '127.0.0.1', ] +RQ_QUEUES = { + 'default': { + 'HOST': 'localhost', + 'PORT': 6379, + 'DB': 0, + 'PASSWORD': 'foobared', + 'DEFAULT_TIMEOUT': 360, + } +} + CACHE_TTL = 60 * 15 MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -THUMBNAIL_DIR = os.path.join(BASE_DIR, 'media/img') +THUMBNAIL_DIR = os.path.join(BASE_DIR, 'media/thumbnails') MEDIA_URL = '/media/' WSGI_APPLICATION = 'videoflix.wsgi.application' diff --git a/backend/videoflix/videoflix/urls.py b/backend/videoflix/videoflix/urls.py index b288903..0d5040b 100644 --- a/backend/videoflix/videoflix/urls.py +++ b/backend/videoflix/videoflix/urls.py @@ -15,7 +15,7 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.conf import settings from django.conf.urls.static import static from users import views as user_views @@ -32,6 +32,7 @@ from debug_toolbar.toolbar import debug_toolbar_urls urlpatterns = [ path('admin/', admin.site.urls), + path('django-rq/', include('django_rq.urls')), # Content URLs path('content/', content_views.video_list),