merge from trunk

This commit is contained in:
ldolse 2010-12-15 13:25:06 -05:00
commit 870929d1c7
27 changed files with 278 additions and 76 deletions

View File

@ -4,6 +4,7 @@ __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
economictimes.indiatimes.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class TheEconomicTimes(BasicNewsRecipe):
@ -32,18 +33,17 @@ class TheEconomicTimes(BasicNewsRecipe):
, 'language' : language
}
keep_only_tags = [dict(attrs={'class':'printdiv'})]
remove_tags = [dict(name=['object','link','embed','iframe','base','table','meta'])]
remove_attributes = ['name']
remove_tags_before = dict(name='h1')
feeds = [(u'All articles', u'http://economictimes.indiatimes.com/rssfeedsdefault.cms')]
def print_version(self, url):
rest, sep, art = url.rpartition('/articleshow/')
return 'http://m.economictimes.com/PDAET/articleshow/' + art
return 'http://economictimes.indiatimes.com/articleshow/' + art + '?prtpage=1'
def get_article_url(self, article):
rurl = article.get('link', None)
rurl = article.get('guid', None)
if (rurl.find('/quickieslist/') > 0) or (rurl.find('/quickiearticleshow/') > 0):
return None
return rurl

View File

@ -8,9 +8,10 @@ class TimesOfIndia(BasicNewsRecipe):
max_articles_per_feed = 25
no_stylesheets = True
keep_only_tags = [dict(attrs={'class':'prttabl'})]
keep_only_tags = [dict(attrs={'class':'maintable12'})]
remove_tags = [
dict(style=lambda x: x and 'float' in x)
dict(style=lambda x: x and 'float' in x),
dict(attrs={'class':'prvnxtbg'}),
]
feeds = [

View File

@ -80,6 +80,34 @@ class Plugin(object): # {{{
'''
pass
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method
will allow you to load resources from the ZIP file.
For example to load an image::
pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png']).itervalues().next())
icon = QIcon(pixmap)
:param names: List of paths to resources in the zip file using / as separator
:return: A dictionary of the form ``{name : file_contents}``. Any names
that were not found in the zip file will not be present in the
dictionary.
'''
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
with zipfile.ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate in names:
ans[candidate] = zf.read(candidate)
return ans
def customization_help(self, gui=False):
'''
Return a string giving help on how to customize this plugin.

View File

@ -101,6 +101,13 @@ class EPUBOutput(OutputFormatPlugin):
)
),
OptionRecommendation(name='epub_flatten', recommended_value=False,
help=_('This option is needed only if you intend to use the EPUB'
' with FBReaderJ. It will flatten the file system inside the'
' EPUB, putting all files into the top level.')
),
])
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
@ -142,8 +149,12 @@ class EPUBOutput(OutputFormatPlugin):
def convert(self, oeb, output_path, input_plugin, opts, log):
self.log, self.opts, self.oeb = log, opts, oeb
#from calibre.ebooks.oeb.transforms.filenames import UniqueFilenames
#UniqueFilenames()(oeb, opts)
if self.opts.epub_flatten:
from calibre.ebooks.oeb.transforms.filenames import FlatFilenames
FlatFilenames()(oeb, opts)
else:
from calibre.ebooks.oeb.transforms.filenames import UniqueFilenames
UniqueFilenames()(oeb, opts)
self.workaround_ade_quirks()
self.workaround_webkit_quirks()

View File

@ -276,6 +276,7 @@ def result_index(source, result):
return -1
def merge_results(one, two):
if two is not None and one is not None:
for x in two:
idx = result_index(one, x)
if idx < 0:
@ -337,7 +338,7 @@ def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
manager(title, author, publisher, isbn, verbose)
manager.join()
results = list(fetchers[0].results)
results = list(fetchers[0].results) if fetchers else []
for fetcher in fetchers[1:]:
merge_results(results, fetcher.results)

View File

@ -29,6 +29,9 @@ from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
from calibre.ebooks.metadata.toc import TOC
class TopazError(ValueError):
pass
class EXTHHeader(object):
def __init__(self, raw, codec, title):
@ -239,7 +242,7 @@ class MobiReader(object):
self.base_css_rules = textwrap.dedent('''
blockquote { margin: 0em 0em 0em 2em; text-align: justify }
p { margin: 0em; text-align: justify }
p { margin: 0em; text-align: justify; text-indent: 1.5em }
.bold { font-weight: bold }
@ -259,7 +262,7 @@ class MobiReader(object):
raw = stream.read()
if raw.startswith('TPZ'):
raise ValueError(_('This is an Amazon Topaz book. It cannot be processed.'))
raise TopazError(_('This is an Amazon Topaz book. It cannot be processed.'))
self.header = raw[0:72]
self.name = self.header[:32].replace('\x00', '')
@ -832,6 +835,15 @@ class MobiReader(object):
im.save(open(path, 'wb'), format='JPEG')
def get_metadata(stream):
stream.seek(0)
try:
raw = stream.read(3)
except:
raw = ''
stream.seek(0)
if raw == 'TPZ':
from calibre.ebooks.metadata.topaz import get_metadata
return get_metadata(stream)
from calibre.utils.logging import Log
log = Log()
mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')])
@ -861,7 +873,10 @@ def get_metadata(stream):
cover_index = mh.first_image_index + mh.exth.cover_offset
data = mh.section_data(int(cover_index))
else:
try:
data = mh.section_data(mh.first_image_index)
except:
data = ''
buf = cStringIO.StringIO(data)
try:
im = PILImage.open(buf)

View File

@ -13,15 +13,16 @@ import cssutils
from calibre.ebooks.oeb.base import rewrite_links, urlnormalize
class RenameFiles(object):
class RenameFiles(object): # {{{
'''
Rename files and adjust all links pointing to them. Note that the spine
and manifest are not touched by this transform.
'''
def __init__(self, rename_map):
def __init__(self, rename_map, renamed_items_map = None):
self.rename_map = rename_map
self.renamed_items_map = renamed_items_map
def __call__(self, oeb, opts):
self.log = oeb.logger
@ -49,7 +50,6 @@ class RenameFiles(object):
if self.oeb.toc:
self.fix_toc_entry(self.oeb.toc)
def fix_toc_entry(self, toc):
if toc.href:
href = urlnormalize(toc.href)
@ -68,16 +68,20 @@ class RenameFiles(object):
def url_replacer(self, orig_url):
url = urlnormalize(orig_url)
path, frag = urldefrag(url)
href = self.current_item.abshref(path)
replacement = self.rename_map.get(href, None)
if replacement is None:
return orig_url
replacement = self.current_item.relhref(replacement)
if self.renamed_items_map:
orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item)
else:
orig_item = self.current_item
href = orig_item.abshref(path)
replacement = self.current_item.relhref(self.rename_map.get(href, href))
if frag:
replacement += '#' + frag
return replacement
class UniqueFilenames(object):
# }}}
class UniqueFilenames(object): # {{{
'Ensure that every item in the manifest has a unique filename'
@ -127,4 +131,48 @@ class UniqueFilenames(object):
candidate = base + suffix + ext
if candidate not in self.seen_filenames:
return suffix
# }}}
class FlatFilenames(object): # {{{
'Ensure that every item in the manifest has a unique filename without subdirectories.'
def __call__(self, oeb, opts):
self.log = oeb.logger
self.opts = opts
self.oeb = oeb
self.rename_map = {}
self.renamed_items_map = {}
for item in list(oeb.manifest.items):
# Flatten URL by removing directories.
# Example: a/b/c/index.html -> a_b_c_index.html
nhref = item.href.replace("/", "_")
if item.href == nhref:
# URL hasn't changed, skip item.
continue
data = item.data
nhref = oeb.manifest.generate(href=nhref)[1]
nitem = oeb.manifest.add(item.id, nhref, item.media_type, data=data,
fallback=item.fallback)
self.rename_map[item.href] = nhref
self.renamed_items_map[nhref] = item
if item.spine_position is not None:
oeb.spine.insert(item.spine_position, nitem, item.linear)
oeb.spine.remove(item)
oeb.manifest.remove(item)
if self.rename_map:
self.log('Found non-flat filenames, renaming to support broken'
' EPUB readers like FBReader...')
from pprint import pformat
self.log.debug(pformat(self.rename_map))
self.log.debug(pformat(self.renamed_items_map))
renamer = RenameFiles(self.rename_map, self.renamed_items_map)
renamer(oeb, opts)
# }}}

