From cddf873db2eefbb86f75310c1dcb22ddf8e31e57 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 26 Aug 2012 12:54:09 +0200 Subject: [PATCH 01/14] Add checkbox to set auto management --- src/calibre/gui2/dialogs/smartdevice.py | 22 ++++++++++++++++++++-- src/calibre/gui2/dialogs/smartdevice.ui | 21 ++++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index 5de933a21c..ea4f6741aa 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -9,6 +9,7 @@ from PyQt4.Qt import (QDialog, QLineEdit, Qt) from calibre.gui2 import error_dialog from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog +from calibre.utils.config import prefs class SmartdeviceDialog(QDialog, Ui_Dialog): @@ -40,6 +41,14 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): 'to the port, try another number. You can use any number between ' '8,000 and 32,000.') + '

') + self.enable_auto_management_box.setToolTip('

' + + _('If this box is checked, calibre will send any changes you made ' + "to book's metadata when your device is connected. If it is not " + 'checked, changes are sent only when you send the book. You can ' + 'get more information or change the preference to some other ' + 'choice at Preferences -> Send to device -> Metadata management') + + '

') + self.show_password.stateChanged[int].connect(self.toggle_password) self.use_fixed_port.stateChanged[int].connect(self.use_fixed_port_changed) @@ -57,13 +66,19 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): self.orig_port_number = self.device_manager.get_option('smartdevice', 'port_number') self.fixed_port.setText(self.orig_port_number) - self.use_fixed_port.setChecked(self.orig_fixed_port); + self.use_fixed_port.setChecked(self.orig_fixed_port) if not self.orig_fixed_port: - self.fixed_port.setEnabled(False); + self.fixed_port.setEnabled(False) if pw: self.password_box.setText(pw) + self.auto_management_is_set = False + if prefs['manage_device_metadata'] == 'on_connect': + self.enable_auto_management_box.setChecked(True) + self.enable_auto_management_box.setEnabled(False) + self.auto_management_is_set = True + self.resize(self.sizeHint()) def use_fixed_port_changed(self, state): @@ -111,5 +126,8 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): self.device_manager.set_option('smartdevice', 'port_number', self.orig_port_number) else: + if not self.auto_management_is_set and \ + self.enable_auto_management_box.isChecked(): + prefs.set('manage_device_metadata', 'on_connect') QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui index 60f1c07be4..fadcf921b4 100644 --- a/src/calibre/gui2/dialogs/smartdevice.ui +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -101,7 +101,21 @@ + + + + &Automatically allow connections at calibre startup + + + + + + &Enable automatic sending of book metadata when your device connects + + + + Qt::Horizontal @@ -111,13 +125,6 @@ - - - - &Automatically allow connections at calibre startup - - - From ffde6a4a043e44d1866d60fe62c4f968a4c7da1e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 27 Aug 2012 13:09:22 +0200 Subject: [PATCH 02/14] Add use of netifaces to show multiple IP addresses --- src/calibre/gui2/actions/device.py | 16 ++++++-- src/calibre/gui2/dialogs/smartdevice.py | 46 ++++++++++++++++++++++ src/calibre/gui2/dialogs/smartdevice.ui | 51 ++++++++++++++++--------- 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index d978f9723f..92fc34b105 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -237,20 +237,28 @@ class ConnectShareAction(InterfaceAction): self.share_conn_menu.hide_smartdevice_menus() def set_smartdevice_action_state(self): - from calibre.utils.mdns import get_external_ip + from calibre.gui2.dialogs.smartdevice import get_all_ip_addresses dm = self.gui.device_manager + all_ips = get_all_ip_addresses() + if len(all_ips) > 3: + formatted_addresses = _('Many IP addresses. See Start/Stop dialog.') + show_port = False + else: + formatted_addresses = ' or '.join(get_all_ip_addresses()) + show_port = True + running = dm.is_running('smartdevice') if not running: text = self.share_conn_menu.DEVICE_MSGS[0] else: use_fixed_port = dm.get_option('smartdevice', 'use_fixed_port') port_number = dm.get_option('smartdevice', 'port_number') - if use_fixed_port: + if show_port and use_fixed_port: text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s port %s]'%( - get_external_ip(), port_number) + formatted_addresses, port_number) else: - text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s]'%get_external_ip() + text = self.share_conn_menu.DEVICE_MSGS[1] + ' [' + formatted_addresses + ']' icon = 'green' if running else 'red' ac = self.share_conn_menu.control_smartdevice_action diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index ea4f6741aa..e3028cb6e1 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -5,12 +5,47 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' +import netifaces, socket + from PyQt4.Qt import (QDialog, QLineEdit, Qt) from calibre.gui2 import error_dialog from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog from calibre.utils.config import prefs +def _cmp_ipaddr(l, r): + lparts = ['%3s'%x for x in l.split('.')] + rparts = ['%3s'%x for x in r.split('.')] + + if lparts[0] in ['192', '170', ' 10']: + if rparts[0] not in ['192', '170', '10']: + return -1 + return cmp(rparts, lparts) + + if rparts[0] in ['192', '170', ' 10']: + return 1 + + return cmp(lparts, rparts) + +def get_all_ip_addresses(): + ip_info = [netifaces.ifaddresses(x).get(netifaces.AF_INET, None) + for x in netifaces.interfaces()] + + all_ipaddrs = list() + for iface in ip_info: + if iface is not None: + for addrs in iface: + if 'netmask' in addrs and addrs['addr'] != '127.0.0.1': + # We get VPN interfaces that were connected and then + # disconnected. Oh well. At least the 'right' IP addr + # is there. + all_ipaddrs.append(addrs['addr']) + + all_ipaddrs.sort(cmp=_cmp_ipaddr) + print(all_ipaddrs) + return all_ipaddrs + + class SmartdeviceDialog(QDialog, Ui_Dialog): def __init__(self, parent): @@ -49,6 +84,15 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): 'choice at Preferences -> Send to device -> Metadata management') + '

