django_rq removed (server too weak), loading spinner added during upload
This commit is contained in:
parent
9465ba7f18
commit
60309ac39c
12 changed files with 141 additions and 61 deletions
|
|
@ -9,8 +9,7 @@ class Video(models.Model):
|
||||||
film_genre = models.CharField(max_length=20, choices=FILM_GENRES, blank=True, null=True)
|
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)
|
video_file = models.FileField(upload_to='videos/', blank=True, null=True)
|
||||||
file_name = models.CharField(max_length=50, blank=True, null=True)
|
file_name = models.CharField(max_length=50, blank=True, null=True)
|
||||||
thumbnail_created = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'({self.id}) {self.title}'
|
return f'({self.id}) {self.title}'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from .models import Video
|
from .models import Video
|
||||||
from .tasks import convert_video_to_hls, create_thumbnails, delete_original_video, update_thumbnail_status
|
from .tasks import convert_video_to_hls, create_thumbnails, delete_original_video
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -13,18 +13,31 @@ def video_post_save(sender, instance, created, **kwargs):
|
||||||
Generates a thumbnail for a newly created `Video` instance.
|
Generates a thumbnail for a newly created `Video` instance.
|
||||||
"""
|
"""
|
||||||
if created:
|
if created:
|
||||||
queue = django_rq.get_queue("default", autocommit=True)
|
|
||||||
|
|
||||||
#Create thumbnail
|
#Create thumbnail
|
||||||
queue.enqueue(create_thumbnails, instance, instance.id)
|
create_thumbnails(instance, instance.id)
|
||||||
|
|
||||||
#Convert video
|
#Convert video
|
||||||
for resolution in ["480", "720", "1080"]:
|
for resolution in ["480", "720", "1080"]:
|
||||||
queue.enqueue(convert_video_to_hls, instance.video_file.path, resolution, instance.id)
|
convert_video_to_hls(instance.video_file.path, resolution, instance.id)
|
||||||
|
|
||||||
# Delete the original video file
|
# Delete the original video file
|
||||||
|
delete_original_video(instance.video_file.path)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# With Django RQ Worker (My server is too slow for it)
|
||||||
|
if created:
|
||||||
|
queue = django_rq.get_queue("default", autocommit=True)
|
||||||
|
|
||||||
|
Create thumbnail
|
||||||
|
queue.enqueue(create_thumbnails, instance, instance.id)
|
||||||
|
|
||||||
|
Convert video
|
||||||
|
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)
|
queue.enqueue(delete_original_video, instance.video_file.path)
|
||||||
|
"""
|
||||||
@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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,6 @@ def create_thumbnails(instance, model_id):
|
||||||
# Generate the thumbnails
|
# 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_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)
|
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:
|
except ffmpeg._run.Error as e:
|
||||||
print(f"Ein Fehler ist aufgetreten: {e.stderr.decode()}")
|
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.")
|
|
||||||
|
|
@ -28,8 +28,4 @@ export class HeroBannerComponent {
|
||||||
playMovieId(videoPath: string) {
|
playMovieId(videoPath: string) {
|
||||||
this.playMovie.emit(videoPath);
|
this.playMovie.emit(videoPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
getImagePath(): any {
|
|
||||||
return '123';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,3 +92,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if(movieData.send){
|
||||||
|
<app-loading-dialog
|
||||||
|
loadingMsg="Movie is uploading, please be patient a moment."
|
||||||
|
></app-loading-dialog>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,17 @@ import { FormsModule, NgForm } from '@angular/forms';
|
||||||
import { ErrorService } from '../../../../services/error.service';
|
import { ErrorService } from '../../../../services/error.service';
|
||||||
import { BtnLargeComponent } from '../../../../shared/components/buttons/btn-large/btn-large.component';
|
import { BtnLargeComponent } from '../../../../shared/components/buttons/btn-large/btn-large.component';
|
||||||
import { MovieService } from '../../../../services/movie.service';
|
import { MovieService } from '../../../../services/movie.service';
|
||||||
|
import { LoadingDialogComponent } from '../../../../shared/components/loading-dialog/loading-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-upload-movie',
|
selector: 'app-upload-movie',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, BtnLargeComponent],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
BtnLargeComponent,
|
||||||
|
LoadingDialogComponent,
|
||||||
|
],
|
||||||
templateUrl: './upload-movie.component.html',
|
templateUrl: './upload-movie.component.html',
|
||||||
styleUrl: './upload-movie.component.scss',
|
styleUrl: './upload-movie.component.scss',
|
||||||
})
|
})
|
||||||
|
|
@ -48,15 +54,8 @@ export class UploadMovieComponent {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.movieData.send = true;
|
this.movieData.send = true;
|
||||||
|
let formData = this.createFormData();
|
||||||
const formData = this.createFormData();
|
await this.movieService.uploadMovie(formData);
|
||||||
const response = await this.movieService
|
|
||||||
.uploadMovie(formData)
|
|
||||||
.toPromise();
|
|
||||||
const videoId = response.id;
|
|
||||||
|
|
||||||
await this.waitForThumbnailCreation(videoId);
|
|
||||||
|
|
||||||
ngForm.resetForm();
|
ngForm.resetForm();
|
||||||
this.uploadMovieOverview();
|
this.uploadMovieOverview();
|
||||||
this.movieData.send = false;
|
this.movieData.send = false;
|
||||||
|
|
@ -77,14 +76,4 @@ export class UploadMovieComponent {
|
||||||
}
|
}
|
||||||
return formData;
|
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
// Development
|
// Development
|
||||||
// baseUrl: 'http://127.0.0.1:8000',
|
baseUrl: 'http://127.0.0.1:8000',
|
||||||
|
|
||||||
// Live
|
// Live
|
||||||
baseUrl: 'https://videoflix-django.andre-kempf.com',
|
// baseUrl: 'https://videoflix-django.andre-kempf.com',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,23 +21,10 @@ export class MovieService {
|
||||||
return lastValueFrom(this.http.get(url, { headers }));
|
return lastValueFrom(this.http.get(url, { headers }));
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadMovie(formData: FormData): Observable<any> {
|
uploadMovie(formData: FormData) {
|
||||||
|
const url = environment.baseUrl + '/content/upload/';
|
||||||
const headers = this.getAuthHeaders();
|
const headers = this.getAuthHeaders();
|
||||||
return this.http.post<any>(
|
return lastValueFrom(this.http.post(url, formData, { headers }));
|
||||||
`${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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAuthHeaders(): HttpHeaders {
|
private getAuthHeaders(): HttpHeaders {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="loading-dialog">
|
||||||
|
<div class="loading-content">
|
||||||
|
<div id="loading" class="loader"></div>
|
||||||
|
<div id="loadingText" class="loading-text">
|
||||||
|
<p>{{ loadingMsg }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
@import "./../../../../assets/style/colors.scss";
|
||||||
|
|
||||||
|
.loading-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
background-color: rgba($black, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 6px solid $white;
|
||||||
|
border-top: 6px solid $blue;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
box-shadow: 0px 0px 15px rgba($blue, 0.5);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 12px;
|
||||||
|
color: $white;
|
||||||
|
text-shadow: 3px 3px 3px $black;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoadingDialogComponent } from './loading-dialog.component';
|
||||||
|
|
||||||
|
describe('LoadingDialogComponent', () => {
|
||||||
|
let component: LoadingDialogComponent;
|
||||||
|
let fixture: ComponentFixture<LoadingDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoadingDialogComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoadingDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-loading-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './loading-dialog.component.html',
|
||||||
|
styleUrl: './loading-dialog.component.scss',
|
||||||
|
})
|
||||||
|
export class LoadingDialogComponent {
|
||||||
|
@Input() loadingMsg: string = '';
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue