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}")
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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}"
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'),
|
||||||
]
|
]
|
|
@ -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
|
|
@ -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}")
|
Loading…
Reference in a new issue