') + self.ip_addresses.setToolTip('

' + + _('These are the IP addresses detected by calibre for the computer ' + 'running calibre. If you decide to have your device connect to ' + 'calibre using a fixed IP address, one of these addresses should ' + 'be the one you use. It is unlikely but possible that the correct ' + 'IP address is not listed here, in which case you will need to go ' + "to your computer's control panel to get a complete list of " + "your computer's network interfaces and IP addresses.") + '

') + self.show_password.stateChanged[int].connect(self.toggle_password) self.use_fixed_port.stateChanged[int].connect(self.use_fixed_port_changed) @@ -79,6 +123,8 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): self.enable_auto_management_box.setEnabled(False) self.auto_management_is_set = True + self.ip_addresses.setText(', '.join(get_all_ip_addresses())) + self.resize(self.sizeHint()) def use_fixed_port_changed(self, state): diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui index fadcf921b4..a8cd546aff 100644 --- a/src/calibre/gui2/dialogs/smartdevice.ui +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -38,7 +38,34 @@ + + + + Calibre's IP addresses: + + + + + + Possibe IP addresses: + + + true + + + + + + + Optional &password: + + + password_box + + + + @@ -54,24 +81,14 @@ - - - - Optional &password: - - - password_box - - - - + &Show password - + Optional &fixed port: @@ -81,7 +98,7 @@ - + @@ -94,28 +111,28 @@ - + &Use a fixed port - + &Automatically allow connections at calibre startup - + &Enable automatic sending of book metadata when your device connects - + Qt::Horizontal From 4ce9dd36faada0ab78c01438c7c8d2ac113a0276 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 09:45:49 +0200 Subject: [PATCH 03/14] Add reverse find to smartdevice using broadcasts Cache the list of IP addresses --- .../devices/smart_device_app/driver.py | 79 +++++++++++++------ src/calibre/gui2/actions/device.py | 2 +- src/calibre/gui2/dialogs/smartdevice.py | 9 ++- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 63119bf79b..5b067b1819 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -88,6 +88,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): SEND_NOOP_EVERY_NTH_PROBE = 5 DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes + BROADCAST_PORTS = [54982, 48123, 39001, 44044, 59678] opcodes = { 'NOOP' : 12, @@ -525,19 +526,27 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self.device_socket = None self.is_connected = False - def _attach_to_port(self, port): + def _attach_to_port(self, sock, port): try: self._debug('try port', port) - self.listen_socket.bind(('', port)) + sock.bind(('', port)) except socket.error: self._debug('socket error on port', port) port = 0 except: - self._debug('Unknown exception while allocating listen socket') + self._debug('Unknown exception while attaching port to socket') traceback.print_exc() raise return port + def _close_listen_socket(self): + self.listen_socket.close() + self.listen_socket = None + self.is_connected = False + if getattr(self, 'broadcast_socket', None) is not None: + self.broadcast_socket.close() + self.broadcast_socket = None + # The public interface methods. @synchronous('sync_lock') @@ -569,6 +578,18 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): except: self._close_device_socket() return (self.is_connected, self) + if getattr(self, 'broadcast_socket', None) is not None: + ans = select.select((self.broadcast_socket,), (), (), 0) + if len(ans[0]) > 0: + try: + packet = self.broadcast_socket.recvfrom(100) + remote = packet[1] + message = str(socket.gethostname().partition('.')[0] + '|') + str(self.port) + self._debug('received broadcast', packet, message) + self.broadcast_socket.sendto(message, remote) + except: + traceback.print_exc() + if getattr(self, 'listen_socket', None) is not None: ans = select.select((self.listen_socket,), (), (), 0) if len(ans[0]) > 0: @@ -976,31 +997,26 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): message = _('Invalid port in options: %s')% \ self.settings().extra_customization[self.OPT_PORT_NUMBER] self._debug(message) - self.listen_socket.close() - self.listen_socket = None - self.is_connected = False + self._close_listen_socket() return message - port = self._attach_to_port(opt_port) + port = self._attach_to_port(self.listen_socket, opt_port) if port == 0: message = _('Failed to connect to port %d. Try a different value.')%opt_port self._debug(message) - self.listen_socket.close() - self.listen_socket = None - self.is_connected = False + self._close_listen_socket() return message else: while i < 100: # try up to 100 random port numbers i += 1 - port = self._attach_to_port(random.randint(8192, 32000)) + port = self._attach_to_port(self.listen_socket, + random.randint(8192, 32000)) if port != 0: break if port == 0: message = _('Failed to allocate a random port') self._debug(message) - self.listen_socket.close() - self.listen_socket = None - self.is_connected = False + self._close_listen_socket() return message try: @@ -1008,9 +1024,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): except: message = 'listen on port %d failed' % port self._debug(message) - self.listen_socket.close() - self.listen_socket = None - self.is_connected = False + self._close_listen_socket() return message try: @@ -1018,21 +1032,40 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): except: message = 'registration with bonjour failed' self._debug(message) - self.listen_socket.close() - self.listen_socket = None - self.is_connected = False + self._close_listen_socket() return message self._debug('listening on port', port) self.port = port + # Now try to open a UDP socket to receive broadcasts on + + try: + self.broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + except: + message = 'creation of broadcast socket failed. This is not fatal.' + self._debug(message) + return message + + for p in self.BROADCAST_PORTS: + port = self._attach_to_port(self.broadcast_socket, p) + if port != 0: + self._debug('broadcast socket listening on port', port) + break + + if port == 0: + self.broadcast_socket.close() + self.broadcast_socket = None + message = 'attaching port to broadcast socket failed. This is not fatal.' + self._debug(message) + return message + + @synchronous('sync_lock') def shutdown(self): if getattr(self, 'listen_socket', None) is not None: do_zeroconf(unpublish_zeroconf, self.port) - self.listen_socket.close() - self.listen_socket = None - self.is_connected = False + self._close_listen_socket() # Methods for dynamic control diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 92fc34b105..a8475c3a3e 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -255,7 +255,7 @@ class ConnectShareAction(InterfaceAction): use_fixed_port = dm.get_option('smartdevice', 'use_fixed_port') port_number = dm.get_option('smartdevice', 'port_number') if show_port and use_fixed_port: - text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s port %s]'%( + text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s, port %s]'%( formatted_addresses, port_number) else: text = self.share_conn_menu.DEVICE_MSGS[1] + ' [' + formatted_addresses + ']' diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index e3028cb6e1..810ce67e91 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -27,7 +27,7 @@ def _cmp_ipaddr(l, r): return cmp(lparts, rparts) -def get_all_ip_addresses(): +def _get_all_ip_addresses(): ip_info = [netifaces.ifaddresses(x).get(netifaces.AF_INET, None) for x in netifaces.interfaces()] @@ -42,9 +42,14 @@ def get_all_ip_addresses(): all_ipaddrs.append(addrs['addr']) all_ipaddrs.sort(cmp=_cmp_ipaddr) - print(all_ipaddrs) return all_ipaddrs +_all_ip_addresses = [] +def get_all_ip_addresses(): + global _all_ip_addresses + if not _all_ip_addresses: + _all_ip_addresses = _get_all_ip_addresses() + return _all_ip_addresses class SmartdeviceDialog(QDialog, Ui_Dialog): From 15749dc4d78fc330919c1417c4621a321186df5c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 10:22:43 +0200 Subject: [PATCH 04/14] Respond to all broadcasts at once, not just to one. --- .../devices/smart_device_app/driver.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 5b067b1819..b321182916 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -579,16 +579,20 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self._close_device_socket() return (self.is_connected, self) if getattr(self, 'broadcast_socket', None) is not None: - ans = select.select((self.broadcast_socket,), (), (), 0) - if len(ans[0]) > 0: - try: - packet = self.broadcast_socket.recvfrom(100) - remote = packet[1] - message = str(socket.gethostname().partition('.')[0] + '|') + str(self.port) - self._debug('received broadcast', packet, message) - self.broadcast_socket.sendto(message, remote) - except: - traceback.print_exc() + while True: + ans = select.select((self.broadcast_socket,), (), (), 0) + if len(ans[0]) > 0: + try: + packet = self.broadcast_socket.recvfrom(100) + remote = packet[1] + message = str(socket.gethostname().partition('.')[0] + + '|') + str(self.port) + self._debug('received broadcast', packet, message) + self.broadcast_socket.sendto(message, remote) + except: + pass + else: + break if getattr(self, 'listen_socket', None) is not None: ans = select.select((self.listen_socket,), (), (), 0) From ff4da40412d96bf8b3038f6d978b7157b57ad76b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 10:46:43 +0200 Subject: [PATCH 05/14] Use comma instead of | to avoid java split problems. Ensure that the sent string is a byte string. --- src/calibre/devices/smart_device_app/driver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index b321182916..6f76217b2b 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -585,8 +585,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): try: packet = self.broadcast_socket.recvfrom(100) remote = packet[1] - message = str(socket.gethostname().partition('.')[0] - + '|') + str(self.port) + message = str(b'calibre smart device client on ' + + str(socket.gethostname().partition('.')[0]) + + b',' + str(self.port)) self._debug('received broadcast', packet, message) self.broadcast_socket.sendto(message, remote) except: From 0c55e125702f17dcf7c8a364f18a4b236d843653 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 11:02:27 +0200 Subject: [PATCH 06/14] Make broadcast connection strings look like mdns services --- src/calibre/devices/smart_device_app/driver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 6f76217b2b..8b4ebb1881 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -88,6 +88,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): SEND_NOOP_EVERY_NTH_PROBE = 5 DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes + ZEROCONF_CLIENT_STRING = b'calibre smart device client' BROADCAST_PORTS = [54982, 48123, 39001, 44044, 59678] opcodes = { @@ -585,9 +586,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): try: packet = self.broadcast_socket.recvfrom(100) remote = packet[1] - message = str(b'calibre smart device client on ' + + message = str(self.ZEROCONF_CLIENT_STRING + b' (on ' + str(socket.gethostname().partition('.')[0]) + - b',' + str(self.port)) + b'),' + str(self.port)) self._debug('received broadcast', packet, message) self.broadcast_socket.sendto(message, remote) except: From cf6a1711f4bae6712074af06be9401c37ed883c0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 12:39:21 +0200 Subject: [PATCH 07/14] Add some explanatory comments to the list of ports --- src/calibre/devices/smart_device_app/driver.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 8b4ebb1881..bbbf5d9e0e 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -89,6 +89,14 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes ZEROCONF_CLIENT_STRING = b'calibre smart device client' + + # A few "random" port numbers to use for detecting clients using broadcast + # The clients are expected to broadcast a UDP 'hi there' on all of these + # ports when they attempt to connect. Calibre will respond with the port + # number the client should use. This scheme backs up mdns. And yes, we + # must hope that no other application on the machine is using one of these + # ports in datagram mode. + # If you change the ports here, all clients will also need to change. BROADCAST_PORTS = [54982, 48123, 39001, 44044, 59678] opcodes = { From 42213fc01e785e3fe5d0bb91adea8c8bfe86c6c7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 15:57:30 +0200 Subject: [PATCH 08/14] 1) Re-enable subdirs in lpaths 2) Make backloading into library work --- .../devices/smart_device_app/driver.py | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index bbbf5d9e0e..16f0bab24e 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -8,6 +8,7 @@ Created on 29 Jun 2012 @author: charles ''' import socket, select, json, inspect, os, traceback, time, sys, random +import posixpath import hashlib, threading from base64 import b64encode, b64decode from functools import wraps @@ -26,6 +27,7 @@ from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.json_codec import JsonCodec from calibre.library import current_library_name +from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.ipc import eintr_retry_call from calibre.utils.config import from_json, tweaks from calibre.utils.date import isoformat, now @@ -70,14 +72,15 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): DEVICE_PLUGBOARD_NAME = 'SMART_DEVICE_APP' CAN_SET_METADATA = [] CAN_DO_DEVICE_DB_PLUGBOARD = False - SUPPORTS_SUB_DIRS = False + SUPPORTS_SUB_DIRS = True MUST_READ_METADATA = True NEWS_IN_FOLDER = False SUPPORTS_USE_AUTHOR_SORT = False WANTS_UPDATED_THUMBNAILS = True - MAX_PATH_LEN = 100 + MAX_PATH_LEN = 250 THUMBNAIL_HEIGHT = 160 PREFIX = '' + BACKLOADING_ERROR_MESSAGE = None # Some network protocol constants BASE_PACKET_LEN = 4096 @@ -206,25 +209,6 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): print() self.debug_time = time.time() - # Various methods required by the plugin architecture - @classmethod - def _default_save_template(cls): - from calibre.library.save_to_disk import config - st = cls.SAVE_TEMPLATE if cls.SAVE_TEMPLATE else \ - config().parse().send_template - if st: - st = os.path.basename(st) - return st - - @classmethod - def save_template(cls): - st = cls.settings().save_template - if st: - st = os.path.basename(st) - else: - st = cls._default_save_template() - return st - # local utilities # copied from USBMS. Perhaps this could be a classmethod in usbms? @@ -286,6 +270,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): extra_components.append(sanitize(fname)) else: extra_components[-1] = sanitize(extra_components[-1]+ext) + self._debug('1', extra_components) if extra_components[-1] and extra_components[-1][0] in ('.', '_'): extra_components[-1] = 'x' + extra_components[-1][1:] @@ -318,7 +303,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): extra_components = list(map(remove_trailing_periods, extra_components)) components = shorten_components_to(maxlen, extra_components) - filepath = os.path.join(*components) + filepath = posixpath.join(*components) return filepath def _strip_prefix(self, path): @@ -964,6 +949,15 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): else: raise ControlError(desc='request for book data failed') + @synchronous('sync_lock') + def prepare_addable_books(self, paths): + for idx, path in enumerate(paths): + (ign, ext) = os.path.splitext(path) + tf = PersistentTemporaryFile(suffix=ext) + self.get_file(path, tf) + paths[idx] = tf.name + return paths + @synchronous('sync_lock') def set_plugboards(self, plugboards, pb_func): self._debug() From 6a96eb62f0149ee727a75b4d85351f4d8c1124e7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 15:57:50 +0200 Subject: [PATCH 09/14] Change the automatic management checkbox into a pushbutton --- src/calibre/gui2/dialogs/smartdevice.py | 35 ++++++++++++++----------- src/calibre/gui2/dialogs/smartdevice.ui | 7 ----- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index 810ce67e91..a30ec888b1 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -5,9 +5,9 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import netifaces, socket +import netifaces -from PyQt4.Qt import (QDialog, QLineEdit, Qt) +from PyQt4.Qt import (QDialog, QLineEdit, Qt, QPushButton, QDialogButtonBox) from calibre.gui2 import error_dialog from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog @@ -81,13 +81,6 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): 'to the port, try another number. You can use any number between ' '8,000 and 32,000.') + '

') - self.enable_auto_management_box.setToolTip('

' + - _('If this box is checked, calibre will send any changes you made ' - "to book's metadata when your device is connected. If it is not " - 'checked, changes are sent only when you send the book. You can ' - 'get more information or change the preference to some other ' - 'choice at Preferences -> Send to device -> Metadata management') - + '

') self.ip_addresses.setToolTip('

' + _('These are the IP addresses detected by calibre for the computer ' @@ -122,16 +115,29 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): if pw: self.password_box.setText(pw) - self.auto_management_is_set = False + auto_mgmt_button = QPushButton(_('Enable automatic metadata management')) + auto_mgmt_button.clicked.connect(self.auto_mgmt_button_clicked) + auto_mgmt_button.setToolTip('

' + + _('Enabling automatic metadata management tells calibre to send any ' + 'changes you made to books\' metadata when your device is ' + 'connected. If it is not enabled, changes are sent only when ' + 'you send a book. You can get more information or change this ' + 'preference to some other choice at Preferences -> ' + 'Send to device -> Metadata management') + + '

