An option to automatically convert any added book to the current output format, found under Preferences->Adding books

E-book viewer: Fix restoring from fullscreen not respecting maximized window state
E-book viewer: Add many more controls to the context menu, particularly useful in full screen mode
E-book viewer: Allow easy searching of the selected word or phrase in google via the context menu
Add a new type of FileType plugin, postimport, that runs after a book has been added to the database.
This commit is contained in:
Kovid Goyal 2012-11-19 15:17:49 +05:30
commit 694c8a654b
11 changed files with 209 additions and 73 deletions

View File

@ -308,6 +308,10 @@ class FileTypePlugin(Plugin): # {{{
#: to the database
on_import = False
#: If True, this plugin is run after books are added
#: to the database
on_postimport = False
#: If True, this plugin is run just before a conversion
on_preprocess = False
@ -337,6 +341,16 @@ class FileTypePlugin(Plugin): # {{{
# Default implementation does nothing
return path_to_ebook
def postimport(self, book_id, book_format, db):
'''
Called post import, i.e., after the book file has been added to the database.
:param book_id: Database id of the added book.
:param book_format: The file type of the book that was added.
:param db: Library database.
'''
pass # Default implementation does nothing
# }}}
class MetadataReaderPlugin(Plugin): # {{{

View File

@ -104,14 +104,17 @@ def is_disabled(plugin):
# File type plugins {{{
_on_import = {}
_on_postimport = {}
_on_preprocess = {}
_on_postprocess = {}
def reread_filetype_plugins():
global _on_import
global _on_postimport
global _on_preprocess
global _on_postprocess
_on_import = {}
_on_postimport = {}
_on_preprocess = {}
_on_postprocess = {}
@ -122,6 +125,10 @@ def reread_filetype_plugins():
if not _on_import.has_key(ft):
_on_import[ft] = []
_on_import[ft].append(plugin)
if plugin.on_postimport:
if not _on_postimport.has_key(ft):
_on_postimport[ft] = []
_on_postimport[ft].append(plugin)
if plugin.on_preprocess:
if not _on_preprocess.has_key(ft):
_on_preprocess[ft] = []
@ -163,6 +170,22 @@ run_plugins_on_preprocess = functools.partial(_run_filetype_plugins,
occasion='preprocess')
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
occasion='postprocess')
def run_plugins_on_postimport(db, book_id, fmt):
customization = config['plugin_customization']
fmt = fmt.lower()
for plugin in _on_postimport.get(fmt, []):
if is_disabled(plugin):
continue
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
plugin.postimport(book_id, fmt, db)
except:
print ('Running file type plugin %s failed with traceback:'%
plugin.name)
traceback.print_exc()
# }}}
# Plugin customization {{{

View File

@ -49,8 +49,9 @@ class EbookIterator(BookmarksMixin):
CHARACTERS_PER_PAGE = 1000
def __init__(self, pathtoebook, log=None):
def __init__(self, pathtoebook, log=None, exclude_cover=False):
self.log = log or default_log
self.exclude_cover = exclude_cover
pathtoebook = pathtoebook.strip()
self.pathtoebook = os.path.abspath(pathtoebook)
self.config = DynamicConfig(name='iterator')
@ -142,16 +143,23 @@ class EbookIterator(BookmarksMixin):
self.log.warn('Missing spine item:', repr(spath))
cover = self.opf.cover
if cover and self.ebook_ext in {'lit', 'mobi', 'prc', 'opf', 'fb2',
'azw', 'azw3'}:
cfile = os.path.join(self.base, 'calibre_iterator_cover.html')
rcpath = os.path.relpath(cover, self.base).replace(os.sep, '/')
chtml = (TITLEPAGE%prepare_string_for_xml(rcpath, True)).encode('utf-8')
with open(cfile, 'wb') as f:
f.write(chtml)
self.spine[0:0] = [Spiny(cfile,
mime_type='application/xhtml+xml')]
self.delete_on_exit.append(cfile)
if cover:
if not self.exclude_cover and self.ebook_ext in {
'lit', 'mobi', 'prc', 'opf', 'fb2', 'azw', 'azw3'}:
cfile = os.path.join(self.base, 'calibre_iterator_cover.html')
rcpath = os.path.relpath(cover, self.base).replace(os.sep, '/')
chtml = (TITLEPAGE%prepare_string_for_xml(rcpath, True)).encode('utf-8')
with open(cfile, 'wb') as f:
f.write(chtml)
self.spine[0:0] = [Spiny(cfile,
mime_type='application/xhtml+xml')]
self.delete_on_exit.append(cfile)
elif self.exclude_cover and self.ebook_ext == 'epub':
try:
if (len(self.spine) > 1 and self.spine.index(cover) == 0):
self.spine = self.spine[1:]
except ValueError:
pass
if self.opf.path_to_html_toc is not None and \
self.opf.path_to_html_toc not in self.spine:

