From 97f0518585479d0001ee25395c540840bb2a1211 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 1 Feb 2011 14:14:03 +0000 Subject: [PATCH 01/19] Fix #8714: Problem sending thumbnails to Sony PRSx50 SD card --- src/calibre/devices/prs505/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/prs505/__init__.py b/src/calibre/devices/prs505/__init__.py index 48b7d98123..1a59cb81a6 100644 --- a/src/calibre/devices/prs505/__init__.py +++ b/src/calibre/devices/prs505/__init__.py @@ -8,5 +8,5 @@ CACHE_XML = 'Sony Reader/database/cache.xml' CACHE_EXT = 'Sony Reader/database/cacheExt.xml' MEDIA_THUMBNAIL = 'database/thumbnail' -CACHE_THUMBNAIL = 'Sony Reader/database/thumbnail' +CACHE_THUMBNAIL = 'Sony Reader/thumbnail' From 31e66b8bc55d69016362ab965a7599963698d2e0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 1 Feb 2011 14:15:04 +0000 Subject: [PATCH 02/19] 1) fix problem where new covers are not sent as thumbnails even if the options ask for it. 2) permit not respecting aspect ratio when generating cover thumbnails --- src/calibre/devices/interface.py | 10 ++++++++++ src/calibre/devices/prs505/driver.py | 20 +++++++++++++++++--- src/calibre/gui2/device.py | 26 ++++++++++++++++++++++++-- src/calibre/utils/magick/draw.py | 11 +++++++++-- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 2a92f46e8d..bc442f5853 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -35,6 +35,16 @@ class DevicePlugin(Plugin): #: Height for thumbnails on the device THUMBNAIL_HEIGHT = 68 + #: Width for thumbnails on the device. Setting this will force thumbnails + #: to this size, not preserving aspect ratio. If it is not set, then + #: the aspect ratio will be preserved and the thumbnail will be no higher + #: than THUMBNAIL_HEIGHT + # THUMBNAIL_WIDTH = 68 + + #: Set this to True if the device supports updating cover thumbnails during + #: sync_booklists. Setting it to true will ask device.py to refresh the + #: cover thumbnails during book matching + WANTS_UPDATED_THUMBNAILS = False #: Whether the metadata on books can be set via the GUI. CAN_SET_METADATA = ['title', 'authors', 'collections'] diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 0f6668891a..4d3ac31540 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -81,12 +81,19 @@ class PRS505(USBMS): _('Set this option to have separate book covers uploaded ' 'every time you connect your device. Unset this option if ' 'you have so many books on the reader that performance is ' - 'unacceptable.') + 'unacceptable.'), + _('Preserve cover aspect ratio when building thumbnails') + + ':::' + + _('Set this option if you want the cover thumbnails to have ' + 'the same aspect ratio (width to height) as the cover. ' + 'Unset it if you want the thumbnail to be the maximum size, ' + 'ignoring aspect ratio.') ] EXTRA_CUSTOMIZATION_DEFAULT = [ ', '.join(['series', 'tags']), False, - False + False, + True ] OPT_COLLECTIONS = 0 @@ -96,7 +103,7 @@ class PRS505(USBMS): plugboard = None plugboard_func = None - THUMBNAIL_HEIGHT = 200 + THUMBNAIL_HEIGHT = 217 MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) + # len('main_thumbnail.jpg') + 1) @@ -138,6 +145,13 @@ class PRS505(USBMS): if not write_cache(self._card_b_prefix): self._card_b_prefix = None self.booklist_class.rebuild_collections = self.rebuild_collections + # Set the thumbnail width to the theoretical max if the user has asked + # that we do not preserve aspect ratio + if not self.settings().extra_customization[3]: + self.THUMBNAIL_WIDTH = 168 + # Set CAN_UPDATE_THUMBNAILS if the user has asked that thumbnails be + # updated on every connect + self.WANTS_UPDATED_THUMBNAILS = self.settings().extra_customization[2] def get_device_information(self, end_session=True): return (self.gui_name, '', '', '') diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index a5066a99ef..bf8c734089 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -871,6 +871,16 @@ class DeviceMixin(object): # {{{ self.send_by_mail(to, fmts, delete) def cover_to_thumbnail(self, data): + if self.device_manager.device and \ + hasattr(self.device_manager.device, 'THUMBNAIL_WIDTH'): + try: + return thumbnail(data, + self.device_manager.device.THUMBNAIL_WIDTH, + self.device_manager.device.THUMBNAIL_HEIGHT, + preserve_aspect_ratio=False) + except: + pass + return ht = self.device_manager.device.THUMBNAIL_HEIGHT \ if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT try: @@ -1272,6 +1282,8 @@ class DeviceMixin(object): # {{{ x = x.lower() if x else '' return string_pat.sub('', x) + update_metadata = prefs['manage_device_metadata'] == 'on_connect' + # Force a reset if the caches are not initialized if reset or not hasattr(self, 'db_book_title_cache'): # Build a cache (map) of the library, so the search isn't On**2 @@ -1284,8 +1296,13 @@ class DeviceMixin(object): # {{{ except: return False + get_covers = False + if update_metadata and self.device_manager.is_device_connected: + if self.device_manager.device.WANTS_UPDATED_THUMBNAILS: + get_covers = True + for id in db.data.iterallids(): - mi = db.get_metadata(id, index_is_id=True) + mi = db.get_metadata(id, index_is_id=True, get_cover=get_covers) title = clean_string(mi.title) if title not in db_book_title_cache: db_book_title_cache[title] = \ @@ -1311,7 +1328,6 @@ class DeviceMixin(object): # {{{ # the application_id to the db_id of the matching book. This value # will be used by books_on_device to indicate matches. - update_metadata = prefs['manage_device_metadata'] == 'on_connect' for booklist in booklists: for book in booklist: book.in_library = None @@ -1382,6 +1398,12 @@ class DeviceMixin(object): # {{{ if update_metadata: if self.device_manager.is_device_connected: + if self.device_manager.device.CAN_UPDATE_THUMBNAILS: + for blist in booklists: + for book in blist: + if book.cover and os.access(book.cover, os.R_OK): + book.thumbnail = \ + self.cover_to_thumbnail(open(book.cover, 'rb').read()) plugboards = self.library_view.model().db.prefs.get('plugboards', {}) self.device_manager.sync_booklists( Dispatcher(self.metadata_synced), booklists, diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index ad4b681b43..111f22cb5b 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -72,11 +72,18 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, f.write(data) return ret -def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'): +def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg', + preserve_aspect_ratio=True): img = Image() img.load(data) owidth, oheight = img.size - scaled, nwidth, nheight = fit_image(owidth, oheight, width, height) + if not preserve_aspect_ratio: + scaled = owidth > width or oheight > height + nwidth = width + nheight = height + else: + scaled, nwidth, nheight = fit_image(owidth, oheight, width, height) + print 'in thumbnail', scaled, nwidth, nheight if scaled: img.size = (nwidth, nheight) canvas = create_canvas(img.size[0], img.size[1], bgcolor) From 00210f4b7b397094ec0b77278a33c07ef98a9268 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 1 Feb 2011 15:16:44 +0000 Subject: [PATCH 03/19] Fix bugs I introduced when I renamed some interface attributes --- src/calibre/devices/prs505/driver.py | 2 +- src/calibre/gui2/device.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 4d3ac31540..3768b8be62 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -149,7 +149,7 @@ class PRS505(USBMS): # that we do not preserve aspect ratio if not self.settings().extra_customization[3]: self.THUMBNAIL_WIDTH = 168 - # Set CAN_UPDATE_THUMBNAILS if the user has asked that thumbnails be + # Set WANTS_UPDATED_THUMBNAILS if the user has asked that thumbnails be # updated on every connect self.WANTS_UPDATED_THUMBNAILS = self.settings().extra_customization[2] diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index bf8c734089..ae38a8321b 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1398,7 +1398,7 @@ class DeviceMixin(object): # {{{ if update_metadata: if self.device_manager.is_device_connected: - if self.device_manager.device.CAN_UPDATE_THUMBNAILS: + if self.device_manager.device.WANTS_UPDATED_THUMBNAILS: for blist in booklists: for book in blist: if book.cover and os.access(book.cover, os.R_OK): From ab264422d302c0855b71dfeadbd307d93532f843 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 08:55:46 -0700 Subject: [PATCH 04/19] ... --- src/calibre/ebooks/metadata/sources/google.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py index 1a3bf6d516..d9efb65ae0 100644 --- a/src/calibre/ebooks/metadata/sources/google.py +++ b/src/calibre/ebooks/metadata/sources/google.py @@ -65,7 +65,7 @@ def to_metadata(browser, log, entry_): mi = Metadata(title_, authors) try: - raw = browser.open(id_url).read() + raw = browser.open_novisit(id_url).read() feed = etree.fromstring(raw) extra = entry(feed)[0] except: @@ -129,7 +129,7 @@ class Worker(Thread): for i in self.entries: try: ans = to_metadata(self.browser, self.log, i) - if ans is not None: + if isinstance(ans, Metadata): self.result_queue.put(ans) except: self.log.exception( From 022b639157583b6bd0e81b812dd5c2230cf0f09c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 09:27:34 -0700 Subject: [PATCH 05/19] ... --- src/calibre/gui2/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index ae38a8321b..48063b11a4 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -838,7 +838,8 @@ class DeviceMixin(object): # {{{ format_count[f] = 1 for f in self.device_manager.device.settings().format_map: if f in format_count.keys(): - formats.append((f, _('%i of %i Books') % (format_count[f], len(rows))), True if f in aval_out_formats else False) + formats.append((f, _('%i of %i Books') % (format_count[f], + len(rows)), True if f in aval_out_formats else False)) elif f in aval_out_formats: formats.append((f, _('0 of %i Books') % len(rows)), True) d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats) From 581fed66c972f2a4816f9385b330e39235d1ca4d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 09:28:01 -0700 Subject: [PATCH 06/19] ... --- src/calibre/gui2/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 48063b11a4..8efa7f154c 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -841,7 +841,7 @@ class DeviceMixin(object): # {{{ formats.append((f, _('%i of %i Books') % (format_count[f], len(rows)), True if f in aval_out_formats else False)) elif f in aval_out_formats: - formats.append((f, _('0 of %i Books') % len(rows)), True) + formats.append((f, _('0 of %i Books') % len(rows), True)) d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats) if d.exec_() != QDialog.Accepted: return From ba09dbb2fde841a3efbf922a5481fe2b1883eae0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 09:30:01 -0700 Subject: [PATCH 07/19] ... --- src/calibre/manual/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 59f6a9b88d..18c53ade5d 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -391,7 +391,7 @@ Take your pick: * A tribute to the SONY Librie which was the first e-ink based e-book reader * My wife chose it ;-) -|app| is pronounced as cal-i-ber *not* ca-libre. If you're wondering, |app| is the British/commonwealth spelling for caliber. Being Indian, that's the natural spelling for me. +|app| is pronounced as cal-i-ber *not* ca-li-bre. If you're wondering, |app| is the British/commonwealth spelling for caliber. Being Indian, that's the natural spelling for me. Why does |app| show only some of my fonts on OS X? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 5ec9e9ee1448a02362f8475666b4ebc541ec8919 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 09:31:33 -0700 Subject: [PATCH 08/19] Fix #8720 (WSJ Recipe Tab Formatting / Section Title) --- resources/recipes/wsj.recipe | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/recipes/wsj.recipe b/resources/recipes/wsj.recipe index 4ce315200c..eb473f1121 100644 --- a/resources/recipes/wsj.recipe +++ b/resources/recipes/wsj.recipe @@ -35,7 +35,7 @@ class WallStreetJournal(BasicNewsRecipe): remove_tags_before = dict(name='h1') remove_tags = [ - dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]), + dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]), {'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]}, dict(rel='shortcut icon'), ] @@ -101,7 +101,7 @@ class WallStreetJournal(BasicNewsRecipe): title = 'Front Section' url = 'http://online.wsj.com' + a['href'] feeds = self.wsj_add_feed(feeds,title,url) - title = 'What''s News' + title = "What's News" url = url.replace('pageone','whatsnews') feeds = self.wsj_add_feed(feeds,title,url) else: From 3d3dcb39159842c7a9335f7678912477dec83864 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 1 Feb 2011 16:42:26 +0000 Subject: [PATCH 09/19] Fix for #7883: 'title_sort' field in save to disk template empty in v 7.33 --- src/calibre/ebooks/metadata/opf2.py | 30 ++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 62d57f2251..456bfb0ea6 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -780,22 +780,30 @@ class OPF(object): # {{{ def title_sort(self): def fget(self): - matches = self.title_path(self.metadata) + matches = self.root.xpath('//*[name() = "meta" and starts-with(@name,' + '"calibre:title_sort") and @content]') if matches: - for match in matches: - ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None) - if not ans: - ans = match.get('file-as', None) - if ans: - return ans + for elem in matches: + return self.get_text(elem) + return None def fset(self, val): + print 'here' + matches = self.root.xpath('//*[name() = "meta" and starts-with(@name,' + '"calibre:title_sort") and @content]') + if matches: + for elem in matches: + elem.getparent().remove(elem) matches = self.title_path(self.metadata) if matches: - for key in matches[0].attrib: - if key.endswith('file-as'): - matches[0].attrib.pop(key) - matches[0].set('{%s}file-as'%self.NAMESPACES['opf'], unicode(val)) + for elem in matches: + parent = elem.getparent() + attrib = {} + attrib['name'] = 'calibre:title_sort' + attrib['content'] = val + e = elem.makeelement('meta', attrib=attrib) + e.tail = '\n'+(' '*8) + parent.append(elem) return property(fget=fget, fset=fset) From 393a876d3a3d0fe7a9466fff6f6b4304d0cc4fa0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 10:23:42 -0700 Subject: [PATCH 10/19] Add a note about the removed header and footer options to the structure detection panel. --- src/calibre/gui2/convert/structure_detection.ui | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui index ef0677a67c..f80e6f8182 100644 --- a/src/calibre/gui2/convert/structure_detection.ui +++ b/src/calibre/gui2/convert/structure_detection.ui @@ -48,10 +48,10 @@ - + - + Qt::Vertical @@ -77,6 +77,16 @@ + + + + The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field. + + + true + + + From 9acd6d6189cdb2f008ea519fb5b0709db93c4601 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 1 Feb 2011 17:28:55 +0000 Subject: [PATCH 11/19] Put file-as back into OPF title_sort.get as a fall back --- src/calibre/ebooks/metadata/opf2.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 456bfb0ea6..a721c5cb2f 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -780,15 +780,24 @@ class OPF(object): # {{{ def title_sort(self): def fget(self): + #first try the title_sort meta tag matches = self.root.xpath('//*[name() = "meta" and starts-with(@name,' '"calibre:title_sort") and @content]') if matches: for elem in matches: return self.get_text(elem) + # fallback to file-as + matches = self.title_path(self.metadata) + if matches: + for match in matches: + ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None) + if not ans: + ans = match.get('file-as', None) + if ans: + return ans return None def fset(self, val): - print 'here' matches = self.root.xpath('//*[name() = "meta" and starts-with(@name,' '"calibre:title_sort") and @content]') if matches: From 1bcef905630d3373afdc05ee11a75847eba8c699 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 13:08:33 -0700 Subject: [PATCH 12/19] MOBI Output: Use the book uuid as the ASIN field and set cdetype to EBOK to allow Amazon furthest read tracking to work with calibre generated MOBI files. Fixes #8721 (Please add support for Mobi EXTH metadata data in fields 113 and 501) --- src/calibre/ebooks/mobi/reader.py | 2 ++ src/calibre/ebooks/mobi/writer.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 0ae3c9ac9d..9576ccb637 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -103,6 +103,8 @@ class EXTHHeader(object): pass elif id == 108: pass # Producer + elif id == 113: + pass # ASIN or UUID #else: # print 'unhandled metadata record', id, repr(content) diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 2a71ecd43b..abba173d69 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -1547,6 +1547,31 @@ class MobiWriter(object): rights = 'Unknown' exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8)) exth.write(rights) + nrecs += 1 + + # Write UUID as ASIN + uuid = None + from calibre.ebooks.oeb.base import OPF + for x in oeb.metadata['identifier']: + if x.get(OPF('scheme'), None).lower() == 'uuid' or unicode(x).startswith('urn:uuid:'): + uuid = unicode(x).split(':')[-1] + break + if uuid is None: + from uuid import uuid4 + uuid = str(uuid4()) + + if isinstance(uuid, unicode): + uuid = uuid.encode('utf-8') + exth.write(pack('>II', 113, len(uuid) + 8)) + exth.write(uuid) + nrecs += 1 + + # Write cdetype + if not self.opts.mobi_periodical: + data = 'EBOK' + exth.write(pack('>II', 501, len(data)+8)) + exth.write(data) + nrecs += 1 # Add a publication date entry if oeb.metadata['date'] != [] : From 72b93454506717a9280bbd16966fd701724239ff Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 13:24:38 -0700 Subject: [PATCH 13/19] Fix mimetype sent by content server for PDB files --- resources/mime.types | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/mime.types b/resources/mime.types index ab98b3bf4a..a2a67c38f9 100644 --- a/resources/mime.types +++ b/resources/mime.types @@ -585,7 +585,6 @@ application/vnd.osa.netdeploy application/vnd.osgi.bundle application/vnd.osgi.dp dp application/vnd.otps.ct-kip+xml -application/vnd.palm oprc pdb pqa application/vnd.paos.xml application/vnd.pg.format str application/vnd.pg.osasli ei6 @@ -1082,7 +1081,6 @@ chemical/x-ncbi-asn1 asn chemical/x-ncbi-asn1-ascii ent prt chemical/x-ncbi-asn1-binary aso val chemical/x-ncbi-asn1-spec asn -chemical/x-pdb ent pdb chemical/x-rosdal ros chemical/x-swissprot sw chemical/x-vamas-iso14976 vms @@ -1379,3 +1377,5 @@ application/x-cbr cbr application/x-cb7 cb7 application/x-koboreader-ebook kobo image/wmf wmf +application/ereader pdb + From 99131cc2e0620ac345b8937e262d2c17146191cf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 13:25:59 -0700 Subject: [PATCH 14/19] Fix #8620 (the cailibre delete my ebook) --- src/calibre/library/database2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 3fc16e99b4..bfe54df36e 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -430,8 +430,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): authors = self.authors(id, index_is_id=True) if not authors: authors = _('Unknown') - author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore') - title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore') + author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') + title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') path = author + '/' + title + ' (%d)'%id return path @@ -442,8 +442,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): authors = self.authors(id, index_is_id=True) if not authors: authors = _('Unknown') - author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace') - title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace') + author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') + title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') name = title + ' - ' + author while name.endswith('.'): name = name[:-1] From 81bf07ddb4b81f5150198fb2f894225b22f31a06 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 14:46:42 -0700 Subject: [PATCH 15/19] Clarify the help texts for runnning the GUI in debug mode with calibre-debug --- src/calibre/debug.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/debug.py b/src/calibre/debug.py index e1c3e1809e..3a080fc57b 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -22,13 +22,15 @@ Run an embedded python interpreter. parser.add_option('-d', '--debug-device-driver', default=False, action='store_true', help='Debug the specified device driver.') parser.add_option('-g', '--gui', default=False, action='store_true', - help='Run the GUI',) + help='Run the GUI with debugging enabled. Debug output is ' + 'printed to stdout and stderr.') parser.add_option('--gui-debug', default=None, help='Run the GUI with a debug console, logging to the' - ' specified path',) + ' specified path. For internal use only, use the -g' + ' option to run the GUI in debug mode',) parser.add_option('--show-gui-debug', default=None, - help='Display the specified log file.',) - + help='Display the specified log file. For internal use' + ' only.',) parser.add_option('-w', '--viewer', default=False, action='store_true', help='Run the ebook viewer',) parser.add_option('--paths', default=False, action='store_true', From 3e895675e29d132acdc5cf1f065d8a44c4852945 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 14:55:08 -0700 Subject: [PATCH 16/19] ... --- src/calibre/gui2/email.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py index 6b2ed81413..426747e044 100644 --- a/src/calibre/gui2/email.py +++ b/src/calibre/gui2/email.py @@ -264,8 +264,9 @@ class EmailMixin(object): # {{{ if _auto_ids != []: for id in _auto_ids: if specific_format == None: - formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')] - formats = formats if formats != None else [] + dbfmts = self.library_view.model().db.formats(id, index_is_id=True) + formats = [f.lower() for f in (dbfmts.split(',') if fmts else + [])] if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []: auto.append(id) else: From 447d1a66d5fdc1ffed5343539d9c57406a4092f1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 Feb 2011 21:09:32 -0700 Subject: [PATCH 17/19] ... --- resources/recipes/wsj_free.recipe | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/recipes/wsj_free.recipe b/resources/recipes/wsj_free.recipe index df8234e8e2..a4a957fc90 100644 --- a/resources/recipes/wsj_free.recipe +++ b/resources/recipes/wsj_free.recipe @@ -10,7 +10,10 @@ class WallStreetJournal(BasicNewsRecipe): title = 'Wall Street Journal (free)' __author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17' - description = 'News and current affairs' + description = '''News and current affairs. This recipe only fetches complete + versions of the articles that are available free on the wsj.com website. + To get the rest of the articles, subscribe to the WSJ and use the other WSJ + recipe.''' language = 'en' cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG' max_articles_per_feed = 1000 @@ -151,6 +154,4 @@ class WallStreetJournal(BasicNewsRecipe): return articles - def cleanup(self): - self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com') From aa7ebf0ac38912890a70a7b972ff15a8490c4c38 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 2 Feb 2011 00:13:57 -0700 Subject: [PATCH 18/19] Nicer completion widgets for author/tags controls --- src/calibre/gui2/complete.py | 354 +++++++++++++++++++++ src/calibre/gui2/metadata/basic_widgets.py | 16 +- 2 files changed, 362 insertions(+), 8 deletions(-) create mode 100644 src/calibre/gui2/complete.py diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py new file mode 100644 index 0000000000..5c5a836d98 --- /dev/null +++ b/src/calibre/gui2/complete.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, QTimer, \ + QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \ + QStyle, QEvent, pyqtSignal + +from calibre.utils.icu import sort_key, lower +from calibre.gui2 import NONE +from calibre.gui2.widgets import EnComboBox + +class CompleterItemDelegate(QItemDelegate): # {{{ + + ''' Renders the current item as thought it were selected ''' + + def __init__(self, view): + self.view = view + QItemDelegate.__init__(self, view) + + def paint(self, p, opt, idx): + opt = QStyleOptionViewItem(opt) + opt.showDecorationSelected = True + if self.view.currentIndex() == idx: + opt.state |= QStyle.State_HasFocus + QItemDelegate.paint(self, p, opt, idx) + +# }}} + +class CompleteWindow(QListView): # {{{ + + ''' + The completion popup. For keyboard and mouse handling see + :meth:`eventFilter`. + ''' + + #: This signal is emitted when the user selects one of the listed + #: completions, by mouse or keyboard + completion_selected = pyqtSignal(object) + + def __init__(self, widget, model): + self.widget = widget + QListView.__init__(self) + self.setVisible(False) + self.setParent(None, Qt.Popup) + self.setAlternatingRowColors(True) + self.setFocusPolicy(Qt.NoFocus) + self._d = CompleterItemDelegate(self) + self.setItemDelegate(self._d) + self.setModel(model) + self.setFocusProxy(widget) + self.installEventFilter(self) + self.clicked.connect(self.do_selected) + self.entered.connect(self.do_entered) + self.setMouseTracking(True) + + def do_entered(self, idx): + if idx.isValid(): + self.setCurrentIndex(idx) + + def do_selected(self, idx=None): + idx = self.currentIndex() if idx is None else idx + if not idx.isValid() and self.model().rowCount() > 0: + idx = self.model().index(0) + if idx.isValid(): + data = unicode(self.model().data(idx, Qt.DisplayRole)) + self.completion_selected.emit(data) + self.hide() + + def eventFilter(self, o, e): + if o is not self: + return False + if e.type() == e.KeyPress: + key = e.key() + if key in (Qt.Key_Escape, Qt.Key_Backtab) or \ + (key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)): + self.hide() + return True + elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab): + self.do_selected() + return True + elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, + Qt.Key_PageDown): + return False + # Send key event to associated line edit + self.widget.eat_focus_out = False + try: + self.widget.event(e) + finally: + self.widget.eat_focus_out = True + if not self.widget.hasFocus(): + # Line edit lost focus + self.hide() + if e.isAccepted(): + # Line edit consumed event + return True + elif e.type() == e.MouseButtonPress: + # Hide popup if user clicks outside it, otherwise + # pass event to popup + if not self.underMouse(): + self.hide() + return True + elif e.type() in (e.InputMethod, e.ShortcutOverride): + QApplication.sendEvent(self.widget, e) + + return False # Do not filter this event + +# }}} + +class CompleteModel(QAbstractListModel): + + def __init__(self, parent=None): + QAbstractListModel.__init__(self, parent) + self.sep = ',' + self.space_before_sep = False + self.items = [] + self.lowered_items = [] + self.matches = [] + + def set_items(self, items): + items = [unicode(x.strip()) for x in items] + self.items = list(sorted(items, key=lambda x: sort_key(x))) + self.lowered_items = [lower(x) for x in self.items] + self.matches = [] + self.reset() + + def rowCount(self, *args): + return len(self.matches) + + def data(self, index, role): + if role == Qt.DisplayRole: + r = index.row() + try: + return self.matches[r] + except IndexError: + pass + return NONE + + def get_matches(self, prefix): + ''' + Return all matches that (case insensitively) start with prefix + ''' + prefix = lower(prefix) + ans = [] + if prefix: + for i, test in enumerate(self.lowered_items): + if test.startswith(prefix): + ans.append(self.items[i]) + return ans + + def update_matches(self, matches): + self.matches = matches + self.reset() + +class MultiCompleteLineEdit(QLineEdit): + ''' + A line edit that completes on multiple items separated by a + separator. Use the :meth:`update_items_cache` to set the list of + all possible completions. Separator can be controlled with the + :meth:`set_separator` and :meth:`set_space_before_sep` methods. + ''' + + def __init__(self, parent=None): + self.eat_focus_out = True + self.max_visible_items = 7 + self.current_prefix = None + QLineEdit.__init__(self, parent) + + self._model = CompleteModel(parent=self) + self.complete_window = CompleteWindow(self, self._model) + self.textChanged.connect(self.text_changed) + self.cursorPositionChanged.connect(self.cursor_position_changed) + self.complete_window.completion_selected.connect(self.completion_selected) + + # Interface {{{ + def update_items_cache(self, complete_items): + self.all_items = complete_items + + def set_separator(self, sep): + self.sep = sep + + def set_space_before_sep(self, space_before): + self.space_before_sep = space_before + + # }}} + + def eventFilter(self, o, e): + if self.eat_focus_out and o is self and e.type() == QEvent.FocusOut: + if self.complete_window.isVisible(): + return True # Filter this event since the cw is visible + return QLineEdit.eventFilter(self, o, e) + + + def text_changed(self, *args): + self.update_completions() + + def cursor_position_changed(self, *args): + self.update_completions() + + def update_completions(self): + ' Update the list of completions ' + cpos = self.cursorPosition() + text = unicode(self.text()) + prefix = text[:cpos] + self.current_prefix = prefix + complete_prefix = prefix.lstrip() + if self.sep: + complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip() + + matches = self._model.get_matches(complete_prefix) + self.update_complete_window(matches) + + def get_completed_text(self, text): + ''' + Get completed text from current cursor position and the completion + text + ''' + if self.sep is None: + return text + else: + cursor_pos = self.cursorPosition() + before_text = unicode(self.text())[:cursor_pos] + after_text = unicode(self.text())[cursor_pos:] + after_parts = after_text.split(self.sep) + if len(after_parts) < 3 and not after_parts[-1].strip(): + after_text = u'' + prefix_len = len(before_text.split(self.sep)[-1].lstrip()) + if self.space_before_sep: + complete_text_pat = '%s%s %s %s' + len_extra = 3 + else: + complete_text_pat = '%s%s%s %s' + len_extra = 2 + return prefix_len, len_extra, complete_text_pat % ( + before_text[:cursor_pos - prefix_len], text, self.sep, after_text) + + def completion_selected(self, text): + prefix_len, len_extra, ctext = self.get_completed_text(text) + if self.sep is None: + self.setText(ctext) + self.setCursorPosition(len(ctext)) + else: + cursor_pos = self.cursorPosition() + self.setText(ctext) + self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra) + + def update_complete_window(self, matches): + self._model.update_matches(matches) + if matches: + self.show_complete_window() + else: + self.complete_window.hide() + + + def position_complete_window(self): + popup = self.complete_window + screen = QApplication.desktop().availableGeometry(self) + h = (popup.sizeHintForRow(0) * min(self.max_visible_items, + popup.model().rowCount()) + 3) + 3 + hsb = popup.horizontalScrollBar() + if hsb and hsb.isVisible(): + h += hsb.sizeHint().height() + + rh = self.height() + pos = self.mapToGlobal(QPoint(0, self.height() - 2)) + w = self.width() + + if w > screen.width(): + w = screen.width() + if (pos.x() + w) > (screen.x() + screen.width()): + pos.setX(screen.x() + screen.width() - w) + if (pos.x() < screen.x()): + pos.setX(screen.x()) + + top = pos.y() - rh - screen.top() + 2 + bottom = screen.bottom() - pos.y() + h = max(h, popup.minimumHeight()) + if h > bottom: + h = min(max(top, bottom), h) + if top > bottom: + pos.setY(pos.y() - h - rh + 2) + + popup.setGeometry(pos.x(), pos.y(), w, h) + + + def show_complete_window(self): + self.position_complete_window() + self.complete_window.show() + + def moveEvent(self, ev): + ret = QLineEdit.moveEvent(self, ev) + QTimer.singleShot(0, self.position_complete_window) + return ret + + def resizeEvent(self, ev): + ret = QLineEdit.resizeEvent(self, ev) + QTimer.singleShot(0, self.position_complete_window) + return ret + + + @dynamic_property + def all_items(self): + def fget(self): + return self._model.items + def fset(self, items): + self._model.set_items(items) + return property(fget=fget, fset=fset) + + @dynamic_property + def sep(self): + def fget(self): + return self._model.sep + def fset(self, val): + self._model.sep = val + return property(fget=fget, fset=fset) + + @dynamic_property + def space_before_sep(self): + def fget(self): + return self._model.space_before_sep + def fset(self, val): + self._model.space_before_sep = val + return property(fget=fget, fset=fset) + +class MultiCompleteComboBox(EnComboBox): + + def __init__(self, *args): + EnComboBox.__init__(self, *args) + self.setLineEdit(MultiCompleteLineEdit(self)) + + def update_items_cache(self, complete_items): + self.lineEdit().update_items_cache(complete_items) + + def set_separator(self, sep): + self.lineEdit().set_separator(sep) + + def set_space_before_sep(self, space_before): + self.lineEdit().set_space_before_sep(space_before) + + + +if __name__ == '__main__': + from PyQt4.Qt import QDialog, QVBoxLayout + app = QApplication([]) + d = QDialog() + d.setLayout(QVBoxLayout()) + le = MultiCompleteLineEdit(d) + d.layout().addWidget(le) + le.all_items = ['one', 'otwo', 'othree', 'ooone', 'ootwo', 'oothree'] + d.exec_() diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index d3fa5958ab..8ec037278e 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -12,8 +12,8 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \ QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ QPushButton, QSpinBox, QLineEdit -from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox, FormatList, ImageView, CompleteLineEdit +from calibre.gui2.widgets import EnLineEdit, EnComboBox, FormatList, ImageView +from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox from calibre.utils.icu import sort_key from calibre.utils.config import tweaks, prefs from calibre.ebooks.metadata import title_sort, authors_to_string, \ @@ -149,14 +149,14 @@ class TitleSortEdit(TitleEdit): # }}} # Authors {{{ -class AuthorsEdit(CompleteComboBox): +class AuthorsEdit(MultiCompleteComboBox): TOOLTIP = '' LABEL = _('&Author(s):') def __init__(self, parent): self.dialog = parent - CompleteComboBox.__init__(self, parent) + MultiCompleteComboBox.__init__(self, parent) self.setToolTip(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP) self.setEditable(True) @@ -814,14 +814,14 @@ class RatingEdit(QSpinBox): # {{{ # }}} -class TagsEdit(CompleteLineEdit): # {{{ +class TagsEdit(MultiCompleteLineEdit): # {{{ LABEL = _('Ta&gs:') TOOLTIP = '