View File

@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from functools import partial
from zipfile import ZipFile
from PyQt4.Qt import QToolButton, QAction, QIcon, QObject
@ -108,6 +109,34 @@ class InterfaceAction(QObject):
setattr(self, attr, action)
return action
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method
will allow you to load resources from the ZIP file.
For example to load an image::
pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png']).itervalues().next())
icon = QIcon(pixmap)
:param names: List of paths to resources in the zip file using / as separator
:return: A dictionary of the form ``{name : file_contents}``. Any names
that were not found in the zip file will not be present in the
dictionary.
'''
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
with ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate in names:
ans[candidate] = zf.read(candidate)
return ans
def genesis(self):
'''
Setup this plugin. Only called once during initialization. self.gui is

View File

@ -120,6 +120,7 @@ class AddAction(InterfaceAction):
if self.gui.current_view() is not self.gui.library_view:
return
db = self.gui.library_view.model().db
cover_changed = False
current_idx = self.gui.library_view.currentIndex()
if not current_idx.isValid(): return
cid = db.id(current_idx.row())
@ -133,12 +134,16 @@ class AddAction(InterfaceAction):
if not pmap.isNull():
accept = True
db.set_cover(cid, pmap)
cover_changed = True
elif ext in BOOK_EXTENSIONS:
db.add_format_with_hooks(cid, ext, path, index_is_id=True)
accept = True
if accept:
event.accept()
self.gui.library_view.model().current_changed(current_idx, current_idx)
if cover_changed:
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
def __add_filesystem_book(self, paths, allow_device=True):
if isinstance(paths, basestring):

