MTP driver: Allow ignoring any folder on the device

MTP driver: Allow ignoring any folder on the device, not just top level
folders. For newly connected devices, scan
/Android/data/com.amazon.kindle for books by default (newer versions of
the Kindle app place downloaded files there).
This commit is contained in:
Kovid Goyal 2013-09-04 15:56:02 +05:30
parent 36f595f5a8
commit f0a5fd521b
5 changed files with 138 additions and 61 deletions

View File

@ -70,20 +70,34 @@ class MTP_DEVICE(BASE):
return self._prefs return self._prefs
def is_folder_ignored(self, storage_or_storage_id, name, def is_folder_ignored(self, storage_or_storage_id, path,
ignored_folders=None): ignored_folders=None):
storage_id = unicode(getattr(storage_or_storage_id, 'object_id', storage_id = unicode(getattr(storage_or_storage_id, 'object_id',
storage_or_storage_id)) storage_or_storage_id))
name = icu_lower(name) lpath = tuple(icu_lower(name) for name in path)
if ignored_folders is None: if ignored_folders is None:
ignored_folders = self.get_pref('ignored_folders') ignored_folders = self.get_pref('ignored_folders')
if storage_id in ignored_folders: if storage_id in ignored_folders:
return name in {icu_lower(x) for x in ignored_folders[storage_id]} # Use the users ignored folders settings
return '/'.join(lpath) in {icu_lower(x) for x in ignored_folders[storage_id]}
return name in { # Implement the default ignore policy
'alarms', 'android', 'dcim', 'movies', 'music', 'notifications',
# Top level ignores
if lpath[0] in {
'alarms', 'dcim', 'movies', 'music', 'notifications',
'pictures', 'ringtones', 'samsung', 'sony', 'htc', 'bluetooth', 'pictures', 'ringtones', 'samsung', 'sony', 'htc', 'bluetooth',
'games', 'lost.dir', 'video', 'whatsapp', 'image', 'com.zinio.mobile.android.reader'} 'games', 'lost.dir', 'video', 'whatsapp', 'image', 'com.zinio.mobile.android.reader'}:
return True
if len(lpath) > 1 and lpath[0] == 'android':
# Ignore everything in Android apart from a few select folders
if lpath[1] != 'data':
return True
if len(lpath) > 2 and lpath[2] != 'com.amazon.kindle':
return True
return False
def configure_for_kindle_app(self): def configure_for_kindle_app(self):
proxy = self.prefs proxy = self.prefs
@ -398,8 +412,8 @@ class MTP_DEVICE(BASE):
for infile, fname, mi in izip(files, names, metadata): for infile, fname, mi in izip(files, names, metadata):
path = self.create_upload_path(prefix, mi, fname, routing) path = self.create_upload_path(prefix, mi, fname, routing)
if path and self.is_folder_ignored(storage, path[0]): if path and self.is_folder_ignored(storage, path):
raise MTPInvalidSendPathError(path[0]) raise MTPInvalidSendPathError('/'.join(path))
parent = self.ensure_parent(storage, path) parent = self.ensure_parent(storage, path)
if hasattr(infile, 'read'): if hasattr(infile, 'read'):
pos = infile.tell() pos = infile.tell()
@ -549,6 +563,3 @@ if __name__ == '__main__':
print ('Prefix for main mem:', dev.prefix_for_location(None)) print ('Prefix for main mem:', dev.prefix_for_location(None))
finally: finally:
dev.shutdown() dev.shutdown()

View File

