diff --git a/backend/videoflix/content/models.py b/backend/videoflix/content/models.py index a522d1f..b733a01 100644 --- a/backend/videoflix/content/models.py +++ b/backend/videoflix/content/models.py @@ -9,6 +9,7 @@ class Video(models.Model): film_genre = models.CharField(max_length=20, choices=FILM_GENRES, blank=True, null=True) video_file = models.FileField(upload_to='videos/', blank=True, null=True) file_name = models.CharField(max_length=50, blank=True, null=True) + thumbnail_created = models.BooleanField(default=False) def __str__(self): return f'({self.id}) {self.title}' diff --git a/backend/videoflix/content/signals.py b/backend/videoflix/content/signals.py index d08057b..4ca402a 100644 --- a/backend/videoflix/content/signals.py +++ b/backend/videoflix/content/signals.py @@ -1,5 +1,5 @@ from .models import Video -from .tasks import convert_video_to_hls, create_thumbnails, delete_original_video +from .tasks import convert_video_to_hls, create_thumbnails, delete_original_video, update_thumbnail_status from django.dispatch import receiver from django.db.models.signals import post_save, post_delete from django.conf import settings @@ -16,13 +16,12 @@ def video_post_save(sender, instance, created, **kwargs): queue = django_rq.get_queue("default", autocommit=True) #Create thumbnail - queue.enqueue(create_thumbnails, instance, instance.id) + create_thumbnails(instance, instance.id) #Convert video - queue.enqueue(convert_video_to_hls, instance.video_file.path, "480", instance.id) - queue.enqueue(convert_video_to_hls, instance.video_file.path, "720", instance.id) - queue.enqueue(convert_video_to_hls, instance.video_file.path, "1080", instance.id) - + for resolution in ["480", "720", "1080"]: + queue.enqueue(convert_video_to_hls, instance.video_file.path, resolution, instance.id) + # Delete the original video file queue.enqueue(delete_original_video, instance.video_file.path) diff --git a/backend/videoflix/content/tasks.py b/backend/videoflix/content/tasks.py index b593eff..abf751c 100644 --- a/backend/videoflix/content/tasks.py +++ b/backend/videoflix/content/tasks.py @@ -4,6 +4,7 @@ import ffmpeg import os from django.conf import settings from .models import Video +from django_rq import get_queue def convert_video_to_hls(source, resolution, model_id): target_dir = os.path.join(os.path.dirname(source), str(model_id)) @@ -38,10 +39,10 @@ def delete_original_video(source): except OSError as e: print(f"Error deleting original file: {e}") -def create_thumbnails(self, model_id): - video_file_path = self.video_file.path +def create_thumbnails(instance, model_id): + video_file_path = instance.video_file.path thumbnail_dir = os.path.join(settings.THUMBNAIL_DIR, str(model_id)) - + base_filename = os.path.splitext(os.path.basename(video_file_path))[0] thumbnail_1080p_filename = base_filename + '_1080p.jpg' thumbnail_1080p_path = os.path.join(thumbnail_dir, thumbnail_1080p_filename) @@ -52,8 +53,19 @@ def create_thumbnails(self, model_id): os.makedirs(thumbnail_dir) try: + # Generate the thumbnails 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) - + # Update thumbnail status + update_thumbnail_status(model_id) + except ffmpeg._run.Error as e: - print(f"Ein Fehler ist aufgetreten: {e.stderr.decode()}") \ No newline at end of file + print(f"Ein Fehler ist aufgetreten: {e.stderr.decode()}") + +def update_thumbnail_status(video_id): + try: + video = Video.objects.get(id=video_id) + video.thumbnail_created = True + video.save() + except Video.DoesNotExist: + print(f"Video with id {video_id} does not exist.") \ No newline at end of file diff --git a/backend/videoflix/content/views.py b/backend/videoflix/content/views.py index eb132ca..9563ec2 100644 --- a/backend/videoflix/content/views.py +++ b/backend/videoflix/content/views.py @@ -16,12 +16,25 @@ CACHETTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT) @permission_classes([IsAuthenticated]) #@cache_page(CACHETTL) def video_list(request): - - if request.method == 'GET': - video = Video.objects.all() - serializer = VideoSerializer(video, many=True) + """ + List all videos. + """ + videos = Video.objects.all() + serializer = VideoSerializer(videos, many=True) return Response(serializer.data) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def check_thumbnail_status(request, video_id): + """ + Check the status of the thumbnail creation for a specific video. + """ + try: + video = Video.objects.get(id=video_id) + return Response({'thumbnail_created': video.thumbnail_created}) + except Video.DoesNotExist: + return Response({'error': 'Video not found'}, status=404) + @api_view(['POST']) @permission_classes([IsAuthenticated]) def video_upload(request): diff --git a/backend/videoflix/videoflix/urls.py b/backend/videoflix/videoflix/urls.py index 2376382..ff761ff 100644 --- a/backend/videoflix/videoflix/urls.py +++ b/backend/videoflix/videoflix/urls.py @@ -37,6 +37,7 @@ urlpatterns = [ # Content URLs path('content/', content_views.video_list), path('content/upload/', content_views.video_upload), + path('content//status/', content_views.check_thumbnail_status), # Users URLs path('users/', user_views.user_list), diff --git a/frontend/src/app/components/home/browse/upload-movie/upload-movie.component.ts b/frontend/src/app/components/home/browse/upload-movie/upload-movie.component.ts index 35926a8..717516d 100644 --- a/frontend/src/app/components/home/browse/upload-movie/upload-movie.component.ts +++ b/frontend/src/app/components/home/browse/upload-movie/upload-movie.component.ts @@ -44,27 +44,47 @@ export class UploadMovieComponent { } async onSubmit(ngForm: NgForm) { - if (ngForm.submitted && ngForm.form.valid) { - const formData = new FormData(); - formData.append('title', this.movieData.title); - formData.append('description', this.movieData.description); - formData.append('film_genre', this.movieData.filmGenre); - if (this.movieData.videoFile) { - formData.append('video_file', this.movieData.videoFile); - } - try { - await this.movieService.uploadMovie(formData); - this.movieData.send = true; - setTimeout(() => { - ngForm.resetForm(); - this.uploadMovieOverview(); - this.movieData.send = false; - window.location.reload(); - }, 1000); - this.errorService.clearError(); - } catch (error) { - this.errorService.handleError(error); - } + if (!ngForm.submitted || !ngForm.form.valid) return; + + try { + this.movieData.send = true; + + const formData = this.createFormData(); + const response = await this.movieService + .uploadMovie(formData) + .toPromise(); + const videoId = response.id; + + await this.waitForThumbnailCreation(videoId); + + ngForm.resetForm(); + this.uploadMovieOverview(); + this.movieData.send = false; + window.location.reload(); + this.errorService.clearError(); + } catch (error) { + this.errorService.handleError(error); + } + } + + private createFormData(): FormData { + const formData = new FormData(); + formData.append('title', this.movieData.title); + formData.append('description', this.movieData.description); + formData.append('film_genre', this.movieData.filmGenre); + if (this.movieData.videoFile) { + formData.append('video_file', this.movieData.videoFile); + } + return formData; + } + + private async waitForThumbnailCreation(videoId: number) { + while (true) { + const statusResponse = await this.movieService + .checkThumbnailStatus(videoId) + .toPromise(); + if (statusResponse?.thumbnail_created) break; + await new Promise((resolve) => setTimeout(resolve, 2000)); } } } diff --git a/frontend/src/app/services/movie.service.ts b/frontend/src/app/services/movie.service.ts index 810e8cc..a2c12a6 100644 --- a/frontend/src/app/services/movie.service.ts +++ b/frontend/src/app/services/movie.service.ts @@ -21,12 +21,22 @@ export class MovieService { return lastValueFrom(this.http.get(url, { headers })); } - async uploadMovie(formData: FormData) { + uploadMovie(formData: FormData): Observable { const headers = this.getAuthHeaders(); - await lastValueFrom( - this.http.post(`${environment.baseUrl}/content/upload/`, formData, { - headers, - }) + return this.http.post( + `${environment.baseUrl}/content/upload/`, + formData, + { headers } + ); + } + + checkThumbnailStatus( + videoId: number + ): Observable<{ thumbnail_created: boolean }> { + const headers = this.getAuthHeaders(); + return this.http.get<{ thumbnail_created: boolean }>( + `${environment.baseUrl}/content/${videoId}/status/`, + { headers } ); }