Pull from Trunk

This commit is contained in:
Timothy Legge 2010-06-02 21:49:47 -03:00
commit 7bc7e3d308
19 changed files with 950 additions and 63 deletions

View File

@ -240,6 +240,9 @@ class OutputProfile(Plugin):
# Device supports displaying a nested TOC
supports_nested_toc = True
# If True output should be optimized for a touchscreen interface
touchscreen = False
@classmethod
def tags_to_string(cls, tags):
return escape(', '.join(tags))
@ -254,6 +257,7 @@ class iPadOutput(OutputProfile):
comic_screen_size = (768, 1024)
dpi = 132.0
supports_nested_toc = False
touchscreen = True
class SonyReaderOutput(OutputProfile):

View File

@ -1229,8 +1229,10 @@ class ITUNES(DevicePlugin):
self.iTunes.delete(cached_book['lib_book'])
elif iswindows:
# Assume we're wrapped in a pythoncom
# Windows stores the book under a common author directory, so we just delete the .epub
'''
Assume we're wrapped in a pythoncom
Windows stores the book under a common author directory, so we just delete the .epub
'''
if DEBUG:
self.log.info("ITUNES._remove_from_iTunes(): '%s'" % cached_book['title'])
book = self._find_library_book(cached_book)

View File

@ -97,7 +97,8 @@ def _config():
help=_('Overwrite author and title with new metadata'))
c.add_opt('enforce_cpu_limit', default=True,
help=_('Limit max simultaneous jobs to number of CPUs'))
c.add_opt('tag_browser_hidden_categories', default=set(),
help=_('tag browser categories not to display'))
return ConfigProxy(c)
config = _config()

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
from calibre.constants import iswindows, isosx
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
from calibre.gui2 import choose_dir, error_dialog, config, \
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog, question_dialog
from calibre.utils.config import prefs
@ -480,6 +480,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
self.device_detection_button.clicked.connect(self.debug_device_detection)
self.port.editingFinished.connect(self.check_port_value)
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True))
def check_port_value(self, *args):
port = self.port.value()
@ -852,6 +854,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
fmts = []
for i in range(self.viewer.count()):
if self.viewer.item(i).checkState() == Qt.Checked:

View File

@ -331,8 +331,8 @@
</layout>
</widget>
<widget class="QWidget" name="page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QCheckBox" name="roman_numerals">
<property name="text">
<string>Use &amp;Roman numerals for series number</string>
@ -342,28 +342,35 @@
</property>
</widget>
</item>
<item>
<item row="1" column="0">
<widget class="QCheckBox" name="systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item>
<item row="1" column="1">
<widget class="QCheckBox" name="systray_notifications">
<property name="text">
<string>Show &amp;notifications in system tray</string>
</property>
</widget>
</item>
<item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="show_splash_screen">
<property name="text">
<string>Show &amp;splash screen at startup</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item>
<item row="4" column="0">
<widget class="QCheckBox" name="search_as_you_type">
<property name="text">
<string>Search as you type</string>
@ -373,21 +380,21 @@
</property>
</widget>
</item>
<item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news">
<property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string>
</property>
</widget>
</item>
<item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news">
<property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string>
</property>
</widget>
</item>
<item>
<item row="7" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
@ -404,7 +411,7 @@
</item>
</layout>
</item>
<item>
<item row="8" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Toolbar</string>
@ -452,7 +459,7 @@
</layout>
</widget>
</item>
<item>
<item row="9" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QGroupBox" name="groupBox">
@ -527,12 +534,12 @@
</item>
<item>
<widget class="QToolButton" name="add_custcol_button">
<property name="text">
<string>...</string>
</property>
<property name="toolTip">
<string>Add a user-defined column</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>

View File

@ -0,0 +1,86 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.constants import islinux
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def __init__(self, window, initial_search=None):
QDialog.__init__(self, window)
Ui_SavedSearchEditor.__init__(self)
self.setupUi(self)
self.connect(self.add_search_button, SIGNAL('clicked()'), self.add_search)
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
self.current_index_changed)
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search)
self.current_search_name = None
self.searches = {}
self.searches_to_delete = []
for name in saved_searches.names():
self.searches[name] = saved_searches.lookup(name)
self.populate_search_list()
if initial_search is not None and initial_search in self.searches:
self.select_search(initial_search)
def populate_search_list(self):
self.search_name_box.clear()
for name in sorted(self.searches.keys()):
self.search_name_box.addItem(name)
def add_search(self):
search_name = unicode(self.input_box.text()).strip()
if search_name == '':
return False
if search_name not in self.searches:
self.searches[search_name] = ''
self.populate_search_list()
self.select_search(search_name)
else:
self.select_search(search_name)
return True
def del_search(self):
if self.current_search_name is not None:
if not confirm('<p>'+_('The current saved search will be '
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'saved_search_editor_delete', self):
return
del self.searches[self.current_search_name]
self.searches_to_delete.append(self.current_search_name)
self.current_search_name = None
self.search_name_box.removeItem(self.search_name_box.currentIndex())
def select_search(self, name):
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
def current_index_changed(self, idx):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
name = unicode(self.search_name_box.itemText(idx))
if name:
self.current_search_name = name
self.search_text.setPlainText(self.searches[name])
else:
self.current_search_name = None
self.search_text.setPlainText('')
def accept(self):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
for name in self.searches_to_delete:
saved_searches.delete(name)
for name in self.searches:
saved_searches.add(name, self.searches[name])
QDialog.accept(self)

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SavedSearchEditor</class>
<widget class="QDialog" name="SavedSearchEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>548</width>
<height>148</height>
</rect>
</property>
<property name="windowTitle">
<string>Saved Search Editor</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
</property>
<layout class="QGridLayout">
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Saved Search: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>search_name_box</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="search_name_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>160</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>145</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Select a saved search to edit</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="delete_search_button">
<property name="toolTip">
<string>Delete this selected saved search</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="4">
<widget class="QLineEdit" name="input_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>60</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a new saved search name.</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QToolButton" name="add_search_button">
<property name="toolTip">
<string>Add the new saved search</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QPlainTextEdit" name="search_text">
<property name="toolTip">
<string>Change the contents of the saved search</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../../calibre_datesearch/resources/images"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SavedSearchEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SavedSearchEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -24,13 +24,12 @@ class Item:
class TagCategories(QDialog, Ui_TagCategories):
category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags']
def __init__(self, window, db, index=None):
def __init__(self, window, db, on_category=None):
QDialog.__init__(self, window)
Ui_TagCategories.__init__(self)
self.setupUi(self)
self.db = db
self.index = index
self.applied_items = []
cc_icon = QIcon(I('column.svg'))
@ -102,8 +101,10 @@ class TagCategories(QDialog, Ui_TagCategories):
self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
self.populate_category_list()
return
self.select_category(0)
if on_category is not None:
l = self.category_box.findText(on_category)
if l >= 0:
self.category_box.setCurrentIndex(l)
def make_list_widget(self, item):
n = item.name if item.exists else item.name + _(' (not on any book)')

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Tag Editor</string>
<string>User Categories Editor</string>
</property>
<property name="windowIcon">
<iconset>

View File

@ -0,0 +1,89 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QListWidgetItem
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
from calibre.gui2 import question_dialog, error_dialog
class TagListEditor(QDialog, Ui_TagListEditor):
def tag_cmp(self, x, y):
return cmp(x.lower(), y.lower())
def __init__(self, window, db, tag_to_match):
QDialog.__init__(self, window)
Ui_TagListEditor.__init__(self)
self.setupUi(self)
self.to_rename = {}
self.to_delete = []
self.db = db
self.all_tags = {}
for k,v in db.get_tags_with_ids():
self.all_tags[v] = k
for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp):
item = QListWidgetItem(tag)
item.setData(Qt.UserRole, self.all_tags[tag])
self.available_tags.addItem(item)
items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly)
if len(items) == 1:
self.available_tags.setCurrentItem(items[0])
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
self.connect(self.rename_button, SIGNAL('clicked()'), self.rename_tag)
self.connect(self.available_tags, SIGNAL('itemDoubleClicked(QListWidgetItem *)'), self._rename_tag)
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
def finish_editing(self, item):
if item.text() != self.item_before_editing.text():
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
error_dialog(self, 'Tag already used',
'The tag %s is already used.'%(item.text())).exec_()
item.setText(self.item_before_editing.text())
return
id,ign = self.item_before_editing.data(Qt.UserRole).toInt()
self.to_rename[item.text()] = id
def rename_tag(self):
item = self.available_tags.currentItem()
self._rename_tag(item)
def _rename_tag(self, item):
if item is None:
error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_()
return
self.item_before_editing = item.clone()
item.setFlags (item.flags() | Qt.ItemIsEditable);
self.available_tags.editItem(item)
def delete_tags(self, item=None):
confirms, deletes = [], []
items = self.available_tags.selectedItems() if item is None else [item]
if not items:
error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_()
return
for item in items:
if self.db.is_tag_used(unicode(item.text())):
confirms.append(item)
else:
deletes.append(item)
if confirms:
ct = ', '.join([unicode(item.text()) for item in confirms])
if question_dialog(self, _('Are your sure?'),
'<p>'+_('The following tags are used by one or more books. '
'Are you certain you want to delete them?')+'<br>'+ct):
deletes += confirms
for item in deletes:
self.to_delete.append(item)
self.available_tags.takeItem(self.available_tags.row(item))
def accept(self):
for text in self.to_rename:
self.db.rename_tag(self.to_rename[text], unicode(text))
for item in self.to_delete:
self.db.delete_tag(unicode(item.text()))
QDialog.accept(self)

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TagListEditor</class>
<widget class="QDialog" name="TagListEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>335</height>
</rect>
</property>
<property name="windowTitle">
<string>Tag Editor</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Tags in use</string>
</property>
<property name="buddy">
<cstring>available_tags</cstring>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QToolButton" name="delete_button">
<property name="toolTip">
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="rename_button">
<property name="toolTip">
<string>Rename the tag everywhere it is used.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="available_tags">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TagListEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TagListEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -5,13 +5,15 @@ import sys, os, time, socket, traceback
from functools import partial
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
QThread, pyqtSignal, Qt, QProgressDialog, QString
QThread, pyqtSignal, Qt, QProgressDialog, QString, QPixmap, \
QSplashScreen, QApplication
from calibre import prints, plugins
from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding
from calibre.constants import iswindows, __appname__, isosx, DEBUG, \
filesystem_encoding
from calibre.utils.ipc import ADDRESS, RC
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \
Application, choose_dir, error_dialog, question_dialog
Application, choose_dir, error_dialog, question_dialog, gprefs
from calibre.gui2.main_window import option_parser as _option_parser
from calibre.utils.config import prefs, dynamic
from calibre.library.database2 import LibraryDatabase2
@ -113,16 +115,25 @@ class GuiRunner(QObject):
initialization'''
def __init__(self, opts, args, actions, listener, app):
self.startup_time = time.time()
self.opts, self.args, self.listener, self.app = opts, args, listener, app
self.actions = actions
self.main = None
QObject.__init__(self)
self.splash_screen = None
self.timer = QTimer.singleShot(1, self.initialize)
if DEBUG:
prints('Starting up...')
def start_gui(self):
from calibre.gui2.ui import Main
main = Main(self.opts)
if self.splash_screen is not None:
self.splash_screen.showMessage(_('Initializing user interface...'))
self.splash_screen.finish(main)
main.initialize(self.library_path, self.db, self.listener, self.actions)
if DEBUG:
prints('Started up in', time.time() - self.startup_time)
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
sys.excepthook = main.unhandled_exception
if len(self.args) > 1:
@ -143,7 +154,7 @@ class GuiRunner(QObject):
if db is None and tb is not None:
# DB Repair failed
error_dialog(None, _('Repairing failed'),
error_dialog(self.splash_screen, _('Repairing failed'),
_('The database repair failed. Starting with '
'a new empty library.'),
det_msg=tb, show=True)
@ -160,7 +171,7 @@ class GuiRunner(QObject):
os.makedirs(x)
except:
x = os.path.expanduser('~')
candidate = choose_dir(None, 'choose calibre library',
candidate = choose_dir(self.splash_screen, 'choose calibre library',
_('Choose a location for your new calibre e-book library'),
default_dir=x)
@ -171,7 +182,7 @@ class GuiRunner(QObject):
self.library_path = candidate
db = LibraryDatabase2(candidate)
except:
error_dialog(None, _('Bad database location'),
error_dialog(self.splash_screen, _('Bad database location'),
_('Bad database location %r. calibre will now quit.'
)%self.library_path,
det_msg=traceback.format_exc(), show=True)
@ -185,7 +196,7 @@ class GuiRunner(QObject):
try:
db = LibraryDatabase2(self.library_path)
except (sqlite.Error, DatabaseException):
repair = question_dialog(None, _('Corrupted database'),
repair = question_dialog(self.splash_screen, _('Corrupted database'),
_('Your calibre database appears to be corrupted. Do '
'you want calibre to try and repair it automatically? '
'If you say No, a new empty calibre library will be created.'),
@ -204,14 +215,27 @@ class GuiRunner(QObject):
self.repair.start()
return
except:
error_dialog(None, _('Bad database location'),
error_dialog(self.splash_screen, _('Bad database location'),
_('Bad database location %r. Will start with '
' a new, empty calibre library')%self.library_path,
det_msg=traceback.format_exc(), show=True)
self.initialize_db_stage2(db, None)
def show_splash_screen(self):
self.splash_pixmap = QPixmap()
self.splash_pixmap.load(I('library.png'))
self.splash_screen = QSplashScreen(self.splash_pixmap,
Qt.SplashScreen|Qt.WindowStaysOnTopHint)
self.splash_screen.showMessage(_('Starting %s: Loading books...') %
__appname__)
self.splash_screen.show()
QApplication.instance().processEvents()
def initialize(self, *args):
if gprefs.get('show_splash_screen', True):
self.show_splash_screen()
self.library_path = get_library_path()
if self.library_path is None:
self.initialization_failed()

View File

@ -8,10 +8,11 @@ Browsing book collection by tags.
'''
from itertools import izip
from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, QSize, QIcon, QPoint, \
QAbstractItemModel, QVariant, QModelIndex
QAbstractItemModel, QVariant, QModelIndex, QMenu
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons
@ -19,9 +20,12 @@ from calibre.utils.search_query_parser import saved_searches
class TagsView(QTreeView): # {{{
need_refresh = pyqtSignal()
restriction_set = pyqtSignal(object)
tags_marked = pyqtSignal(object, object)
need_refresh = pyqtSignal()
restriction_set = pyqtSignal(object)
tags_marked = pyqtSignal(object, object)
user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object)
saved_search_edit = pyqtSignal(object)
def __init__(self, *args):
QTreeView.__init__(self, *args)
@ -31,13 +35,16 @@ class TagsView(QTreeView): # {{{
self.tag_match = None
def set_database(self, db, tag_match, popularity, restriction):
self._model = TagsModel(db, parent=self)
self.hidden_categories = config['tag_browser_hidden_categories']
self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories)
self.popularity = popularity
self.restriction = restriction
self.tag_match = tag_match
self.db = db
self.setModel(self._model)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
self.popularity.setChecked(config['sort_by_popularity'])
self.popularity.stateChanged.connect(self.sort_changed)
self.restriction.activated[str].connect(self.search_restriction_set)
@ -45,10 +52,6 @@ class TagsView(QTreeView): # {{{
db.add_listener(self.database_changed)
self.saved_searches_changed(recount=False)
def create_tag_category(self, name, tag_list):
self._model.create_tag_category(name, tag_list)
self.recount()
def database_changed(self, event, ids):
self.need_refresh.emit()
@ -72,12 +75,87 @@ class TagsView(QTreeView): # {{{
self.recount() # Must happen after the emission of the restriction_set signal
self.tags_marked.emit(self._model.tokens(), self.match_all)
def mouseReleaseEvent(self, event):
# Swallow everything except leftButton so context menus work correctly
if event.button() == Qt.LeftButton:
QTreeView.mouseReleaseEvent(self, event)
def toggle(self, index):
modifiers = int(QApplication.keyboardModifiers())
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
if self._model.toggle(index, exclusive):
self.tags_marked.emit(self._model.tokens(), self.match_all)
def context_menu_handler(self, action=None, category=None):
if not action:
return
try:
if action == 'manage_tags':
self.tag_list_edit.emit(category)
return
if action == 'manage_categories':
self.user_category_edit.emit(category)
return
if action == 'manage_searches':
self.saved_search_edit.emit(category)
return
if action == 'hide':
self.hidden_categories.add(category)
elif action == 'show':
self.hidden_categories.discard(category)
elif action == 'defaults':
self.hidden_categories.clear()
config.set('tag_browser_hidden_categories', self.hidden_categories)
self.set_new_model()
except:
return
def show_context_menu(self, point):
index = self.indexAt(point)
if not index.isValid():
return False
item = index.internalPointer()
tag_name = ''
if item.type == TagTreeItem.TAG:
tag_name = item.tag.name
item = item.parent
if item.type == TagTreeItem.CATEGORY:
category = unicode(item.name.toString())
self.context_menu = QMenu(self)
self.context_menu.addAction(_('Hide %s') % category,
partial(self.context_menu_handler, action='hide', category=category))
if self.hidden_categories:
self.context_menu.addSeparator()
m = self.context_menu.addMenu(_('Show category'))
for col in self.hidden_categories:
m.addAction(col,
partial(self.context_menu_handler, action='show', category=col))
self.context_menu.addSeparator()
self.context_menu.addAction(_('Restore defaults'),
partial(self.context_menu_handler, action='defaults'))
self.context_menu.addSeparator()
self.context_menu.addAction(_('Manage Tags'),
partial(self.context_menu_handler, action='manage_tags',
category=tag_name))
if category in prefs['user_categories'].keys():
self.context_menu.addAction(_('Manage User Categories'),
partial(self.context_menu_handler, action='manage_categories',
category=category))
else:
self.context_menu.addAction(_('Manage User Categories'),
partial(self.context_menu_handler, action='manage_categories',
category=None))
self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches',
category=tag_name))
self.context_menu.popup(self.mapToGlobal(point))
return True
def clear(self):
self.model().clear_state()
@ -110,13 +188,12 @@ class TagsView(QTreeView): # {{{
self.setCurrentIndex(idx)
self.scrollTo(idx, QTreeView.PositionAtCenter)
'''
If the number of user categories changed, or if custom columns have come or gone,
we must rebuild the model. Reason: it is much easier to do that than to reconstruct
the browser tree.
'''
# If the number of user categories changed, if custom columns have come or
# gone, or if columns have been hidden or restored, we must rebuild the
# model. Reason: it is much easier than reconstructing the browser tree.
def set_new_model(self):
self._model = TagsModel(self.db, parent=self)
self._model = TagsModel(self.db, parent=self,
hidden_categories=self.hidden_categories)
self.setModel(self._model)
# }}}
@ -200,7 +277,7 @@ class TagTreeItem(object): # {{{
class TagsModel(QAbstractItemModel): # {{{
def __init__(self, db, parent=None):
def __init__(self, db, parent=None, hidden_categories=None):
QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication
@ -220,6 +297,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db
self.hidden_categories = hidden_categories
self.search_restriction = ''
self.ignore_next_search = 0
@ -237,6 +315,8 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(config['sort_by_popularity'])
self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
if self.db.field_metadata[r]['kind'] != 'user':
tt = _('The lookup/search name is "{0}"').format(r)
else:
@ -271,12 +351,16 @@ class TagsModel(QAbstractItemModel): # {{{
def refresh(self):
data = self.get_node_tree(config['sort_by_popularity']) # get category data
row_index = -1
for i, r in enumerate(self.row_map):
category = self.root_item.children[i]
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
row_index += 1
category = self.root_item.children[row_index]
names = [t.tag.name for t in category.children]
states = [t.tag.state for t in category.children]
state_map = dict(izip(names, states))
category_index = self.index(i, 0, QModelIndex())
category_index = self.index(row_index, 0, QModelIndex())
if len(category.children) > 0:
self.beginRemoveRows(category_index, 0,
len(category.children)-1)
@ -401,16 +485,20 @@ class TagsModel(QAbstractItemModel): # {{{
def tokens(self):
ans = []
tags_seen = set()
row_index = -1
for i, key in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
row_index += 1
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
continue
category_item = self.root_item.children[i]
category_item = self.root_item.children[row_index]
for tag_item in category_item.children:
tag = tag_item.tag
if tag.state > 0:
prefix = ' not ' if tag.state == 2 else ''
category = key if key != 'news' else 'tag'
if tag.name[0] == u'\u2605': # char is a star. Assume rating
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else:
if category == 'tags':