') + self.buttonBox.addButton(auto_mgmt_button, QDialogButtonBox.ActionRole) + self.set_auto_management = False if prefs['manage_device_metadata'] == 'on_connect': - self.enable_auto_management_box.setChecked(True) - self.enable_auto_management_box.setEnabled(False) - self.auto_management_is_set = True + auto_mgmt_button.setText(_('Automatic metadata management is enabled')) + auto_mgmt_button.setEnabled(False) self.ip_addresses.setText(', '.join(get_all_ip_addresses())) self.resize(self.sizeHint()) + def auto_mgmt_button_clicked(self): + self.set_auto_management = True + def use_fixed_port_changed(self, state): self.fixed_port.setEnabled(state == Qt.Checked) @@ -177,8 +183,7 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): self.device_manager.set_option('smartdevice', 'port_number', self.orig_port_number) else: - if not self.auto_management_is_set and \ - self.enable_auto_management_box.isChecked(): + if self.set_auto_management: prefs.set('manage_device_metadata', 'on_connect') QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui index a8cd546aff..f8e42d5d0e 100644 --- a/src/calibre/gui2/dialogs/smartdevice.ui +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -125,13 +125,6 @@
- - - - &Enable automatic sending of book metadata when your device connects - - - From a003802c9a8b5c5a5b3482564d7fc857500cd689 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 16:03:11 +0200 Subject: [PATCH 10/14] Change tooltip --- src/calibre/gui2/dialogs/smartdevice.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index a30ec888b1..d993ce52b9 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -120,10 +120,11 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): auto_mgmt_button.setToolTip('

' + _('Enabling automatic metadata management tells calibre to send any ' 'changes you made to books\' metadata when your device is ' - 'connected. If it is not enabled, changes are sent only when ' - 'you send a book. You can get more information or change this ' - 'preference to some other choice at Preferences -> ' - 'Send to device -> Metadata management') + 'connected, which is the most useful setting when using the wireless ' + 'device interface. If automatic metadata management is not ' + 'enabled, changes are sent only when you send a book. You can ' + 'get more information or change this preference to some other ' + 'choice at Preferences -> Send to device -> Metadata management') + '