View File

@ -93,6 +93,7 @@ gprefs.defaults['tag_browser_dont_collapse'] = []
gprefs.defaults['edit_metadata_single_layout'] = 'default'
gprefs.defaults['default_author_link'] = 'http://en.wikipedia.org/w/index.php?search={author}'
gprefs.defaults['preserve_date_on_ctl'] = True
gprefs.defaults['manual_add_auto_convert'] = False
gprefs.defaults['cb_fullscreen'] = False
gprefs.defaults['worker_max_time'] = 0
gprefs.defaults['show_files_after_save'] = True

View File

@ -267,8 +267,8 @@ class ViewAction(InterfaceAction):
def _view_books(self, rows):
if not rows or len(rows) == 0:
self._launch_viewer()
return
return error_dialog(self.gui, _('Cannot view'),
_('No books selected'), show=True)
if not self._view_check(len(rows)):
return

View File

@ -42,6 +42,7 @@ class DuplicatesAdder(QObject): # {{{
# here we add all the formats for dupe book record created above
self.db_adder.add_formats(id, formats)
self.db_adder.number_of_books_added += 1
self.db_adder.auto_convert_books.add(id)
self.count += 1
self.added.emit(self.count)
single_shot(self.add_one)
@ -107,8 +108,16 @@ class DBAdder(QObject): # {{{
self.input_queue = Queue()
self.output_queue = Queue()
self.merged_books = set([])
self.auto_convert_books = set()
def end(self):
if (gprefs['manual_add_auto_convert'] and
self.auto_convert_books):
from calibre.gui2.ui import get_gui
gui = get_gui()
gui.iactions['Convert Books'].auto_convert_auto_add(
self.auto_convert_books)
self.input_queue.put((None, None, None))
def start(self):
@ -152,7 +161,6 @@ class DBAdder(QObject): # {{{
fmts[-1] = fmt
return fmts
def add(self, id, opf, cover, name):
formats = self.ids.pop(id)
if opf.endswith('.error'):
@ -219,6 +227,7 @@ class DBAdder(QObject): # {{{
self.duplicates.append((mi, cover, orig_formats))
else:
self.add_formats(id_, formats)
self.auto_convert_books.add(id_)
self.number_of_books_added += 1
else:
self.names.append(name)

View File