View File

@ -160,9 +160,11 @@ class ChooseLibraryAction(InterfaceAction):
self.action_choose.triggered.connect(self.choose_library,
type=Qt.QueuedConnection)
self.choose_menu = QMenu(self.gui)
self.choose_menu.addAction(self.action_choose)
self.qaction.setMenu(self.choose_menu)
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
self.choose_menu.addAction(self.action_choose)
self.quick_menu = QMenu(_('Quick switch'))
self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
self.rename_menu = QMenu(_('Rename library'))
@ -223,6 +225,8 @@ class ChooseLibraryAction(InterfaceAction):
self.library_changed(self.gui.library_view.model().db)
def build_menus(self):
if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
return
db = self.gui.library_view.model().db
locations = list(self.stats.locations(db))
for ac in self.switch_actions:
@ -387,6 +391,11 @@ class ChooseLibraryAction(InterfaceAction):
c.exec_()
def change_library_allowed(self):
if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
warning_dialog(self.gui, _('Not allowed'),
_('You cannot change libraries while using the environment'
' variable CALIBRE_OVERRIDE_DATABASE_PATH.'), show=True)
return False
if self.gui.job_manager.has_jobs():
warning_dialog(self.gui, _('Not allowed'),
_('You cannot change libraries while jobs'

View File

@ -12,7 +12,7 @@ from threading import Thread
from PyQt4.Qt import QMenu, QToolButton
from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2 import error_dialog, Dispatcher, warning_dialog
from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.utils.config import prefs, tweaks
@ -106,6 +106,9 @@ class CopyToLibraryAction(InterfaceAction):
def build_menus(self):
self.menu.clear()
if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
self.menu.addAction('disabled', self.cannot_do_dialog)
return
db = self.gui.library_view.model().db
locations = list(self.stats.locations(db))
for name, loc in locations:
@ -160,5 +163,9 @@ class CopyToLibraryAction(InterfaceAction):
self.gui.iactions['Remove Books'].library_ids_deleted(
self.worker.processed, row)
def cannot_do_dialog(self):
warning_dialog(self.gui, _('Not allowed'),
_('You cannot use other libraries while using the environment'
' variable CALIBRE_OVERRIDE_DATABASE_PATH.'), show=True)

View File

@ -21,7 +21,7 @@ class PluginWidget(Widget, Ui_Form):
Widget.__init__(self, parent,
['dont_split_on_page_breaks', 'flow_size',
'no_default_epub_cover', 'no_svg_cover',
'preserve_cover_aspect_ratio',]
'preserve_cover_aspect_ratio', 'epub_flatten']
)
for i in range(2):
self.opt_no_svg_cover.toggle()

View File

@ -81,6 +81,13 @@
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="opt_epub_flatten">
<property name="text">
<string>&amp;Flatten EPUB file structure</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -166,7 +166,9 @@ class DeviceManager(Thread): # {{{
report_progress=self.report_progress)
dev.open()
except OpenFeedback, e:
if dev not in self.ejected_devices:
self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg)
self.ejected_devices.add(dev)
continue
except:
tb = traceback.format_exc()

