From 85ae7c7d4f9a016502b77b1e2f2a4dc65eeb57ba Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 13:25:21 -0700 Subject: [PATCH 01/20] ... --- src/calibre/gui2/dialogs/metadata_bulk.ui | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index cd644f88ba..ecb34d8e5b 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -399,14 +399,11 @@ Future conversion of these books will use the default settings. Change &cover - + - + - &No change - - - true + &Generate default cover @@ -417,13 +414,6 @@ Future conversion of these books will use the default settings. - - - - &Generate default cover - - - From 1a90427d8d2c2220c379146f76c52355470179f1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 13:31:09 -0700 Subject: [PATCH 02/20] get_categories(): ignore empty is_mult values --- src/calibre/library/database2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 33e4295f05..23375995ae 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -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) From 1b2bdfae81d83132cb02c1a48b1d558175824a27 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 13:46:09 -0700 Subject: [PATCH 03/20] EPUB Output: Ensure all files inside the generated EPUB have unique filenames, to support broken EPUB readers like Stanza, Aldiko, FBReader and Sigil --- src/calibre/ebooks/epub/output.py | 4 +- .../ebooks/oeb/transforms/filenames.py | 76 +++++++++++++++---- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py index c5d11edc2b..4bc0f679cc 100644 --- a/src/calibre/ebooks/epub/output.py +++ b/src/calibre/ebooks/epub/output.py @@ -142,8 +142,8 @@ 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) + from calibre.ebooks.oeb.transforms.filenames import UniqueFilenames + UniqueFilenames()(oeb, opts) self.workaround_ade_quirks() self.workaround_webkit_quirks() diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py index 2b22474d30..46f9fc5539 100644 --- a/src/calibre/ebooks/oeb/transforms/filenames.py +++ b/src/calibre/ebooks/oeb/transforms/filenames.py @@ -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) @@ -66,18 +66,22 @@ class RenameFiles(object): self.fix_toc_entry(x) 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 frag: - replacement += '#' + frag - return replacement + url = urlnormalize(orig_url) + path, frag = urldefrag(url) + 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 -class UniqueFilenames(object): + 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): # {{{ '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) +# }}} From 5f91edf752bf938ddd6935156b676a30dbe28dc4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 14:23:28 -0700 Subject: [PATCH 04/20] EPUB Output: Add an option to flatten the EPUB file structure, specially for FBReaderJ. Fixes #7788 (Flatten content of EPUB created by recipes to make them more compatible) --- src/calibre/ebooks/epub/output.py | 15 +++++++++++++-- src/calibre/gui2/convert/epub_output.py | 2 +- src/calibre/gui2/convert/epub_output.ui | 7 +++++++ src/calibre/gui2/tools.py | 4 ++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py index 4bc0f679cc..2e254e99cc 100644 --- a/src/calibre/ebooks/epub/output.py +++ b/src/calibre/ebooks/epub/output.py @@ -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() diff --git a/src/calibre/gui2/convert/epub_output.py b/src/calibre/gui2/convert/epub_output.py index c1ef6282f7..2fcbd751fe 100644 --- a/src/calibre/gui2/convert/epub_output.py +++ b/src/calibre/gui2/convert/epub_output.py @@ -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() diff --git a/src/calibre/gui2/convert/epub_output.ui b/src/calibre/gui2/convert/epub_output.ui index abca2405e8..a0d9570226 100644 --- a/src/calibre/gui2/convert/epub_output.ui +++ b/src/calibre/gui2/convert/epub_output.ui @@ -81,6 +81,13 @@ + + + + &Flatten EPUB file structure + + + diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index fc84e88d09..d18cc61baf 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -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)) From b3e77bac61060055de3e16208bce47335f1541d7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 14:49:49 -0700 Subject: [PATCH 05/20] Fix #7890 (Drag/drop of new cover to book detail panel does not update cover browser) --- src/calibre/gui2/actions/add.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 014fa573d2..38c28661b7 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -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): From 4881958675282342b8f5c092be2bf0b6bdb3d613 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 17:44:33 -0700 Subject: [PATCH 06/20] Fix Times of India recipe --- resources/recipes/toi.recipe | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/recipes/toi.recipe b/resources/recipes/toi.recipe index 9539bcade7..643d120a36 100644 --- a/resources/recipes/toi.recipe +++ b/resources/recipes/toi.recipe @@ -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 = [ From a9ffac7885ce88baa95e475ce397401236d450a2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 18:46:00 -0700 Subject: [PATCH 07/20] ... --- src/calibre/gui2/preferences/look_feel.py | 2 +- src/calibre/gui2/preferences/look_feel.ui | 42 +++++++++++++++-------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index b2ba87d1e0..de1116c231 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -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): diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 91f45a155f..8e57f8c17e 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -183,6 +183,34 @@ + + + + + + Interface font: + + + font_display + + + + + + + true + + + + + + + + + Change &font (needs restart) + + + @@ -196,20 +224,6 @@ - - - - true - - - - - - - Change &font (needs restart) - - - From 491d1a8954519f4443c998f8f002ce809c7c498e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 18:57:04 -0700 Subject: [PATCH 08/20] MOBI metadata reader: Handle invalid PRC files with spurious image_offset headers --- src/calibre/ebooks/mobi/reader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 48ece79f45..b39c4483ec 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -861,7 +861,10 @@ def get_metadata(stream): cover_index = mh.first_image_index + mh.exth.cover_offset data = mh.section_data(int(cover_index)) else: - data = mh.section_data(mh.first_image_index) + try: + data = mh.section_data(mh.first_image_index) + except: + data = '' buf = cStringIO.StringIO(data) try: im = PILImage.open(buf) From 61fb4f30e04733c7233162590dd56c03095a8ce8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 19:06:44 -0700 Subject: [PATCH 09/20] MOBI Input: Handle the (rare) MOBI files that do not specify per paragraph text indents correctly. Fixes #7869 (Converting mobipocket to mobipocket removes paragraph indentation) --- src/calibre/ebooks/mobi/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index b39c4483ec..ec55e67899 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -239,7 +239,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 } From fd20c4ac7f0351ada9a6288da4e69b16e36586ef Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 19:18:14 -0700 Subject: [PATCH 10/20] Topaz metadata: Read metadata correctly from Topaz files that have MOBI file extensions --- src/calibre/ebooks/mobi/reader.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index ec55e67899..02abc51cd3 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -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): @@ -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')]) From 4c947784ce5f503f3da90135664074fb91391fa2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 20:29:36 -0700 Subject: [PATCH 11/20] Fix #7887 (The news fetch for "Times of India" and "The Economic Times" (ENGLISH_ INDIA) not working.) --- resources/recipes/theeconomictimes_india.recipe | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/recipes/theeconomictimes_india.recipe b/resources/recipes/theeconomictimes_india.recipe index 92d2a64a70..d87eb91d0f 100644 --- a/resources/recipes/theeconomictimes_india.recipe +++ b/resources/recipes/theeconomictimes_india.recipe @@ -4,6 +4,7 @@ __copyright__ = '2008-2010, Darko Miletic ' 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 From a1c7d0615c8c18c1920873d01c151777bbf30f88 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 20:33:00 -0700 Subject: [PATCH 12/20] Handle case of no metadata fetchers or fetcher returning None result --- src/calibre/ebooks/metadata/fetch.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/calibre/ebooks/metadata/fetch.py b/src/calibre/ebooks/metadata/fetch.py index b797a477d6..390f288d8e 100644 --- a/src/calibre/ebooks/metadata/fetch.py +++ b/src/calibre/ebooks/metadata/fetch.py @@ -145,7 +145,7 @@ class MetadataSource(Plugin): # {{{ setattr(w, '_'+x, cb) cb.setChecked(c.get(x, True)) w._layout.addWidget(cb) - + if self.has_html_comments: cb = QCheckBox(_('Convert comments downloaded from %s to plain text')%(self.name)) setattr(w, '_textcomments', cb) @@ -276,12 +276,13 @@ def result_index(source, result): return -1 def merge_results(one, two): - for x in two: - idx = result_index(one, x) - if idx < 0: - one.append(x) - else: - one[idx].smart_update(x) + if two is not None and one is not None: + for x in two: + idx = result_index(one, x) + if idx < 0: + one.append(x) + else: + one[idx].smart_update(x) class MetadataSources(object): @@ -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) From ef23a7c8c4685c46c8f200fa6fb552de7b7a092a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 21:22:30 -0700 Subject: [PATCH 13/20] Fix #7876 (Books excluded by saved search via tag are visible on server) --- src/calibre/library/server/browse.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index fa058eb303..37f024c08d 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -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': From ec7bd8628841063f90b35d8498eba2acdaf0eb6f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 23:23:17 -0700 Subject: [PATCH 14/20] When sorting the book list, keep the current book visible after the sort completes. Fixes #7504 (Book List to Maintain book pointer when the list is sorted by another column) --- src/calibre/gui2/library/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index f724ca7b58..0e0cc0eec2 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -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 = [] # }}} From 6c2ea46ce3a62c63cc7a3355d840ad94229f7612 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 15 Dec 2010 10:00:10 +0000 Subject: [PATCH 15/20] Disable multiple libraries while env var CALIBRE_OVERRIDE_DATABASE_PATH is set --- src/calibre/gui2/actions/choose_library.py | 23 ++++++++++++++------- src/calibre/gui2/actions/copy_to_library.py | 9 +++++++- src/calibre/manual/portable.rst | 2 ++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index eb5902be48..e789ae62e6 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -160,15 +160,17 @@ 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) - self.quick_menu = QMenu(_('Quick switch')) - self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu) - self.rename_menu = QMenu(_('Rename library')) - self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu) - self.delete_menu = QMenu(_('Delete library')) - self.delete_menu_action = self.choose_menu.addMenu(self.delete_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')) + self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu) + self.delete_menu = QMenu(_('Delete library')) + self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu) self.rename_separator = self.choose_menu.addSeparator() @@ -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' diff --git a/src/calibre/gui2/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py index 47f7904841..0668baeac6 100644 --- a/src/calibre/gui2/actions/copy_to_library.py +++ b/src/calibre/gui2/actions/copy_to_library.py @@ -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) diff --git a/src/calibre/manual/portable.rst b/src/calibre/manual/portable.rst index a2c8e323d8..76776e3603 100644 --- a/src/calibre/manual/portable.rst +++ b/src/calibre/manual/portable.rst @@ -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. \ No newline at end of file From eaea0d2aa656630274c8b4eb18ebed7e9ae7b765 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 15 Dec 2010 12:48:42 +0000 Subject: [PATCH 16/20] Implement a 'remember current page' option in the viewer. --- src/calibre/gui2/viewer/config.ui | 7 +++++++ src/calibre/gui2/viewer/documentview.py | 4 ++++ src/calibre/gui2/viewer/main.py | 10 +++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/viewer/config.ui b/src/calibre/gui2/viewer/config.ui index 09c38fc908..4066daada2 100644 --- a/src/calibre/gui2/viewer/config.ui +++ b/src/calibre/gui2/viewer/config.ui @@ -171,6 +171,13 @@ + + + + Remember the &current page when quitting + + + diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 8ce0b46a6d..f5d986e757 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -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 documentwhen 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())) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 96fe758ca6..f7b8a8d401 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -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) From fb73e74ff7acde7a9458efae01d669b0b52b4f0e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 15 Dec 2010 14:41:36 +0000 Subject: [PATCH 17/20] schema upgrade to remove commas from tags --- src/calibre/library/schema_upgrades.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index e35c8521ce..15c6f5c800 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -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, \',\', \';\')') + From 0b57b1ae823e5a84d72c5f8075f0f6b260720d18 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Dec 2010 08:36:20 -0700 Subject: [PATCH 18/20] Add a load_resource method to the InterfaceAction class to facilitate loading of resources from plugin ZIP files --- src/calibre/gui2/actions/__init__.py | 25 +++++++++++++++++++++++++ src/calibre/gui2/ui.py | 1 + 2 files changed, 26 insertions(+) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index e595c53601..4ab15f6099 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' from functools import partial +from zipfile import ZipFile from PyQt4.Qt import QToolButton, QAction, QIcon, QObject @@ -108,6 +109,30 @@ class InterfaceAction(QObject): setattr(self, attr, action) return action + def load_resource(self, name): + ''' + 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_resource('images/icon.png')) + icon = QIcon(pixmap) + + :param name: Path to resource in zip file using / as separator + + ''' + if self.plugin_path is None: + raise ValueError('This plugin was not loaded from a ZIP file') + with ZipFile(self.plugin_path, 'r') as zf: + for candidate in zf.namelist(): + if candidate == name: + return zf.read(name) + raise ValueError('The name %r was not found in the plugin zip' + ' file'%name) + + def genesis(self): ''' Setup this plugin. Only called once during initialization. self.gui is diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 7279b7f8df..c3e0bcb0da 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -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 From ed4709f9ac827aa0171e65f324712baca08bb4b4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Dec 2010 09:02:35 -0700 Subject: [PATCH 19/20] Refactor ZIP plugin resource loading for greater efficiency and add method to Plugin class as well for use in non GUI plugins --- src/calibre/customize/__init__.py | 28 ++++++++++++++++++++++++++++ src/calibre/gui2/actions/__init__.py | 18 +++++++++++------- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index b4e9b6c448..a76cb71acd 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -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. diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 4ab15f6099..c88203593b 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -109,7 +109,7 @@ class InterfaceAction(QObject): setattr(self, attr, action) return action - def load_resource(self, name): + 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. @@ -117,20 +117,24 @@ class InterfaceAction(QObject): For example to load an image:: pixmap = QPixmap() - pixmap.loadFromData(self.load_resource('images/icon.png')) + pixmap.loadFromData(self.load_resources(['images/icon.png']).itervalues().next()) icon = QIcon(pixmap) - :param name: Path to resource in zip file using / as separator + :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 == name: - return zf.read(name) - raise ValueError('The name %r was not found in the plugin zip' - ' file'%name) + if candidate in names: + ans[candidate] = zf.read(candidate) + return ans def genesis(self): From 5dacd76a4e89935ac3e2ce334f784aeda6d65eec Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Dec 2010 09:34:33 -0700 Subject: [PATCH 20/20] Fix #7905 (calibre keeps on trying to connect to the plugged device even error happens) --- src/calibre/gui2/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 07bfeccc4f..92b5932406 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -166,7 +166,9 @@ class DeviceManager(Thread): # {{{ report_progress=self.report_progress) dev.open() except OpenFeedback, e: - self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg) + 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()