From 378d13d041c157a8475999a9e1985c9da564f11f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 08:47:53 +0530 Subject: [PATCH 01/11] Turn on sending of azw3 files to kindles by default, since the KK now has azw3 support --- src/calibre/devices/kindle/driver.py | 5 ++--- src/calibre/gui2/convert/mobi_output.ui | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 1971faef60..ac3bcb4bc1 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -288,7 +288,7 @@ class KINDLE2(KINDLE): name = 'Kindle 2/3/4/Touch Device Interface' description = _('Communicate with the Kindle 2/3/4/Touch eBook reader.') - FORMATS = KINDLE.FORMATS + ['pdf', 'azw4', 'pobi'] + FORMATS = ['azw3'] + KINDLE.FORMATS + ['pdf', 'azw4', 'pobi'] DELETE_EXTS = KINDLE.DELETE_EXTS + ['.mbp1', '.mbs', '.sdr'] PRODUCT_ID = [0x0002, 0x0004] @@ -449,7 +449,7 @@ class KINDLE_DX(KINDLE2): name = 'Kindle DX Device Interface' description = _('Communicate with the Kindle DX eBook reader.') - + FORMATS = KINDLE2.FORMATS[1:] PRODUCT_ID = [0x0003] BCD = [0x0100] @@ -462,7 +462,6 @@ class KINDLE_FIRE(KINDLE2): description = _('Communicate with the Kindle Fire') gui_name = 'Fire' FORMATS = list(KINDLE2.FORMATS) - FORMATS.insert(0, 'azw3') PRODUCT_ID = [0x0006] BCD = [0x216, 0x100] diff --git a/src/calibre/gui2/convert/mobi_output.ui b/src/calibre/gui2/convert/mobi_output.ui index 8c1c107620..71c19fb0c4 100644 --- a/src/calibre/gui2/convert/mobi_output.ui +++ b/src/calibre/gui2/convert/mobi_output.ui @@ -7,7 +7,7 @@ 0 0 588 - 342 + 416 @@ -91,23 +91,33 @@ - + Personal Doc tag: - + - + Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing + + + + <b>WARNING:</b> Various Kindle devices have trouble displaying the new or both MOBI filetypes. If you wish to use the new format on your device, convert to AZW3 instead of MOBI. + + + true + + + From b4772cf0382e0f65de0977e996465e3182e39851 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 10:57:13 +0530 Subject: [PATCH 02/11] Show path on device in the book details panel in the device view --- src/calibre/gui2/book_details.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index f03015f4ad..90284df809 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -152,8 +152,16 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): scheme = u'devpath' if isdevice else u'path' url = prepare_string_for_xml(path if isdevice else unicode(mi.id), True) - link = u'%s' % (scheme, url, - prepare_string_for_xml(path, True), _('Click to open')) + pathstr = _('Click to open') + extra = '' + if isdevice: + durl = url + if durl.startswith('mtp:::'): + durl = ':::'.join( (durl.split(':::'))[2:] ) + extra = '
%s'%( + prepare_string_for_xml(durl)) + link = u'%s%s' % (scheme, url, + prepare_string_for_xml(path, True), pathstr, extra) ans.append((field, u'%s%s'%(name, link))) elif field == 'formats': if isdevice: continue From 3032d61204b45b20c2d50dc4ed13640460e6a243 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 13:38:31 +0530 Subject: [PATCH 03/11] MTP: Return a correct driveinfo dict --- src/calibre/devices/mtp/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index b43d3db3df..ed187c33c4 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -53,6 +53,7 @@ class MTP_DEVICE(BASE): p.defaults['send_template'] = config().parse().send_template p.defaults['blacklist'] = [] p.defaults['history'] = {} + p.defaults['rules'] = [] return self._prefs @@ -106,7 +107,7 @@ class MTP_DEVICE(BASE): dinfo['mtp_prefix'] = storage.storage_prefix raw = json.dumps(dinfo, default=to_json) self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw)) - self.driveinfo = dinfo + self.driveinfo[location_code] = dinfo def get_device_information(self, end_session=True): self.report_progress(1.0, _('Get device information...')) From ac9ade0409a485b0e0c23530b3f2fac4a241b2cb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 13:51:40 +0530 Subject: [PATCH 04/11] Add CC directory to the default list of folders to send to --- src/calibre/devices/mtp/driver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index ed187c33c4..c999bd9c1d 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -47,9 +47,9 @@ class MTP_DEVICE(BASE): from calibre.library.save_to_disk import config self._prefs = p = JSONConfig('mtp_devices') p.defaults['format_map'] = self.FORMATS - p.defaults['send_to'] = ['Books', 'eBooks/import', 'eBooks', - 'wordplayer/calibretransfer', 'sdcard/ebooks', - 'kindle'] + p.defaults['send_to'] = ['Calibre_Companion', 'Books', + 'eBooks/import', 'eBooks', 'wordplayer/calibretransfer', + 'sdcard/ebooks', 'kindle'] p.defaults['send_template'] = config().parse().send_template p.defaults['blacklist'] = [] p.defaults['history'] = {} From d26bf70d547da7b27587fa24635451296af89ce6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 14:15:54 +0530 Subject: [PATCH 05/11] UI for creating format routing rules for MTP devices --- src/calibre/gui2/device_drivers/mtp_config.py | 153 ++++++++++++++++-- 1 file changed, 139 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/device_drivers/mtp_config.py b/src/calibre/gui2/device_drivers/mtp_config.py index 261fea3df2..7481cbf19c 100644 --- a/src/calibre/gui2/device_drivers/mtp_config.py +++ b/src/calibre/gui2/device_drivers/mtp_config.py @@ -11,7 +11,8 @@ import weakref from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel, QTabWidget, QGridLayout, QListWidget, QIcon, QLineEdit, QVBoxLayout, - QPushButton) + QPushButton, QGroupBox, QScrollArea, QHBoxLayout, QComboBox, + pyqtSignal, QSizePolicy, QDialog, QDialogButtonBox) from calibre.ebooks import BOOK_EXTENSIONS from calibre.gui2 import error_dialog @@ -86,7 +87,7 @@ class TemplateConfig(QWidget): # {{{ m.setBuddy(t) l.addWidget(m, 0, 0, 1, 2) l.addWidget(t, 1, 0, 1, 1) - b = self.b = QPushButton(_('Template editor')) + b = self.b = QPushButton(_('&Template editor')) l.addWidget(b, 1, 1, 1, 1) b.clicked.connect(self.edit_template) @@ -176,6 +177,113 @@ class IgnoredDevices(QWidget): # {{{ # }}} +# Rules {{{ + +class Rule(QWidget): + + remove = pyqtSignal(object) + + def __init__(self, rule=None): + QWidget.__init__(self) + + self.l = l = QHBoxLayout() + self.setLayout(l) + + self.l1 = l1 = QLabel(_('Send the ')) + l.addWidget(l1) + self.fmt = f = QComboBox(self) + l.addWidget(f) + self.l2 = l2 = QLabel(_(' format to the folder: ')) + l.addWidget(l2) + self.folder = f = QLineEdit(self) + f.setPlaceholderText(_('Folder on the device')) + l.addWidget(f) + self.rb = rb = QPushButton(QIcon(I('list_remove.png')), + _('&Remove rule'), self) + l.addWidget(rb) + rb.clicked.connect(self.removed) + + for fmt in sorted(BOOK_EXTENSIONS): + self.fmt.addItem(fmt.upper(), fmt.lower()) + + self.fmt.setCurrentIndex(0) + + if rule is not None: + fmt, folder = rule + idx = self.fmt.findText(fmt.upper()) + if idx > -1: + self.fmt.setCurrentIndex(idx) + self.folder.setText(folder) + + self.ignore = False + + def removed(self): + self.remove.emit(self) + + @property + def rule(self): + folder = unicode(self.folder.text()).strip() + if folder: + return ( + unicode(self.fmt.itemData(self.fmt.currentIndex()).toString()), + folder + ) + return None + +class FormatRules(QGroupBox): + + def __init__(self, rules): + QGroupBox.__init__(self, _('Format specific sending')) + self.l = l = QVBoxLayout() + self.setLayout(l) + self.la = la = QLabel('

