A bunch of fixes and improvements
This commit is contained in:
parent
dd11adcced
commit
5d142034ae
6 changed files with 182 additions and 78 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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}"
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
]
|
|
@ -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
|
|
@ -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}")
|
Loading…
Reference in a new issue