') self.buttonBox.addButton(auto_mgmt_button, QDialogButtonBox.ActionRole) self.set_auto_management = False From a7863ba70bec56ca6ec9a1cf28a34047afeaf450 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 28 Aug 2012 18:03:02 +0200 Subject: [PATCH 11/14] Fix path attribute to have forward slashes. --- src/calibre/devices/smart_device_app/driver.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 16f0bab24e..ae765b4fd4 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -51,6 +51,12 @@ def do_zeroconf(f, port): '_calibresmartdeviceapp._tcp', port, {}) +class SDBook(Book): + def __init__(self, prefix, lpath, size=None, other=None): + Book.__init__(self, prefix, lpath, size=size, other=other) + path = getattr(self, 'path', lpath) + self.path = path.replace('\\', '/') + class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): name = 'SmartDevice App Interface' gui_name = _('SmartDevice') @@ -795,7 +801,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): if opcode == 'OK': if '_series_sort_' in result: del result['_series_sort_'] - book = self.json_codec.raw_to_book(result, Book, self.PREFIX) + book = self.json_codec.raw_to_book(result, SDBook, self.PREFIX) self._set_known_metadata(book) bl.add_book(book, replace_metadata=True) else: @@ -867,7 +873,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): lpath = self._create_upload_path(mdata, fname, create_dirs=False) if not hasattr(infile, 'read'): infile = USBMS.normalize_path(infile) - book = Book(self.PREFIX, lpath, other=mdata) + book = SDBook(self.PREFIX, lpath, other=mdata) length = self._put_file(infile, lpath, book, i, len(files)) if length < 0: raise ControlError(desc='Sending book %s to device failed' % lpath) @@ -892,7 +898,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): lpath = location[0] length = location[1] lpath = self._strip_prefix(lpath) - book = Book(self.PREFIX, lpath, other=info) + book = SDBook(self.PREFIX, lpath, other=info) if book.size is None: book.size = length b = booklists[0].add_book(book, replace_metadata=True) From 7bf5361d58810788f8f59fe7758c445d8dced0ee Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 29 Aug 2012 11:59:41 +0200 Subject: [PATCH 12/14] Send date formats to device --- src/calibre/devices/smart_device_app/driver.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index ae765b4fd4..361fbb98a1 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -651,11 +651,14 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): challenge = '' hash_digest = '' opcode, result = self._call_client('GET_INITIALIZATION_INFO', - {'serverProtocolVersion': self.PROTOCOL_VERSION, - 'validExtensions': self.ALL_FORMATS, - 'passwordChallenge': challenge, - 'currentLibraryName': self.current_library_name, - 'currentLibraryUUID': library_uuid}) + {'serverProtocolVersion': self.PROTOCOL_VERSION, + 'validExtensions': self.ALL_FORMATS, + 'passwordChallenge': challenge, + 'currentLibraryName': self.current_library_name, + 'currentLibraryUUID': library_uuid, + 'pubdateFormat': tweaks['gui_pubdate_display_format'], + 'timestampFormat': tweaks['gui_timestamp_display_format'], + 'lastModifiedFormat': tweaks['gui_last_modified_display_format']}) if opcode != 'OK': # Something wrong with the return. Close the socket # and continue. From 9657de451bb3b5a63e5b534c70510436c87f29c1 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 29 Aug 2012 12:47:14 +0200 Subject: [PATCH 13/14] Improve wording and ergonomics of smartdevice menu --- src/calibre/gui2/dialogs/smartdevice.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index d993ce52b9..4428da473e 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -115,29 +115,31 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): if pw: self.password_box.setText(pw) - auto_mgmt_button = QPushButton(_('Enable automatic metadata management')) - auto_mgmt_button.clicked.connect(self.auto_mgmt_button_clicked) - auto_mgmt_button.setToolTip('