@ -28,6 +28,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('swap_author_names', prefs)
r('add_formats_to_existing', prefs)
r('preserve_date_on_ctl', gprefs)
r('manual_add_auto_convert', gprefs)
choices = [
(_('Ignore duplicate incoming formats'), 'ignore'),
(_('Overwrite existing duplicate formats'), 'overwrite'),

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>753</width>
<width>1035</width>
<height>547</height>
</rect>
</property>
@ -24,6 +24,36 @@
<string>The Add &amp;Process</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_add_formats_to_existing">
<property name="toolTip">
<string>Automerge: If books with similar titles and authors found, merge the incoming formats automatically into
existing book records. The box to the right controls what happens when an existing record already has
the incoming format. Note that this option also affects the Copy to library action.
Title match ignores leading indefinite articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;), punctuation, case, etc. Author match is exact.</string>
</property>
<property name="text">
<string>&amp;Automerge added books if they already exist in the calibre library:</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QComboBox" name="opt_automerge">
<property name="toolTip">
<string>Automerge: If books with similar titles and authors found, merge the incoming formats automatically into
existing book records. This box controls what happens when an existing record already has
the incoming format:
Ignore duplicate incoming files - means that existing files in your calibre library will not be replaced
Overwrite existing duplicate files - means that existing files in your calibre library will be replaced
Create new record for each duplicate file - means that a new book entry will be created for each duplicate file
Title matching ignores leading indefinite articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;), punctuation, case, etc.
Author matching is exact.</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="label_6">
<property name="text">
@ -68,44 +98,7 @@
</item>
</layout>
</item>
<item row="2" column="0" colspan="3">
<widget class="QCheckBox" name="opt_preserve_date_on_ctl">
<property name="text">
<string>When using the &quot;&amp;Copy to library&quot; action to copy books between libraries, preserve the date</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="opt_add_formats_to_existing">
<property name="toolTip">
<string>Automerge: If books with similar titles and authors found, merge the incoming formats automatically into
existing book records. The box to the right controls what happens when an existing record already has
the incoming format. Note that this option also affects the Copy to library action.
Title match ignores leading indefinite articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;), punctuation, case, etc. Author match is exact.</string>
</property>
<property name="text">
<string>&amp;Automerge added books if they already exist in the calibre library:</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="opt_automerge">
<property name="toolTip">
<string>Automerge: If books with similar titles and authors found, merge the incoming formats automatically into
existing book records. This box controls what happens when an existing record already has
the incoming format:
Ignore duplicate incoming files - means that existing files in your calibre library will not be replaced
Overwrite existing duplicate files - means that existing files in your calibre library will be replaced
Create new record for each duplicate file - means that a new book entry will be created for each duplicate file
Title matching ignores leading indefinite articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;), punctuation, case, etc.
Author matching is exact.</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_230">
<property name="text">
<string>&amp;Tags to apply when adding a book:</string>
@ -115,14 +108,14 @@ Author matching is exact.</string>
</property>
</widget>
</item>
<item row="4" column="2">
<item row="5" column="2">
<widget class="QLineEdit" name="opt_new_book_tags">
<property name="toolTip">
<string>A comma-separated list of tags that will be applied to books added to the library</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<item row="6" column="0" colspan="3">
<widget class="QGroupBox" name="metadata_box">
<property name="title">
<string>&amp;Configure metadata from file name</string>
@ -144,6 +137,20 @@ Author matching is exact.</string>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QCheckBox" name="opt_preserve_date_on_ctl">
<property name="text">
<string>When using the &quot;&amp;Copy to library&quot; action to copy books between libraries, preserve the date</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="opt_manual_add_auto_convert">
<property name="text">
<string>Automatically &amp;convert added books to the current output format</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_4">

View File