'+_( + '''You can create rules that control where ebooks of a specific + format are sent to on the device. These will take precedence over + the folders specified above.''')) + la.setWordWrap(True) + l.addWidget(la) + self.sa = sa = QScrollArea(self) + sa.setWidgetResizable(True) + self.w = w = QWidget(self) + w.l = QVBoxLayout() + w.setLayout(w.l) + sa.setWidget(w) + l.addWidget(sa) + self.widgets = [] + for rule in rules: + r = Rule(rule) + self.widgets.append(r) + w.l.addWidget(r) + r.remove.connect(self.remove_rule) + + if not self.widgets: + self.add_rule() + + self.b = b = QPushButton(QIcon(I('plus.png')), _('Add a &new rule')) + l.addWidget(b) + b.clicked.connect(self.add_rule) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) + + def add_rule(self): + r = Rule() + self.widgets.append(r) + self.w.l.addWidget(r) + r.remove.connect(self.remove_rule) + self.sa.verticalScrollBar().setValue(self.sa.verticalScrollBar().maximum()) + + def remove_rule(self, rule): + rule.setVisible(False) + rule.ignore = True + + @property + def rules(self): + for w in self.widgets: + if not w.ignore: + r = w.rule + if r is not None: + yield r +# }}} + class MTPConfig(QTabWidget): def __init__(self, device, parent=None): @@ -185,8 +293,8 @@ class MTPConfig(QTabWidget): cd = msg = None if device.current_friendly_name is not None: if device.current_serial_num is None: - msg = '

' + _('The %s device has no serial number, ' - 'it cannot be configured'%device.current_friendly_name) + msg = '

' + (_('The %s device has no serial number, ' + 'it cannot be configured')%device.current_friendly_name) else: cd = 'device-'+device.current_serial_num else: @@ -211,6 +319,7 @@ class MTPConfig(QTabWidget): l = self.base.l = QGridLayout(self.base) self.base.setLayout(l) + self.rules = r = FormatRules(self.get_pref('rules')) self.formats = FormatsConfig(set(BOOK_EXTENSIONS), self.get_pref('format_map')) self.send_to = SendToConfig(self.get_pref('send_to')) @@ -218,17 +327,20 @@ class MTPConfig(QTabWidget): 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, 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.b = b = QPushButton(QIcon(I('list_remove.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) + l.addWidget(b, 0, 0, 1, 2) + l.addWidget(la, 1, 0, 1, 1) + l.addWidget(self.formats, 2, 0, 3, 1) + l.addWidget(self.send_to, 2, 1, 1, 1) + l.addWidget(self.template, 3, 1, 1, 1) + l.setRowStretch(4, 10) + l.addWidget(r, 5, 0, 1, 2) + l.setRowStretch(5, 100) + self.igntab = IgnoredDevices(self.device.prefs['history'], self.device.prefs['blacklist']) self.addTab(self.igntab, _('Ignored devices')) @@ -280,6 +392,11 @@ class MTPConfig(QTabWidget): if s and s != self.device.prefs['send_to']: p['send_to'] = s + p.pop('rules', None) + r = list(self.rules.rules) + if r and r != self.device.prefs['rules']: + p['rules'] = r + self.device.prefs[self.current_device_key] = p self.device.prefs['blacklist'] = self.igntab.blacklist @@ -296,8 +413,16 @@ if __name__ == '__main__': cd = dev.detect_managed_devices(s.devices) dev.open(cd, 'test') cw = dev.config_widget() - cw.show() - app.exec_() + d = QDialog() + d.l = QVBoxLayout() + d.setLayout(d.l) + d.l.addWidget(cw) + bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + d.l.addWidget(bb) + bb.accepted.connect(d.accept) + bb.rejected.connect(d.reject) + if d.exec_() == d.Accepted: + cw.commit() dev.shutdown() From 4b8933f9fbb83ea6226487659244213a321361a9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 14:30:50 +0530 Subject: [PATCH 06/11] MTP: Implement format routing --- src/calibre/devices/mtp/driver.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index c999bd9c1d..caf3174a11 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -274,15 +274,18 @@ class MTP_DEVICE(BASE): self.plugboards = plugboards self.plugboard_func = pb_func - def create_upload_path(self, path, mdata, fname): + def create_upload_path(self, path, mdata, fname, routing): from calibre.devices.utils import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize + ext = fname.rpartition('.')[-1].lower() + path = routing.get(ext, path) + filepath = create_upload_path(mdata, fname, self.save_template, sanitize, prefix_path=path, path_type=posixpath, maxlen=self.MAX_PATH_LEN, - use_subdirs = True, - news_in_folder = self.NEWS_IN_FOLDER, + use_subdirs=True, + news_in_folder=self.NEWS_IN_FOLDER, ) return tuple(x for x in filepath.split('/')) @@ -330,8 +333,10 @@ class MTP_DEVICE(BASE): self.report_progress(0, _('Transferring books to device...')) i, total = 0, len(files) + routing = {fmt:dest for fmt,dest in self.get_pref('rules')} + for infile, fname, mi in izip(files, names, metadata): - path = self.create_upload_path(prefix, mi, fname) + path = self.create_upload_path(prefix, mi, fname, routing) parent = self.ensure_parent(storage, path) if hasattr(infile, 'read'): pos = infile.tell() From c0aee6772dc23a6e411c341f2b16ddb377c70df8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 14:50:59 +0530 Subject: [PATCH 07/11] ... --- src/calibre/devices/mtp/unix/driver.py | 7 +++++-- src/calibre/devices/mtp/windows/driver.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 31f886b875..6e5d91c0a0 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import operator, traceback, pprint, sys +import operator, traceback, pprint, sys, time from threading import RLock from collections import namedtuple from functools import partial @@ -16,7 +16,7 @@ from calibre import prints, as_unicode from calibre.constants import plugins from calibre.ptempfile import SpooledTemporaryFile from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice -from calibre.devices.mtp.base import MTPDeviceBase, synchronous +from calibre.devices.mtp.base import MTPDeviceBase, synchronous, debug MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' 'bcd serial manufacturer product') @@ -193,6 +193,8 @@ class MTP_DEVICE(MTPDeviceBase): @property def filesystem_cache(self): if self._filesystem_cache is None: + st = time.time() + debug('Loading filesystem metadata...') from calibre.devices.mtp.filesystem_cache import FilesystemCache with self.lock: storage, all_items, all_errs = [], [], [] @@ -220,6 +222,7 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name, self.format_errorstack(all_errs))) self._filesystem_cache = FilesystemCache(storage, all_items) + debug('Filesystem metadata loaded in %g seconds'%(time.time()-st)) return self._filesystem_cache @synchronous diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 50638496d1..3290115028 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -16,7 +16,7 @@ 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, BlacklistedDevice -from calibre.devices.mtp.base import MTPDeviceBase +from calibre.devices.mtp.base import MTPDeviceBase, debug class ThreadingViolation(Exception): @@ -199,6 +199,8 @@ class MTP_DEVICE(MTPDeviceBase): @property def filesystem_cache(self): if self._filesystem_cache is None: + debug('Loading filesystem metadata...') + st = time.time() from calibre.devices.mtp.filesystem_cache import FilesystemCache ts = self.total_space() all_storage = [] @@ -218,6 +220,7 @@ class MTP_DEVICE(MTPDeviceBase): all_storage.append(storage) items.append(id_map.itervalues()) self._filesystem_cache = FilesystemCache(all_storage, chain(*items)) + debug('Filesystem metadata loaded in %g seconds'%(time.time()-st)) return self._filesystem_cache @same_thread From 5008cc5d92b561c849417d510d4bca3a6970fb0a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 15:26:08 +0530 Subject: [PATCH 08/11] MTP: only populate driveinfo during the fetching of the book lists so that the get_device_information() job is not slow --- src/calibre/devices/interface.py | 16 ++++++++++++++++ src/calibre/devices/mtp/driver.py | 17 ++++++++++++----- src/calibre/devices/mtp/filesystem_cache.py | 3 +++ src/calibre/devices/mtp/unix/driver.py | 3 ++- src/calibre/devices/mtp/windows/driver.py | 3 ++- src/calibre/gui2/device.py | 10 ++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 58b097f951..c345045e7e 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -95,6 +95,10 @@ class DevicePlugin(Plugin): #: call post_yank_cleanup(). MANAGES_DEVICE_PRESENCE = False + #: If set the True, calibre will call the :method:`get_driveinfo()` method + #: after the books lists have been loaded to get the driveinfo. + SLOW_DRIVEINFO = False + @classmethod def get_gui_name(cls): if hasattr(cls, 'gui_name'): @@ -352,6 +356,18 @@ class DevicePlugin(Plugin): """ raise NotImplementedError() + def get_driveinfo(self): + ''' + Return the driveinfo dictionary. Usually called from + get_device_information(), but if loading the driveinfo is slow for this + driver, then it should set SLOW_DRIVEINFO. In this case, this method + will be called by calibre after the book lists have been loaded. Note + that it is not called on the device thread, so the driver should cache + the drive info in the books() method and this function should return + the cached data. + ''' + return {} + def card_prefix(self, end_session=True): ''' Return a 2 element list of the prefix to paths on the cards. diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index caf3174a11..de06955bfe 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -35,6 +35,7 @@ class MTP_DEVICE(BASE): MANAGES_DEVICE_PRESENCE = True FORMATS = ['epub', 'azw3', 'mobi', 'pdf'] DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE' + SLOW_DRIVEINFO = True def __init__(self, *args, **kwargs): BASE.__init__(self, *args, **kwargs) @@ -76,6 +77,7 @@ class MTP_DEVICE(BASE): def open(self, devices, library_uuid): self.current_library_uuid = library_uuid self.location_paths = None + self.driveinfo = {} BASE.open(self, devices, library_uuid) h = self.prefs['history'] if self.current_serial_num: @@ -109,13 +111,17 @@ class MTP_DEVICE(BASE): self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw)) self.driveinfo[location_code] = dinfo + def get_driveinfo(self): + if not self.driveinfo: + self.driveinfo = {} + for sid, location_code in ( (self._main_id, 'main'), (self._carda_id, + 'A'), (self._cardb_id, 'B')): + if sid is None: continue + self._update_drive_info(self.filesystem_cache.storage(sid), location_code) + return self.driveinfo + def get_device_information(self, end_session=True): self.report_progress(1.0, _('Get device information...')) - self.driveinfo = {} - for sid, location_code in ( (self._main_id, 'main'), (self._carda_id, - 'A'), (self._cardb_id, 'B')): - if sid is None: continue - self._update_drive_info(self.filesystem_cache.storage(sid), location_code) dinfo = self.get_basic_device_information() return tuple( list(dinfo) + [self.driveinfo] ) @@ -135,6 +141,7 @@ class MTP_DEVICE(BASE): def books(self, oncard=None, end_session=True): from calibre.devices.mtp.books import JSONCodec from calibre.devices.mtp.books import BookList, Book + self.get_driveinfo() # Ensure driveinfo is loaded sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard, self._main_id) if sid is None: diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 8f4d20ae18..b1c2828b8c 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -230,6 +230,9 @@ class FilesystemCache(object): continue # Ignore .txt files in the root yield x + def __len__(self): + return len(self.id_map) + def resolve_mtp_id_path(self, path): if not path.startswith('mtp:::'): raise ValueError('%s is not a valid MTP path'%path) diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 6e5d91c0a0..4b9ed9e928 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -222,7 +222,8 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name, self.format_errorstack(all_errs))) self._filesystem_cache = FilesystemCache(storage, all_items) - debug('Filesystem metadata loaded in %g seconds'%(time.time()-st)) + debug('Filesystem metadata loaded in %g seconds (%d objects)'%( + time.time()-st, len(self._filesystem_cache))) return self._filesystem_cache @synchronous diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 3290115028..3f79e7d991 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -220,7 +220,8 @@ class MTP_DEVICE(MTPDeviceBase): all_storage.append(storage) items.append(id_map.itervalues()) self._filesystem_cache = FilesystemCache(all_storage, chain(*items)) - debug('Filesystem metadata loaded in %g seconds'%(time.time()-st)) + debug('Filesystem metadata loaded in %g seconds (%d objects)'%( + time.time()-st, len(self._filesystem_cache))) return self._filesystem_cache @same_thread diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 8466fe9320..553532e95d 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -433,6 +433,15 @@ class DeviceManager(Thread): # {{{ return self.create_job_step(self._get_device_information, done, description=_('Get device information'), to_job=add_as_step_to_job) + def slow_driveinfo(self): + ''' Update the stored device information with the driveinfo if the + device indicates that getting driveinfo is slow ''' + info = self._device_information['info'] + if (not info[4] and self.device.SLOW_DRIVEINFO): + info = list(info) + info[4] = self.device.get_driveinfo() + self._device_information['info'] = tuple(info) + def get_current_device_information(self): return self._device_information @@ -1023,6 +1032,7 @@ class DeviceMixin(object): # {{{ if job.failed: self.device_job_exception(job) return + self.device_manager.slow_driveinfo() # set_books_in_library might schedule a sync_booklists job self.set_books_in_library(job.result, reset=True, add_as_step_to_job=job) mainlist, cardalist, cardblist = job.result From e35622a98b9fa95eb8af921eed6033ed235baf37 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 17:15:22 +0530 Subject: [PATCH 09/11] MTP: Filesystem browser widget --- .../gui2/device_drivers/mtp_folder_browser.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/calibre/gui2/device_drivers/mtp_folder_browser.py diff --git a/src/calibre/gui2/device_drivers/mtp_folder_browser.py b/src/calibre/gui2/device_drivers/mtp_folder_browser.py new file mode 100644 index 0000000000..97b75c9de2 --- /dev/null +++ b/src/calibre/gui2/device_drivers/mtp_folder_browser.py @@ -0,0 +1,96 @@ +#!/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 operator import attrgetter + +from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, + QDialogButtonBox, QVBoxLayout, QSize) + +from calibre.gui2 import file_icon_provider + +def item(f, parent): + name = f.name + if not f.is_folder: + name += ' [%s]'%f.last_mod_string + ans = QTreeWidgetItem(parent, [name]) + ans.setData(0, Qt.UserRole, f.full_path) + if f.is_folder: + ext = 'dir' + else: + ext = f.name.rpartition('.')[-1] + ans.setData(0, Qt.DecorationRole, file_icon_provider().icon_from_ext(ext)) + + return ans + +class Storage(QTreeWidget): + + def __init__(self, storage, show_files): + QTreeWidget.__init__(self) + self.show_files = show_files + self.create_children(storage, self) + self.name = storage.name + self.object_id = storage.persistent_id + self.setMinimumHeight(350) + self.setHeaderHidden(True) + + def create_children(self, f, parent): + for child in sorted(f.folders, key=attrgetter('name')): + i = item(child, parent) + self.create_children(child, i) + if self.show_files: + for child in sorted(f.files, key=attrgetter('name')): + i = item(child, parent) + +class Folders(QTabWidget): + + def __init__(self, filesystem_cache, show_files=True): + QTabWidget.__init__(self) + self.fs = filesystem_cache + for storage in self.fs.entries: + w = Storage(storage, show_files) + self.addTab(w, w.name) + + self.setCurrentIndex(0) + +class Browser(QDialog): + + def __init__(self, filesystem_cache, show_files=True, parent=None): + QDialog.__init__(self, parent) + self.l = l = QVBoxLayout() + self.setLayout(l) + self.folders = cw = Folders(filesystem_cache, show_files=show_files) + l.addWidget(cw) + bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + l.addWidget(bb) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + self.setMinimumSize(QSize(500, 500)) + +def browse(): + from calibre.gui2 import Application + from calibre.devices.mtp.driver import MTP_DEVICE + from calibre.devices.scanner import DeviceScanner + s = DeviceScanner() + s.scan() + app = Application([]) + app + dev = MTP_DEVICE(None) + dev.startup() + cd = dev.detect_managed_devices(s.devices) + if cd is None: + raise ValueError('No MTP device found') + dev.open(cd, 'test') + d = Browser(dev.filesystem_cache) + if d.exec_() == d.Accepted: + pass + dev.shutdown() + +if __name__ == '__main__': + browse() + From 903af8f4e545c0c9c7db5ae18cc296deb3c1ab50 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 21:18:16 +0530 Subject: [PATCH 10/11] Allow the user to graphically select folders when configuring the MTP device --- src/calibre/gui2/device_drivers/mtp_config.py | 60 +++++++++++++++---- .../gui2/device_drivers/mtp_folder_browser.py | 29 +++++++-- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/device_drivers/mtp_config.py b/src/calibre/gui2/device_drivers/mtp_config.py index 7481cbf19c..0187915a4a 100644 --- a/src/calibre/gui2/device_drivers/mtp_config.py +++ b/src/calibre/gui2/device_drivers/mtp_config.py @@ -18,6 +18,7 @@ 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 +from calibre.gui2.device_drivers.mtp_folder_browser import Browser class FormatsConfig(QWidget): # {{{ @@ -117,19 +118,36 @@ class TemplateConfig(QWidget): # {{{ class SendToConfig(QWidget): # {{{ - def __init__(self, val): + def __init__(self, val, device): QWidget.__init__(self) self.t = t = QLineEdit(self) t.setText(', '.join(val or [])) t.setCursorPosition(0) - self.l = l = QVBoxLayout(self) + self.l = l = QGridLayout(self) self.setLayout(l) self.m = m = QLabel('

'+_('''A list of &folders on the device to which to send ebooks. The first one that exists will be used:''')) m.setWordWrap(True) m.setBuddy(t) - l.addWidget(m) - l.addWidget(t) + l.addWidget(m, 0, 0, 1, 2) + l.addWidget(t, 1, 0) + self.b = b = QToolButton() + l.addWidget(b, 1, 1) + b.setIcon(QIcon(I('document_open.png'))) + b.clicked.connect(self.browse) + b.setToolTip(_('Browse for a folder on the device')) + self._device = weakref.ref(device) + + @property + def device(self): + return self._device() + + def browse(self): + b = Browser(self.device.filesystem_cache, show_files=False, + parent=self) + if b.exec_() == b.Accepted: + sid, path = b.current_item + self.t.setText('/'.join(path[1:])) @property def value(self): @@ -183,8 +201,9 @@ class Rule(QWidget): remove = pyqtSignal(object) - def __init__(self, rule=None): + def __init__(self, device, rule=None): QWidget.__init__(self) + self._device = weakref.ref(device) self.l = l = QHBoxLayout() self.setLayout(l) @@ -198,6 +217,11 @@ class Rule(QWidget): self.folder = f = QLineEdit(self) f.setPlaceholderText(_('Folder on the device')) l.addWidget(f) + self.b = b = QToolButton() + l.addWidget(b) + b.setIcon(QIcon(I('document_open.png'))) + b.clicked.connect(self.browse) + b.setToolTip(_('Browse for a folder on the device')) self.rb = rb = QPushButton(QIcon(I('list_remove.png')), _('&Remove rule'), self) l.addWidget(rb) @@ -217,6 +241,17 @@ class Rule(QWidget): self.ignore = False + @property + def device(self): + return self._device() + + def browse(self): + b = Browser(self.device.filesystem_cache, show_files=False, + parent=self) + if b.exec_() == b.Accepted: + sid, path = b.current_item + self.folder.setText('/'.join(path[1:])) + def removed(self): self.remove.emit(self) @@ -232,8 +267,9 @@ class Rule(QWidget): class FormatRules(QGroupBox): - def __init__(self, rules): + def __init__(self, device, rules): QGroupBox.__init__(self, _('Format specific sending')) + self._device = weakref.ref(device) self.l = l = QVBoxLayout() self.setLayout(l) self.la = la = QLabel('

'+_( @@ -251,7 +287,7 @@ class FormatRules(QGroupBox): l.addWidget(sa) self.widgets = [] for rule in rules: - r = Rule(rule) + r = Rule(device, rule) self.widgets.append(r) w.l.addWidget(r) r.remove.connect(self.remove_rule) @@ -264,8 +300,12 @@ class FormatRules(QGroupBox): b.clicked.connect(self.add_rule) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) + @property + def device(self): + return self._device() + def add_rule(self): - r = Rule() + r = Rule(self.device) self.widgets.append(r) self.w.l.addWidget(r) r.remove.connect(self.remove_rule) @@ -319,10 +359,10 @@ class MTPConfig(QTabWidget): l = self.base.l = QGridLayout(self.base) self.base.setLayout(l) - self.rules = r = FormatRules(self.get_pref('rules')) + self.rules = r = FormatRules(self.device, self.get_pref('rules')) self.formats = FormatsConfig(set(BOOK_EXTENSIONS), self.get_pref('format_map')) - self.send_to = SendToConfig(self.get_pref('send_to')) + self.send_to = SendToConfig(self.get_pref('send_to'), self.device) 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) diff --git a/src/calibre/gui2/device_drivers/mtp_folder_browser.py b/src/calibre/gui2/device_drivers/mtp_folder_browser.py index 97b75c9de2..de9562fee7 100644 --- a/src/calibre/gui2/device_drivers/mtp_folder_browser.py +++ b/src/calibre/gui2/device_drivers/mtp_folder_browser.py @@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en' from operator import attrgetter from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, - QDialogButtonBox, QVBoxLayout, QSize) + QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal) from calibre.gui2 import file_icon_provider @@ -47,17 +47,33 @@ class Storage(QTreeWidget): for child in sorted(f.files, key=attrgetter('name')): i = item(child, parent) + @property + def current_item(self): + item = self.currentItem() + if item is not None: + return (self.object_id, item.data(0, Qt.UserRole).toPyObject()) + return None + class Folders(QTabWidget): + selected = pyqtSignal() + def __init__(self, filesystem_cache, show_files=True): QTabWidget.__init__(self) self.fs = filesystem_cache for storage in self.fs.entries: w = Storage(storage, show_files) self.addTab(w, w.name) + w.doubleClicked.connect(self.selected) self.setCurrentIndex(0) + @property + def current_item(self): + w = self.currentWidget() + if w is not None: + return w.current_item + class Browser(QDialog): def __init__(self, filesystem_cache, show_files=True, parent=None): @@ -71,6 +87,11 @@ class Browser(QDialog): bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.setMinimumSize(QSize(500, 500)) + self.folders.selected.connect(self.accept) + + @property + def current_item(self): + return self.folders.current_item def browse(): from calibre.gui2 import Application @@ -87,10 +108,10 @@ def browse(): raise ValueError('No MTP device found') dev.open(cd, 'test') d = Browser(dev.filesystem_cache) - if d.exec_() == d.Accepted: - pass + d.exec_() dev.shutdown() + return d.current_item if __name__ == '__main__': - browse() + print (browse()) From 23999f9b423aea5fea80367782086bfce6608730 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 12 Sep 2012 23:18:11 +0530 Subject: [PATCH 11/11] ... --- src/calibre/gui2/__init__.py | 1 + src/calibre/gui2/device_drivers/mtp_folder_browser.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index da254a7b3f..dc4646f208 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -499,6 +499,7 @@ class FileIconProvider(QFileIconProvider): self.icons = {} for key in self.__class__.ICONS.keys(): self.icons[key] = I('mimetypes/')+self.__class__.ICONS[key]+'.png' + self.icons['calibre'] = I('lt.png') for i in ('dir', 'default', 'zero'): self.icons[i] = QIcon(self.icons[i]) diff --git a/src/calibre/gui2/device_drivers/mtp_folder_browser.py b/src/calibre/gui2/device_drivers/mtp_folder_browser.py index de9562fee7..960f821b57 100644 --- a/src/calibre/gui2/device_drivers/mtp_folder_browser.py +++ b/src/calibre/gui2/device_drivers/mtp_folder_browser.py @@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en' from operator import attrgetter from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, - QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal) + QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal, QIcon) from calibre.gui2 import file_icon_provider @@ -88,6 +88,8 @@ class Browser(QDialog): bb.rejected.connect(self.reject) self.setMinimumSize(QSize(500, 500)) self.folders.selected.connect(self.accept) + self.setWindowTitle(_('Choose folder on device')) + self.setWindowIcon(QIcon(I('devices/galaxy_s3.png'))) @property def current_item(self):