View File

@ -399,14 +399,11 @@ Future conversion of these books will use the default settings.</string>
<property name="title">
<string>Change &amp;cover</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="cover_no_change">
<widget class="QRadioButton" name="cover_generate">
<property name="text">
<string>&amp;No change</string>
</property>
<property name="checked">
<bool>true</bool>
<string>&amp;Generate default cover</string>
</property>
</widget>
</item>
@ -417,13 +414,6 @@ Future conversion of these books will use the default settings.</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="cover_generate">
<property name="text">
<string>&amp;Generate default cover</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -105,7 +105,8 @@ class BooksView(QTableView): # {{{
hv.setCursor(Qt.PointingHandCursor)
self.selected_ids = []
self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
self._model.sorting_done.connect(self.sorting_done)
self._model.sorting_done.connect(self.sorting_done,
type=Qt.QueuedConnection)
# Column Header Context Menu {{{
def column_header_context_handler(self, action=None, column=None):
@ -227,6 +228,7 @@ class BooksView(QTableView): # {{{
sm = self.selectionModel()
for idx in indices:
sm.select(idx, sm.Select|sm.Rows)
self.scroll_to_row(indices[0].row())
self.selected_ids = []
# }}}

View File

@ -88,7 +88,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
name = unicode(fi.family())
self.font_display.setFont(font)
self.font_display.setText(_('Current font:') + ' ' + name +
self.font_display.setText(name +
' [%dpt]'%fi.pointSize())
def change_font(self, *args):

View File

@ -183,6 +183,34 @@
</layout>
</widget>
</item>
<item row="8" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Interface font:</string>
</property>
<property name="buddy">
<cstring>font_display</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
@ -196,20 +224,6 @@
</property>
</spacer>
</item>
<item row="8" column="0">
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -236,6 +236,10 @@ def fetch_scheduled_recipe(arg):
recs.append(('header', True, OptionRecommendation.HIGH))
recs.append(('header_format', '%t', OptionRecommendation.HIGH))
epub = load_defaults('epub_output')
if epub.get('epub_flatten', False):
recs.append(('epub_flatten', True, OptionRecommendation.HIGH))
args = [arg['recipe'], pt.name, recs]
if arg['username'] is not None:
recs.append(('username', arg['username'], OptionRecommendation.HIGH))

View File

@ -103,6 +103,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
acmap = OrderedDict()
for action in interface_actions():
ac = action.load_actual_plugin(self)
ac.plugin_path = action.plugin_path
if ac.name in acmap:
if ac.priority >= acmap[ac.name].priority:
acmap[ac.name] = ac

View File

@ -171,6 +171,13 @@
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remember_current_page">
<property name="text">
<string>Remember the &amp;current page when quitting</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="max_view_width">
<property name="suffix">

View File

@ -50,6 +50,8 @@ def config(defaults=None):
c.add_opt('hyphenate', default=False, help=_('Hyphenate text'))
c.add_opt('hyphenate_default_lang', default='en',
help=_('Default language for hyphenation rules'))
c.add_opt('remember_current_page', default=True,
help=_('Save the current position in the document, when quitting'))
fonts = c.add_group('FONTS', _('Font options'))
fonts('serif_family', default='Times New Roman' if iswindows else 'Liberation Serif',
@ -72,6 +74,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
opts = config().parse()
self.opt_remember_window_size.setChecked(opts.remember_window_size)
self.opt_remember_current_page.setChecked(opts.remember_current_page)
self.serif_family.setCurrentFont(QFont(opts.serif_family))
self.sans_family.setCurrentFont(QFont(opts.sans_family))
self.mono_family.setCurrentFont(QFont(opts.mono_family))
@ -118,6 +121,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
c.set('fit_images', self.opt_fit_images.isChecked())
c.set('max_view_width', int(self.max_view_width.value()))
c.set('hyphenate', self.hyphenate.isChecked())
c.set('remember_current_page', self.opt_remember_current_page.isChecked())
idx = self.hyphenate_default_lang.currentIndex()
c.set('hyphenate_default_lang',
str(self.hyphenate_default_lang.itemData(idx).toString()))

View File

@ -328,6 +328,11 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
c = config().parse()
self.frame.setMaximumWidth(c.max_view_width)
def get_remember_current_page_opt(self):
from calibre.gui2.viewer.documentview import config
c = config().parse()
return c.remember_current_page
def print_book(self, preview):
Printing(self.iterator.spine, preview)
@ -578,7 +583,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
current_page = None
self.existing_bookmarks = []
for bm in bookmarks:
if bm[0] == 'calibre_current_page_bookmark':
if bm[0] == 'calibre_current_page_bookmark' and \
self.get_remember_current_page_opt():
current_page = bm
else:
self.existing_bookmarks.append(bm[0])
@ -598,6 +604,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.set_bookmarks(bookmarks)
def save_current_position(self):
if not self.get_remember_current_page_opt():
return
try:
pos = self.view.bookmark()
bookmark = '%d#%s'%(self.current_index, pos)

View File

@ -1153,6 +1153,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
else:
vals = book[dex].split(mult)
for val in vals:
if not val: continue
try:
(item_id, sort_val) = tids[cat][val] # let exceptions fly
item = tcategories[cat].get(val, None)

View File

@ -425,3 +425,7 @@ class SchemaUpgrade(object):
ids = [(x[0],) for x in data if has_cover(x[1])]
self.conn.executemany('UPDATE books SET has_cover=1 WHERE id=?', ids)
def upgrade_version_15(self):
'Remove commas from tags'
self.conn.execute("UPDATE tags SET name=REPLACE(name, ',', ';')")

View File

@ -552,16 +552,18 @@ class BrowseServer(object):
ids = self.search_cache('search:"%s"'%which)
except:
raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
elif category == 'newest':
ids = self.search_cache('')
all_ids = self.search_cache('')
if category == 'newest':
ids = all_ids
hide_sort = 'true'
elif category == 'allbooks':
ids = self.search_cache('')
ids = all_ids
else:
q = category
if q == 'news':
q = 'tags'
ids = self.db.get_books_for_category(q, cid)
ids = [x for x in ids if x in all_ids]
items = [self.db.data._data[x] for x in ids]
if category == 'newest':

View File

@ -72,3 +72,5 @@ Precautions
--------------
Portable media can occasionally fail so you should make periodic backups of you Calibre library. This can be done by making a copy of the CalibreLibrary folder and all its contents. There are many freely available tools around that can optimise such back processes, well known ones being RoboCopy and RichCopy. However you can simply use a Windows copy facility if you cannot be bothered to use a specialised tools.
Using the environment variable CALIBRE_OVERRIDE_DATABASE_PATH disables multiple-library support in |app|. Avoid setting this variable in calibre-portable.bat unless you really need it.