mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
More work on a server conversion interface
This commit is contained in:
parent
76335ad2c2
commit
fbea3b9b75
@ -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
|
||||
|
@ -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'
|
||||
|
||||
@ -24,21 +25,67 @@ add_extra_css(def():
|
||||
|
||||
|
||||
conversion_data = None
|
||||
conversion_data_load_status = {'loading':True, 'ok':False, 'error_html':None, 'current_fetch': 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(
|
||||
style='display: table-row',
|
||||
E.div(style=tcell, _('Output format:')),
|
||||
E.div(generate_choice('output_formats'), style=tcell),
|
||||
)
|
||||
),
|
||||
E.div(
|
||||
_('Output format')
|
||||
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':
|
||||
|
Loading…
x
Reference in New Issue
Block a user