From 5a854040e7721b405c82c517c2ee11748666ee03 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 23 Jul 2012 18:55:02 +0200
Subject: [PATCH] Base changes required to add the smart device driver
---
src/calibre/devices/interface.py | 18 +++++++++
.../ebooks/metadata/book/json_codec.py | 36 +++++++++++-------
src/calibre/gui2/device.py | 38 ++++++++++++++++---
.../gui2/device_drivers/configwidget.py | 3 ++
.../gui2/device_drivers/configwidget.ui | 13 +++++++
src/calibre/library/server/base.py | 7 +++-
src/calibre/utils/Zeroconf.py | 5 +++
src/calibre/utils/mdns.py | 27 +++++++++++++
8 files changed, 126 insertions(+), 21 deletions(-)
diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py
index 9510dcf3d1..26239b59e7 100644
--- a/src/calibre/devices/interface.py
+++ b/src/calibre/devices/interface.py
@@ -15,6 +15,8 @@ class DevicePlugin(Plugin):
#: Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"]
+ # If True, the config dialog will not show the formats box
+ HIDE_FORMATS_CONFIG_BOX = False
#: VENDOR_ID can be either an integer, a list of integers or a dictionary
#: If it is a dictionary, it must be a dictionary of dictionaries,
@@ -496,6 +498,22 @@ class DevicePlugin(Plugin):
'''
return paths
+ def startup(self):
+ '''
+ Called when calibre is is starting the device. Do any initialization
+ required. Note that multiple instances of the class can be instantiated,
+ and thus __init__ can be called multiple times, but only one instance
+ will have this method called.
+ '''
+ pass
+
+ def shutdown(self):
+ '''
+ Called when calibre is shutting down, either for good or in preparation
+ to restart. Do any cleanup required.
+ '''
+ pass
+
class BookList(list):
'''
A list of books. Each Book object must have the fields
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index 3b52821c1b..8e2dc64383 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -117,8 +117,8 @@ class JsonCodec(object):
def __init__(self):
self.field_metadata = FieldMetadata()
- def encode_to_file(self, file, booklist):
- file.write(json.dumps(self.encode_booklist_metadata(booklist),
+ def encode_to_file(self, file_, booklist):
+ file_.write(json.dumps(self.encode_booklist_metadata(booklist),
indent=2, encoding='utf-8'))
def encode_booklist_metadata(self, booklist):
@@ -156,21 +156,29 @@ class JsonCodec(object):
else:
return object_to_unicode(value)
- def decode_from_file(self, file, booklist, book_class, prefix):
+ def decode_from_file(self, file_, booklist, book_class, prefix):
js = []
try:
- js = json.load(file, encoding='utf-8')
+ js = json.load(file_, encoding='utf-8')
+ self.raw_to_booklist(js, booklist, book_class, prefix)
for item in js:
- book = book_class(prefix, item.get('lpath', None))
- for key in item.keys():
- meta = self.decode_metadata(key, item[key])
- if key == 'user_metadata':
- book.set_all_user_metadata(meta)
- else:
- if key == 'classifiers':
- key = 'identifiers'
- setattr(book, key, meta)
- booklist.append(book)
+ booklist.append(self.raw_to_book(item, book_class, prefix))
+ except:
+ print 'exception during JSON decode_from_file'
+ traceback.print_exc()
+
+ def raw_to_book(self, json_book, book_class, prefix):
+ try:
+ book = book_class(prefix, json_book.get('lpath', None))
+ for key,val in json_book.iteritems():
+ meta = self.decode_metadata(key, val)
+ if key == 'user_metadata':
+ book.set_all_user_metadata(meta)
+ else:
+ if key == 'classifiers':
+ key = 'identifiers'
+ setattr(book, key, meta)
+ return book
except:
print 'exception during JSON decoding'
traceback.print_exc()
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index 1dcadf7b65..da2c45fd9c 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -144,6 +144,7 @@ class DeviceManager(Thread): # {{{
self.open_feedback_msg = open_feedback_msg
self._device_information = None
self.current_library_uuid = None
+ self.call_shutdown_on_disconnect = False
def report_progress(self, *args):
pass
@@ -197,6 +198,13 @@ class DeviceManager(Thread): # {{{
self.ejected_devices.remove(self.connected_device)
else:
self.connected_slot(False, self.connected_device_kind)
+ if self.call_shutdown_on_disconnect:
+ # The current device is an instance of a plugin class instantiated
+ # to handle this connection, probably as a mounted device. We are
+ # now abandoning the instance that we created, so we tell it that it
+ # is being shut down.
+ self.connected_device.shutdown()
+ self.call_shutdown_on_disconnect = False
self.connected_device = None
self._device_information = None
@@ -265,7 +273,20 @@ class DeviceManager(Thread): # {{{
except Queue.Empty:
pass
+ def run_startup(self, dev):
+ name = 'unknown'
+ try:
+ name = dev.__class__.__name__
+ dev.startup()
+ except:
+ prints('Startup method for device %s threw exception'%name)
+ traceback.print_exc()
+
def run(self):
+ # Do any device-specific startup processing.
+ for d in self.devices:
+ self.run_startup(d)
+
while self.keep_going:
kls = None
while True:
@@ -277,6 +298,11 @@ class DeviceManager(Thread): # {{{
if kls is not None:
try:
dev = kls(folder_path)
+ # We just created a new device instance. Call its startup
+ # method and set the flag to call the shutdown method when
+ # it disconnects.
+ self.run_startup(dev)
+ self.call_shutdown_on_disconnect = True
self.do_connect([[dev, None],], device_kind=device_kind)
except:
prints('Unable to open %s as device (%s)'%(device_kind, folder_path))
@@ -295,6 +321,13 @@ class DeviceManager(Thread): # {{{
break
time.sleep(self.sleep_time)
+ # We are exiting. Call the shutdown method for each plugin
+ for p in self.devices:
+ try:
+ p.shutdown()
+ except:
+ pass
+
def create_job_step(self, func, done, description, to_job, args=[], kwargs={}):
job = DeviceJob(func, done, self.job_manager,
args=args, kwargs=kwargs, description=description)
@@ -934,11 +967,6 @@ class DeviceMixin(object): # {{{
fmt = None
if specific:
- if (not self.device_connected or not self.device_manager or
- self.device_manager.device is None):
- error_dialog(self, _('No device'),
- _('No device connected'), show=True)
- return
formats = []
aval_out_formats = available_output_formats()
format_count = {}
diff --git a/src/calibre/gui2/device_drivers/configwidget.py b/src/calibre/gui2/device_drivers/configwidget.py
index 94843f90e3..b47a80b6ad 100644
--- a/src/calibre/gui2/device_drivers/configwidget.py
+++ b/src/calibre/gui2/device_drivers/configwidget.py
@@ -43,6 +43,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
+ if device.HIDE_FORMATS_CONFIG_BOX:
+ self.groupBox.hide()
+
if supports_subdirs:
self.opt_use_subdirs.setChecked(self.settings.use_subdirs)
else:
diff --git a/src/calibre/gui2/device_drivers/configwidget.ui b/src/calibre/gui2/device_drivers/configwidget.ui
index 92324fd1a7..d8c3c44e22 100644
--- a/src/calibre/gui2/device_drivers/configwidget.ui
+++ b/src/calibre/gui2/device_drivers/configwidget.ui
@@ -103,6 +103,19 @@
-
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
-
diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py
index 0b5fead634..a61d2dcc3e 100644
--- a/src/calibre/library/server/base.py
+++ b/src/calibre/library/server/base.py
@@ -17,7 +17,7 @@ from calibre.utils.date import fromtimestamp
from calibre.library.server import listen_on, log_access_file, log_error_file
from calibre.library.server.utils import expose, AuthController
from calibre.utils.mdns import publish as publish_zeroconf, \
- stop_server as stop_zeroconf, get_external_ip
+ unpublish as unpublish_zeroconf, get_external_ip
from calibre.library.server.content import ContentServer
from calibre.library.server.mobile import MobileServer
from calibre.library.server.xml import XMLServer
@@ -94,7 +94,10 @@ class BonJour(SimplePlugin): # {{{
def stop(self):
try:
- stop_zeroconf()
+ unpublish_zeroconf('Books in calibre', '_stanza._tcp',
+ self.port, {'path':self.prefix+'/stanza'})
+ unpublish_zeroconf('Books in calibre', '_calibre._tcp',
+ self.port, {'path':self.prefix+'/opds'})
except:
import traceback
cherrypy.log.error('Failed to stop BonJour:')
diff --git a/src/calibre/utils/Zeroconf.py b/src/calibre/utils/Zeroconf.py
index 0e55e8f516..b722865101 100755
--- a/src/calibre/utils/Zeroconf.py
+++ b/src/calibre/utils/Zeroconf.py
@@ -871,6 +871,8 @@ class Engine(threading.Thread):
from calibre.constants import DEBUG
try:
rr, wr, er = select.select(rs, [], [], self.timeout)
+ if globals()['_GLOBAL_DONE']:
+ continue
for socket in rr:
try:
self.readers[socket].handle_read()
@@ -1419,6 +1421,9 @@ class Zeroconf(object):
i += 1
nextTime += _UNREGISTER_TIME
+ def countRegisteredServices(self):
+ return len(self.services)
+
def checkService(self, info):
"""Checks the network for a unique service name, modifying the
ServiceInfo passed in if it is not unique."""
diff --git a/src/calibre/utils/mdns.py b/src/calibre/utils/mdns.py
index 2be6bef49b..42e846577e 100644
--- a/src/calibre/utils/mdns.py
+++ b/src/calibre/utils/mdns.py
@@ -76,6 +76,33 @@ def publish(desc, type, port, properties=None, add_hostname=True):
server=hostname+'.local.')
server.registerService(service)
+def unpublish(desc, type, port, properties=None, add_hostname=True):
+ '''
+ Unpublish a service.
+
+ The parameters must be the same as used in the corresponding call to publish
+ '''
+ port = int(port)
+ server = start_server()
+ try:
+ hostname = socket.gethostname().partition('.')[0]
+ except:
+ hostname = 'Unknown'
+
+ if add_hostname:
+ desc += ' (on %s)'%hostname
+ local_ip = get_external_ip()
+ type = type+'.local.'
+ from calibre.utils.Zeroconf import ServiceInfo
+ service = ServiceInfo(type, desc+'.'+type,
+ address=socket.inet_aton(local_ip),
+ port=port,
+ properties=properties,
+ server=hostname+'.local.')
+ server.unregisterService(service)
+ if server.countRegisteredServices() == 0:
+ stop_server()
+
def stop_server():
global _server
if _server is not None: