mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
194 lines
7.1 KiB
Python
194 lines
7.1 KiB
Python
#!/usr/bin/env python
|
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
|
|
|
|
|
__license__ = 'GPL v3'
|
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
__docformat__ = 'restructuredtext en'
|
|
|
|
import os
|
|
from collections import OrderedDict
|
|
|
|
from PyQt5.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel,
|
|
QDialogButtonBox, QIcon)
|
|
|
|
from calibre.gui2 import error_dialog, gprefs
|
|
from calibre.gui2.actions import InterfaceAction
|
|
from calibre.utils.monotonic import monotonic
|
|
from polyglot.builtins import iteritems, unicode_type
|
|
|
|
SUPPORTED = {'EPUB', 'AZW3'}
|
|
|
|
|
|
class ChooseFormat(QDialog): # {{{
|
|
|
|
def __init__(self, formats, parent=None):
|
|
QDialog.__init__(self, parent)
|
|
self.setWindowTitle(_('Choose format to edit'))
|
|
self.setWindowIcon(QIcon(I('dialog_question.png')))
|
|
l = self.l = QGridLayout()
|
|
self.setLayout(l)
|
|
la = self.la = QLabel(_('Choose which format you want to edit:'))
|
|
formats = sorted(formats)
|
|
l.addWidget(la, 0, 0, 1, -1)
|
|
self.buttons = []
|
|
for i, f in enumerate(formats):
|
|
b = QCheckBox('&' + f, self)
|
|
l.addWidget(b, 1, i)
|
|
self.buttons.append(b)
|
|
self.formats = gprefs.get('edit_toc_last_selected_formats', ['EPUB',])
|
|
bb = self.bb = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel)
|
|
bb.addButton(_('&All formats'),
|
|
bb.ActionRole).clicked.connect(self.do_all)
|
|
bb.accepted.connect(self.accept)
|
|
bb.rejected.connect(self.reject)
|
|
l.addWidget(bb, l.rowCount(), 0, 1, -1)
|
|
self.resize(self.sizeHint())
|
|
connect_lambda(self.finished, self, lambda self, code:gprefs.set('edit_toc_last_selected_formats', list(self.formats)))
|
|
|
|
def do_all(self):
|
|
for b in self.buttons:
|
|
b.setChecked(True)
|
|
self.accept()
|
|
|
|
@property
|
|
def formats(self):
|
|
for b in self.buttons:
|
|
if b.isChecked():
|
|
yield unicode_type(b.text())[1:]
|
|
|
|
@formats.setter
|
|
def formats(self, formats):
|
|
formats = {x.upper() for x in formats}
|
|
for b in self.buttons:
|
|
b.setChecked(b.text()[1:] in formats)
|
|
|
|
# }}}
|
|
|
|
|
|
class ToCEditAction(InterfaceAction):
|
|
|
|
name = 'Edit ToC'
|
|
action_spec = (_('Edit ToC'), 'toc.png',
|
|
_('Edit the Table of Contents in your books'), _('K'))
|
|
dont_add_to = frozenset(['context-menu-device'])
|
|
action_type = 'current'
|
|
accepts_drops = True
|
|
|
|
def accept_enter_event(self, event, mime_data):
|
|
if mime_data.hasFormat("application/calibre+from_library"):
|
|
return True
|
|
return False
|
|
|
|
def accept_drag_move_event(self, event, mime_data):
|
|
if mime_data.hasFormat("application/calibre+from_library"):
|
|
return True
|
|
return False
|
|
|
|
def drop_event(self, event, mime_data):
|
|
mime = 'application/calibre+from_library'
|
|
if mime_data.hasFormat(mime):
|
|
self.dropped_ids = tuple(map(int, mime_data.data(mime).data().split()))
|
|
QTimer.singleShot(1, self.do_drop)
|
|
return True
|
|
return False
|
|
|
|
def do_drop(self):
|
|
book_id_map = self.get_supported_books(self.dropped_ids)
|
|
del self.dropped_ids
|
|
if book_id_map:
|
|
self.do_edit(book_id_map)
|
|
|
|
def genesis(self):
|
|
self.qaction.triggered.connect(self.edit_books)
|
|
self.jobs = []
|
|
|
|
def get_supported_books(self, book_ids):
|
|
db = self.gui.library_view.model().db
|
|
supported = set(SUPPORTED)
|
|
ans = [(x, set((db.formats(x, index_is_id=True) or '').split(','))
|
|
.intersection(supported)) for x in book_ids]
|
|
ans = [x for x in ans if x[1]]
|
|
if not ans:
|
|
error_dialog(self.gui, _('Cannot edit ToC'),
|
|
_('Editing Table of Contents is only supported for books in the %s'
|
|
' formats. Convert to one of those formats before polishing.')
|
|
%_(' or ').join(sorted(supported)), show=True)
|
|
ans = OrderedDict(ans)
|
|
return ans
|
|
|
|
def get_books_for_editing(self):
|
|
rows = [r.row() for r in
|
|
self.gui.library_view.selectionModel().selectedRows()]
|
|
if not rows or len(rows) == 0:
|
|
d = error_dialog(self.gui, _('Cannot edit ToC'),
|
|
_('No books selected'))
|
|
d.exec_()
|
|
return None
|
|
db = self.gui.current_db
|
|
ans = (db.id(r) for r in rows)
|
|
return self.get_supported_books(ans)
|
|
|
|
def do_edit(self, book_id_map):
|
|
for book_id, fmts in iteritems(book_id_map):
|
|
if len(fmts) > 1:
|
|
d = ChooseFormat(fmts, self.gui)
|
|
if d.exec_() != QDialog.DialogCode.Accepted:
|
|
return
|
|
fmts = d.formats
|
|
for fmt in fmts:
|
|
self.do_one(book_id, fmt)
|
|
|
|
def do_one(self, book_id, fmt):
|
|
db = self.gui.current_db
|
|
path = db.format(book_id, fmt, index_is_id=True, as_path=True)
|
|
title = db.title(book_id, index_is_id=True) + ' [%s]'%fmt
|
|
data = {'path': path, 'title': title}
|
|
self.gui.job_manager.launch_gui_app('toc-dialog', kwargs=data)
|
|
job = data.copy()
|
|
job.update({'book_id': book_id, 'fmt': fmt, 'library_id': db.new_api.library_id, 'started': False, 'start_time': monotonic()})
|
|
self.jobs.append(job)
|
|
self.check_for_completions()
|
|
|
|
def check_for_completions(self):
|
|
from calibre.utils.lock import lock_file
|
|
for job in tuple(self.jobs):
|
|
lock_path = job['path'] + '.lock'
|
|
if job['started']:
|
|
if not os.path.exists(lock_path):
|
|
self.jobs.remove(job)
|
|
continue
|
|
try:
|
|
lf = lock_file(lock_path, timeout=0.01, sleep_time=0.005)
|
|
except EnvironmentError:
|
|
continue
|
|
else:
|
|
self.jobs.remove(job)
|
|
ret = int(lf.read().decode('ascii'))
|
|
lf.close()
|
|
os.remove(lock_path)
|
|
if ret == 0:
|
|
db = self.gui.current_db
|
|
if db.new_api.library_id != job['library_id']:
|
|
error_dialog(self.gui, _('Library changed'), _(
|
|
'Cannot save changes made to {0} by the ToC editor as'
|
|
' the calibre library has changed.').format(job['title']), show=True)
|
|
else:
|
|
db.new_api.add_format(job['book_id'], job['fmt'], job['path'], run_hooks=False)
|
|
os.remove(job['path'])
|
|
else:
|
|
if monotonic() - job['start_time'] > 10:
|
|
self.jobs.remove(job)
|
|
continue
|
|
if os.path.exists(lock_path):
|
|
job['started'] = True
|
|
if self.jobs:
|
|
QTimer.singleShot(100, self.check_for_completions)
|
|
|
|
def edit_books(self):
|
|
book_id_map = self.get_books_for_editing()
|
|
if not book_id_map:
|
|
return
|
|
self.do_edit(book_id_map)
|