@ -21,6 +21,7 @@ from calibre.devices.mtp.base import MTPDeviceBase, synchronous, debug
MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id '
'bcd serial manufacturer product') 'bcd serial manufacturer product')
null = object()
def fingerprint(d): def fingerprint(d):
return MTPDevice(d.busnum, d.devnum, d.vendor_id, d.product_id, d.bcd, return MTPDevice(d.busnum, d.devnum, d.vendor_id, d.product_id, d.bcd,
d.serial, d.manufacturer, d.product) d.serial, d.manufacturer, d.product)
@ -230,13 +231,23 @@ class MTP_DEVICE(MTPDeviceBase):
ans += pprint.pformat(storage) ans += pprint.pformat(storage)
return ans return ans
def _filesystem_callback(self, entry, level): def _filesystem_callback(self, fs_map, entry, level):
name = entry.get('name', '') name = entry.get('name', '')
self.filesystem_callback(_('Found object: %s')%name) self.filesystem_callback(_('Found object: %s')%name)
if (level == 0 and fs_map[entry.get('id', null)] = entry
self.is_folder_ignored(self._currently_getting_sid, name)): path = [name]
return False pid = entry.get('parent_id', 0)
return True while pid != 0 and pid in fs_map:
parent = fs_map[pid]
path.append(parent.get('name', ''))
pid = parent.get('parent_id', 0)
if fs_map.get(pid, None) is parent:
break # An object is its own parent
path = tuple(reversed(path))
ok = not self.is_folder_ignored(self._currently_getting_sid, path)
if not ok:
debug('Ignored object: %s' % '/'.join(path))
return ok
@property @property
def filesystem_cache(self): def filesystem_cache(self):
@ -260,7 +271,7 @@ class MTP_DEVICE(MTPDeviceBase):
'is_system':True}) 'is_system':True})
self._currently_getting_sid = unicode(sid) self._currently_getting_sid = unicode(sid)
items, errs = self.dev.get_filesystem(sid, items, errs = self.dev.get_filesystem(sid,
self._filesystem_callback) partial(self._filesystem_callback, {}))
all_items.extend(items), all_errs.extend(errs) all_items.extend(items), all_errs.extend(errs)
if not all_items and all_errs: if not all_items and all_errs:
raise DeviceError( raise DeviceError(

View File

@ -18,6 +18,8 @@ from calibre.ptempfile import SpooledTemporaryFile
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
from calibre.devices.mtp.base import MTPDeviceBase, debug from calibre.devices.mtp.base import MTPDeviceBase, debug
null = object()
class ThreadingViolation(Exception): class ThreadingViolation(Exception):
def __init__(self): def __init__(self):
@ -219,14 +221,26 @@ class MTP_DEVICE(MTPDeviceBase):
return True return True
def _filesystem_callback(self, obj, level): def _filesystem_callback(self, fs_map, obj, level):
n = obj.get('name', '') name = obj.get('name', '')
msg = _('Found object: %s')%n self.filesystem_callback(_('Found object: %s')%name)
if (level == 0 and if not obj.get('is_folder', False):
self.is_folder_ignored(self._currently_getting_sid, n)):
return False return False
self.filesystem_callback(msg) fs_map[obj.get('id', null)] = obj
return obj.get('is_folder', False) path = [name]
pid = obj.get('parent_id', 0)
while pid != 0 and pid in fs_map:
parent = fs_map[pid]
path.append(parent.get('name', ''))
pid = parent.get('parent_id', 0)
if fs_map.get(pid, None) is parent:
break # An object is its own parent
path = tuple(reversed(path))
ok = not self.is_folder_ignored(self._currently_getting_sid, path)
if not ok:
debug('Ignored object: %s' % '/'.join(path))
return ok
@property @property
def filesystem_cache(self): def filesystem_cache(self):
@ -249,8 +263,8 @@ class MTP_DEVICE(MTPDeviceBase):
storage = {'id':storage_id, 'size':capacity, 'name':name, storage = {'id':storage_id, 'size':capacity, 'name':name,
'is_folder':True, 'can_delete':False, 'is_system':True} 'is_folder':True, 'can_delete':False, 'is_system':True}
self._currently_getting_sid = unicode(storage_id) self._currently_getting_sid = unicode(storage_id)
id_map = self.dev.get_filesystem(storage_id, id_map = self.dev.get_filesystem(storage_id, partial(
self._filesystem_callback) self._filesystem_callback, {}))
for x in id_map.itervalues(): for x in id_map.itervalues():
x['storage_id'] = storage_id x['storage_id'] = storage_id
all_storage.append(storage) all_storage.append(storage)

View File

@ -19,7 +19,7 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.utils.date import parse_date from calibre.utils.date import parse_date
from calibre.gui2.device_drivers.mtp_folder_browser import Browser, TopLevel from calibre.gui2.device_drivers.mtp_folder_browser import Browser, IgnoredFolders
class FormatsConfig(QWidget): # {{{ class FormatsConfig(QWidget): # {{{
@ -424,7 +424,7 @@ class MTPConfig(QTabWidget):
d.exec_() d.exec_()
def change_ignored_folders(self): def change_ignored_folders(self):
d = TopLevel(self.device, d = IgnoredFolders(self.device,
self.current_ignored_folders, parent=self) self.current_ignored_folders, parent=self)
if d.exec_() == d.Accepted: if d.exec_() == d.Accepted:
self.current_ignored_folders = d.ignored_folders self.current_ignored_folders = d.ignored_folders

View File

@ -10,12 +10,11 @@ __docformat__ = 'restructuredtext en'
from operator import attrgetter from operator import attrgetter
from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog,
QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal, QIcon, QLabel, QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal, QIcon, QLabel)
QListWidget, QListWidgetItem)
from calibre.gui2 import file_icon_provider from calibre.gui2 import file_icon_provider
def item(f, parent): def browser_item(f, parent):
name = f.name name = f.name
if not f.is_folder: if not f.is_folder:
name += ' [%s]'%f.last_mod_string name += ' [%s]'%f.last_mod_string
@ -31,22 +30,24 @@ def item(f, parent):
class Storage(QTreeWidget): class Storage(QTreeWidget):
def __init__(self, storage, show_files): def __init__(self, storage, show_files=False, item_func=browser_item):
QTreeWidget.__init__(self) QTreeWidget.__init__(self)
self.item_func = item_func
self.show_files = show_files self.show_files = show_files
self.create_children(storage, self) self.create_children(storage, self)
self.name = storage.name self.name = storage.name
self.object_id = storage.persistent_id self.object_id = storage.persistent_id
self.setMinimumHeight(350) self.setMinimumHeight(350)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.storage = storage
def create_children(self, f, parent): def create_children(self, f, parent):
for child in sorted(f.folders, key=attrgetter('name')): for child in sorted(f.folders, key=attrgetter('name')):
i = item(child, parent) i = self.item_func(child, parent)
self.create_children(child, i) self.create_children(child, i)
if self.show_files: if self.show_files:
for child in sorted(f.files, key=attrgetter('name')): for child in sorted(f.files, key=attrgetter('name')):
i = item(child, parent) i = self.item_func(child, parent)
@property @property
def current_item(self): def current_item(self):
@ -96,14 +97,14 @@ class Browser(QDialog):
def current_item(self): def current_item(self):
return self.folders.current_item return self.folders.current_item
class TopLevel(QDialog): class IgnoredFolders(QDialog):
def __init__(self, dev, ignored_folders=None, parent=None): def __init__(self, dev, ignored_folders=None, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.l = l = QVBoxLayout() self.l = l = QVBoxLayout()
self.setLayout(l) self.setLayout(l)
self.la = la = QLabel('<p>'+ _('<b>Scanned folders:</b>') + ' ' + self.la = la = QLabel('<p>'+ _('<b>Scanned folders:</b>') + ' ' +
_('You can select which top level folders calibre will ' _('You can select which folders calibre will '
'scan when searching this device for books.')) 'scan when searching this device for books.'))
la.setWordWrap(True) la.setWordWrap(True)
l.addWidget(la) l.addWidget(la)
@ -112,17 +113,18 @@ class TopLevel(QDialog):
self.widgets = [] self.widgets = []
for storage in dev.filesystem_cache.entries: for storage in dev.filesystem_cache.entries:
w = QListWidget(self) self.dev = dev
w.storage = storage w = Storage(storage, item_func=self.create_item)
del self.dev
self.tabs.addTab(w, storage.name) self.tabs.addTab(w, storage.name)
self.widgets.append(w) self.widgets.append(w)
for child in sorted(storage.folders, key=attrgetter('name')): w.itemChanged.connect(self.item_changed)
i = QListWidgetItem(child.name)
i.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) self.la2 = la = QLabel(_(
i.setCheckState(Qt.Unchecked if 'If you a select a previously unselected folder, any sub-folders'
dev.is_folder_ignored(storage, child.name, ' will not be visible until you restart calibre.'))
ignored_folders=ignored_folders) else Qt.Checked) l.addWidget(la)
w.addItem(i) la.setWordWrap(True)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok | self.bb = QDialogButtonBox(QDialogButtonBox.Ok |
QDialogButtonBox.Cancel) QDialogButtonBox.Cancel)
@ -136,29 +138,68 @@ class TopLevel(QDialog):
self.setWindowTitle(_('Choose folders to scan')) self.setWindowTitle(_('Choose folders to scan'))
self.setWindowIcon(QIcon(I('devices/tablet.png'))) self.setWindowIcon(QIcon(I('devices/tablet.png')))
self.resize(500, 500) self.resize(600, 500)
def item_changed(self, item, column):
w = item.treeWidget()
root = w.invisibleRootItem()
w.itemChanged.disconnect(self.item_changed)
try:
if item.checkState(0) == Qt.Checked:
# Ensure that the parents of this item are checked
p = item.parent()
while p is not None and p is not root:
p.setCheckState(0, Qt.Checked)
p = p.parent()
# Set the state of all descendants to the same state as this item
for child in self.iterchildren(item):
child.setCheckState(0, item.checkState(0))
finally:
w.itemChanged.connect(self.item_changed)
def iterchildren(self, node):
' Iterate over all descendants of node '
for i in xrange(node.childCount()):
child = node.child(i)
yield child
for gc in self.iterchildren(child):
yield gc
def create_item(self, f, parent):
name = f.name
ans = QTreeWidgetItem(parent, [name])
ans.setData(0, Qt.UserRole, '/'.join(f.full_path[1:]))
ans.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
ans.setCheckState(0,
Qt.Unchecked if self.dev.is_folder_ignored(f.storage_id, f.full_path[1:]) else Qt.Checked)
ans.setData(0, Qt.DecorationRole, file_icon_provider().icon_from_ext('dir'))
return ans
def select_all(self): def select_all(self):
w = self.tabs.currentWidget() w = self.tabs.currentWidget()
for i in xrange(w.count()): for i in xrange(w.invisibleRootItem().childCount()):
x = w.item(i) c = w.invisibleRootItem().child(i)
x.setCheckState(Qt.Checked) c.setCheckState(0, Qt.Checked)
def select_none(self): def select_none(self):
w = self.tabs.currentWidget() w = self.tabs.currentWidget()
for i in xrange(w.count()): for i in xrange(w.invisibleRootItem().childCount()):
x = w.item(i) c = w.invisibleRootItem().child(i)
x.setCheckState(Qt.Unchecked) c.setCheckState(0, Qt.Unchecked)
@property @property
def ignored_folders(self): def ignored_folders(self):
ans = {} ans = {}
for w in self.widgets: for w in self.widgets:
ans[unicode(w.storage.object_id)] = folders = [] folders = set()
for i in xrange(w.count()): for node in self.iterchildren(w.invisibleRootItem()):
x = w.item(i) if node.checkState(0) == Qt.Checked:
if x.checkState() != Qt.Checked: continue
folders.append(unicode(x.text())) path = unicode(node.data(0, Qt.UserRole).toString())
parent = path.rpartition('/')[0]
if '/' not in path or icu_lower(parent) not in folders:
folders.add(icu_lower(path))
ans[unicode(w.storage.storage_id)] = list(folders)
return ans return ans
def setup_device(): def setup_device():
@ -184,17 +225,17 @@ def browse():
dev.shutdown() dev.shutdown()
return d.current_item return d.current_item
def top_level(): def ignored_folders():
from calibre.gui2 import Application from calibre.gui2 import Application
app = Application([]) app = Application([])
app app
dev = setup_device() dev = setup_device()
d = TopLevel(dev, None) d = IgnoredFolders(dev)
d.exec_() d.exec_()
dev.shutdown() dev.shutdown()
return d.ignored_folders return d.ignored_folders
if __name__ == '__main__': if __name__ == '__main__':
# print (browse()) print (browse())
print ('Ignored:', top_level()) # print ('Ignored:', ignored_folders())