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}")
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
@ -243,7 +243,7 @@ class PILStitcher(BaseStitcher):
PIL.Image.frombytes("RGB", (t_width, t_height), bytes(raw)).save(bio, "PNG")
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
@ -292,7 +292,7 @@ class PILStitcher(BaseStitcher):
result.save(bio, "PNG")
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

View file

@ -1,6 +1,7 @@
from django.db import models
from django.contrib.auth import get_user_model
import mimetypes
import uuid
def file_upload_path(instance, filename) -> str:
@ -46,6 +47,7 @@ class ConversionStatus(models.IntegerChoices):
PROCESSING = 1
DONE = 2
FAILED = -1
DISMISSED = -2
DOWNLOADING = 10
STITCHING = 11
@ -82,4 +84,18 @@ class Conversion(models.Model):
Raises:
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");
var intervals = {};
function toggleOptions() {
$("#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) {
var text =
'<div class="col-sm-3" id="' +
@ -33,6 +80,18 @@ function addcard(jobid, title) {
$("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) {
Notification.requestPermission(function (permission) {
if (permission === "granted") {
@ -44,7 +103,9 @@ function failcard(jobid, title) {
var text =
'<div class="card"> <div style="text-align: center; color: red; font-weight: bold;" class="card-block">' +
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 +
'\');" class="btn btn-danger">Hide</a></div> </div>';
$("#" + jobid).html(text);
@ -61,24 +122,26 @@ function finishcard(jobid, title, video) {
var text =
'<div class="card"> <img ' +
(video ? 'id="' + jobid + '-thumb"' : "") +
' class="card-img-top img-fluid" src="/getjob/' +
' class="card-img-top img-fluid" download src="/download/' +
jobid +
(video ? "-thumb" : "") +
'" alt="Final ' +
(video ? "Video" : "Image") +
'"><div style="text-align: center; font-weight: bold;" class="card-block">' +
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 +
'" class="btn btn-primary">Download</a> <a onclick="deletecard(\'' +
jobid +
'\');" class="btn btn-danger">Hide</a></div> </div>';
'\');" class="btn btn-danger">Hide</a> </div> </div>';
$("#" + jobid).html(text);
var counter = 0;
var interval = setInterval(function () {
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) {
window.clearInterval(interval);
}
@ -95,57 +158,34 @@ $("#theform").submit(function (event) {
success: function (msg) {
var title = $("#title").val() ? $("#title").val() : "No title";
var interval = setInterval(checkServerForFile, 3000, msg.id, title);
window.panaxworking = false;
intervals[msg.id] = interval;
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 () {
if (Notification.permission !== "granted") {
Notification.requestPermission();
}
};
initialize();
});

View file

@ -1,6 +1,6 @@
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 = [
path('', ConverterView.as_view(), name='converter'),
@ -10,4 +10,6 @@ urlpatterns = [
path('list', ConversionListView.as_view(), name='conversion_list'),
path('delete/<uuid:id>', ConversionDeleteView.as_view(), name='conversion_delete'),
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:
response['status'] = "completed"
response['result'] = conversion.result.file.path
response['result'] = conversion.result.file.url
response['content_type'] = conversion.result.mime_type
elif conversion.status == ConversionStatus.FAILED:
response['status'] = "failed"
@ -127,7 +127,49 @@ class ConversionDeleteView(LoginRequiredMixin, View):
'error': 'Conversion not found'
}, status=404)
conversion.user = None
conversion.status = ConversionStatus.DISMISSED
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 django.conf import settings
from django.db import transaction
import multiprocessing
import logging
@ -65,30 +66,33 @@ class Worker(multiprocessing.Process):
"""
while True:
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:
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
if conversion:
conversion.status = ConversionStatus.PROCESSING
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:
self.logger.debug("No conversion to process")
time.sleep(1)
else:
self.logger.debug("No conversion to process")
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:
self.logger.error(f"Worker error: {e}")