@ -16,6 +16,7 @@ from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from calibre.gui2.viewer.flip import SlideFlip
from calibre.gui2.shortcuts import Shortcuts
from calibre.gui2 import open_url
from calibre import prints
from calibre.customize.ui import all_viewer_plugins
from calibre.gui2.viewer.keys import SHORTCUTS
@ -481,7 +482,12 @@ class DocumentView(QWebView): # {{{
d = self.document
self.unimplemented_actions = list(map(self.pageAction,
[d.DownloadImageToDisk, d.OpenLinkInNewWindow, d.DownloadLinkToDisk,
d.OpenImageInNewWindow, d.OpenLink, d.Reload]))
d.OpenImageInNewWindow, d.OpenLink, d.Reload, d.InspectElement]))
self.search_online_action = QAction(QIcon(I('search.png')), '', self)
self.search_online_action.setShortcut(Qt.CTRL+Qt.Key_E)
self.search_online_action.triggered.connect(self.search_online)
self.addAction(self.search_online_action)
self.dictionary_action = QAction(QIcon(I('dictionary.png')),
_('&Lookup in dictionary'), self)
self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L)
@ -526,6 +532,10 @@ class DocumentView(QWebView): # {{{
self.goto_location_action.setMenu(self.goto_location_menu)
self.grabGesture(Qt.SwipeGesture)
self.restore_fonts_action = QAction(_('Normal font size'), self)
self.restore_fonts_action.setCheckable(True)
self.restore_fonts_action.triggered.connect(self.restore_font_size)
def goto_next_section(self, *args):
if self.manager is not None:
self.manager.goto_next_section()
@ -582,6 +592,15 @@ class DocumentView(QWebView): # {{{
if self.manager is not None:
self.manager.selection_changed(unicode(self.document.selectedText()))
def _selectedText(self):
t = unicode(self.selectedText()).strip()
if not t:
return u''
if len(t) > 40:
t = t[:40] + u'...'
t = t.replace(u'&', u'&&')
return _("S&earch Google for '%s'")%t
def contextMenuEvent(self, ev):
mf = self.document.mainFrame()
r = mf.hitTestContent(ev.pos())
@ -591,17 +610,48 @@ class DocumentView(QWebView): # {{{
menu = self.document.createStandardContextMenu()
for action in self.unimplemented_actions:
menu.removeAction(action)
text = unicode(self.selectedText())
if text:
menu.insertAction(list(menu.actions())[0], self.dictionary_action)
menu.insertAction(list(menu.actions())[0], self.search_action)
if not img.isNull():
menu.addAction(self.view_image_action)
menu.addSeparator()
menu.addAction(self.goto_location_action)
if self.document.in_fullscreen_mode and self.manager is not None:
text = self._selectedText()
if text and img.isNull():
self.search_online_action.setText(text)
menu.addAction(self.search_online_action)
menu.addAction(self.dictionary_action)
menu.addAction(self.search_action)
if not text and img.isNull():
menu.addSeparator()
menu.addAction(self.manager.toggle_toolbar_action)
if self.manager.action_back.isEnabled():
menu.addAction(self.manager.action_back)
if self.manager.action_forward.isEnabled():
menu.addAction(self.manager.action_forward)
menu.addAction(self.goto_location_action)
if self.manager is not None:
menu.addSeparator()
menu.addAction(self.manager.action_table_of_contents)
menu.addSeparator()
menu.addAction(self.manager.action_font_size_larger)
self.restore_fonts_action.setChecked(self.multiplier == 1)
menu.addAction(self.restore_fonts_action)
menu.addAction(self.manager.action_font_size_smaller)
menu.addSeparator()
inspectAction = self.pageAction(self.document.InspectElement)
menu.addAction(inspectAction)
if not text and img.isNull() and self.manager is not None:
menu.addSeparator()
if self.document.in_fullscreen_mode and self.manager is not None:
menu.addAction(self.manager.toggle_toolbar_action)
menu.addAction(self.manager.action_full_screen)
menu.addSeparator()
menu.addAction(self.manager.action_quit)
menu.exec_(ev.globalPos())
def lookup(self, *args):
@ -616,6 +666,12 @@ class DocumentView(QWebView): # {{{
if t:
self.manager.search.set_search_string(t)
def search_online(self):
t = unicode(self.selectedText()).strip()
if t:
url = 'https://www.google.com/search?q=' + QUrl().toPercentEncoding(t)
open_url(QUrl.fromEncoded(url))
def set_manager(self, manager):
self.manager = manager
self.scrollbar = manager.horizontal_scrollbar
@ -993,6 +1049,11 @@ class DocumentView(QWebView): # {{{
self.multiplier -= amount
return self.document.scroll_fraction
def restore_font_size(self):
with self.document.page_position:
self.multiplier = 1
return self.document.scroll_fraction
def changeEvent(self, event):
if event.type() == event.EnabledChange:
self.update()

View File

