A bunch of fixes and improvements

This commit is contained in:
Kumi 2023-10-27 20:03:51 +02:00
parent dd11adcced
commit 5d142034ae
Signed by: kumi
GPG key ID: ECBCC9082395383F
6 changed files with 182 additions and 78 deletions

View file

@ -145,7 +145,7 @@ class BlenderStitcher(BaseStitcher):
raise StitchingError(f"cube2sphere stitching failed for conversion {files[0].conversion.id}") raise StitchingError(f"cube2sphere stitching failed for conversion {files[0].conversion.id}")
with (Path(tempdir) / "out0001.png").open("rb") as f: with (Path(tempdir) / "out0001.png").open("rb") as f:
result = File.objects.create(conversion=files[0].conversion, file=ContentFile(f.read(), name="result.png")) result = File.objects.create(conversion=files[0].conversion, file=ContentFile(f.read(), name="result.png"), mime_type="image/png")
return result return result
@ -243,7 +243,7 @@ class PILStitcher(BaseStitcher):
PIL.Image.frombytes("RGB", (t_width, t_height), bytes(raw)).save(bio, "PNG") PIL.Image.frombytes("RGB", (t_width, t_height), bytes(raw)).save(bio, "PNG")
bio.seek(0) bio.seek(0)
result = File.objects.create(conversion=files[0].conversion, file=ContentFile(output, name="result.png")) result = File.objects.create(conversion=files[0].conversion, file=ContentFile(output, name="result.png", mime_type="image/png"))
return result return result
@ -292,7 +292,7 @@ class PILStitcher(BaseStitcher):
result.save(bio, "PNG") result.save(bio, "PNG")
bio.seek(0) bio.seek(0)
result_file = File.objects.create(conversion=files[0][0].conversion, file=ContentFile(bio.read(), name="result.png")) result_file = File.objects.create(conversion=files[0][0].conversion, file=ContentFile(bio.read(), name="result.png"), mime_type="image/png")
return result_file return result_file

View file

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
import mimetypes
import uuid import uuid
def file_upload_path(instance, filename) -> str: def file_upload_path(instance, filename) -> str:
@ -46,6 +47,7 @@ class ConversionStatus(models.IntegerChoices):
PROCESSING = 1 PROCESSING = 1
DONE = 2 DONE = 2
FAILED = -1 FAILED = -1
DISMISSED = -2
DOWNLOADING = 10 DOWNLOADING = 10
STITCHING = 11 STITCHING = 11
@ -82,4 +84,18 @@ class Conversion(models.Model):
Raises: Raises:
File.DoesNotExist: If no result file exists File.DoesNotExist: If no result file exists
""" """
return File.objects.get(conversion=self, is_result=True) return File.objects.get(conversion=self, is_result=True)
def get_result_filename(self) -> str:
"""Get the final filename for the result file
Returns:
str: Filename for the result file
Raises:
File.DoesNotExist: If no result file exists
"""
basename = "".join([c for c in self.title if c.isalnum() or c in "._- "])
extension = mimetypes.guess_extension(self.result.mime_type)
return f"{basename}{extension}"

View file