'+_('Tags categorize the book. This is particularly ' 'useful while searching.

They can be any words' 'or phrases, separated by commas.') def __init__(self, parent): - CompleteLineEdit.__init__(self, parent) + MultiCompleteLineEdit.__init__(self, parent) self.setToolTip(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP) @@ -839,7 +839,7 @@ class TagsEdit(CompleteLineEdit): # {{{ tags = db.tags(id_, index_is_id=True) tags = tags.split(',') if tags else [] self.current_val = tags - self.update_items_cache(db.all_tags()) + self.all_items = db.all_tags() self.original_val = self.current_val @property @@ -860,7 +860,7 @@ class TagsEdit(CompleteLineEdit): # {{{ d = TagEditor(self, db, id_) if d.exec_() == TagEditor.Accepted: self.current_val = d.tags - self.update_items_cache(db.all_tags()) + self.all_items = db.all_tags() def commit(self, db, id_): From 7852275d9325b3d8b32a97ef37838bcd1a5831dd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 2 Feb 2011 00:19:21 -0700 Subject: [PATCH 19/19] ... --- src/calibre/gui2/complete.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index 5c5a836d98..ce8609fc99 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -162,6 +162,9 @@ class MultiCompleteLineEdit(QLineEdit): separator. Use the :meth:`update_items_cache` to set the list of all possible completions. Separator can be controlled with the :meth:`set_separator` and :meth:`set_space_before_sep` methods. + + A call to self.set_separator(None) will allow this widget to be used + to complete non multiple fields as well. ''' def __init__(self, parent=None):