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):