View File

@ -61,6 +61,8 @@ from calibre.library.database2 import LibraryDatabase2
from calibre.library.caches import CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
class SaveMenu(QMenu):
@ -542,19 +544,23 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_edit_categories)
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
self.tags_view.restriction_set.connect(x)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.search.search.connect(self.tags_view.model().reinit)
for x in (self.location_view.count_changed, self.tags_view.recount,
self.restriction_count_changed):
self.library_view.model().count_changed_signal.connect(x)
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection)
self.connect(self.saved_search, SIGNAL('changed()'),
self.tags_view.saved_searches_changed, Qt.QueuedConnection)
if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -647,13 +653,28 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
def do_edit_categories(self):
d = TagCategories(self, self.library_view.model().db)
def do_user_categories_edit(self, on_category=None):
d = TagCategories(self, self.library_view.model().db, on_category)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
def do_tags_list_edit(self, tag):
d = TagListEditor(self, self.library_view.model().db, tag)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
self.library_view.model().refresh()
def do_saved_search_edit(self, search):
d = SavedSearchEditor(self, search)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.saved_searches_changed(recount=True)
self.saved_search.clear_to_help()
def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150)

View File

@ -648,6 +648,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True):
if not tag.strip():
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?',
(id_,))
self.conn.execute('DELETE FROM tags WHERE id=?', (id_,))
self.clean_custom()
self.conn.commit()
@ -980,6 +985,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify:
self.notify('metadata', [id])
# Convenience method for tags_list_editor
def get_tags_with_ids(self):
result = self.conn.get('SELECT * FROM tags')
if not result:
return {}
r = []
for k,v in result:
r.append((k,v))
return r
def rename_tag(self, id, new):
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id))
self.conn.commit()
def get_tags(self, id):
result = self.conn.get(
'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',

View File

@ -115,7 +115,7 @@ Pre/post processing of downloaded HTML
.. automethod:: BasicNewsRecipe.postprocess_html
.. automethod:: BasicNewsRecipe.populate_article_metadata
Convenience methods

View File

@ -254,7 +254,7 @@ class BasicNewsRecipe(Recipe):
#: will remove everythong from `<!--Article ends here-->` to `</body>`.
preprocess_regexps = []
#: The CSS that is used to styles the templates, i.e., the navigation bars and
#: The CSS that is used to style the templates, i.e., the navigation bars and
#: the Tables of Contents. Rather than overriding this variable, you should
#: use `extra_css` in your recipe to customize look and feel.
template_css = u'''
@ -517,6 +517,19 @@ class BasicNewsRecipe(Recipe):
'''
raise NotImplementedError
def populate_article_metadata(self, article, soup, first):
'''
Called when each HTML page belonging to article is downloaded.
Intended to be used to get article metadata like author/summary/etc.
from the parsed HTML (soup).
:param article: A object of class :class:`calibre.web.feeds.Article`.
If you chane the sumamry, remeber to also change the
text_summary
:param soup: Parsed HTML belonging to this article
:param first: True iff the parsed HTML is the first page of the article.
'''
pass
def postprocess_book(self, oeb, opts, log):
'''
Run any needed post processing on the parsed downloaded e-book.
@ -544,6 +557,8 @@ class BasicNewsRecipe(Recipe):
self.username = options.username
self.password = options.password
self.lrf = options.lrf
self.output_profile = options.output_profile
self.touchscreen = getattr(self.output_profile, 'touchscreen', False)
self.output_dir = os.path.abspath(self.output_dir)
if options.test:
@ -597,7 +612,7 @@ class BasicNewsRecipe(Recipe):
if self.delay > 0:
self.simultaneous_downloads = 1
self.navbar = templates.NavBarTemplate()
self.navbar = templates.TouchscreenNavBarTemplate() if self.touchscreen else templates.NavBarTemplate()
self.failed_downloads = []
self.partial_failures = []
@ -638,7 +653,15 @@ class BasicNewsRecipe(Recipe):
for base in list(soup.findAll(['base', 'iframe'])):
base.extract()
return self.postprocess_html(soup, first_fetch)
ans = self.postprocess_html(soup, first_fetch)
try:
article = self.feed_objects[f].articles[a]
except:
self.log.exception('Failed to get article object for postprocessing')
pass
else:
self.populate_article_metadata(article, ans, first_fetch)
return ans
def download(self):
@ -674,7 +697,11 @@ class BasicNewsRecipe(Recipe):
def feeds2index(self, feeds):
templ = templates.IndexTemplate()
css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '')
return templ.generate(self.title, self.timefmt, feeds,
timefmt = self.timefmt
if self.touchscreen:
templ = templates.TouchscreenIndexTemplate()
timefmt = '%A, %d %b %Y'
return templ.generate(self.title, "mastheadImage.jpg", timefmt, feeds,
extra_css=css).render(doctype='xhtml')
@classmethod
@ -727,6 +754,44 @@ class BasicNewsRecipe(Recipe):
templ = templates.FeedTemplate()
css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '')
if self.touchscreen:
touchscreen_css = u'''
.summary_headline {
font-size:large; font-weight:bold; margin-top:0px; margin-bottom:0px;
}
.summary_byline {
font-size:small; margin-top:0px; margin-bottom:0px;
}
.summary_text {
margin-top:0px; margin-bottom:0px;
}
.feed {
font-family:sans-serif; font-weight:bold; font-size:larger;
}
.calibre_navbar {
font-family:monospace;
}
hr {
border-color:gray;
border-style:solid;
border-width:thin;
}
table.toc {
font-size:large;
}
td.article_count {
text-align:right;
}
'''
templ = templates.TouchscreenFeedTemplate()
css = touchscreen_css + '\n\n' + (self.extra_css if self.extra_css else '')
return templ.generate(feed, self.description_limiter,
extra_css=css).render(doctype='xhtml')
@ -820,6 +885,7 @@ class BasicNewsRecipe(Recipe):
if hasattr(feed, 'reverse'):
feed.reverse()
self.feed_objects = feeds
for f, feed in enumerate(feeds):
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
if not os.path.isdir(feed_dir):
@ -1053,6 +1119,9 @@ class BasicNewsRecipe(Recipe):
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
mi.publisher = __appname__
mi.author_sort = __appname__
if self.output_profile.name == 'iPad':
mi.authors = [strftime('%A, %d %B %Y')]
mi.author_sort = strftime('%Y-%m-%d')
mi.publication_type = 'periodical:'+self.publication_type
mi.timestamp = nowf()
mi.comments = self.description

View File

@ -5,7 +5,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from lxml import html, etree
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \
STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT
STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \
TABLE, TD, TR
from calibre import preferred_encoding, strftime, isbytestring
@ -89,12 +90,55 @@ class NavBarTemplate(Template):
self.root = HTML(head, BODY(navbar))
class TouchscreenNavBarTemplate(Template):
def _generate(self, bottom, feed, art, number_of_articles_in_feed,
two_levels, url, __appname__, prefix='', center=True,
extra_css=None, style=None):
head = HEAD(TITLE('navbar'))
if style:
head.append(STYLE(style, type='text/css'))
if extra_css:
head.append(STYLE(extra_css, type='text/css'))
if prefix and not prefix.endswith('/'):
prefix += '/'
align = 'center' if center else 'left'
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100',
style='text-align:'+align))
if bottom:
navbar.append(HR())
text = 'This article was downloaded by '
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
p[0].tail = ' from '
navbar.append(BR())
navbar.append(BR())
else:
next = 'feed_%d'%(feed+1) if art == number_of_articles_in_feed - 1 \
else 'article_%d'%(art+1)
up = '../..' if art == number_of_articles_in_feed - 1 else '..'
href = '%s%s/%s/index.html'%(prefix, up, next)
navbar.text = '| '
navbar.append(A('Next', href=href))
href = '%s../index.html#article_%d'%(prefix, art)
navbar.iterchildren(reversed=True).next().tail = ' | '
navbar.append(A('Section Menu', href=href))
href = '%s../../index.html#feed_%d'%(prefix, feed)
navbar.iterchildren(reversed=True).next().tail = ' | '
navbar.append(A('Main Menu', href=href))
if art > 0 and not bottom:
href = '%s../article_%d/index.html'%(prefix, art-1)
navbar.iterchildren(reversed=True).next().tail = ' | '
navbar.append(A('Previous', href=href))
navbar.iterchildren(reversed=True).next().tail = ' | '
if not bottom:
navbar.append(HR())
self.root = HTML(head, BODY(navbar))
class IndexTemplate(Template):
def _generate(self, title, datefmt, feeds, extra_css=None, style=None):
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
if isinstance(datefmt, unicode):
datefmt = datefmt.encode(preferred_encoding)
date = strftime(datefmt)
@ -110,12 +154,40 @@ class IndexTemplate(Template):
href='feed_%d/index.html'%i)), id='feed_%d'%i)
ul.append(li)
div = DIV(
H1(title, CLASS('calibre_recipe_title', 'calibre_rescale_180')),
PT(IMG(src=masthead,alt="masthead"),style='text-align:center'),
PT(date, style='text-align:right'),
ul,
CLASS('calibre_rescale_100'))
self.root = HTML(head, BODY(div))
class TouchscreenIndexTemplate(Template):
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
if isinstance(datefmt, unicode):
datefmt = datefmt.encode(preferred_encoding)
date = strftime(datefmt)
masthead_img = IMG(src=masthead,alt="masthead")
head = HEAD(TITLE(title))
if style:
head.append(STYLE(style, type='text/css'))
if extra_css:
head.append(STYLE(extra_css, type='text/css'))
toc = TABLE(CLASS('toc'),width="100%",border="0",cellpadding="3px")
for i, feed in enumerate(feeds):
if feed:
tr = TR()
tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i)))
tr.append(TD( CLASS('article_count'),'%d' % len(feed.articles)))
toc.append(tr)
div = DIV(
PT(masthead_img,style='text-align:center'),
PT(date, style='text-align:center'),
toc,
CLASS('calibre_rescale_100'))
self.root = HTML(head, BODY(div))
class FeedTemplate(Template):
def _generate(self, feed, cutoff, extra_css=None, style=None):
@ -166,6 +238,56 @@ class FeedTemplate(Template):
self.root = HTML(head, body)
class TouchscreenFeedTemplate(Template):
def _generate(self, feed, cutoff, extra_css=None, style=None):
head = HEAD(TITLE(feed.title))
if style:
head.append(STYLE(style, type='text/css'))
if extra_css:
head.append(STYLE(extra_css, type='text/css'))
body = BODY(style='page-break-before:always')
div = DIV(
H2(feed.title,
CLASS('calibre_feed_title', 'calibre_rescale_160')),
CLASS('calibre_rescale_100')
)
body.append(div)
if getattr(feed, 'image', None):
div.append(DIV(IMG(
alt = feed.image_alt if feed.image_alt else '',
src = feed.image_url
),
CLASS('calibre_feed_image')))
if getattr(feed, 'description', None):
d = DIV(feed.description, CLASS('calibre_feed_description',
'calibre_rescale_80'))
d.append(BR())
div.append(d)
toc = TABLE(CLASS('toc'),width="100%",border="0",cellpadding="3px")
for i, article in enumerate(feed.articles):
if not getattr(article, 'downloaded', False):
continue
tr = TR()
td = TD(
A(article.title, CLASS('article calibre_rescale_100',
href=article.url))
)
if article.summary:
td.append(DIV(cutoff(article.text_summary),
CLASS('article_description', 'calibre_rescale_80')))
tr.append(td)
toc.append(tr)
div.append(toc)
navbar = DIV('| ', CLASS('calibre_navbar', 'calibre_rescale_100'),style='text-align:center')
link = A('Up one level', href="../index.html")
link.tail = ' |'
navbar.append(link)
div.append(navbar)
self.root = HTML(head, body)
class EmbeddedContent(Template):

View File

@ -328,6 +328,9 @@ class RecursiveFetcher(object):
continue
try:
data = self.fetch_url(iurl)
if data == 'GIF89a\x01':
# Skip empty GIF files as PIL errors on them anyway
continue
except Exception:
self.log.exception('Could not fetch image %s'% iurl)
continue