From 4ea961ba6298a905bc50136e8054117d77a18575 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 08:34:22 +0100 Subject: [PATCH 01/11] From Greg --- src/calibre/customize/builtins.py | 5 +- src/calibre/devices/apple/driver.py | 51 +++++++------ .../devices/content_server/__init__.py | 10 +++ src/calibre/devices/content_server/driver.py | 74 +++++++++++++++++++ src/calibre/gui2/actions/catalog.py | 2 +- src/calibre/gui2/device.py | 2 +- src/calibre/gui2/dialogs/tweak_epub.py | 11 ++- src/calibre/library/server/content.py | 27 ++++++- 8 files changed, 154 insertions(+), 28 deletions(-) create mode 100644 src/calibre/devices/content_server/__init__.py create mode 100644 src/calibre/devices/content_server/driver.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d3b0b8409d..458bfec3fd 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -582,6 +582,7 @@ from calibre.ebooks.snb.output import SNBOutput from calibre.customize.profiles import input_profiles, output_profiles from calibre.devices.apple.driver import ITUNES +from calibre.devices.content_server.driver import CONTENT_SERVER_FOR_CONFIG from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX, SPECTRA from calibre.devices.blackberry.driver import BLACKBERRY from calibre.devices.cybook.driver import CYBOOK, ORIZON @@ -753,7 +754,9 @@ plugins += [ EEEREADER, NEXTBOOK, ITUNES, -] + CONTENT_SERVER_FOR_CONFIG + ] + plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ x.__name__.endswith('MetadataReader')] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 2cc478603a..d7811f0a22 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -201,8 +201,9 @@ class ITUNES(DriverBase): # 0x1294 iPhone 3GS # 0x1297 iPhone 4 # 0x129a iPad + # 0x12a2 iPad2 VENDOR_ID = [0x05ac] - PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a] + PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a,0x12a2] BCD = [0x01] # Plugboard ID @@ -421,7 +422,7 @@ class ITUNES(DriverBase): cached_books[this_book.path] = { 'title':book.name(), - 'author':[book.artist()], + 'author':book.artist().split(' & '), 'lib_book':library_books[this_book.path] if this_book.path in library_books else None, 'dev_book':book, 'uuid': book.composer() @@ -459,7 +460,7 @@ class ITUNES(DriverBase): cached_books[this_book.path] = { 'title':book.Name, - 'author':book.Artist, + 'author':book.artist().split(' & '), 'lib_book':library_books[this_book.path] if this_book.path in library_books else None, 'uuid': book.Composer, 'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub' @@ -1021,7 +1022,9 @@ class ITUNES(DriverBase): if isosx: for (i,file) in enumerate(files): format = file.rpartition('.')[2].lower() - path = self.path_template % (metadata[i].title, metadata[i].author[0],format) + path = self.path_template % (metadata[i].title, + authors_to_string(metadata[i].authors), + format) self._remove_existing_copy(path, metadata[i]) fpath = self._get_fpath(file, metadata[i], format, update_md=True) db_added, lb_added = self._add_new_copy(fpath, metadata[i]) @@ -1034,9 +1037,11 @@ class ITUNES(DriverBase): if DEBUG: self.log.info("ITUNES.upload_books()") self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" % - ( metadata[i].title, metadata[i].author, metadata[i].uuid)) + (metadata[i].title, + authors_to_string(metadata[i].authors), + metadata[i].uuid)) self.cached_books[this_book.path] = { - 'author': metadata[i].author, + 'author': authors_to_string(metadata[i].authors), 'dev_book': db_added, 'format': format, 'lib_book': lb_added, @@ -1055,7 +1060,9 @@ class ITUNES(DriverBase): for (i,file) in enumerate(files): format = file.rpartition('.')[2].lower() - path = self.path_template % (metadata[i].title, metadata[i].author[0],format) + path = self.path_template % (metadata[i].title, + authors_to_string(metadata[i].authors), + format) self._remove_existing_copy(path, metadata[i]) fpath = self._get_fpath(file, metadata[i],format, update_md=True) db_added, lb_added = self._add_new_copy(fpath, metadata[i]) @@ -1075,9 +1082,11 @@ class ITUNES(DriverBase): if DEBUG: self.log.info("ITUNES.upload_books()") self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" % - ( metadata[i].title, metadata[i].author, metadata[i].uuid)) + (metadata[i].title, + authors_to_string(metadata[i].authors), + metadata[i].uuid)) self.cached_books[this_book.path] = { - 'author': metadata[i].author[0], + 'author': authors_to_string(metadata[i].authors), 'dev_book': db_added, 'format': format, 'lib_book': lb_added, @@ -1190,7 +1199,7 @@ class ITUNES(DriverBase): base_fn = base_fn.rpartition('.')[0] db_added = self._find_device_book( { 'title': base_fn if format == 'pdf' else metadata.title, - 'author': metadata.authors[0], + 'author': authors_to_string(metadata.authors), 'uuid': metadata.uuid, 'format': format}) return db_added @@ -1255,7 +1264,7 @@ class ITUNES(DriverBase): base_fn = base_fn.rpartition('.')[0] added = self._find_library_book( { 'title': base_fn if format == 'pdf' else metadata.title, - 'author': metadata.author[0], + 'author': authors_to_string(metadata.authors), 'uuid': metadata.uuid, 'format': format}) return added @@ -1314,7 +1323,7 @@ class ITUNES(DriverBase): with open(metadata.cover,'r+b') as cd: cover_data = cd.read() except: - self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0])) + self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors))) self.log.error(" error scaling '%s' for '%s'" % (metadata.cover,metadata.title)) import traceback @@ -1389,7 +1398,7 @@ class ITUNES(DriverBase): thumb_path = path.rpartition('.')[0] + '.jpg' zfw.writestr(thumb_path, thumb) except: - self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0])) + self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors))) self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title)) finally: try: @@ -1407,7 +1416,7 @@ class ITUNES(DriverBase): if DEBUG: self.log.info(" ITUNES._create_new_book()") - this_book = Book(metadata.title, authors_to_string(metadata.author)) + this_book = Book(metadata.title, authors_to_string(metadata.authors)) this_book.datetime = time.gmtime() this_book.db_id = None this_book.device_collections = [] @@ -2451,7 +2460,7 @@ class ITUNES(DriverBase): for book in self.cached_books: if self.cached_books[book]['uuid'] == metadata.uuid or \ (self.cached_books[book]['title'] == metadata.title and \ - self.cached_books[book]['author'] == metadata.authors[0]): + self.cached_books[book]['author'] == authors_to_string(metadata.authors)): self.update_list.append(self.cached_books[book]) self._remove_from_device(self.cached_books[book]) if DEBUG: @@ -2470,7 +2479,7 @@ class ITUNES(DriverBase): for book in self.cached_books: if self.cached_books[book]['uuid'] == metadata.uuid or \ (self.cached_books[book]['title'] == metadata.title and \ - self.cached_books[book]['author'] == metadata.authors[0]): + self.cached_books[book]['author'] == authors_to_string(metadata.authors)): self.update_list.append(self.cached_books[book]) self._remove_from_iTunes(self.cached_books[book]) if DEBUG: @@ -2939,13 +2948,13 @@ class ITUNES(DriverBase): def _xform_metadata_via_plugboard(self, book, format): ''' Transform book metadata from plugboard templates ''' if DEBUG: - self.log.info(" ITUNES._xform_metadata_via_plugboard()") + self.log.info(" ITUNES._xform_metadata_via_plugboard()") if self.plugboard_func: pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards) newmi = book.deepcopy_metadata() newmi.template_to_attribute(book, pb) - if DEBUG: + if pb is not None and DEBUG: self.log.info(" transforming %s using %s:" % (format, pb)) self.log.info(" title: %s %s" % (book.title, ">>> %s" % newmi.title if book.title != newmi.title else '')) @@ -3062,7 +3071,7 @@ class ITUNES_ASYNC(ITUNES): cached_books[this_book.path] = { 'title':library_books[book].name(), - 'author':[library_books[book].artist()], + 'author':library_books[book].artist().split(' & '), 'lib_book':library_books[book], 'dev_book':None, 'uuid': library_books[book].composer(), @@ -3102,7 +3111,7 @@ class ITUNES_ASYNC(ITUNES): cached_books[this_book.path] = { 'title':library_books[book].Name, - 'author':library_books[book].Artist, + 'author':library_books[book].Artist.split(' & '), 'lib_book':library_books[book], 'uuid': library_books[book].Composer, 'format': format @@ -3288,7 +3297,7 @@ class Book(Metadata): See ebooks.metadata.book.base ''' def __init__(self,title,author): - Metadata.__init__(self, title, authors=[author]) + Metadata.__init__(self, title, authors=author.split(' & ')) @property def title_sorter(self): diff --git a/src/calibre/devices/content_server/__init__.py b/src/calibre/devices/content_server/__init__.py new file mode 100644 index 0000000000..3d1a86922e --- /dev/null +++ b/src/calibre/devices/content_server/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + + diff --git a/src/calibre/devices/content_server/driver.py b/src/calibre/devices/content_server/driver.py new file mode 100644 index 0000000000..84b14f8e62 --- /dev/null +++ b/src/calibre/devices/content_server/driver.py @@ -0,0 +1,74 @@ +''' +Created on 17 Apr 2011 + +@author: GRiker, modeled on charles's Folder Device + +''' + +from calibre.constants import DEBUG +from calibre.devices.interface import DevicePlugin +from calibre.devices.usbms.deviceconfig import DeviceConfig +from calibre.devices.usbms.driver import USBMS, BookList + +class DriverBase(DeviceConfig, DevicePlugin): + # Reduce to just the formats eligible for plugboard xforms + # These formats are shown in the customization dialog + FORMATS = ['epub', 'mobi'] + USER_CAN_ADD_NEW_FORMATS = False + + # Hide the standard customization widgets + SUPPORTS_SUB_DIRS = False + MUST_READ_METADATA = True + SUPPORTS_USE_AUTHOR_SORT = False + + +# This class is added to the standard device plugin chain, so that it can +# be configured. It has invalid vendor_id etc, so it will never match a +# device. The 'real' CONTENT_SERVER will use the config from it. +class CONTENT_SERVER_FOR_CONFIG(USBMS): + name = 'Content Server Interface' + gui_name = 'Content Server' + description = _('Enables metadata plugboards to be used with Content Server.') + author = 'GRiker' + supported_platforms = ['windows', 'osx', 'linux'] + + VENDOR_ID = [0xffff] + PRODUCT_ID = [0xffff] + BCD = [0xffff] + DEVICE_PLUGBOARD_NAME = 'CONTENT_SERVER' + + def config_widget(cls): + ''' + Configure a minimal QWidget + Better to simply disable the config_widget altogether + ''' + cw = DriverBase.config_widget() + # Turn off the Save template + cw.opt_save_template.setVisible(False) + cw.label.setVisible(False) + # Hide the up/down arrows + cw.column_up.setVisible(False) + cw.column_down.setVisible(False) + # Retitle + cw.groupBox.setTitle(_("Enable metadata plugboards for the following formats:")) + return cw + +class CONTENT_SERVER(USBMS): + + FORMATS = CONTENT_SERVER_FOR_CONFIG.FORMATS + DEVICE_PLUGBOARD_NAME = 'CONTENT_SERVER' + + def __init__(self): + if DEBUG: + print("CONTENT_SERVER.init()") + pass + + def set_plugboards(self, plugboards, pb_func): + # This method is called with the plugboard that matches the format + # declared in use_plugboard_ext and a device name of CONTENT_SERVER + if DEBUG: + print("CONTENT_SERVER.set_plugboards()") + print(' using plugboard %s' % plugboards) + self.plugboards = plugboards + self.plugboard_func = pb_func + diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index fad6e59294..093985d041 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction class GenerateCatalogAction(InterfaceAction): name = 'Generate Catalog' - action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None) + action_spec = (_('Create a catalog of the books in your calibre library'), 'catalog.png', 'Catalog builder', None) dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device']) def generate_catalog(self): diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 4d4f66eab1..8f21c17eaf 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -892,7 +892,7 @@ class DeviceMixin(object): # {{{ sub_dest_parts.append('') to = sub_dest_parts[0] fmts = sub_dest_parts[1] - subject = ';'.join(sub_dest_parts[2:]) + subject = ';'.join(sub_dest_parts[2:]) fmts = [x.strip().lower() for x in fmts.split(',')] self.send_by_mail(to, fmts, delete, subject=subject) diff --git a/src/calibre/gui2/dialogs/tweak_epub.py b/src/calibre/gui2/dialogs/tweak_epub.py index db6e93fd7a..a42fb07e40 100755 --- a/src/calibre/gui2/dialogs/tweak_epub.py +++ b/src/calibre/gui2/dialogs/tweak_epub.py @@ -12,6 +12,7 @@ from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED from PyQt4.Qt import QDialog +from calibre.constants import isosx, iswindows from calibre.gui2 import open_local_file from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog from calibre.libunzip import extract as zipextract @@ -42,11 +43,19 @@ class TweakEpub(QDialog, Ui_Dialog): self.move(parent_loc.x(),parent_loc.y()) def cleanup(self): + if isosx: + try: + import appscript + self.finder = appscript.app('Finder') + self.finder.Finder_windows[os.path.basename(self._exploded)].close() + except: + # appscript fails to load on 10.4 + pass + # Delete directory containing exploded ePub if self._exploded is not None: shutil.rmtree(self._exploded, ignore_errors=True) - def display_exploded(self): ''' Generic subprocess launch of native file browser diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 0c3edd1627..faa0a61baf 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -183,16 +183,37 @@ class ContentServer(object): if fmt is None: raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format)) if format == 'EPUB': + # Get the original metadata + mi = self.db.get_metadata(id, index_is_id=True) + + # Instantiate the CONTENT_SERVER driver + from calibre.devices.content_server.driver import CONTENT_SERVER + cs = CONTENT_SERVER() + + # Get any EPUB plugboards for the content server + from calibre.gui2.device import find_plugboard, device_name_for_plugboards + plugboards = self.db.prefs.get('plugboards', {}) + + # Transform the metadata via the plugboard + if hasattr(cs, 'set_plugboards') and callable(cs.set_plugboards): + cs.set_plugboards(plugboards, find_plugboard) + cpb = find_plugboard(device_name_for_plugboards(cs), format.lower(), plugboards) + if cpb: + newmi = mi.deepcopy_metadata() + newmi.template_to_attribute(mi, cpb) + else: + newmi = mi + + # Write the updated file from tempfile import TemporaryFile from calibre.ebooks.metadata.meta import set_metadata raw = fmt.read() fmt = TemporaryFile() fmt.write(raw) fmt.seek(0) - set_metadata(fmt, self.db.get_metadata(id, index_is_id=True, - get_cover=True), - 'epub') + set_metadata(fmt, newmi, 'epub') fmt.seek(0) + mt = guess_type('dummy.'+format.lower())[0] if mt is None: mt = 'application/octet-stream' From 91c5356ac5d14ef807cce610431ec44aa6ab0ff0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 09:26:34 +0100 Subject: [PATCH 02/11] Suggested content server plugboard implementation for Greg --- src/calibre/customize/builtins.py | 2 - .../devices/content_server/__init__.py | 10 --- src/calibre/devices/content_server/driver.py | 74 ------------------- src/calibre/gui2/device.py | 20 +---- src/calibre/gui2/preferences/plugboard.py | 4 +- src/calibre/library/save_to_disk.py | 32 ++++---- src/calibre/library/server/content.py | 27 +++---- 7 files changed, 34 insertions(+), 135 deletions(-) delete mode 100644 src/calibre/devices/content_server/__init__.py delete mode 100644 src/calibre/devices/content_server/driver.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 458bfec3fd..8f50481f84 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -582,7 +582,6 @@ from calibre.ebooks.snb.output import SNBOutput from calibre.customize.profiles import input_profiles, output_profiles from calibre.devices.apple.driver import ITUNES -from calibre.devices.content_server.driver import CONTENT_SERVER_FOR_CONFIG from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX, SPECTRA from calibre.devices.blackberry.driver import BLACKBERRY from calibre.devices.cybook.driver import CYBOOK, ORIZON @@ -754,7 +753,6 @@ plugins += [ EEEREADER, NEXTBOOK, ITUNES, - CONTENT_SERVER_FOR_CONFIG ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ diff --git a/src/calibre/devices/content_server/__init__.py b/src/calibre/devices/content_server/__init__.py deleted file mode 100644 index 3d1a86922e..0000000000 --- a/src/calibre/devices/content_server/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -from __future__ import with_statement - -__license__ = 'GPL v3' -__copyright__ = '2009, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - - - diff --git a/src/calibre/devices/content_server/driver.py b/src/calibre/devices/content_server/driver.py deleted file mode 100644 index 84b14f8e62..0000000000 --- a/src/calibre/devices/content_server/driver.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -Created on 17 Apr 2011 - -@author: GRiker, modeled on charles's Folder Device - -''' - -from calibre.constants import DEBUG -from calibre.devices.interface import DevicePlugin -from calibre.devices.usbms.deviceconfig import DeviceConfig -from calibre.devices.usbms.driver import USBMS, BookList - -class DriverBase(DeviceConfig, DevicePlugin): - # Reduce to just the formats eligible for plugboard xforms - # These formats are shown in the customization dialog - FORMATS = ['epub', 'mobi'] - USER_CAN_ADD_NEW_FORMATS = False - - # Hide the standard customization widgets - SUPPORTS_SUB_DIRS = False - MUST_READ_METADATA = True - SUPPORTS_USE_AUTHOR_SORT = False - - -# This class is added to the standard device plugin chain, so that it can -# be configured. It has invalid vendor_id etc, so it will never match a -# device. The 'real' CONTENT_SERVER will use the config from it. -class CONTENT_SERVER_FOR_CONFIG(USBMS): - name = 'Content Server Interface' - gui_name = 'Content Server' - description = _('Enables metadata plugboards to be used with Content Server.') - author = 'GRiker' - supported_platforms = ['windows', 'osx', 'linux'] - - VENDOR_ID = [0xffff] - PRODUCT_ID = [0xffff] - BCD = [0xffff] - DEVICE_PLUGBOARD_NAME = 'CONTENT_SERVER' - - def config_widget(cls): - ''' - Configure a minimal QWidget - Better to simply disable the config_widget altogether - ''' - cw = DriverBase.config_widget() - # Turn off the Save template - cw.opt_save_template.setVisible(False) - cw.label.setVisible(False) - # Hide the up/down arrows - cw.column_up.setVisible(False) - cw.column_down.setVisible(False) - # Retitle - cw.groupBox.setTitle(_("Enable metadata plugboards for the following formats:")) - return cw - -class CONTENT_SERVER(USBMS): - - FORMATS = CONTENT_SERVER_FOR_CONFIG.FORMATS - DEVICE_PLUGBOARD_NAME = 'CONTENT_SERVER' - - def __init__(self): - if DEBUG: - print("CONTENT_SERVER.init()") - pass - - def set_plugboards(self, plugboards, pb_func): - # This method is called with the plugboard that matches the format - # declared in use_plugboard_ext and a device name of CONTENT_SERVER - if DEBUG: - print("CONTENT_SERVER.set_plugboards()") - print(' using plugboard %s' % plugboards) - self.plugboards = plugboards - self.plugboard_func = pb_func - diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 8f21c17eaf..2e252047af 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -29,8 +29,7 @@ from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import DEBUG from calibre.utils.config import prefs, tweaks from calibre.utils.magick.draw import thumbnail -from calibre.library.save_to_disk import plugboard_any_device_value, \ - plugboard_any_format_value +from calibre.library.save_to_disk import find_plugboard # }}} class DeviceJob(BaseJob): # {{{ @@ -93,23 +92,6 @@ class DeviceJob(BaseJob): # {{{ # }}} -def find_plugboard(device_name, format, plugboards): - cpb = None - if format in plugboards: - cpb = plugboards[format] - elif plugboard_any_format_value in plugboards: - cpb = plugboards[plugboard_any_format_value] - if cpb is not None: - if device_name in cpb: - cpb = cpb[device_name] - elif plugboard_any_device_value in cpb: - cpb = cpb[plugboard_any_device_value] - else: - cpb = None - if DEBUG: - prints('Device using plugboard', format, device_name, cpb) - return cpb - def device_name_for_plugboards(device_class): if hasattr(device_class, 'DEVICE_PLUGBOARD_NAME'): return device_class.DEVICE_PLUGBOARD_NAME diff --git a/src/calibre/gui2/preferences/plugboard.py b/src/calibre/gui2/preferences/plugboard.py index 8f2b084d76..c5db7074dc 100644 --- a/src/calibre/gui2/preferences/plugboard.py +++ b/src/calibre/gui2/preferences/plugboard.py @@ -15,6 +15,7 @@ from calibre.gui2.preferences.plugboard_ui import Ui_Form from calibre.customize.ui import metadata_writers, device_plugins from calibre.library.save_to_disk import plugboard_any_format_value, \ plugboard_any_device_value, plugboard_save_to_disk_value +from calibre.library.server.content import plugboard_content_server_value from calibre.utils.formatter import validation_formatter @@ -74,7 +75,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.devices.append(n) self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) self.devices.insert(1, plugboard_save_to_disk_value) - self.devices.insert(2, plugboard_any_device_value) + self.devices.insert(1, plugboard_content_server_value) + self.devices.insert(1, plugboard_any_device_value) self.new_device.addItems(self.devices) self.formats = [''] diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 96c42e6e0e..3c57af40a8 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -51,6 +51,23 @@ for x in FORMAT_ARG_DESCS: FORMAT_ARGS[x] = '' +def find_plugboard(device_name, format, plugboards): + cpb = None + if format in plugboards: + cpb = plugboards[format] + elif plugboard_any_format_value in plugboards: + cpb = plugboards[plugboard_any_format_value] + if cpb is not None: + if device_name in cpb: + cpb = cpb[device_name] + elif plugboard_any_device_value in cpb: + cpb = cpb[plugboard_any_device_value] + else: + cpb = None + if DEBUG: + prints('Device using plugboard', format, device_name, cpb) + return cpb + def config(defaults=None): if defaults is None: c = Config('save_to_disk', _('Options to control saving to disk')) @@ -279,20 +296,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards, written = False for fmt in formats: global plugboard_save_to_disk_value, plugboard_any_format_value - dev_name = plugboard_save_to_disk_value - cpb = None - if fmt in plugboards: - cpb = plugboards[fmt] - if dev_name in cpb: - cpb = cpb[dev_name] - else: - cpb = None - if cpb is None and plugboard_any_format_value in plugboards: - cpb = plugboards[plugboard_any_format_value] - if dev_name in cpb: - cpb = cpb[dev_name] - else: - cpb = None + cpb = find_plugboard(plugboard_save_to_disk_value, fmt, plugboards) # Leave this here for a while, in case problems arise. if cpb is not None: prints('Save-to-disk using plugboard:', fmt, cpb) diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index faa0a61baf..8d9e71c528 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -12,9 +12,13 @@ import cherrypy from calibre import fit_image, guess_type from calibre.utils.date import fromtimestamp from calibre.library.caches import SortKeyGenerator +from calibre.library.save_to_disk import find_plugboard + from calibre.utils.magick.draw import save_cover_data_to, Image, \ thumbnail as generate_thumbnail +plugboard_content_server_value = 'content_server' + class CSSortKeyGenerator(SortKeyGenerator): def __init__(self, fields, fm, db_prefs): @@ -186,23 +190,16 @@ class ContentServer(object): # Get the original metadata mi = self.db.get_metadata(id, index_is_id=True) - # Instantiate the CONTENT_SERVER driver - from calibre.devices.content_server.driver import CONTENT_SERVER - cs = CONTENT_SERVER() - # Get any EPUB plugboards for the content server - from calibre.gui2.device import find_plugboard, device_name_for_plugboards plugboards = self.db.prefs.get('plugboards', {}) - - # Transform the metadata via the plugboard - if hasattr(cs, 'set_plugboards') and callable(cs.set_plugboards): - cs.set_plugboards(plugboards, find_plugboard) - cpb = find_plugboard(device_name_for_plugboards(cs), format.lower(), plugboards) - if cpb: - newmi = mi.deepcopy_metadata() - newmi.template_to_attribute(mi, cpb) - else: - newmi = mi + cpb = find_plugboard(plugboard_content_server_value, + 'epub', plugboards) + if cpb: + # Transform the metadata via the plugboard + newmi = mi.deepcopy_metadata() + newmi.template_to_attribute(mi, cpb) + else: + newmi = mi # Write the updated file from tempfile import TemporaryFile From 3709dcbc621e152472184bacbb056e16e1aef1fe Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 10:43:20 +0100 Subject: [PATCH 03/11] Add check for valid formats --- src/calibre/gui2/preferences/plugboard.py | 16 +++++++++++++++- src/calibre/library/server/content.py | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/preferences/plugboard.py b/src/calibre/gui2/preferences/plugboard.py index c5db7074dc..7036ddf8f3 100644 --- a/src/calibre/gui2/preferences/plugboard.py +++ b/src/calibre/gui2/preferences/plugboard.py @@ -15,7 +15,8 @@ from calibre.gui2.preferences.plugboard_ui import Ui_Form from calibre.customize.ui import metadata_writers, device_plugins from calibre.library.save_to_disk import plugboard_any_format_value, \ plugboard_any_device_value, plugboard_save_to_disk_value -from calibre.library.server.content import plugboard_content_server_value +from calibre.library.server.content import plugboard_content_server_value, \ + plugboard_content_server_formats from calibre.utils.formatter import validation_formatter @@ -69,13 +70,17 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.device_label.setText(_('Device currently connected: None')) self.devices = ['', 'APPLE', 'FOLDER_DEVICE'] + self.device_to_formats_map = {} for device in device_plugins(): n = device_name_for_plugboards(device) + self.device_to_formats_map[n] = device.FORMATS if n not in self.devices: self.devices.append(n) self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) self.devices.insert(1, plugboard_save_to_disk_value) self.devices.insert(1, plugboard_content_server_value) + self.device_to_formats_map[plugboard_content_server_value] = \ + plugboard_content_server_formats self.devices.insert(1, plugboard_any_device_value) self.new_device.addItems(self.devices) @@ -232,6 +237,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): show=True) self.new_device.setCurrentIndex(0) return + if self.current_device in self.device_to_formats_map: + allowable_formats = self.device_to_formats_map[self.current_device] + if self.current_format not in allowable_formats: + error_dialog(self, '', + _('The {0} device does not support the {1} format.'). + format(self.current_device, self.current_format), + show=True) + self.new_device.setCurrentIndex(0) + return self.set_fields() def new_format_changed(self, txt): diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 8d9e71c528..08de4faecd 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -18,6 +18,7 @@ from calibre.utils.magick.draw import save_cover_data_to, Image, \ thumbnail as generate_thumbnail plugboard_content_server_value = 'content_server' +plugboard_content_server_formats = ['epub'] class CSSortKeyGenerator(SortKeyGenerator): From 204b95289276a73ff54a3352c43a829f702602d1 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 19:23:53 +0100 Subject: [PATCH 04/11] Add signal to indicate that the metadata for books on the connected device is available --- src/calibre/gui2/device.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 4d4f66eab1..f012028c8a 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -607,6 +607,8 @@ class DeviceMenu(QMenu): # {{{ class DeviceMixin(object): # {{{ + device_metadata_available = pyqtSignal() + def __init__(self): self.device_error_dialog = error_dialog(self, _('Error'), _('Error communicating with device'), ' ') @@ -791,6 +793,7 @@ class DeviceMixin(object): # {{{ self.sync_news() self.sync_catalogs() self.refresh_ondevice() + self.device_metadata_available.emit() def refresh_ondevice(self, reset_only = False): ''' @@ -892,7 +895,7 @@ class DeviceMixin(object): # {{{ sub_dest_parts.append('') to = sub_dest_parts[0] fmts = sub_dest_parts[1] - subject = ';'.join(sub_dest_parts[2:]) + subject = ';'.join(sub_dest_parts[2:]) fmts = [x.strip().lower() for x in fmts.split(',')] self.send_by_mail(to, fmts, delete, subject=subject) From b0ec35f0d310d861aa72a423a6337acd0bb25da5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 18 Apr 2011 14:06:07 -0600 Subject: [PATCH 05/11] ... --- src/calibre/gui2/metadata/single_download.py | 11 ++++------- src/calibre/gui2/preferences/__init__.py | 8 +++++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 7e30f02420..c4e13a90f8 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -30,7 +30,6 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.gui2 import error_dialog, NONE from calibre.utils.date import utcnow, fromordinal, format_date from calibre.library.comments import comments_to_html -from calibre.constants import islinux from calibre import force_unicode # }}} @@ -117,12 +116,10 @@ class CoverDelegate(QStyledItemDelegate): # {{{ def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) - if islinux: - # On linux for some reason the selected color is drawn on top of - # the decoration - style = QApplication.style() - style.drawItemPixmap(painter, option.rect, Qt.AlignTop|Qt.AlignHCenter, - QPixmap(index.data(Qt.DecorationRole))) + # Ensure the cover is rendered over any selection rect + style = QApplication.style() + style.drawItemPixmap(painter, option.rect, Qt.AlignTop|Qt.AlignHCenter, + QPixmap(index.data(Qt.DecorationRole))) if self.timer.isActive() and index.data(Qt.UserRole).toBool(): rect = QRect(0, 0, self.spinner_width, self.spinner_width) rect.moveCenter(option.rect.center()) diff --git a/src/calibre/gui2/preferences/__init__.py b/src/calibre/gui2/preferences/__init__.py index 649a58448d..5b0a05ba40 100644 --- a/src/calibre/gui2/preferences/__init__.py +++ b/src/calibre/gui2/preferences/__init__.py @@ -337,7 +337,13 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False, bb.button(bb.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults) bb.button(bb.Apply).setEnabled(False) bb.button(bb.Apply).clicked.connect(d.accept) - w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnabled(True)) + def onchange(): + b = bb.button(bb.Apply) + b.setEnabled(True) + b.setDefault(True) + b.setAutoDefault(True) + w.changed_signal.connect(onchange) + bb.button(bb.Cancel).setFocus(True) l = QVBoxLayout() d.setLayout(l) l.addWidget(w) From 97c5bf39c13ec466712869526bc82d9f4566ef62 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 18 Apr 2011 14:08:03 -0600 Subject: [PATCH 06/11] ... --- src/calibre/devices/android/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 44d9bc1e49..7fe246f450 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -108,10 +108,10 @@ class ANDROID(USBMS): 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2', - 'MB860', 'MULTI-CARD', 'MID7015A'] + 'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', - 'A70S', 'A101IT', '7'] + 'A70S', 'A101IT', '7', 'INCREDIBLE'] OSX_MAIN_MEM = 'Android Device Main Memory' From 1267df3a69db2c048be36065f89d1d2d1132caa9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 21:16:01 +0100 Subject: [PATCH 07/11] Add the device_connection_changed signal --- src/calibre/gui2/device.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index f012028c8a..8b0e6eefef 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -608,6 +608,7 @@ class DeviceMenu(QMenu): # {{{ class DeviceMixin(object): # {{{ device_metadata_available = pyqtSignal() + device_connection_changed = pyqtSignal(object) def __init__(self): self.device_error_dialog = error_dialog(self, _('Error'), @@ -755,6 +756,7 @@ class DeviceMixin(object): # {{{ self.location_manager.update_devices() self.library_view.set_device_connected(self.device_connected) self.refresh_ondevice() + self.device_connection_changed.emit(connected) def info_read(self, job): ''' From b79faeff5691fb11e110c24bad69cb60fc05ce82 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 21:38:27 +0100 Subject: [PATCH 08/11] Change author_sort_copy_method default from invert to comma. --- resources/default_tweaks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index c4c951f980..091aa9a34d 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -48,7 +48,7 @@ authors_completer_append_separator = False # When this tweak is changed, the author_sort values stored with each author # must be recomputed by right-clicking on an author in the left-hand tags pane, # selecting 'manage authors', and pressing 'Recalculate all author sort values'. -author_sort_copy_method = 'invert' +author_sort_copy_method = 'comma' #: Use author sort in Tag Browser # Set which author field to display in the tags pane (the list of authors, From b6f44d0b7c0de9b2b9a6bfbb29d2874ea9718e7b Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 18 Apr 2011 18:52:14 -0400 Subject: [PATCH 09/11] Store: Search allows for main window location and boolean filtering. --- src/calibre/gui2/store/search.py | 91 +++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/store/search.py b/src/calibre/gui2/store/search.py index 1d263959ef..ce74d52547 100644 --- a/src/calibre/gui2/store/search.py +++ b/src/calibre/gui2/store/search.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' import re import time +import traceback from contextlib import closing from random import shuffle from threading import Thread @@ -20,9 +21,12 @@ from calibre import browser from calibre.gui2 import NONE from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.store.search_ui import Ui_Dialog +from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ + REGEXP_MATCH from calibre.utils.config import DynamicConfig from calibre.utils.icu import sort_key from calibre.utils.magick.draw import thumbnail +from calibre.utils.search_query_parser import SearchQueryParser HANG_TIME = 75000 # milliseconds seconds TIMEOUT = 75 # seconds @@ -290,11 +294,15 @@ class SearchThread(Thread): while self._run and not self.tasks.empty(): try: query, store_name, store_plugin, timeout = self.tasks.get() - for res in store_plugin.search(query, timeout=timeout): + squery = query + for loc in SearchFilter.USABLE_LOCATIONS: + squery = re.sub(r'%s:"?(?P[^\s"]+)"?' % loc, '\g', squery) + for res in store_plugin.search(squery, timeout=timeout): if not self._run: return res.store_name = store_name - self.results.put(res) + if SearchFilter(res).parse(query): + self.results.put(res) self.tasks.task_done() except: pass @@ -450,3 +458,82 @@ class Matches(QAbstractItemModel): if reset: self.reset() + +class SearchFilter(SearchQueryParser): + + USABLE_LOCATIONS = [ + 'all', + 'author', + 'authors', + 'cover', + 'price', + 'title', + 'store', + ] + + def __init__(self, search_result): + SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS) + self.search_result = search_result + + def universal_set(self): + return set([self.search_result]) + + def get_matches(self, location, query): + location = location.lower().strip() + if location == 'authors': + location = 'author' + + matchkind = CONTAINS_MATCH + if len(query) > 1: + if query.startswith('\\'): + query = query[1:] + elif query.startswith('='): + matchkind = EQUALS_MATCH + query = query[1:] + elif query.startswith('~'): + matchkind = REGEXP_MATCH + query = query[1:] + if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + query = query.lower() + + if location not in self.USABLE_LOCATIONS: + return set([]) + matches = set([]) + all_locs = set(self.USABLE_LOCATIONS) - set(['all']) + locations = all_locs if location == 'all' else [location] + q = { + 'author': self.search_result.author.lower(), + 'cover': self.search_result.cover_url, + 'format': '', + 'price': self.search_result.price, + 'store': self.search_result.store_name.lower(), + 'title': self.search_result.title.lower(), + } + for x in ('author', 'format'): + q[x+'s'] = q[x] + for locvalue in locations: + ac_val = q[locvalue] + if query == 'true': + if ac_val is not None: + matches.add(self.search_result) + continue + if query == 'false': + if ac_val is None: + matches.add(self.search_result) + continue + try: + ### Can't separate authors because comma is used for name sep and author sep + ### Exact match might not get what you want. For that reason, turn author + ### exactmatch searches into contains searches. + if locvalue == 'author' and matchkind == EQUALS_MATCH: + m = CONTAINS_MATCH + else: + m = matchkind + + vals = [ac_val] + if _match(query, vals, m): + matches.add(self.search_result) + break + except ValueError: # Unicode errors + traceback.print_exc() + return matches From 234248cd23826764240e6ea1a4ac91c02cc23371 Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 18 Apr 2011 18:57:28 -0400 Subject: [PATCH 10/11] Store: Fix issue with using proxy when an arument is None. --- src/calibre/gui2/store/web_control.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/store/web_control.py b/src/calibre/gui2/store/web_control.py index 874328f872..0b79c526a8 100644 --- a/src/calibre/gui2/store/web_control.py +++ b/src/calibre/gui2/store/web_control.py @@ -31,10 +31,14 @@ class NPWebView(QWebView): proxy_parts = urlparse(http_proxy) proxy = QNetworkProxy() proxy.setType(QNetworkProxy.HttpProxy) - proxy.setUser(proxy_parts.username) - proxy.setPassword(proxy_parts.password) - proxy.setHostName(proxy_parts.hostname) - proxy.setPort(proxy_parts.port) + if proxy_parts.username: + proxy.setUser(proxy_parts.username) + if proxy_parts.password: + proxy.setPassword(proxy_parts.password) + if proxy_parts.hostname: + proxy.setHostName(proxy_parts.hostname) + if proxy_parts.port: + proxy.setPort(proxy_parts.port) self.page().networkAccessManager().setProxy(proxy) self.page().setForwardUnsupportedContent(True) From 8d174eaffdfcda971885b80e0705bd221ea11f79 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 18 Apr 2011 20:56:56 -0600 Subject: [PATCH 11/11] ... --- src/calibre/ebooks/metadata/sources/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index 37407a0656..86a9fe1133 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -377,8 +377,9 @@ class Source(Plugin): This URL must be browseable to by a human using a browser. It is meant to provide a clickable link for the user to easily visit the books page at this source. - If no URL is found, return None. This method must be quick, either it - should construct the URL using a known URL scheme or use a cached URL. + If no URL is found, return None. This method must be quick, and + consistent, so only implement it if it is possible to construct the URL + from a known scheme given identifiers. ''' return None