@ -2,6 +2,8 @@ $("#options").hide();
$body = $("body"); $body = $("body");
var intervals = {};
function toggleOptions() { function toggleOptions() {
$("#options").toggle(); $("#options").toggle();
} }
@ -22,6 +24,51 @@ function deletecard(jobid) {
} }
} }
function checkServerForFile(jobid, title, interval) {
$.ajax({
type: "GET",
cache: false,
url: "/status/" + jobid,
statusCode: {
403: function () {
Notification.requestPermission(function (permission) {
if (permission === "granted") {
var notification = new Notification("PIX360", {
body: "Your session has expired. Please log in again.",
});
}
});
window.location.href = "/";
},
404: function () {
clearInterval(intervals[jobid]);
failcard(jobid, title);
return;
},
200: function (data, tstatus, xhr) {
if (data.status == "completed") {
clearInterval(intervals[jobid]);
finishcard(
jobid,
title,
data.content_type == "video/mp4"
);
return;
} else if (data.status == "failed") {
clearInterval(intervals[jobid]);
failcard(jobid, title);
return;
}
},
500: function () {
clearInterval(intervals[jobid]);
failcard(jobid, title);
return;
},
},
});
}
function addcard(jobid, title) { function addcard(jobid, title) {
var text = var text =
'<div class="col-sm-3" id="' + '<div class="col-sm-3" id="' +
@ -33,6 +80,18 @@ function addcard(jobid, title) {
$("html,body").animate({ scrollTop: $("#" + jobid).offset().top }); $("html,body").animate({ scrollTop: $("#" + jobid).offset().top });
} }
function restartconversion(jobid, title) {
$.ajax({
type: "GET",
url: "/retry/" + jobid,
success: function (msg) {
var interval = setInterval(checkServerForFile, 3000, msg.id, title);
intervals[msg.id] = interval;
addcard(msg.id, title);
},
});
}
function failcard(jobid, title) { function failcard(jobid, title) {
Notification.requestPermission(function (permission) { Notification.requestPermission(function (permission) {
if (permission === "granted") { if (permission === "granted") {
@ -44,7 +103,9 @@ function failcard(jobid, title) {
var text = var text =
'<div class="card"> <div style="text-align: center; color: red; font-weight: bold;" class="card-block">' + '<div class="card"> <div style="text-align: center; color: red; font-weight: bold;" class="card-block">' +
title + title +
': Export failed.</div><div style="text-align: center;" class="card-block"> <a style="color: white;" onclick="deletecard(\'' + ': Export failed.</div><div style="text-align: center;" class="card-block"><a style="color: white;" onclick="restartconversion (\'' +
jobid + '\', \'' + title +
'\');" class="btn btn-info">Retry</a> <a style="color: white;" onclick="deletecard(\'' +
jobid + jobid +
'\');" class="btn btn-danger">Hide</a></div> </div>'; '\');" class="btn btn-danger">Hide</a></div> </div>';
$("#" + jobid).html(text); $("#" + jobid).html(text);
@ -61,24 +122,26 @@ function finishcard(jobid, title, video) {
var text = var text =
'<div class="card"> <img ' + '<div class="card"> <img ' +
(video ? 'id="' + jobid + '-thumb"' : "") + (video ? 'id="' + jobid + '-thumb"' : "") +
' class="card-img-top img-fluid" src="/getjob/' + ' class="card-img-top img-fluid" download src="/download/' +
jobid + jobid +
(video ? "-thumb" : "") + (video ? "-thumb" : "") +
'" alt="Final ' + '" alt="Final ' +
(video ? "Video" : "Image") + (video ? "Video" : "Image") +
'"><div style="text-align: center; font-weight: bold;" class="card-block">' + '"><div style="text-align: center; font-weight: bold;" class="card-block">' +
title + title +
'</div> <div style="text-align: center; color: white;" class="card-block"> <a href="/getjob/' + '</div> <div style="text-align: center; color: white;" class="card-block"><a style="color: white;" onclick="restartconversion (\'' +
jobid + '\', \'' + title +
'\');" class="btn btn-info">Retry</a><a href="/download/' +
jobid + jobid +
'" class="btn btn-primary">Download</a> <a onclick="deletecard(\'' + '" class="btn btn-primary">Download</a> <a onclick="deletecard(\'' +
jobid + jobid +
'\');" class="btn btn-danger">Hide</a></div> </div>'; '\');" class="btn btn-danger">Hide</a> </div> </div>';
$("#" + jobid).html(text); $("#" + jobid).html(text);
var counter = 0; var counter = 0;
var interval = setInterval(function () { var interval = setInterval(function () {
var image = document.getElementById(jobid + "-thumb"); var image = document.getElementById(jobid + "-thumb");
image.src = "/getjob/" + jobid + "-thumb?rand=" + Math.random(); image.src = "/download/" + jobid + "-thumb?rand=" + Math.random();
if (++counter === 10) { if (++counter === 10) {
window.clearInterval(interval); window.clearInterval(interval);
} }
@ -95,57 +158,34 @@ $("#theform").submit(function (event) {
success: function (msg) { success: function (msg) {
var title = $("#title").val() ? $("#title").val() : "No title"; var title = $("#title").val() ? $("#title").val() : "No title";
var interval = setInterval(checkServerForFile, 3000, msg.id, title); var interval = setInterval(checkServerForFile, 3000, msg.id, title);
window.panaxworking = false; intervals[msg.id] = interval;
addcard(msg.id, title); addcard(msg.id, title);
function checkServerForFile(jobid, title) {
if (!window.panaxworking) {
window.panaxworking = true;
$.ajax({
type: "GET",
cache: false,
url: "/status/" + jobid,
statusCode: {
403: function () {
window.location.href = "/";
},
404: function () {
clearInterval(interval);
failcard(jobid, title);
return;
},
200: function (data, tstatus, xhr) {
if (data.status == "finished") {
clearInterval(interval);
finishcard(
jobid,
title,
data.content_type == "video/mp4"
);
return;
} else if (data.status == "failed") {
clearInterval(interval);
failcard(jobid, title);
return;
}
},
500: function () {
clearInterval(interval);
failcard(jobid, title);
return;
},
},
});
window.panaxworking = false;
}
}
}, },
}); });
} }
}); });
function initialize() {
$.ajax({
type: "GET",
url: "/list",
success: function (msg) {
for (var i = 0; i < msg["conversions"].length; i++) {
var job = msg["conversions"][i];
if (job.status >= 0) {
var title = job.title ? job.title : "No title";
addcard(job.id, title);
var interval = setInterval(checkServerForFile, 3000, job.id, title);
intervals[job.id] = interval;
}
}
}
});
}
$(document).ready(function () { $(document).ready(function () {
if (Notification.permission !== "granted") { if (Notification.permission !== "granted") {
Notification.requestPermission(); Notification.requestPermission();
} };
initialize();
}); });

View file

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from .views import ConverterView, StartConversionView, ConversionStatusView, ConversionLogView, ConversionListView, ConversionDeleteView, ConversionResultView from .views import ConverterView, StartConversionView, ConversionStatusView, ConversionLogView, ConversionListView, ConversionDeleteView, ConversionResultView, ConversionRetryView, ConversionDownloadView
urlpatterns = [ urlpatterns = [
path('', ConverterView.as_view(), name='converter'), path('', ConverterView.as_view(), name='converter'),
@ -10,4 +10,6 @@ urlpatterns = [
path('list', ConversionListView.as_view(), name='conversion_list'), path('list', ConversionListView.as_view(), name='conversion_list'),
path('delete/<uuid:id>', ConversionDeleteView.as_view(), name='conversion_delete'), path('delete/<uuid:id>', ConversionDeleteView.as_view(), name='conversion_delete'),
path('result/<uuid:id>', ConversionResultView.as_view(), name='conversion_result'), path('result/<uuid:id>', ConversionResultView.as_view(), name='conversion_result'),
path('retry/<uuid:id>', ConversionRetryView.as_view(), name='conversion_retry'),
path('download/<uuid:id>', ConversionDownloadView.as_view(), name='conversion_download'),
] ]

View file

@ -47,7 +47,7 @@ class ConversionStatusView(LoginRequiredMixin, View):
if conversion.status == ConversionStatus.DONE: if conversion.status == ConversionStatus.DONE:
response['status'] = "completed" response['status'] = "completed"
response['result'] = conversion.result.file.path response['result'] = conversion.result.file.url
response['content_type'] = conversion.result.mime_type response['content_type'] = conversion.result.mime_type
elif conversion.status == ConversionStatus.FAILED: elif conversion.status == ConversionStatus.FAILED:
response['status'] = "failed" response['status'] = "failed"
@ -127,7 +127,49 @@ class ConversionDeleteView(LoginRequiredMixin, View):
'error': 'Conversion not found' 'error': 'Conversion not found'
}, status=404) }, status=404)
conversion.user = None conversion.status = ConversionStatus.DISMISSED
conversion.save() conversion.save()
return JsonResponse({}) return JsonResponse({})
class ConversionRetryView(LoginRequiredMixin, View):
"""View for retrying a conversion
"""
def get(self, request, *args, **kwargs):
"""Handle the POST request
"""
conversion = Conversion.objects.filter(id=kwargs['id']).first()
if not conversion or not (conversion.user == request.user):
return JsonResponse({
'error': 'Conversion not found'
}, status=404)
new_conversion = Conversion.objects.create(url=conversion.url, title=conversion.title, user=request.user)
return JsonResponse({
'id': new_conversion.id
})
class ConversionDownloadView(LoginRequiredMixin, View):
"""View for downloading a conversion
"""
def get(self, request, *args, **kwargs):
"""Handle the GET request
"""
conversion = Conversion.objects.filter(id=kwargs['id']).first()
if not conversion or not (conversion.user == request.user):
return JsonResponse({
'error': 'Conversion not found'
}, status=404)
file = conversion.result
if not file:
return JsonResponse({
'error': 'Conversion not done'
}, status=404)
content = file.file.read()
response = HttpResponse(content, content_type=file.mime_type)
response['Content-Disposition'] = f'attachment; filename="{conversion.get_result_filename()}"'
return response

View file

@ -3,6 +3,7 @@ from .models import Conversion, File, ConversionStatus
from .classes import ConversionError from .classes import ConversionError
from django.conf import settings from django.conf import settings
from django.db import transaction
import multiprocessing import multiprocessing
import logging import logging
@ -65,30 +66,33 @@ class Worker(multiprocessing.Process):
""" """
while True: while True:
try: try:
conversion = Conversion.objects.filter(status=ConversionStatus.PENDING).first() with transaction.atomic():
conversion = Conversion.objects.select_for_update().filter(status=ConversionStatus.PENDING).first()
if conversion: if conversion:
conversion.status = ConversionStatus.PROCESSING conversion.status = ConversionStatus.PROCESSING
conversion.save()
self.logger.info(f"Processing conversion {conversion.id}")
try:
result = self.process_conversion(conversion)
result.is_result = True
result.save()
conversion.status = ConversionStatus.DONE
conversion.save() conversion.save()
self.logger.info(f"Conversion {conversion.id} done")
except Exception as e:
conversion.status = ConversionStatus.FAILED
conversion.log = traceback.format_exc()
conversion.save()
self.logger.error(f"Conversion {conversion.id} failed: {e}")
self.logger.debug(traceback.format_exc())
else: else:
self.logger.debug("No conversion to process") self.logger.debug("No conversion to process")
time.sleep(1) time.sleep(1)
continue
self.logger.info(f"Processing conversion {conversion.id}")
try:
result = self.process_conversion(conversion)
result.is_result = True
result.save()
conversion.status = ConversionStatus.DONE
conversion.save()
self.logger.info(f"Conversion {conversion.id} done")
except Exception as e:
conversion.status = ConversionStatus.FAILED
conversion.log = traceback.format_exc()
conversion.save()
self.logger.error(f"Conversion {conversion.id} failed: {e}")
self.logger.debug(traceback.format_exc())
except Exception as e: except Exception as e:
self.logger.error(f"Worker error: {e}") self.logger.error(f"Worker error: {e}")