%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/ |
| Current File : //lib/calibre/calibre/srv/convert.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import shutil
import tempfile
from threading import Lock
from calibre.customize.ui import input_profiles, output_profiles
from calibre.db.errors import NoSuchBook
from calibre.srv.changes import formats_added
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
from calibre.utils.shared_file import share_open
from polyglot.builtins import iteritems
receive_data_methods = {'GET', 'POST'}
conversion_jobs = {}
cache_lock = Lock()
class JobStatus:
def __init__(self, job_id, book_id, tdir, library_id, pathtoebook, conversion_data):
self.job_id = job_id
self.log = self.traceback = ''
self.book_id = book_id
self.output_path = os.path.join(
tdir, 'output.' + conversion_data['output_fmt'].lower())
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()
self.was_aborted = False
def cleanup(self):
safe_delete_tree(self.tdir)
self.log = self.traceback = ''
@property
def current_status(self):
try:
with share_open(os.path.join(self.tdir, 'status'), 'rb') as f:
lines = f.read().decode('utf-8').splitlines()
except Exception:
lines = ()
for line in reversed(lines):
if line.endswith('|||'):
p, msg = line.partition(':')[::2]
percent = float(p)
msg = msg[:-3]
return percent, msg
return 0, ''
def expire_old_jobs():
now = monotonic()
with cache_lock:
remove = [job_id for job_id, job_status in iteritems(conversion_jobs) if now - job_status.last_check_at >= 360]
for job_id in remove:
job_status = conversion_jobs.pop(job_id)
job_status.cleanup()
def safe_delete_file(path):
try:
os.remove(path)
except OSError:
pass
def safe_delete_tree(path):
try:
shutil.rmtree(path, ignore_errors=True)
except OSError:
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.log = job.read_log()
job_status.was_aborted = job.was_aborted
job_status.traceback = job.traceback
safe_delete_file(job_status.pathtoebook)
def convert_book(path_to_ebook, opf_path, cover_path, output_fmt, recs):
from calibre.customize.conversion import OptionRecommendation
from calibre.ebooks.conversion.plumber import Plumber
from calibre.utils.logging import Log
recs.append(('verbose', 2, OptionRecommendation.HIGH))
recs.append(('read_metadata_from_opf', opf_path,
OptionRecommendation.HIGH))
if cover_path:
recs.append(('cover', cover_path, OptionRecommendation.HIGH))
log = Log()
os.chdir(os.path.dirname(path_to_ebook))
status_file = share_open('status', 'wb')
def notification(percent, msg=''):
status_file.write(f'{percent}:{msg}|||\n'.encode())
status_file.flush()
output_path = os.path.abspath('output.' + output_fmt.lower())
plumber = Plumber(path_to_ebook, output_path, log,
report_progress=notification, override_input_metadata=True)
plumber.merge_ui_recommendations(recs)
plumber.run()
def queue_job(ctx, rd, library_id, db, fmt, book_id, conversion_data):
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.conversion.config import GuiRecommendations, save_specifics
from calibre.customize.conversion import OptionRecommendation
tdir = tempfile.mkdtemp(dir=rd.tdir)
with tempfile.NamedTemporaryFile(prefix='', suffix=('.' + fmt.lower()), dir=tdir, delete=False) as src_file:
db.copy_format_to(book_id, fmt, src_file)
with tempfile.NamedTemporaryFile(prefix='', suffix='.jpg', dir=tdir, delete=False) as cover_file:
cover_copied = db.copy_cover_to(book_id, cover_file)
cover_path = cover_file.name if cover_copied else None
mi = db.get_metadata(book_id)
mi.application_id = mi.uuid
raw = metadata_to_opf(mi)
with tempfile.NamedTemporaryFile(prefix='', suffix='.opf', dir=tdir, delete=False) as opf_file:
opf_file.write(raw)
recs = GuiRecommendations()
recs.update(conversion_data['options'])
recs['gui_preferred_input_format'] = conversion_data['input_fmt'].lower()
save_specifics(db, book_id, recs)
recs = [(k, v, OptionRecommendation.HIGH) for k, v in iteritems(recs)]
job_id = ctx.start_job(
f'Convert book {book_id} ({fmt})', 'calibre.srv.convert',
'convert_book', args=(
src_file.name, opf_file.name, cover_path, conversion_data['output_fmt'], recs),
job_done_callback=job_done
)
expire_old_jobs()
with cache_lock:
conversion_jobs[job_id] = JobStatus(
job_id, book_id, tdir, library_id, src_file.name, 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, db, input_fmt, book_id, data)
return job_id
@endpoint('/conversion/status/{job_id}', postprocess=json, needs_db_write=True, types={'job_id': int}, methods=receive_data_methods)
def conversion_status(ctx, rd, job_id):
with cache_lock:
job_status = conversion_jobs.get(job_id)
if job_status is None:
raise HTTPNotFound(f'No job with id: {job_id}')
job_status.last_check_at = monotonic()
if job_status.running:
percent, msg = job_status.current_status
if rd.query.get('abort_job'):
ctx.abort_job(job_id)
return {'running': True, 'percent': percent, 'msg': msg}
del conversion_jobs[job_id]
try:
ans = {'running': False, 'ok': job_status.ok, 'was_aborted':
job_status.was_aborted, 'traceback': job_status.traceback,
'log': job_status.log}
if job_status.ok:
db, library_id = get_library_data(ctx, rd)[:2]
if library_id != job_status.library_id:
raise HTTPNotFound('job library_id does not match')
fmt = job_status.output_path.rpartition('.')[-1]
try:
db.add_format(job_status.book_id, fmt, job_status.output_path)
except NoSuchBook:
raise HTTPNotFound(
f'book_id {job_status.book_id} not found in library')
formats_added({job_status.book_id: (fmt,)})
ans['size'] = os.path.getsize(job_status.output_path)
ans['fmt'] = fmt
return ans
finally:
job_status.cleanup()
def get_conversion_options(input_fmt, output_fmt, book_id, db):
from calibre.ebooks.conversion.plumber import create_dummy_plumber
from calibre.ebooks.conversion.config import (
load_specifics, load_defaults, OPTIONS, options_for_input_fmt, options_for_output_fmt)
from calibre.customize.conversion import OptionRecommendation
plumber = create_dummy_plumber(input_fmt, output_fmt)
specifics = load_specifics(db, book_id)
ans = {'options': {}, 'disabled': set(), 'defaults': {}, 'help': {}}
ans['input_plugin_name'] = plumber.input_plugin.commit_name
ans['output_plugin_name'] = plumber.output_plugin.commit_name
ans['input_ui_data'] = plumber.input_plugin.ui_data
ans['output_ui_data'] = plumber.output_plugin.ui_data
def merge_group(group_name, option_names):
if not group_name or group_name in ('debug', 'metadata'):
return
defs = load_defaults(group_name)
defs.merge_recommendations(
plumber.get_option_by_name, OptionRecommendation.LOW, option_names)
specifics.merge_recommendations(
plumber.get_option_by_name, OptionRecommendation.HIGH, option_names, only_existing=True)
defaults = defs.as_dict()['options']
for k in defs:
if k in specifics:
defs[k] = specifics[k]
defs = defs.as_dict()
ans['options'].update(defs['options'])
ans['disabled'] |= set(defs['disabled'])
ans['defaults'].update(defaults)
ans['help'] = plumber.get_all_help()
for group_name, option_names in iteritems(OPTIONS['pipe']):
merge_group(group_name, option_names)
group_name, option_names = options_for_input_fmt(input_fmt)
merge_group(group_name, option_names)
group_name, option_names = options_for_output_fmt(output_fmt)
merge_group(group_name, option_names)
ans['disabled'] = tuple(ans['disabled'])
return ans
def profiles():
ans = getattr(profiles, 'ans', None)
if ans is None:
def desc(profile):
w, h = profile.screen_size
if w >= 10000:
ss = _('unlimited')
else:
ss = _('%(width)d x %(height)d pixels') % dict(width=w, height=h)
ss = _('Screen size: %s') % ss
return {'name': profile.name, 'description': (f'{profile.description} [{ss}]')}
ans = profiles.ans = {}
ans['input'] = {p.short_name: desc(p) for p in input_profiles()}
ans['output'] = {p.short_name: desc(p) for p in output_profiles()}
return ans
@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 (
NoSupportedInputFormats, get_input_format_for_book, get_sorted_output_formats)
db = get_library_data(ctx, rd)[0]
if not ctx.has_id(rd, db, book_id):
raise BookNotFound(book_id, db)
try:
input_format, input_formats = get_input_format_for_book(db, book_id)
except NoSupportedInputFormats:
input_formats = []
else:
if rd.query.get('input_fmt') and rd.query.get('input_fmt').lower() in input_formats:
input_format = rd.query.get('input_fmt').lower()
if input_format in input_formats:
input_formats.remove(input_format)
input_formats.insert(0, input_format)
input_fmt = input_formats[0] if input_formats else 'epub'
output_formats = get_sorted_output_formats(rd.query.get('output_fmt'))
ans = {
'input_formats': [x.upper() for x in input_formats],
'output_formats': output_formats,
'profiles': profiles(),
'conversion_options': get_conversion_options(input_fmt, output_formats[0], book_id, db),
'title': db.field_for('title', book_id),
'authors': db.field_for('authors', book_id),
'book_id': book_id
}
return ans