diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index d906bb86c8..56f9b1460d 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -110,3 +110,9 @@ class WrongDestinationError(PathError): trying to send books to a non existant storage card.''' pass +class BlacklistedDevice(OpenFailed): + ''' Raise this error during open() when the device being opened has been + blacklisted by the user. Only used in drivers that manage device presence, + like the MTP driver. ''' + pass + diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index a5885ca964..4ada58ecef 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -59,4 +59,7 @@ class MTPDeviceBase(DevicePlugin): from calibre.devices.utils import build_template_regexp return build_template_regexp(self.save_template) + def is_customizable(self): + return True + diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 92184af8ff..55472d3d44 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -16,7 +16,7 @@ from calibre.constants import iswindows, numeric_version from calibre.devices.mtp.base import debug from calibre.ptempfile import SpooledTemporaryFile, PersistentTemporaryDirectory from calibre.utils.config import from_json, to_json, JSONConfig -from calibre.utils.date import now, isoformat +from calibre.utils.date import now, isoformat, utcnow BASE = importlib.import_module('calibre.devices.mtp.%s.driver'%( 'windows' if iswindows else 'unix')).MTP_DEVICE @@ -51,6 +51,8 @@ class MTP_DEVICE(BASE): 'wordplayer/calibretransfer', 'Books', 'sdcard/ebooks', 'eBooks', 'kindle'] p.defaults['send_template'] = config().parse().send_template + p.defaults['blacklist'] = [] + p.defaults['history'] = {} return self._prefs @@ -74,6 +76,11 @@ class MTP_DEVICE(BASE): self.current_library_uuid = library_uuid self.location_paths = None BASE.open(self, devices, library_uuid) + h = self.prefs['history'] + if self.current_serial_num: + h[self.current_serial_num] = (self.current_friendly_name, + isoformat(utcnow())) + self.prefs['history'] = h # Device information {{{ def _update_drive_info(self, storage, location_code, name=None): diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 3792bb2fcc..31f886b875 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -15,7 +15,7 @@ from functools import partial from calibre import prints, as_unicode from calibre.constants import plugins from calibre.ptempfile import SpooledTemporaryFile -from calibre.devices.errors import OpenFailed, DeviceError +from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice from calibre.devices.mtp.base import MTPDeviceBase, synchronous MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' @@ -99,19 +99,25 @@ class MTP_DEVICE(MTPDeviceBase): return False p('Known MTP devices connected:') for d in devs: p(d) - d = devs[0] - p('\nTrying to open:', d) - try: - self.open(d, 'debug') - except: - p('Opening device failed:') - p(traceback.format_exc()) - return False - p('Opened', self.current_friendly_name, 'successfully') - p('Storage info:') - p(pprint.pformat(self.dev.storage_info)) - self.eject() - return True + + for d in devs: + p('\nTrying to open:', d) + try: + self.open(d, 'debug') + except BlacklistedDevice: + p('This device has been blacklisted by the user') + continue + except: + p('Opening device failed:') + p(traceback.format_exc()) + return False + else: + p('Opened', self.current_friendly_name, 'successfully') + p('Storage info:') + p(pprint.pformat(self.dev.storage_info)) + self.post_yank_cleanup() + return True + return False @synchronous def create_device(self, connected_device): @@ -167,6 +173,12 @@ class MTP_DEVICE(MTPDeviceBase): if not storage: self.blacklisted_devices.add(connected_device) raise OpenFailed('No storage found for device %s'%(connected_device,)) + snum = self.dev.serial_number + if snum in self.prefs.get('blacklist', []): + self.blacklisted_devices.add(connected_device) + self.dev = None + raise BlacklistedDevice( + 'The %s device has been blacklisted by the user'%(connected_device,)) self._main_id = storage[0]['id'] self._carda_id = self._cardb_id = None if len(storage) > 1: @@ -176,7 +188,7 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name = self.dev.friendly_name if not self.current_friendly_name: self.current_friendly_name = self.dev.model_name or _('Unknown MTP device') - self.current_serial_num = self.dev.serial_number + self.current_serial_num = snum @property def filesystem_cache(self): diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 2f606b42d1..50638496d1 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -15,7 +15,7 @@ from itertools import chain from calibre import as_unicode, prints from calibre.constants import plugins, __appname__, numeric_version from calibre.ptempfile import SpooledTemporaryFile -from calibre.devices.errors import OpenFailed, DeviceError +from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice from calibre.devices.mtp.base import MTPDeviceBase class ThreadingViolation(Exception): @@ -163,6 +163,9 @@ class MTP_DEVICE(MTPDeviceBase): p('\nTrying to open:', pnp_id) try: self.open(pnp_id, 'debug-detection') + except BlacklistedDevice: + p('This device has been blacklisted by the user') + continue except: p('Open failed:') p(traceback.format_exc()) @@ -172,7 +175,7 @@ class MTP_DEVICE(MTPDeviceBase): p('Opened', self.current_friendly_name, 'successfully') p('Device info:') p(pprint.pformat(self.dev.data)) - self.eject() + self.post_yank_cleanup() return True p('No suitable MTP devices found') return False @@ -225,7 +228,6 @@ class MTP_DEVICE(MTPDeviceBase): self._main_id = self._carda_id = self._cardb_id = None self.dev = self._filesystem_cache = None - @same_thread def post_yank_cleanup(self): self.currently_connected_pnp_id = self.current_friendly_name = None @@ -256,6 +258,13 @@ class MTP_DEVICE(MTPDeviceBase): if not storage: self.blacklisted_devices.add(connected_device) raise OpenFailed('No storage found for device %s'%(connected_device,)) + snum = devdata.get('serial_number', None) + if snum in self.prefs.get('blacklist', []): + self.blacklisted_devices.add(connected_device) + self.dev = None + raise BlacklistedDevice( + 'The %s device has been blacklisted by the user'%(connected_device,)) + self._main_id = storage[0]['id'] if len(storage) > 1: self._carda_id = storage[1]['id'] @@ -266,7 +275,7 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name = devdata.get('model_name', _('Unknown MTP device')) self.currently_connected_pnp_id = connected_device - self.current_serial_num = devdata.get('serial_number', None) + self.current_serial_num = snum @same_thread def get_basic_device_information(self): diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 6d638ef9c2..8466fe9320 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -24,7 +24,8 @@ from calibre.gui2 import (config, error_dialog, Dispatcher, dynamic, from calibre.ebooks.metadata import authors_to_string from calibre import preferred_encoding, prints, force_unicode, as_unicode from calibre.utils.filenames import ascii_filename -from calibre.devices.errors import FreeSpaceError, WrongDestinationError +from calibre.devices.errors import (FreeSpaceError, WrongDestinationError, + BlacklistedDevice) from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.devices.bambook.driver import BAMBOOK, BAMBOOKWifi @@ -252,6 +253,9 @@ class DeviceManager(Thread): # {{{ if cd is not None: try: dev.open(cd, self.current_library_uuid) + except BlacklistedDevice as e: + prints('Ignoring blacklisted device: %s'% + as_unicode(e)) except: prints('Error while trying to open %s (Driver: %s)'% (cd, dev)) diff --git a/src/calibre/gui2/device_drivers/mtp_config.py b/src/calibre/gui2/device_drivers/mtp_config.py index b6628f4e65..261fea3df2 100644 --- a/src/calibre/gui2/device_drivers/mtp_config.py +++ b/src/calibre/gui2/device_drivers/mtp_config.py @@ -16,6 +16,7 @@ from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel, from calibre.ebooks import BOOK_EXTENSIONS from calibre.gui2 import error_dialog from calibre.gui2.dialogs.template_dialog import TemplateDialog +from calibre.utils.date import parse_date class FormatsConfig(QWidget): # {{{ @@ -136,6 +137,45 @@ class SendToConfig(QWidget): # {{{ # }}} +class IgnoredDevices(QWidget): # {{{ + + def __init__(self, devs, blacklist): + QWidget.__init__(self) + self.l = l = QVBoxLayout() + self.setLayout(l) + self.la = la = QLabel('

'+_( + '''Select the devices to be ignored. calibre will not + connect to devices with a checkmark next to their names.''')) + la.setWordWrap(True) + l.addWidget(la) + self.f = f = QListWidget(self) + l.addWidget(f) + + devs = [(snum, (x[0], parse_date(x[1]))) for snum, x in + devs.iteritems()] + for dev, x in sorted(devs, key=lambda x:x[1][1], reverse=True): + name = x[0] + name = '%s [%s]'%(name, dev) + item = QListWidgetItem(name, f) + item.setData(Qt.UserRole, dev) + item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable) + item.setCheckState(Qt.Checked if dev in blacklist else Qt.Unchecked) + + @property + def blacklist(self): + return [unicode(self.f.item(i).data(Qt.UserRole).toString()) for i in + xrange(self.f.count()) if self.f.item(i).checkState()==Qt.Checked] + + def ignore_device(self, snum): + for i in xrange(self.f.count()): + i = self.f.item(i) + c = unicode(i.data(Qt.UserRole).toString()) + if c == snum: + i.setCheckState(Qt.Checked) + break + +# }}} + class MTPConfig(QTabWidget): def __init__(self, device, parent=None): @@ -162,6 +202,8 @@ class MTPConfig(QTabWidget): l = QLabel(msg) l.setWordWrap(True) l.setStyleSheet('QLabel { margin-left: 2em }') + l.setMinimumWidth(500) + l.setMinimumHeight(400) self.insertTab(0, l, _('Cannot configure')) else: self.base = QWidget(self) @@ -173,16 +215,34 @@ class MTPConfig(QTabWidget): self.get_pref('format_map')) self.send_to = SendToConfig(self.get_pref('send_to')) self.template = TemplateConfig(self.get_pref('send_template')) - self.base.la = la = QLabel(_('Choose the formats to send to the %s')%self.device.current_friendly_name) + self.base.la = la = QLabel(_( + 'Choose the formats to send to the %s')%self.device.current_friendly_name) la.setWordWrap(True) l.addWidget(la, 0, 0, 1, 1) - l.addWidget(self.formats, 1, 0, 3, 1) + l.addWidget(self.formats, 1, 0, 2, 1) l.addWidget(self.send_to, 1, 1, 1, 1) l.addWidget(self.template, 2, 1, 1, 1) l.setRowStretch(2, 10) + self.base.b = b = QPushButton(QIcon(I('minus.png')), + _('Ignore the %s in calibre')%device.current_friendly_name, + self.base) + l.addWidget(b, 3, 0, 1, 2) + b.clicked.connect(self.ignore_device) + + self.igntab = IgnoredDevices(self.device.prefs['history'], + self.device.prefs['blacklist']) + self.addTab(self.igntab, _('Ignored devices')) self.setCurrentIndex(0) + def ignore_device(self): + self.igntab.ignore_device(self.device.current_serial_num) + self.base.b.setEnabled(False) + self.base.b.setText(_('The %s will be ignored in calibre')% + self.device.current_friendly_name) + self.base.b.setStyleSheet('QPushButton { font-weight: bold }') + self.base.setEnabled(False) + def get_pref(self, key): p = self.device.prefs.get(self.current_device_key, {}) if not p: @@ -194,31 +254,35 @@ class MTPConfig(QTabWidget): return self._device() def validate(self): - if not self.formats.validate(): - return False - if not self.template.validate(): - return False + if hasattr(self, 'formats'): + if not self.formats.validate(): + return False + if not self.template.validate(): + return False return True def commit(self): p = self.device.prefs.get(self.current_device_key, {}) - p.pop('format_map', None) - f = self.formats.format_map - if f and f != self.device.prefs['format_map']: - p['format_map'] = f + if hasattr(self, 'formats'): + p.pop('format_map', None) + f = self.formats.format_map + if f and f != self.device.prefs['format_map']: + p['format_map'] = f - p.pop('send_template', None) - t = self.template.template - if t and t != self.device.prefs['send_template']: - p['send_template'] = t + p.pop('send_template', None) + t = self.template.template + if t and t != self.device.prefs['send_template']: + p['send_template'] = t - p.pop('send_to', None) - s = self.send_to.value - if s and s != self.device.prefs['send_to']: - p['send_to'] = s + p.pop('send_to', None) + s = self.send_to.value + if s and s != self.device.prefs['send_to']: + p['send_to'] = s - self.device.prefs[self.current_device_key] = p + self.device.prefs[self.current_device_key] = p + + self.device.prefs['blacklist'] = self.igntab.blacklist if __name__ == '__main__': from calibre.gui2 import Application