' + + self.auto_mgmt_button = QPushButton(_('Enable automatic metadata management')) + self.auto_mgmt_button.clicked.connect(self.auto_mgmt_button_clicked) + self.auto_mgmt_button.setToolTip('

' + _('Enabling automatic metadata management tells calibre to send any ' 'changes you made to books\' metadata when your device is ' 'connected, which is the most useful setting when using the wireless ' 'device interface. If automatic metadata management is not ' - 'enabled, changes are sent only when you send a book. You can ' + 'enabled, changes are sent only when you re-send the book. You can ' 'get more information or change this preference to some other ' - 'choice at Preferences -> Send to device -> Metadata management') + 'choice at Preferences -> Sending books to devices -> ' + 'Metadata management') + '

') - self.buttonBox.addButton(auto_mgmt_button, QDialogButtonBox.ActionRole) - self.set_auto_management = False + self.buttonBox.addButton(self.auto_mgmt_button, QDialogButtonBox.ActionRole) if prefs['manage_device_metadata'] == 'on_connect': - auto_mgmt_button.setText(_('Automatic metadata management is enabled')) - auto_mgmt_button.setEnabled(False) + self.auto_mgmt_button.setText(_('Automatic metadata management is enabled')) + self.auto_mgmt_button.setEnabled(False) self.ip_addresses.setText(', '.join(get_all_ip_addresses())) self.resize(self.sizeHint()) def auto_mgmt_button_clicked(self): - self.set_auto_management = True + self.auto_mgmt_button.setText(_('Automatic metadata management is enabled')) + self.auto_mgmt_button.setEnabled(False) + prefs.set('manage_device_metadata', 'on_connect') def use_fixed_port_changed(self, state): self.fixed_port.setEnabled(state == Qt.Checked) @@ -184,7 +186,5 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): self.device_manager.set_option('smartdevice', 'port_number', self.orig_port_number) else: - if self.set_auto_management: - prefs.set('manage_device_metadata', 'on_connect') QDialog.accept(self) From e4b09eabfff10b117539fe18c37f5fba66e6d614 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 29 Aug 2012 14:48:37 +0200 Subject: [PATCH 14/14] Use "standard" get_all_ips --- src/calibre/gui2/dialogs/smartdevice.py | 30 +++++++------------------ 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index 4428da473e..4e10f77cc9 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -12,6 +12,7 @@ from PyQt4.Qt import (QDialog, QLineEdit, Qt, QPushButton, QDialogButtonBox) from calibre.gui2 import error_dialog from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog from calibre.utils.config import prefs +from calibre.utils.mdns import get_all_ips def _cmp_ipaddr(l, r): lparts = ['%3s'%x for x in l.split('.')] @@ -27,29 +28,14 @@ def _cmp_ipaddr(l, r): return cmp(lparts, rparts) -def _get_all_ip_addresses(): - ip_info = [netifaces.ifaddresses(x).get(netifaces.AF_INET, None) - for x in netifaces.interfaces()] - - all_ipaddrs = list() - for iface in ip_info: - if iface is not None: - for addrs in iface: - if 'netmask' in addrs and addrs['addr'] != '127.0.0.1': - # We get VPN interfaces that were connected and then - # disconnected. Oh well. At least the 'right' IP addr - # is there. - all_ipaddrs.append(addrs['addr']) - - all_ipaddrs.sort(cmp=_cmp_ipaddr) - return all_ipaddrs - -_all_ip_addresses = [] def get_all_ip_addresses(): - global _all_ip_addresses - if not _all_ip_addresses: - _all_ip_addresses = _get_all_ip_addresses() - return _all_ip_addresses + ipaddrs = list() + for iface in get_all_ips().itervalues(): + for addrs in iface: + if 'broadcast' in addrs and addrs['addr'] != '127.0.0.1': + ipaddrs.append(addrs['addr']) + ipaddrs.sort(cmp=_cmp_ipaddr) + return ipaddrs class SmartdeviceDialog(QDialog, Ui_Dialog):