@ -178,6 +178,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.pending_restore = False
self.existing_bookmarks= []
self.selected_text = None
self.was_maximized = False
self.read_settings()
self.dictionary_box.hide()
self.close_dictionary_view.clicked.connect(lambda
@ -207,7 +208,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.view.set_manager(self)
self.pi = ProgressIndicator(self)
self.toc.setVisible(False)
self.action_quit = QAction(self)
self.action_quit = QAction(_('&Quit'), self)
self.addAction(self.action_quit)
self.view_resized_timer = QTimer(self)
self.view_resized_timer.timeout.connect(self.viewport_resize_finished)
@ -299,6 +300,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
''')
self.window_mode_changed = None
self.toggle_toolbar_action = QAction(_('Show/hide controls'), self)
self.toggle_toolbar_action.setCheckable(True)
self.toggle_toolbar_action.triggered.connect(self.toggle_toolbars)
self.addAction(self.toggle_toolbar_action)
self.full_screen_label_anim = QPropertyAnimation(
@ -421,7 +423,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
def save_state(self):
state = bytearray(self.saveState(self.STATE_VERSION))
vprefs['viewer_toolbar_state'] = state
vprefs.set('viewer_window_geometry', bytearray(self.saveGeometry()))
if not self.isFullScreen():
vprefs.set('viewer_window_geometry', bytearray(self.saveGeometry()))
if self.current_book_has_toc:
vprefs.set('viewer_toc_isvisible', bool(self.toc.isVisible()))
if self.toc.isVisible():
@ -488,6 +491,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.window_mode_changed = 'fullscreen'
self.tool_bar.setVisible(False)
self.tool_bar2.setVisible(False)
self.was_maximized = self.isMaximized()
if not self.view.document.fullscreen_scrollbar:
self.vertical_scrollbar.setVisible(False)
self.frame.layout().setSpacing(0)
@ -574,7 +578,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
om = self._original_frame_margins
self.centralwidget.layout().setContentsMargins(om[0])
self.frame.layout().setContentsMargins(om[1])
super(EbookViewer, self).showNormal()
if self.was_maximized:
super(EbookViewer, self).showMaximized()
else:
super(EbookViewer, self).showNormal()
def handle_window_mode_toggle(self):
if self.window_mode_changed:
@ -681,13 +688,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
def font_size_larger(self):
self.view.magnify_fonts()
self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)
def font_size_smaller(self):
self.view.shrink_fonts()
self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)
def magnification_changed(self, val):
tt = _('Make font size %(which)s\nCurrent magnification: %(mag).1f')
@ -695,6 +698,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
tt %dict(which=_('larger'), mag=val))
self.action_font_size_smaller.setToolTip(
tt %dict(which=_('smaller'), mag=val))
self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)
def find(self, text, repeat=False, backwards=False):
if not text:

View File

@ -28,7 +28,8 @@ from calibre.ebooks.metadata.book.base import Metadata
from calibre.constants import preferred_encoding, iswindows, filesystem_encoding
from calibre.ptempfile import (PersistentTemporaryFile,
base_dir, SpooledTemporaryFile)
from calibre.customize.ui import run_plugins_on_import
from calibre.customize.ui import (run_plugins_on_import,
run_plugins_on_postimport)
from calibre import isbytestring
from calibre.utils.filenames import (ascii_filename, samefile,
WindowsAtomicFolderMove, hardlink_file)
@ -1495,8 +1496,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
format = os.path.splitext(npath)[-1].lower().replace('.', '').upper()
stream = lopen(npath, 'rb')
format = check_ebook_format(stream, format)
return self.add_format(index, format, stream,
retval = self.add_format(index, format, stream,
index_is_id=index_is_id, path=path, notify=notify)
run_plugins_on_postimport(self, id, format)
return retval
def add_format(self, index, format, stream, index_is_id=False, path=None,
notify=True, replace=True, copy_function=None):
@ -3475,6 +3478,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
formats, metadata = iter(formats), iter(metadata)
duplicates = []
ids = []
postimport = []
for path in paths:
mi = metadata.next()
self._add_newbook_tag(mi)
@ -3506,8 +3510,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
format = check_ebook_format(stream, format)
self.add_format(id, format, stream, index_is_id=True)
stream.close()
postimport.append((id, format))
self.conn.commit()
self.data.refresh_ids(self, ids) # Needed to update format list and size
for book_id, fmt in postimport:
run_plugins_on_postimport(self, book_id, fmt)
if duplicates:
paths = list(duplicate[0] for duplicate in duplicates)
formats = list(duplicate[1] for duplicate in duplicates)