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 __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.routes import endpoint, json
|
||||||
from calibre.srv.utils import get_library_data
|
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():
|
def conversion_defaults():
|
||||||
@ -17,6 +50,75 @@ def conversion_defaults():
|
|||||||
return ans
|
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})
|
@endpoint('/conversion/book-data/{book_id}', postprocess=json, types={'book_id': int})
|
||||||
def conversion_data(ctx, rd, book_id):
|
def conversion_data(ctx, rd, book_id):
|
||||||
from calibre.ebooks.conversion.config import (
|
from calibre.ebooks.conversion.config import (
|
||||||
@ -40,5 +142,6 @@ def conversion_data(ctx, rd, book_id):
|
|||||||
'conversion_specifics': load_specifics(db, book_id),
|
'conversion_specifics': load_specifics(db, book_id),
|
||||||
'title': db.field_for('title', book_id),
|
'title': db.field_for('title', book_id),
|
||||||
'authors': db.field_for('authors', book_id),
|
'authors': db.field_for('authors', book_id),
|
||||||
|
'book_id': book_id
|
||||||
}
|
}
|
||||||
return ans
|
return ans
|
||||||
|
@ -5,14 +5,15 @@ from __python__ import bound_methods, hash_literals
|
|||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
from gettext import gettext as _
|
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.book_details import report_load_failure
|
||||||
from book_list.library_data import load_status, url_books_query
|
from book_list.library_data import load_status, url_books_query
|
||||||
from book_list.router import back, report_a_load_failure
|
from book_list.router import back, report_a_load_failure
|
||||||
from book_list.top_bar import create_top_bar, set_title
|
from book_list.top_bar import create_top_bar, set_title
|
||||||
from book_list.ui import set_panel_handler
|
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 utils import conditional_timeout, parse_url_params
|
||||||
|
from widgets import create_button
|
||||||
|
|
||||||
CLASS_NAME = 'convert-book-panel'
|
CLASS_NAME = 'convert-book-panel'
|
||||||
|
|
||||||
@ -24,21 +25,67 @@ add_extra_css(def():
|
|||||||
|
|
||||||
|
|
||||||
conversion_data = None
|
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):
|
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))
|
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(
|
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(
|
E.div(
|
||||||
_('Output format')
|
style='margin: 1em',
|
||||||
|
create_button(_('Start conversion'), action=start_conversion)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
container.appendChild(top)
|
container.lastChild.appendChild(start_conv)
|
||||||
|
|
||||||
|
|
||||||
# Initialization {{{
|
# Initialization {{{
|
||||||
@ -52,7 +99,6 @@ def on_data_loaded(end_type, xhr, ev):
|
|||||||
|
|
||||||
def bad_load(msg):
|
def bad_load(msg):
|
||||||
conversion_data_load_status.ok = False
|
conversion_data_load_status.ok = False
|
||||||
conversion_data_load_status.loading = False
|
|
||||||
conversion_data_load_status.error_html = msg or xhr.error_html
|
conversion_data_load_status.error_html = msg or xhr.error_html
|
||||||
|
|
||||||
if end_type is 'load':
|
if end_type is 'load':
|
||||||
|
Loading…
x
Reference in New Issue
Block a user