From 5301f28eb327c9724b17d05da09b99e380e385e7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 09:43:55 +0530 Subject: [PATCH 01/15] ... --- src/calibre/gui2/preferences/server.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/preferences/server.ui b/src/calibre/gui2/preferences/server.ui index 674e4bdbc2..85d27eab57 100644 --- a/src/calibre/gui2/preferences/server.ui +++ b/src/calibre/gui2/preferences/server.ui @@ -129,7 +129,7 @@ - Max. OPDS &ungrouped items: + Max. &ungrouped items: opt_max_opds_ungrouped_items From 6031fa67b818d5e4975d99212fbfd3bbd2ef69b3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 10:43:30 +0530 Subject: [PATCH 02/15] Metadata download: WHen showing downloaded covers, allow right clicking on a cover to view a full size version. Fixes #1170544 ((enhancement) Zooming in on searched cover images) --- src/calibre/gui2/metadata/single_download.py | 52 +++++++++++++------- src/calibre/gui2/viewer/image_popup.py | 21 +++++--- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index ffa83b6ea8..3e9bb87687 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -21,7 +21,7 @@ from PyQt4.Qt import ( QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex, - QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel) + QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel, QMenu, QCursor) from PyQt4.QtWebKit import QWebView from calibre.customize.ui import metadata_plugins @@ -40,7 +40,7 @@ from calibre.utils.ipc.simple_worker import fork_job, WorkerError from calibre.ptempfile import TemporaryDirectory # }}} -class RichTextDelegate(QStyledItemDelegate): # {{{ +class RichTextDelegate(QStyledItemDelegate): # {{{ def __init__(self, parent=None, max_width=160): QStyledItemDelegate.__init__(self, parent) @@ -77,7 +77,7 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} -class CoverDelegate(QStyledItemDelegate): # {{{ +class CoverDelegate(QStyledItemDelegate): # {{{ needs_redraw = pyqtSignal() @@ -143,7 +143,7 @@ class CoverDelegate(QStyledItemDelegate): # {{{ # }}} -class ResultsModel(QAbstractTableModel): # {{{ +class ResultsModel(QAbstractTableModel): # {{{ COLUMNS = ( '#', _('Title'), _('Published'), _('Has cover'), _('Has summary') @@ -182,7 +182,6 @@ class ResultsModel(QAbstractTableModel): # {{{ p = book.publisher if book.publisher else '' return '%s
%s' % (d, p) - def data(self, index, role): row, col = index.row(), index.column() try: @@ -233,7 +232,7 @@ class ResultsModel(QAbstractTableModel): # {{{ # }}} -class ResultsView(QTableView): # {{{ +class ResultsView(QTableView): # {{{ show_details_signal = pyqtSignal(object) book_selected = pyqtSignal(object) @@ -316,7 +315,7 @@ class ResultsView(QTableView): # {{{ # }}} -class Comments(QWebView): # {{{ +class Comments(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) @@ -384,7 +383,7 @@ class Comments(QWebView): # {{{ return QSize(800, 300) # }}} -class IdentifyWorker(Thread): # {{{ +class IdentifyWorker(Thread): # {{{ def __init__(self, log, abort, title, authors, identifiers, caches): Thread.__init__(self) @@ -441,7 +440,7 @@ class IdentifyWorker(Thread): # {{{ # }}} -class IdentifyWidget(QWidget): # {{{ +class IdentifyWidget(QWidget): # {{{ rejected = pyqtSignal() results_found = pyqtSignal() @@ -552,12 +551,11 @@ class IdentifyWidget(QWidget): # {{{ self.results_view.show_results(self.worker.results) self.results_found.emit() - def cancel(self): self.abort.set() # }}} -class CoverWorker(Thread): # {{{ +class CoverWorker(Thread): # {{{ def __init__(self, log, abort, title, authors, identifiers, caches): Thread.__init__(self) @@ -609,7 +607,8 @@ class CoverWorker(Thread): # {{{ def scan_once(self, tdir, seen): for x in list(os.listdir(tdir)): - if x in seen: continue + if x in seen: + continue if x.endswith('.cover') and os.path.exists(os.path.join(tdir, x+'.done')): name = x.rpartition('.')[0] @@ -635,7 +634,7 @@ class CoverWorker(Thread): # {{{ # }}} -class CoversModel(QAbstractListModel): # {{{ +class CoversModel(QAbstractListModel): # {{{ def __init__(self, current_cover, parent=None): QAbstractListModel.__init__(self, parent) @@ -770,7 +769,7 @@ class CoversModel(QAbstractListModel): # {{{ # }}} -class CoversView(QListView): # {{{ +class CoversView(QListView): # {{{ chosen = pyqtSignal() @@ -793,6 +792,8 @@ class CoversView(QListView): # {{{ type=Qt.QueuedConnection) self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_context_menu) def select(self, num): current = self.model().index(num) @@ -814,9 +815,24 @@ class CoversView(QListView): # {{{ else: self.select(self.m.index_from_pointer(pointer).row()) + def show_context_menu(self, point): + idx = self.currentIndex() + if idx and idx.isValid() and not idx.data(Qt.UserRole).toPyObject(): + m = QMenu() + m.addAction(QIcon(I('view.png')), _('View this cover at full size'), self.show_cover) + m.exec_(QCursor.pos()) + + def show_cover(self): + idx = self.currentIndex() + pmap = self.model().cover_pixmap(idx) + if pmap is not None: + from calibre.gui2.viewer.image_popup import ImageView + d = ImageView(self, pmap, unicode(idx.data(Qt.DisplayRole).toString()), geom_name='metadata_download_cover_popup_geom') + d(use_exec=True) + # }}} -class CoversWidget(QWidget): # {{{ +class CoversWidget(QWidget): # {{{ chosen = pyqtSignal() finished = pyqtSignal() @@ -922,7 +938,7 @@ class CoversWidget(QWidget): # {{{ # }}} -class LogViewer(QDialog): # {{{ +class LogViewer(QDialog): # {{{ def __init__(self, log, parent=None): QDialog.__init__(self, parent) @@ -970,7 +986,7 @@ class LogViewer(QDialog): # {{{ # }}} -class FullFetch(QDialog): # {{{ +class FullFetch(QDialog): # {{{ def __init__(self, current_cover=None, parent=None): QDialog.__init__(self, parent) @@ -1085,7 +1101,7 @@ class FullFetch(QDialog): # {{{ return self.exec_() # }}} -class CoverFetch(QDialog): # {{{ +class CoverFetch(QDialog): # {{{ def __init__(self, current_cover=None, parent=None): QDialog.__init__(self, parent) diff --git a/src/calibre/gui2/viewer/image_popup.py b/src/calibre/gui2/viewer/image_popup.py index 075143f3c3..1b616a12b3 100644 --- a/src/calibre/gui2/viewer/image_popup.py +++ b/src/calibre/gui2/viewer/image_popup.py @@ -15,16 +15,17 @@ from calibre.gui2 import choose_save_file, gprefs class ImageView(QDialog): - def __init__(self, parent, current_img, current_url): + def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'): QDialog.__init__(self) dw = QApplication.instance().desktop() self.avail_geom = dw.availableGeometry(parent) self.current_img = current_img self.current_url = current_url self.factor = 1.0 + self.geom_name = geom_name self.label = l = QLabel() - l.setBackgroundRole(QPalette.Base); + l.setBackgroundRole(QPalette.Base) l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) l.setScaledContents(True) @@ -88,21 +89,27 @@ class ImageView(QDialog): self.label.setPixmap(pm) self.label.adjustSize() - def __call__(self): + def __call__(self, use_exec=False): geom = self.avail_geom self.label.setPixmap(self.current_img) self.label.adjustSize() self.resize(QSize(int(geom.width()/2.5), geom.height()-50)) - geom = gprefs.get('viewer_image_popup_geometry', None) + geom = gprefs.get(self.geom_name, None) if geom is not None: self.restoreGeometry(geom) - self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1] + try: + self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1] + except AttributeError: + self.current_image_name = self.current_url title = _('View Image: %s')%self.current_image_name self.setWindowTitle(title) - self.show() + if use_exec: + self.exec_() + else: + self.show() def done(self, e): - gprefs['viewer_image_popup_geometry'] = bytearray(self.saveGeometry()) + gprefs[self.geom_name] = bytearray(self.saveGeometry()) return QDialog.done(self, e) def wheelEvent(self, event): From 4dc99bfcaa09f6d124829bfc0667af7988eb11ad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 16:37:08 +0530 Subject: [PATCH 03/15] Device driver: Detect Laser EB720 with newer firmware. Fixes #1171341 (Calibre wont recognise Laser EB720 ereader) --- src/calibre/devices/teclast/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/teclast/driver.py b/src/calibre/devices/teclast/driver.py index acd20308ad..95d8c3cf3f 100644 --- a/src/calibre/devices/teclast/driver.py +++ b/src/calibre/devices/teclast/driver.py @@ -58,8 +58,8 @@ class PICO(NEWSMY): gui_name = 'Pico' description = _('Communicate with the Pico reader.') - VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', ''] - WINDOWS_MAIN_MEM = ['USBDISK__USER', 'EB720'] + VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', 'LASER', ''] + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['USBDISK__USER', 'EB720', 'EBOOK-EB720'] EBOOK_DIR_MAIN = 'Books' FORMATS = ['EPUB', 'FB2', 'TXT', 'LRC', 'PDB', 'PDF', 'HTML', 'WTXT'] SCAN_FROM_ROOT = True From 5ff3b9e811c5c77da2526b9be75f553c610a02da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 17:11:02 +0530 Subject: [PATCH 04/15] ... --- src/calibre/ebooks/conversion/plugins/epub_input.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py index b602ac9cd0..b6024c9b84 100644 --- a/src/calibre/ebooks/conversion/plugins/epub_input.py +++ b/src/calibre/ebooks/conversion/plugins/epub_input.py @@ -188,7 +188,6 @@ class EPUBInput(InputFormatPlugin): raise DRMError(os.path.basename(path)) self.encrypted_fonts = self._encrypted_font_uris - if len(parts) > 1 and parts[0]: delta = '/'.join(parts[:-1])+'/' for elem in opf.itermanifest(): From 68959f76527ba0d0ed7041d7d070019ab28802e2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 17:38:21 +0530 Subject: [PATCH 05/15] ... --- src/calibre/ebooks/oeb/reader.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index d0474fa7e8..8d63f30526 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -148,7 +148,6 @@ class OEBReader(object): if not has_aut: m.add('creator', self.oeb.translate(__('Unknown')), role='aut') - def _manifest_prune_invalid(self): ''' Remove items from manifest that contain invalid data. This prevents @@ -293,7 +292,7 @@ class OEBReader(object): continue try: href = item.abshref(urlnormalize(href)) - except ValueError: # Malformed URL + except ValueError: # Malformed URL continue if href not in manifest.hrefs: continue @@ -394,9 +393,9 @@ class OEBReader(object): authorElement = xpath(child, 'descendant::calibre:meta[@name = "author"]') - if authorElement : + if authorElement: author = authorElement[0].text - else : + else: author = None descriptionElement = xpath(child, @@ -406,7 +405,7 @@ class OEBReader(object): method='text', encoding=unicode).strip() if not description: description = None - else : + else: description = None index_image = xpath(child, @@ -497,7 +496,8 @@ class OEBReader(object): titles = [] headers = [] for item in self.oeb.spine: - if not item.linear: continue + if not item.linear: + continue html = item.data title = ''.join(xpath(html, '/h:html/h:head/h:title/text()')) title = COLLAPSE_RE.sub(' ', title.strip()) @@ -515,17 +515,21 @@ class OEBReader(object): if len(titles) > len(set(titles)): use = headers for title, item in izip(use, self.oeb.spine): - if not item.linear: continue + if not item.linear: + continue toc.add(title, item.href) return True def _toc_from_opf(self, opf, item): self.oeb.auto_generated_toc = False - if self._toc_from_ncx(item): return + if self._toc_from_ncx(item): + return # Prefer HTML to tour based TOC, since several LIT files # have good HTML TOCs but bad tour based TOCs - if self._toc_from_html(opf): return - if self._toc_from_tour(opf): return + if self._toc_from_html(opf): + return + if self._toc_from_tour(opf): + return self._toc_from_spine(opf) self.oeb.auto_generated_toc = True @@ -589,8 +593,10 @@ class OEBReader(object): return True def _pages_from_opf(self, opf, item): - if self._pages_from_ncx(opf, item): return - if self._pages_from_page_map(opf): return + if self._pages_from_ncx(opf, item): + return + if self._pages_from_page_map(opf): + return return def _cover_from_html(self, hcover): From 763c92110855bd446cbcf0e1b060a7f6bcac30de Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 17:44:58 +0530 Subject: [PATCH 06/15] pep8 --- src/calibre/ebooks/oeb/base.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 2a2d89b894..29a809e190 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -40,11 +40,11 @@ CALIBRE_NS = 'http://calibre.kovidgoyal.net/2009/metadata' RE_NS = 'http://exslt.org/regular-expressions' MBP_NS = 'http://www.mobipocket.com' -XPNSMAP = {'h' : XHTML_NS, 'o1' : OPF1_NS, 'o2' : OPF2_NS, - 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, - 'xsi': XSI_NS, 'dt' : DCTERMS_NS, 'ncx': NCX_NS, - 'svg': SVG_NS, 'xl' : XLINK_NS, 're': RE_NS, - 'mbp': MBP_NS, 'calibre': CALIBRE_NS } +XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS, + 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, + 'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS, + 'svg': SVG_NS, 'xl': XLINK_NS, 're': RE_NS, + 'mbp': MBP_NS, 'calibre': CALIBRE_NS} OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS} OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS, @@ -142,7 +142,6 @@ def iterlinks(root, find_links_in_css=True): if attr in link_attrs: yield (el, attr, attribs[attr], 0) - if not find_links_in_css: continue if tag == XHTML('style') and el.text: @@ -497,7 +496,7 @@ class DirContainer(object): return False try: path = os.path.join(self.rootdir, self._unquote(path)) - except ValueError: #Happens if path contains quoted special chars + except ValueError: # Happens if path contains quoted special chars return False try: return os.path.isfile(path) @@ -577,12 +576,13 @@ class Metadata(object): allowed = self.allowed if allowed is not None and term not in allowed: raise AttributeError( - 'attribute %r not valid for metadata term %r' \ + 'attribute %r not valid for metadata term %r' % (self.attr(term), barename(obj.term))) return self.attr(term) def __get__(self, obj, cls): - if obj is None: return None + if obj is None: + return None return obj.attrib.get(self.term_attr(obj), '') def __set__(self, obj, value): @@ -628,8 +628,8 @@ class Metadata(object): self.value = value return property(fget=fget, fset=fset) - scheme = Attribute(lambda term: 'scheme' if \ - term == OPF('meta') else OPF('scheme'), + scheme = Attribute(lambda term: 'scheme' if + term == OPF('meta') else OPF('scheme'), [DC('identifier'), OPF('meta')]) file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'), DC('title')]) @@ -882,7 +882,6 @@ class Manifest(object): return self._parse_xhtml(convert_markdown(data, title=title)) - def _parse_css(self, data): from cssutils import CSSParser, log, resolveImports log.setLevel(logging.WARN) @@ -935,7 +934,7 @@ class Manifest(object): data = self._loader(getattr(self, 'html_input_href', self.href)) if not isinstance(data, basestring): - pass # already parsed + pass # already parsed elif self.media_type.lower() in OEB_DOCS: data = self._parse_xhtml(data) elif self.media_type.lower()[-4:] in ('+xml', '/xml'): @@ -1022,7 +1021,8 @@ class Manifest(object): target, frag = urldefrag(href) target = target.split('/') for index in xrange(min(len(base), len(target))): - if base[index] != target[index]: break + if base[index] != target[index]: + break else: index += 1 relhref = (['..'] * (len(base) - index)) + target[index:] From 6dbd826c513628e11d974428ca9b87fd2365d7da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 18:23:36 +0530 Subject: [PATCH 07/15] EPUB Input: Fix handling of EPUB files that contain images with non-ascii filenames. Fixes #1171186 (Private bug) --- src/calibre/ebooks/oeb/base.py | 18 +++++++++++++++--- src/calibre/ebooks/oeb/reader.py | 2 ++ .../ebooks/oeb/transforms/trimmanifest.py | 2 ++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 29a809e190..21c0c60a55 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -1,7 +1,6 @@ ''' Basic support for manipulating OEB 1.x/2.0 content and metadata. ''' -from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Marshall T. Vandegrift ' @@ -11,7 +10,7 @@ import os, re, uuid, logging from collections import defaultdict from itertools import count from urlparse import urldefrag, urlparse, urlunparse, urljoin -from urllib import unquote as urlunquote +from urllib import unquote from lxml import etree, html from calibre.constants import filesystem_encoding, __version__ @@ -372,6 +371,19 @@ def urlquote(href): result.append(char) return ''.join(result) +def urlunquote(href): + # unquote must run on a bytestring and will return a bytestring + # If it runs on a unicode object, it returns a double encoded unicode + # string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8') + # and the latter is correct + want_unicode = isinstance(href, unicode) + if want_unicode: + href = href.encode('utf-8') + href = unquote(href) + if want_unicode: + href = href.decode('utf-8') + return href + def urlnormalize(href): """Convert a URL into normalized form, with all and only URL-unsafe characters URL quoted. @@ -468,7 +480,7 @@ class DirContainer(object): return def _unquote(self, path): - # urlunquote must run on a bytestring and will return a bytestring + # unquote must run on a bytestring and will return a bytestring # If it runs on a unicode object, it returns a double encoded unicode # string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8') # and the latter is correct diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 8d63f30526..6a3747d2d3 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -196,6 +196,8 @@ class OEBReader(object): item.media_type[-4:] in ('/xml', '+xml')): hrefs = [r[2] for r in iterlinks(data)] for href in hrefs: + if isinstance(href, bytes): + href = href.decode('utf-8') href, _ = urldefrag(href) if not href: continue diff --git a/src/calibre/ebooks/oeb/transforms/trimmanifest.py b/src/calibre/ebooks/oeb/transforms/trimmanifest.py index 3d56f0ef3d..67d55a581e 100644 --- a/src/calibre/ebooks/oeb/transforms/trimmanifest.py +++ b/src/calibre/ebooks/oeb/transforms/trimmanifest.py @@ -47,6 +47,8 @@ class ManifestTrimmer(object): item.data is not None: hrefs = [r[2] for r in iterlinks(item.data)] for href in hrefs: + if isinstance(href, bytes): + href = href.decode('utf-8') try: href = item.abshref(urlnormalize(href)) except: From c0d5f59d5b2e31302fa4849feeb70d054bd94453 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Apr 2013 18:44:02 +0530 Subject: [PATCH 08/15] ... --- src/calibre/ebooks/oeb/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 21c0c60a55..eb5b0042e7 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -361,7 +361,9 @@ URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ' URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE] def urlquote(href): - """Quote URL-unsafe characters, allowing IRI-safe characters.""" + """ Quote URL-unsafe characters, allowing IRI-safe characters. + That is, this function returns valid IRIs not valid URIs. In particular, + IRIs can contain non-ascii characters. """ result = [] unsafe = 0 if isinstance(href, unicode) else 1 unsafe = URL_UNSAFE[unsafe] From d3a58c69ea004246a3f7ef6876eb3409769e8997 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 23 Apr 2013 10:30:47 +0530 Subject: [PATCH 09/15] Driver for Easy player cyber book e touch. Fixes #1171633 (Unrecognized Device) --- src/calibre/devices/misc.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index f033fb9a2f..4a2e6aa864 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -95,7 +95,6 @@ class PDNOVEL(USBMS): SUPPORTS_SUB_DIRS = False DELETE_EXTS = ['.jpg', '.jpeg', '.png'] - def upload_cover(self, path, filename, metadata, filepath): coverdata = getattr(metadata, 'thumbnail', None) if coverdata and coverdata[2]: @@ -226,9 +225,9 @@ class TREKSTOR(USBMS): VENDOR_ID = [0x1e68] PRODUCT_ID = [0x0041, 0x0042, 0x0052, 0x004e, 0x0056, - 0x0067, # This is for the Pyrus Mini - 0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091 - 0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318 + 0x0067, # This is for the Pyrus Mini + 0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091 + 0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318 ] BCD = [0x0002, 0x100] @@ -427,8 +426,8 @@ class WAYTEQ(USBMS): EBOOK_DIR_MAIN = 'Documents' SCAN_FROM_ROOT = True - VENDOR_NAME = 'ROCKCHIP' - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'RK28_SDK_DEMO' + VENDOR_NAME = ['ROCKCHIP', 'CBR'] + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['RK28_SDK_DEMO', 'EINK_EBOOK_READE'] SUPPORTS_SUB_DIRS = True def get_gui_name(self): @@ -445,7 +444,8 @@ class WAYTEQ(USBMS): return self.EBOOK_DIR_CARD_A def windows_sort_drives(self, drives): - if len(drives) < 2: return drives + if len(drives) < 2: + return drives main = drives.get('main', None) carda = drives.get('carda', None) if main and carda: @@ -455,7 +455,8 @@ class WAYTEQ(USBMS): def linux_swap_drives(self, drives): # See https://bugs.launchpad.net/bugs/1151901 - if len(drives) < 2 or not drives[1] or not drives[2]: return drives + if len(drives) < 2 or not drives[1] or not drives[2]: + return drives drives = list(drives) t = drives[0] drives[0] = drives[1] @@ -463,7 +464,8 @@ class WAYTEQ(USBMS): return tuple(drives) def osx_sort_names(self, names): - if len(names) < 2: return names + if len(names) < 2: + return names main = names.get('main', None) card = names.get('carda', None) From f0396d021cf940353487fec0fe782229965d72cb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 23 Apr 2013 12:58:36 +0530 Subject: [PATCH 10/15] Implement refresh() --- src/calibre/db/cache.py | 13 +++++++++++++ src/calibre/db/legacy.py | 20 ++++++++++++++++++++ src/calibre/db/tests/legacy.py | 12 ++++++++++++ src/calibre/db/view.py | 8 ++++++++ 4 files changed, 53 insertions(+) diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 0fa280d997..7c2176ec31 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -114,6 +114,19 @@ class Cache(object): if self.dirtied_cache: self.dirtied_sequence = max(self.dirtied_cache.itervalues())+1 + @write_api + def initialize_template_cache(self): + self.formatter_template_cache = {} + + @write_api + def refresh(self): + self._initialize_template_cache() + for field in self.fields.itervalues(): + if hasattr(field, 'clear_cache'): + field.clear_cache() # Clear the composite cache + if hasattr(field, 'table'): + field.table.read(self.backend) # Reread data from metadata.db + @property def field_metadata(self): return self.backend.field_metadata diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index d1b76cd8bd..3c06f21fed 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -12,6 +12,7 @@ from functools import partial from calibre.db.backend import DB from calibre.db.cache import Cache from calibre.db.view import View +from calibre.utils.date import utcnow class LibraryDatabase(object): @@ -50,6 +51,8 @@ class LibraryDatabase(object): setattr(self, prop, partial(self.get_property, loc=self.FIELD_MAP[fm])) + self.last_update_check = self.last_modified() + def close(self): self.backend.close() @@ -74,6 +77,11 @@ class LibraryDatabase(object): def last_modified(self): return self.backend.last_modified() + def check_if_modified(self): + if self.last_modified() > self.last_update_check: + self.refresh() + self.last_update_check = utcnow() + @property def custom_column_num_map(self): return self.backend.custom_column_num_map @@ -86,9 +94,21 @@ class LibraryDatabase(object): def FIELD_MAP(self): return self.backend.FIELD_MAP + @property + def formatter_template_cache(self): + return self.data.cache.formatter_template_cache + + def initialize_template_cache(self): + self.data.cache.initialize_template_cache() + def all_ids(self): for book_id in self.data.cache.all_book_ids(): yield book_id + + def refresh(self, field=None, ascending=True): + self.data.cache.refresh() + self.data.refresh(field=field, ascending=ascending) + # }}} diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 6d5734d6b5..2eba15c375 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -64,3 +64,15 @@ class LegacyTest(BaseTest): # }}} + def test_refresh(self): # {{{ + ' Test refreshing the view after a change to metadata.db ' + db = self.init_legacy() + db2 = self.init_legacy() + self.assertEqual(db2.data.cache.set_field('title', {1:'xxx'}), set([1])) + db2.close() + del db2 + self.assertNotEqual(db.title(1, index_is_id=True), 'xxx') + db.check_if_modified() + self.assertEqual(db.title(1, index_is_id=True), 'xxx') + # }}} + diff --git a/src/calibre/db/view.py b/src/calibre/db/view.py index 4ffa1dd074..ff41f20614 100644 --- a/src/calibre/db/view.py +++ b/src/calibre/db/view.py @@ -294,3 +294,11 @@ class View(object): self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode, id_dict.itervalues()))) + def refresh(self, field=None, ascending=True): + self._map = tuple(self.cache.all_book_ids()) + self._map_filtered = tuple(self._map) + if field is not None: + self.sort(field, ascending) + if self.search_restriction or self.base_restriction: + self.search('', return_matches=False) + From b28a069e187cd33fb854bde8b090c2e96768709b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 23 Apr 2013 15:45:13 +0530 Subject: [PATCH 11/15] Add tests for legacy getters --- src/calibre/db/legacy.py | 19 +++++++++++++++++++ src/calibre/db/tests/legacy.py | 18 +++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index 3c06f21fed..0c07814ab1 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -74,6 +74,14 @@ class LibraryDatabase(object): def library_id(self): return self.backend.library_id + @property + def library_path(self): + return self.backend.library_path + + @property + def dbpath(self): + return self.backend.dbpath + def last_modified(self): return self.backend.last_modified() @@ -111,4 +119,15 @@ class LibraryDatabase(object): # }}} + def path(self, index, index_is_id=False): + 'Return the relative path to the directory containing this books files as a unicode string.' + book_id = index if index_is_id else self.data.index_to_id(index) + return self.data.cache.field_for('path', book_id).replace('/', os.sep) + + def abspath(self, index, index_is_id=False, create_dirs=True): + 'Return the absolute path to the directory containing this books files as a unicode string.' + path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id)) + if create_dirs and not os.path.exists(path): + os.makedirs(path) + return path diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 2eba15c375..0f378c566c 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -16,7 +16,7 @@ class LegacyTest(BaseTest): 'Test library wide properties' def get_props(db): props = ('user_version', 'is_second_db', 'library_id', 'field_metadata', - 'custom_column_label_map', 'custom_column_num_map') + 'custom_column_label_map', 'custom_column_num_map', 'library_path', 'dbpath') fprops = ('last_modified', ) ans = {x:getattr(db, x) for x in props} ans.update({x:getattr(db, x)() for x in fprops}) @@ -76,3 +76,19 @@ class LegacyTest(BaseTest): self.assertEqual(db.title(1, index_is_id=True), 'xxx') # }}} + def test_legacy_getters(self): # {{{ + old = self.init_old() + getters = ('path', 'abspath', 'title', 'authors', 'series', + 'publisher', 'author_sort', 'authors', 'comments', + 'comment', 'publisher', 'rating', 'series_index', 'tags', + 'timestamp', 'uuid', 'pubdate', 'ondevice', + 'metadata_last_modified', 'languages') + oldvals = {g:tuple(getattr(old, g)(x) for x in xrange(3)) for g in getters} + old.close() + db = self.init_legacy() + newvals = {g:tuple(getattr(db, g)(x) for x in xrange(3)) for g in getters} + for x in (oldvals, newvals): + x['tags'] = tuple(set(y.split(',')) if y else y for y in x['tags']) + self.assertEqual(oldvals, newvals) + # }}} + From 568faa1952bdb2fa46e739139f697e0d06327c61 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 23 Apr 2013 15:50:42 +0530 Subject: [PATCH 12/15] ... --- src/calibre/db/tests/legacy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 0f378c566c..7c9385f8df 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -83,10 +83,10 @@ class LegacyTest(BaseTest): 'comment', 'publisher', 'rating', 'series_index', 'tags', 'timestamp', 'uuid', 'pubdate', 'ondevice', 'metadata_last_modified', 'languages') - oldvals = {g:tuple(getattr(old, g)(x) for x in xrange(3)) for g in getters} + oldvals = {g:tuple(getattr(old, g)(x) for x in xrange(3)) + tuple(getattr(old, g)(x, True) for x in (1,2,3)) for g in getters} old.close() db = self.init_legacy() - newvals = {g:tuple(getattr(db, g)(x) for x in xrange(3)) for g in getters} + newvals = {g:tuple(getattr(db, g)(x) for x in xrange(3)) + tuple(getattr(db, g)(x, True) for x in (1,2,3)) for g in getters} for x in (oldvals, newvals): x['tags'] = tuple(set(y.split(',')) if y else y for y in x['tags']) self.assertEqual(oldvals, newvals) From e54625a320ed3be96f4927c9902ddcf3464e3a66 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 23 Apr 2013 15:57:47 +0530 Subject: [PATCH 13/15] Fix test_get_property to ignore series_sort when lang is not eng --- src/calibre/db/tests/legacy.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 7c9385f8df..353e1bc4b5 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -51,6 +51,11 @@ class LegacyTest(BaseTest): if label in {'tags', 'formats'}: # Order is random in the old db for these ans[label] = tuple(set(x.split(',')) if x else x for x in ans[label]) + if label == 'series_sort': + # The old db code did not take book language into account + # when generating series_sort values (the first book has + # lang=deu) + ans[label] = ans[label][1:] return ans old = self.init_old() From 9548ad62069d8ded50568e0d654801ec5cc21534 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 23 Apr 2013 16:10:15 +0530 Subject: [PATCH 14/15] Legacy listeners interface --- src/calibre/db/legacy.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index 0c07814ab1..36e6fa152d 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -import os +import os, traceback from functools import partial from calibre.db.backend import DB @@ -30,6 +30,7 @@ class LibraryDatabase(object): progress_callback=lambda x, y:True, restore_all_prefs=False): self.is_second_db = is_second_db # TODO: Use is_second_db + self.listeners = set([]) backend = self.backend = DB(library_path, default_prefs=default_prefs, read_only=read_only, restore_all_prefs=restore_all_prefs, @@ -117,6 +118,22 @@ class LibraryDatabase(object): self.data.cache.refresh() self.data.refresh(field=field, ascending=ascending) + def add_listener(self, listener): + ''' + Add a listener. Will be called on change events with two arguments. + Event name and list of affected ids. + ''' + self.listeners.add(listener) + + def notify(self, event, ids=[]): + 'Notify all listeners' + for listener in self.listeners: + try: + listener(event, ids) + except: + traceback.print_exc() + continue + # }}} def path(self, index, index_is_id=False): From d244bbd93fa354e09472c0d089090beb83b27922 Mon Sep 17 00:00:00 2001 From: davidfor Date: Tue, 23 Apr 2013 21:34:25 +1000 Subject: [PATCH 15/15] Add support for Kobo Aura HD - Add device id for Aura HD - Add cover image sixes - Change gui_name to show model names - Display correct model when connected - Bug#1169571 --- src/calibre/devices/kobo/driver.py | 56 ++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 1d4e4f73b2..800dfd9d88 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -1199,9 +1199,9 @@ class KOBO(USBMS): class KOBOTOUCH(KOBO): name = 'KoboTouch' - gui_name = 'Kobo Touch' + gui_name = 'Kobo Touch/Glo/Mini/Aura HD' author = 'David Forrester' - description = 'Communicate with the Kobo Touch, Glo and Mini firmware. Based on the existing Kobo driver by %s.' % (KOBO.author) + description = 'Communicate with the Kobo Touch, Glo, Mini and Aura HD ereaders. Based on the existing Kobo driver by %s.' % (KOBO.author) # icon = I('devices/kobotouch.jpg') supported_dbversion = 80 @@ -1297,12 +1297,13 @@ class KOBOTOUCH(KOBO): TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ" - GLO_PRODUCT_ID = [0x4173] - MINI_PRODUCT_ID = [0x4183] - TOUCH_PRODUCT_ID = [0x4163] - PRODUCT_ID = GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID + AURA_HD_PRODUCT_ID = [0x4193] + GLO_PRODUCT_ID = [0x4173] + MINI_PRODUCT_ID = [0x4183] + TOUCH_PRODUCT_ID = [0x4163] + PRODUCT_ID = AURA_HD_PRODUCT_ID + GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID - BCD = [0x0110, 0x0326] + BCD = [0x0110, 0x0326] # Image file name endings. Made up of: image size, min_dbversion, max_dbversion, COVER_FILE_ENDINGS = { @@ -1319,6 +1320,11 @@ class KOBOTOUCH(KOBO): # ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,], # ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,], } + AURA_HD_COVER_FILE_ENDINGS = { + ' - N3_FULL.parsed': [(1080,1440), 0, 99,True,], # Used for screensaver, home screen + ' - N3_LIBRARY_FULL.parsed':[(355, 471), 0, 99,False,], # Used for Details screen + ' - N3_LIBRARY_GRID.parsed':[(149, 198), 0, 99,False,], # Used for library lists + } #Following are the sizes used with pre2.1.4 firmware # COVER_FILE_ENDINGS = { # ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen @@ -1334,6 +1340,10 @@ class KOBOTOUCH(KOBO): super(KOBOTOUCH, self).initialize() self.bookshelvelist = [] + def get_device_information(self, end_session=True): + self.set_device_name() + return super(KOBOTOUCH, self).get_device_information(end_session) + def books(self, oncard=None, end_session=True): debug_print("KoboTouch:books - oncard='%s'"%oncard) from calibre.ebooks.metadata.meta import path_to_ext @@ -1366,7 +1376,7 @@ class KOBOTOUCH(KOBO): except: self.fwversion = (0,0,0) - + debug_print('Kobo device: %s' % self.gui_name) debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs) debug_print('Version of firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs) @@ -1379,7 +1389,7 @@ class KOBOTOUCH(KOBO): debug_print(opts.extra_customization) if opts.extra_customization: debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] - debug_print("KoboTouch:books - set_debugging_title to", debugging_title ) + debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title ) bl.set_debugging_title(debugging_title) debug_print("KoboTouch:books - length bl=%d"%len(bl)) need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) @@ -1930,7 +1940,7 @@ class KOBOTOUCH(KOBO): delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves() update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series() debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] - debug_print("KoboTouch:update_device_database_collections - set_debugging_title to", debugging_title ) + debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title ) booklists.set_debugging_title(debugging_title) else: delete_empty_shelves = False @@ -2516,6 +2526,8 @@ class KOBOTOUCH(KOBO): return opts + def isAuraHD(self): + return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID def isGlo(self): return self.detected_device.idProduct in self.GLO_PRODUCT_ID def isMini(self): @@ -2524,7 +2536,21 @@ class KOBOTOUCH(KOBO): return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID def cover_file_endings(self): - return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.COVER_FILE_ENDINGS + return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.AURA_HD_COVER_FILE_ENDINGS if self.isAuraHD() else self.COVER_FILE_ENDINGS + + def set_device_name(self): + device_name = self.gui_name + if self.isAuraHD(): + device_name = 'Kobo Aura HD' + elif self.isGlo(): + device_name = 'Kobo Glo' + elif self.isMini(): + device_name = 'Kobo Mini' + elif self.isTouch(): + device_name = 'Kobo Touch' + self.__class__.gui_name = device_name + return device_name + def copying_covers(self): opts = self.settings() @@ -2582,14 +2608,6 @@ class KOBOTOUCH(KOBO): # Supported database version return True -# @classmethod -# def get_gui_name(cls): -# if hasattr(cls, 'gui_name'): -# return cls.gui_name -# if hasattr(cls, '__name__'): -# return cls.__name__ -# return cls.name - @classmethod def is_debugging_title(cls, title):