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: