diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 91dc0bf6ef..3e7dc63f87 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -26,11 +26,6 @@ class MTPDeviceBase(DevicePlugin): author = 'Kovid Goyal' version = (1, 0, 0) - # Invalid USB vendor information so the scanner will never match - VENDOR_ID = [0xffff] - PRODUCT_ID = [0xffff] - BCD = [0xffff] - THUMBNAIL_HEIGHT = 128 CAN_SET_METADATA = [] @@ -51,4 +46,10 @@ class MTPDeviceBase(DevicePlugin): def get_gui_name(self): return self.current_friendly_name or self.name + def is_usb_connected(self, devices_on_system, debug=False, + only_presence=False): + # We manage device presence ourselves, so this method should always + # return False + return False + diff --git a/src/calibre/devices/mtp/unix/detect.py b/src/calibre/devices/mtp/unix/detect.py deleted file mode 100644 index 9e913dd9cf..0000000000 --- a/src/calibre/devices/mtp/unix/detect.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2012, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -from calibre.constants import plugins - -class MTPDetect(object): - - def __init__(self): - p = plugins['libmtp'] - self.libmtp = p[0] - if self.libmtp is None: - print ('Failed to load libmtp, MTP device detection disabled') - print (p[1]) - self.cache = {} - - def __call__(self, devices): - ''' - Given a list of devices as returned by LinuxScanner, return the set of - devices that are likely to be MTP devices. This class maintains a cache - to minimize USB polling. Note that detection is partially based on a - list of known vendor and product ids. This is because polling some - older devices causes problems. Therefore, if this method identifies a - device as MTP, it is not actually guaranteed that it will be a working - MTP device. - ''' - # First drop devices that have been disconnected from the cache - connected_devices = {(d.busnum, d.devnum, d.vendor_id, d.product_id, - d.bcd, d.serial) for d in devices} - for d in tuple(self.cache.iterkeys()): - if d not in connected_devices: - del self.cache[d] - - # Since is_mtp_device() can cause USB traffic by probing the device, we - # cache its result - mtp_devices = set() - if self.libmtp is None: - return mtp_devices - - for d in devices: - ans = self.cache.get((d.busnum, d.devnum, d.vendor_id, d.product_id, - d.bcd, d.serial), None) - if ans is None: - ans = self.libmtp.is_mtp_device(d.busnum, d.devnum, - d.vendor_id, d.product_id) - self.cache[(d.busnum, d.devnum, d.vendor_id, d.product_id, - d.bcd, d.serial)] = ans - if ans: - mtp_devices.add(d) - return mtp_devices - - def create_device(self, connected_device): - d = connected_device - return self.libmtp.Device(d.busnum, d.devnum, d.vendor_id, - d.product_id, d.manufacturer, d.product, d.serial) - -if __name__ == '__main__': - from calibre.devices.scanner import linux_scanner - mtp_detect = MTPDetect() - devs = mtp_detect(linux_scanner()) - print ('Found %d MTP devices:'%len(devs)) - for dev in devs: - print (dev, 'at busnum=%d and devnum=%d'%(dev.busnum, dev.devnum)) - print() - - diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index ef1c671cde..f3f660c368 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -10,11 +10,19 @@ __docformat__ = 'restructuredtext en' import time, operator from threading import RLock from io import BytesIO +from collections import namedtuple +from calibre.constants import plugins from calibre.devices.errors import OpenFailed, DeviceError from calibre.devices.mtp.base import MTPDeviceBase, synchronous from calibre.devices.mtp.filesystem_cache import FilesystemCache -from calibre.devices.mtp.unix.detect import MTPDetect + +MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' + 'bcd serial manufacturer product') + +def fingerprint(d): + return MTPDevice(d.busnum, d.devnum, d.vendor_id, d.product_id, d.bcd, + d.serial, d.manufacturer, d.product) class MTP_DEVICE(MTPDeviceBase): @@ -22,13 +30,18 @@ class MTP_DEVICE(MTPDeviceBase): def __init__(self, *args, **kwargs): MTPDeviceBase.__init__(self, *args, **kwargs) + self.libmtp = None + self.detect_cache = {} + self.dev = None self._filesystem_cache = None self.lock = RLock() self.blacklisted_devices = set() + self.ejected_devices = set() + self.currently_connected_dev = None def set_debug_level(self, lvl): - self.detect.libmtp.set_debug_level(lvl) + self.libmtp.set_debug_level(lvl) def report_progress(self, sent, total): try: @@ -39,40 +52,67 @@ class MTP_DEVICE(MTPDeviceBase): self.progress_reporter(p) @synchronous - def is_usb_connected(self, devices_on_system, debug=False, - only_presence=False): - + def detect_managed_devices(self, devices_on_system): + if self.libmtp is None: return None # First remove blacklisted devices. - devs = [] + devs = set() for d in devices_on_system: - if (d.busnum, d.devnum, d.vendor_id, - d.product_id, d.bcd, d.serial) not in self.blacklisted_devices: - devs.append(d) + fp = fingerprint(d) + if fp not in self.blacklisted_devices: + devs.add(fp) - devs = self.detect(devs) - if self.dev is not None: - # Check if the currently opened device is still connected - ids = self.dev.ids - found = False - for d in devs: - if ( (d.busnum, d.devnum, d.vendor_id, d.product_id, d.serial) - == ids ): - found = True - break - return found - # Check if any MTP capable device is present - return len(devs) > 0 + # Clean up ejected devices + self.ejected_devices = devs.intersection(self.ejected_devices) + + # Check if the currently connected device is still present + if self.currently_connected_dev is not None: + return (self.currently_connected_dev if + self.currently_connected_dev in devs else None) + + # Remove ejected devices + devs = devs - self.ejected_devices + + # Now check for MTP devices + cache = self.detect_cache + for d in devs: + ans = cache.get(d, None) + if ans is None: + ans = self.libmtp.is_mtp_device(d.busnum, d.devnum, + d.vendor_id, d.product_id) + cache[d] = ans + if ans: + return d + + return None + + @synchronous + def create_device(self, connected_device): + d = connected_device + return self.libmtp.Device(d.busnum, d.devnum, d.vendor_id, + d.product_id, d.manufacturer, d.product, d.serial) + + @synchronous + def eject(self): + if self.currently_connected_dev is None: return + self.ejected_devices.add(self.currently_connected_dev) + self.post_yank_cleanup() @synchronous def post_yank_cleanup(self): self.dev = self._filesystem_cache = self.current_friendly_name = None + self.currently_connected_dev = None @synchronous def startup(self): - self.detect = MTPDetect() - for x in vars(self.detect.libmtp): + p = plugins['libmtp'] + self.libmtp = p[0] + if self.libmtp is None: + print ('Failed to load libmtp, MTP device detection disabled') + print (p[1]) + + for x in vars(self.libmtp): if x.startswith('LIBMTP'): - setattr(self, x, getattr(self.detect.libmtp, x)) + setattr(self, x, getattr(self.libmtp, x)) @synchronous def shutdown(self): @@ -85,29 +125,25 @@ class MTP_DEVICE(MTPDeviceBase): @synchronous def open(self, connected_device, library_uuid): self.dev = self._filesystem_cache = None - def blacklist_device(): - d = connected_device - self.blacklisted_devices.add((d.busnum, d.devnum, d.vendor_id, - d.product_id, d.bcd, d.serial)) try: - self.dev = self.detect.create_device(connected_device) - except ValueError: + self.dev = self.create_device(connected_device) + except self.libmtp.MTPError: # Give the device some time to settle time.sleep(2) try: - self.dev = self.detect.create_device(connected_device) - except ValueError: + self.dev = self.create_device(connected_device) + except self.libmtp.MTPError: # Black list this device so that it is ignored for the # remainder of this session. - blacklist_device() + self.blacklisted_devices.add(connected_device) raise OpenFailed('%s is not a MTP device'%(connected_device,)) except TypeError: - blacklist_device() + self.blacklisted_devices.add(connected_device) raise OpenFailed('') storage = sorted(self.dev.storage_info, key=operator.itemgetter('id')) if not storage: - blacklist_device() + self.blacklisted_devices.add(connected_device) raise OpenFailed('No storage found for device %s'%(connected_device,)) self._main_id = storage[0]['id'] self._carda_id = self._cardb_id = None