More work on a server conversion interface

This commit is contained in:
Kovid Goyal 2018-06-25 16:06:10 +05:30
parent 76335ad2c2
commit fbea3b9b75
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 159 additions and 10 deletions

View File

@ -4,9 +4,42 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from calibre.srv.errors import BookNotFound
import os
import shutil
import tempfile
from functools import partial
from threading import Lock
from calibre.srv.errors import BookNotFound, HTTPNotFound
from calibre.srv.routes import endpoint, json
from calibre.srv.utils import get_library_data
from calibre.utils.monotonic import monotonic
receive_data_methods = {'GET', 'POST'}
conversion_jobs = {}
cache_lock = Lock()
class JobStatus(object):
def __init__(self, job_id, tdir, library_id, pathtoebook, conversion_data):
self.job_id = job_id
self.tdir = tdir
self.library_id, self.pathtoebook = library_id, pathtoebook
self.conversion_data = conversion_data
self.running = self.ok = True
self.last_check_at = monotonic()
def cleanup(self):
safe_delete_tree(self.tdir)
def expire_old_jobs():
now = monotonic()
with cache_lock:
remove = [job_id for job_id, job_status in conversion_jobs.iteritems() if now - job_status.last_check_at >= 360]
for job_id in remove:
conversion_jobs.pop(job_id)
def conversion_defaults():
@ -17,6 +50,75 @@ def conversion_defaults():
return ans
def safe_delete_file(path):
try:
os.remove(path)
except EnvironmentError:
pass
def safe_delete_tree(path):
try:
shutil.rmtree(path, ignore_errors=True)
except EnvironmentError:
pass
def job_done(job):
with cache_lock:
try:
job_status = conversion_jobs[job.job_id]
except KeyError:
return
job_status.running = False
if job.failed:
job_status.ok = False
job_status.was_aborted = job.was_aborted
job_status.traceback = job.traceback
safe_delete_file(job_status.pathtoebook)
def queue_job(ctx, rd, library_id, copy_format_to, fmt, book_id, conversion_data):
tdir = tempfile.mkdtemp(dir=rd.tdir)
fd, pathtoebook = tempfile.mkstemp(prefix='', suffix=('.' + fmt.lower()), dir=tdir)
with os.fdopen(fd, 'wb') as f:
copy_format_to(f)
job_id = ctx.start_job(
'Convert book %s (%s)' % (book_id, fmt), 'calibre.srv.convert_book',
'convert_book', args=(pathtoebook, conversion_data),
job_done_callback=job_done
)
expire_old_jobs()
with cache_lock:
conversion_jobs[job_id] = JobStatus(job_id, tdir, library_id, pathtoebook, conversion_data)
return job_id
@endpoint('/conversion/start/{book_id}', postprocess=json, needs_db_write=True, types={'book_id': int}, methods=receive_data_methods)
def start_conversion(ctx, rd, book_id):
db, library_id = get_library_data(ctx, rd)[:2]
if not ctx.has_id(rd, db, book_id):
raise BookNotFound(book_id, db)
data = json.loads(rd.request_body_file.read())
input_fmt = data['input_fmt']
job_id = queue_job(ctx, rd, library_id, partial(db.copy_format_to, book_id, input_fmt), input_fmt, book_id, data)
return job_id
@endpoint('/conversion/status/{job_id}', postprocess=json, needs_db_write=True, types={'job_id': int})
def conversion_status(ctx, rd, job_id):
with cache_lock:
job_status = conversion_jobs.get(job_id)
if job_status is None:
raise HTTPNotFound('No job with id: {}'.format(job_id))
job_status.last_check_at = monotonic()
if job_status.running:
pass
else:
del conversion_jobs[job_id]
job_status.cleanup()
@endpoint('/conversion/book-data/{book_id}', postprocess=json, types={'book_id': int})
def conversion_data(ctx, rd, book_id):
from calibre.ebooks.conversion.config import (
@ -40,5 +142,6 @@ def conversion_data(ctx, rd, book_id):
'conversion_specifics': load_specifics(db, book_id),
'title': db.field_for('title', book_id),
'authors': db.field_for('authors', book_id),
'book_id': book_id
}
return ans

View File

@ -5,14 +5,15 @@ from __python__ import bound_methods, hash_literals
from elementmaker import E
from gettext import gettext as _
from ajax import ajax
from ajax import ajax, ajax_send
from book_list.book_details import report_load_failure
from book_list.library_data import load_status, url_books_query
from book_list.router import back, report_a_load_failure
from book_list.top_bar import create_top_bar, set_title
from book_list.ui import set_panel_handler
from dom import add_extra_css, build_rule, clear
from dom import add_extra_css, build_rule, clear, ensure_id
from utils import conditional_timeout, parse_url_params
from widgets import create_button
CLASS_NAME = 'convert-book-panel'
@ -27,18 +28,64 @@ conversion_data = None
conversion_data_load_status = {'loading':True, 'ok':False, 'error_html':None, 'current_fetch':None}
def on_conversion_started(end_type, xhr, ev):
pass
def get_conversion_options(container):
ans = {}
for k in Object.keys(conversion_data.conversion_defaults):
if Object.prototype.hasOwnProperty.call(conversion_data.conversion_specifics, k):
ans[k] = conversion_data.conversion_specifics[k]
else:
ans[k] = conversion_data.conversion_defaults[k]
return ans
def start_conversion():
container = document.getElementById(conversion_data.container_id)
data = {
'input_fmt': container.querySelector('select[name="input_formats"]').value,
'output_fmt': container.querySelector('select[name="output_formats"]').value,
'options': get_conversion_options(container),
'book_id': conversion_data.book_id,
}
query = url_books_query()
ajax_send(f'/conversion/start/{conversion_data.book_id}', data, on_conversion_started, query=query)
def create_convert_book(container):
conversion_data.container_id = container.id
conversion_data.container_id = ensure_id(container)
set_title(container.parentNode, _('Convert: {}').format(conversion_data.title))
top = E.div(style="display: flex",
container.appendChild(E.div(class_='top'))
def generate_choice(name):
formats = conversion_data[name]
ans = E.select(name=name)
for fmt in formats:
ans.appendChild(E.option(fmt, value=fmt))
return ans
tcell = 'display: table-cell; padding-top: 1em; padding-left: 1em'
start_conv = E.div(
E.div(
_('Input format')
E.div(
style='display: table-row',
E.div(style=tcell, _('Input format:')),
E.div(generate_choice('input_formats'), style=tcell),
),
E.div(
_('Output format')
style='display: table-row',
E.div(style=tcell, _('Output format:')),
E.div(generate_choice('output_formats'), style=tcell),
)
),
E.div(
style='margin: 1em',
create_button(_('Start conversion'), action=start_conversion)
)
)
container.appendChild(top)
container.lastChild.appendChild(start_conv)
# Initialization {{{
@ -52,7 +99,6 @@ def on_data_loaded(end_type, xhr, ev):
def bad_load(msg):
conversion_data_load_status.ok = False
conversion_data_load_status.loading = False
conversion_data_load_status.error_html = msg or xhr.error_html
if end_type is 'load':