1__license__ = 'GPL v3' 2__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' 3import os 4from collections import namedtuple 5 6from calibre import prints 7from calibre.constants import iswindows 8from calibre.customize import Plugin 9 10 11class DevicePlugin(Plugin): 12 """ 13 Defines the interface that should be implemented by backends that 14 communicate with an e-book reader. 15 """ 16 type = _('Device interface') 17 18 #: Ordered list of supported formats 19 FORMATS = ["lrf", "rtf", "pdf", "txt"] 20 # If True, the config dialog will not show the formats box 21 HIDE_FORMATS_CONFIG_BOX = False 22 23 #: VENDOR_ID can be either an integer, a list of integers or a dictionary 24 #: If it is a dictionary, it must be a dictionary of dictionaries, 25 #: of the form:: 26 #: 27 #: { 28 #: integer_vendor_id : { product_id : [list of BCDs], ... }, 29 #: ... 30 #: } 31 #: 32 VENDOR_ID = 0x0000 33 34 #: An integer or a list of integers 35 PRODUCT_ID = 0x0000 36 #: BCD can be either None to not distinguish between devices based on BCD, or 37 #: it can be a list of the BCD numbers of all devices supported by this driver. 38 BCD = None 39 40 #: Height for thumbnails on the device 41 THUMBNAIL_HEIGHT = 68 42 43 #: Compression quality for thumbnails. Set this closer to 100 to have better 44 #: quality thumbnails with fewer compression artifacts. Of course, the 45 #: thumbnails get larger as well. 46 THUMBNAIL_COMPRESSION_QUALITY = 75 47 48 #: Set this to True if the device supports updating cover thumbnails during 49 #: sync_booklists. Setting it to true will ask device.py to refresh the 50 #: cover thumbnails during book matching 51 WANTS_UPDATED_THUMBNAILS = False 52 53 #: Whether the metadata on books can be set via the GUI. 54 CAN_SET_METADATA = ['title', 'authors', 'collections'] 55 56 #: Whether the device can handle device_db metadata plugboards 57 CAN_DO_DEVICE_DB_PLUGBOARD = False 58 59 # Set this to None if the books on the device are files that the GUI can 60 # access in order to add the books from the device to the library 61 BACKLOADING_ERROR_MESSAGE = _('Cannot get files from this device') 62 63 #: Path separator for paths to books on device 64 path_sep = os.sep 65 66 #: Icon for this device 67 icon = I('reader.png') 68 69 # Encapsulates an annotation fetched from the device 70 UserAnnotation = namedtuple('Annotation','type, value') 71 72 #: GUI displays this as a message if not None. Useful if opening can take a 73 #: long time 74 OPEN_FEEDBACK_MESSAGE = None 75 76 #: Set of extensions that are "virtual books" on the device 77 #: and therefore cannot be viewed/saved/added to library. 78 #: For example: ``frozenset(['kobo'])`` 79 VIRTUAL_BOOK_EXTENSIONS = frozenset() 80 81 #: Message to display to user for virtual book extensions. 82 VIRTUAL_BOOK_EXTENSION_MESSAGE = None 83 84 #: Whether to nuke comments in the copy of the book sent to the device. If 85 #: not None this should be short string that the comments will be replaced 86 #: by. 87 NUKE_COMMENTS = None 88 89 #: If True indicates that this driver completely manages device detection, 90 #: ejecting and so forth. If you set this to True, you *must* implement the 91 #: detect_managed_devices and debug_managed_device_detection methods. 92 #: A driver with this set to true is responsible for detection of devices, 93 #: managing a blacklist of devices, a list of ejected devices and so forth. 94 #: calibre will periodically call the detect_managed_devices() method and 95 #: if it returns a detected device, calibre will call open(). open() will 96 #: be called every time a device is returned even if previous calls to open() 97 #: failed, therefore the driver must maintain its own blacklist of failed 98 #: devices. Similarly, when ejecting, calibre will call eject() and then 99 #: assuming the next call to detect_managed_devices() returns None, it will 100 #: call post_yank_cleanup(). 101 MANAGES_DEVICE_PRESENCE = False 102 103 #: If set the True, calibre will call the :meth:`get_driveinfo()` method 104 #: after the books lists have been loaded to get the driveinfo. 105 SLOW_DRIVEINFO = False 106 107 #: If set to True, calibre will ask the user if they want to manage the 108 #: device with calibre, the first time it is detected. If you set this to 109 #: True you must implement :meth:`get_device_uid()` and 110 #: :meth:`ignore_connected_device()` and 111 #: :meth:`get_user_blacklisted_devices` and 112 #: :meth:`set_user_blacklisted_devices` 113 ASK_TO_ALLOW_CONNECT = False 114 115 #: Set this to a dictionary of the form {'title':title, 'msg':msg, 'det_msg':detailed_msg} to have calibre popup 116 #: a message to the user after some callbacks are run (currently only upload_books). 117 #: Be careful to not spam the user with too many messages. This variable is checked after *every* callback, 118 #: so only set it when you really need to. 119 user_feedback_after_callback = None 120 121 @classmethod 122 def get_gui_name(cls): 123 if hasattr(cls, 'gui_name'): 124 return cls.gui_name 125 if hasattr(cls, '__name__'): 126 return cls.__name__ 127 return cls.name 128 129 # Device detection {{{ 130 def test_bcd(self, bcdDevice, bcd): 131 if bcd is None or len(bcd) == 0: 132 return True 133 for c in bcd: 134 if c == bcdDevice: 135 return True 136 return False 137 138 def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): 139 ''' 140 Return True, device_info if a device handled by this plugin is currently connected. 141 142 :param devices_on_system: List of devices currently connected 143 144 ''' 145 vendors_on_system = {x[0] for x in devices_on_system} 146 vendors = set(self.VENDOR_ID) if hasattr(self.VENDOR_ID, '__len__') else {self.VENDOR_ID} 147 if hasattr(self.VENDOR_ID, 'keys'): 148 products = [] 149 for ven in self.VENDOR_ID: 150 products.extend(self.VENDOR_ID[ven].keys()) 151 else: 152 products = self.PRODUCT_ID if hasattr(self.PRODUCT_ID, '__len__') else [self.PRODUCT_ID] 153 154 ch = self.can_handle_windows if iswindows else self.can_handle 155 for vid in vendors_on_system.intersection(vendors): 156 for dev in devices_on_system: 157 cvid, pid, bcd = dev[:3] 158 if cvid == vid: 159 if pid in products: 160 if hasattr(self.VENDOR_ID, 'keys'): 161 try: 162 cbcd = self.VENDOR_ID[vid][pid] 163 except KeyError: 164 # Vendor vid does not have product pid, pid 165 # exists for some other vendor in this 166 # device 167 continue 168 else: 169 cbcd = self.BCD 170 if self.test_bcd(bcd, cbcd): 171 if debug: 172 prints(dev) 173 if ch(dev, debug=debug): 174 return True, dev 175 return False, None 176 177 def detect_managed_devices(self, devices_on_system, force_refresh=False): 178 ''' 179 Called only if MANAGES_DEVICE_PRESENCE is True. 180 181 Scan for devices that this driver can handle. Should return a device 182 object if a device is found. This object will be passed to the open() 183 method as the connected_device. If no device is found, return None. The 184 returned object can be anything, calibre does not use it, it is only 185 passed to open(). 186 187 This method is called periodically by the GUI, so make sure it is not 188 too resource intensive. Use a cache to avoid repeatedly scanning the 189 system. 190 191 :param devices_on_system: Set of USB devices found on the system. 192 193 :param force_refresh: If True and the driver uses a cache to prevent 194 repeated scanning, the cache must be flushed. 195 196 ''' 197 raise NotImplementedError() 198 199 def debug_managed_device_detection(self, devices_on_system, output): 200 ''' 201 Called only if MANAGES_DEVICE_PRESENCE is True. 202 203 Should write information about the devices detected on the system to 204 output, which is a file like object. 205 206 Should return True if a device was detected and successfully opened, 207 otherwise False. 208 ''' 209 raise NotImplementedError() 210 211 # }}} 212 213 def reset(self, key='-1', log_packets=False, report_progress=None, 214 detected_device=None): 215 """ 216 :param key: The key to unlock the device 217 :param log_packets: If true the packet stream to/from the device is logged 218 :param report_progress: Function that is called with a % progress 219 (number between 0 and 100) for various tasks. 220 If it is called with -1 that means that the 221 task does not have any progress information 222 :param detected_device: Device information from the device scanner 223 224 """ 225 raise NotImplementedError() 226 227 def can_handle_windows(self, usbdevice, debug=False): 228 ''' 229 Optional method to perform further checks on a device to see if this driver 230 is capable of handling it. If it is not it should return False. This method 231 is only called after the vendor, product ids and the bcd have matched, so 232 it can do some relatively time intensive checks. The default implementation 233 returns True. This method is called only on Windows. See also 234 :meth:`can_handle`. 235 236 Note that for devices based on USBMS this method by default delegates 237 to :meth:`can_handle`. So you only need to override :meth:`can_handle` 238 in your subclass of USBMS. 239 240 :param usbdevice: A usbdevice as returned by :func:`calibre.devices.winusb.scan_usb_devices` 241 ''' 242 return True 243 244 def can_handle(self, device_info, debug=False): 245 ''' 246 Unix version of :meth:`can_handle_windows`. 247 248 :param device_info: Is a tuple of (vid, pid, bcd, manufacturer, product, 249 serial number) 250 251 ''' 252 253 return True 254 can_handle.is_base_class_implementation = True 255 256 def open(self, connected_device, library_uuid): 257 ''' 258 Perform any device specific initialization. Called after the device is 259 detected but before any other functions that communicate with the device. 260 For example: For devices that present themselves as USB Mass storage 261 devices, this method would be responsible for mounting the device or 262 if the device has been automounted, for finding out where it has been 263 mounted. The method :meth:`calibre.devices.usbms.device.Device.open` has 264 an implementation of 265 this function that should serve as a good example for USB Mass storage 266 devices. 267 268 This method can raise an OpenFeedback exception to display a message to 269 the user. 270 271 :param connected_device: The device that we are trying to open. It is 272 a tuple of (vendor id, product id, bcd, manufacturer name, product 273 name, device serial number). However, some devices have no serial 274 number and on Windows only the first three fields are present, the 275 rest are None. 276 277 :param library_uuid: The UUID of the current calibre library. Can be 278 None if there is no library (for example when used from the command 279 line). 280 281 ''' 282 raise NotImplementedError() 283 284 def eject(self): 285 ''' 286 Un-mount / eject the device from the OS. This does not check if there 287 are pending GUI jobs that need to communicate with the device. 288 289 NOTE: That this method may not be called on the same thread as the rest 290 of the device methods. 291 ''' 292 raise NotImplementedError() 293 294 def post_yank_cleanup(self): 295 ''' 296 Called if the user yanks the device without ejecting it first. 297 ''' 298 raise NotImplementedError() 299 300 def set_progress_reporter(self, report_progress): 301 ''' 302 Set a function to report progress information. 303 304 :param report_progress: Function that is called with a % progress 305 (number between 0 and 100) for various tasks. 306 If it is called with -1 that means that the 307 task does not have any progress information 308 309 ''' 310 raise NotImplementedError() 311 312 def get_device_information(self, end_session=True): 313 """ 314 Ask device for device information. See L{DeviceInfoQuery}. 315 316 :return: (device name, device version, software version on device, MIME type) 317 The tuple can optionally have a fifth element, which is a 318 drive information dictionary. See usbms.driver for an example. 319 320 """ 321 raise NotImplementedError() 322 323 def get_driveinfo(self): 324 ''' 325 Return the driveinfo dictionary. Usually called from 326 get_device_information(), but if loading the driveinfo is slow for this 327 driver, then it should set SLOW_DRIVEINFO. In this case, this method 328 will be called by calibre after the book lists have been loaded. Note 329 that it is not called on the device thread, so the driver should cache 330 the drive info in the books() method and this function should return 331 the cached data. 332 ''' 333 return {} 334 335 def card_prefix(self, end_session=True): 336 ''' 337 Return a 2 element list of the prefix to paths on the cards. 338 If no card is present None is set for the card's prefix. 339 E.G. 340 ('/place', '/place2') 341 (None, 'place2') 342 ('place', None) 343 (None, None) 344 ''' 345 raise NotImplementedError() 346 347 def total_space(self, end_session=True): 348 """ 349 Get total space available on the mountpoints: 350 1. Main memory 351 2. Memory Card A 352 3. Memory Card B 353 354 :return: A 3 element list with total space in bytes of (1, 2, 3). If a 355 particular device doesn't have any of these locations it should return 0. 356 357 """ 358 raise NotImplementedError() 359 360 def free_space(self, end_session=True): 361 """ 362 Get free space available on the mountpoints: 363 1. Main memory 364 2. Card A 365 3. Card B 366 367 :return: A 3 element list with free space in bytes of (1, 2, 3). If a 368 particular device doesn't have any of these locations it should return -1. 369 370 """ 371 raise NotImplementedError() 372 373 def books(self, oncard=None, end_session=True): 374 """ 375 Return a list of e-books on the device. 376 377 :param oncard: If 'carda' or 'cardb' return a list of e-books on the 378 specific storage card, otherwise return list of e-books 379 in main memory of device. If a card is specified and no 380 books are on the card return empty list. 381 382 :return: A BookList. 383 384 """ 385 raise NotImplementedError() 386 387 def upload_books(self, files, names, on_card=None, end_session=True, 388 metadata=None): 389 ''' 390 Upload a list of books to the device. If a file already 391 exists on the device, it should be replaced. 392 This method should raise a :class:`FreeSpaceError` if there is not enough 393 free space on the device. The text of the FreeSpaceError must contain the 394 word "card" if ``on_card`` is not None otherwise it must contain the word "memory". 395 396 :param files: A list of paths 397 :param names: A list of file names that the books should have 398 once uploaded to the device. len(names) == len(files) 399 :param metadata: If not None, it is a list of :class:`Metadata` objects. 400 The idea is to use the metadata to determine where on the device to 401 put the book. len(metadata) == len(files). Apart from the regular 402 cover (path to cover), there may also be a thumbnail attribute, which should 403 be used in preference. The thumbnail attribute is of the form 404 (width, height, cover_data as jpeg). 405 406 :return: A list of 3-element tuples. The list is meant to be passed 407 to :meth:`add_books_to_metadata`. 408 ''' 409 raise NotImplementedError() 410 411 @classmethod 412 def add_books_to_metadata(cls, locations, metadata, booklists): 413 ''' 414 Add locations to the booklists. This function must not communicate with 415 the device. 416 417 :param locations: Result of a call to L{upload_books} 418 :param metadata: List of :class:`Metadata` objects, same as for 419 :meth:`upload_books`. 420 :param booklists: A tuple containing the result of calls to 421 (:meth:`books(oncard=None)`, 422 :meth:`books(oncard='carda')`, 423 :meth`books(oncard='cardb')`). 424 425 ''' 426 raise NotImplementedError() 427 428 def delete_books(self, paths, end_session=True): 429 ''' 430 Delete books at paths on device. 431 ''' 432 raise NotImplementedError() 433 434 @classmethod 435 def remove_books_from_metadata(cls, paths, booklists): 436 ''' 437 Remove books from the metadata list. This function must not communicate 438 with the device. 439 440 :param paths: paths to books on the device. 441 :param booklists: A tuple containing the result of calls to 442 (:meth:`books(oncard=None)`, 443 :meth:`books(oncard='carda')`, 444 :meth`books(oncard='cardb')`). 445 446 ''' 447 raise NotImplementedError() 448 449 def sync_booklists(self, booklists, end_session=True): 450 ''' 451 Update metadata on device. 452 453 :param booklists: A tuple containing the result of calls to 454 (:meth:`books(oncard=None)`, 455 :meth:`books(oncard='carda')`, 456 :meth`books(oncard='cardb')`). 457 458 ''' 459 raise NotImplementedError() 460 461 def get_file(self, path, outfile, end_session=True): 462 ''' 463 Read the file at ``path`` on the device and write it to outfile. 464 465 :param outfile: file object like ``sys.stdout`` or the result of an 466 :func:`open` call. 467 468 ''' 469 raise NotImplementedError() 470 471 @classmethod 472 def config_widget(cls): 473 ''' 474 Should return a QWidget. The QWidget contains the settings for the 475 device interface 476 ''' 477 raise NotImplementedError() 478 479 @classmethod 480 def save_settings(cls, settings_widget): 481 ''' 482 Should save settings to disk. Takes the widget created in 483 :meth:`config_widget` and saves all settings to disk. 484 ''' 485 raise NotImplementedError() 486 487 @classmethod 488 def settings(cls): 489 ''' 490 Should return an opts object. The opts object should have at least one 491 attribute `format_map` which is an ordered list of formats for the 492 device. 493 ''' 494 raise NotImplementedError() 495 496 def set_plugboards(self, plugboards, pb_func): 497 ''' 498 provide the driver the current set of plugboards and a function to 499 select a specific plugboard. This method is called immediately before 500 add_books and sync_booklists. 501 502 pb_func is a callable with the following signature:: 503 def pb_func(device_name, format, plugboards) 504 505 You give it the current device name (either the class name or 506 DEVICE_PLUGBOARD_NAME), the format you are interested in (a 'real' 507 format or 'device_db'), and the plugboards (you were given those by 508 set_plugboards, the same place you got this method). 509 510 :return: None or a single plugboard instance. 511 512 ''' 513 pass 514 515 def set_driveinfo_name(self, location_code, name): 516 ''' 517 Set the device name in the driveinfo file to 'name'. This setting will 518 persist until the file is re-created or the name is changed again. 519 520 Non-disk devices should implement this method based on the location 521 codes returned by the get_device_information() method. 522 ''' 523 pass 524 525 def prepare_addable_books(self, paths): 526 ''' 527 Given a list of paths, returns another list of paths. These paths 528 point to addable versions of the books. 529 530 If there is an error preparing a book, then instead of a path, the 531 position in the returned list for that book should be a three tuple: 532 (original_path, the exception instance, traceback) 533 ''' 534 return paths 535 536 def startup(self): 537 ''' 538 Called when calibre is starting the device. Do any initialization 539 required. Note that multiple instances of the class can be instantiated, 540 and thus __init__ can be called multiple times, but only one instance 541 will have this method called. This method is called on the device 542 thread, not the GUI thread. 543 ''' 544 pass 545 546 def shutdown(self): 547 ''' 548 Called when calibre is shutting down, either for good or in preparation 549 to restart. Do any cleanup required. This method is called on the 550 device thread, not the GUI thread. 551 ''' 552 pass 553 554 def get_device_uid(self): 555 ''' 556 Must return a unique id for the currently connected device (this is 557 called immediately after a successful call to open()). You must 558 implement this method if you set ASK_TO_ALLOW_CONNECT = True 559 ''' 560 raise NotImplementedError() 561 562 def ignore_connected_device(self, uid): 563 ''' 564 Should ignore the device identified by uid (the result of a call to 565 get_device_uid()) in the future. You must implement this method if you 566 set ASK_TO_ALLOW_CONNECT = True. Note that this function is called 567 immediately after open(), so if open() caches some state, the driver 568 should reset that state. 569 ''' 570 raise NotImplementedError() 571 572 def get_user_blacklisted_devices(self): 573 ''' 574 Return map of device uid to friendly name for all devices that the user 575 has asked to be ignored. 576 ''' 577 return {} 578 579 def set_user_blacklisted_devices(self, devices): 580 ''' 581 Set the list of device uids that should be ignored by this driver. 582 ''' 583 pass 584 585 def specialize_global_preferences(self, device_prefs): 586 ''' 587 Implement this method if your device wants to override a particular 588 preference. You must ensure that all call sites that want a preference 589 that can be overridden use device_prefs['something'] instead 590 of prefs['something']. Your 591 method should call device_prefs.set_overrides(pref=val, pref=val, ...). 592 Currently used for: 593 metadata management (prefs['manage_device_metadata']) 594 ''' 595 device_prefs.set_overrides() 596 597 def set_library_info(self, library_name, library_uuid, field_metadata): 598 ''' 599 Implement this method if you want information about the current calibre 600 library. This method is called at startup and when the calibre library 601 changes while connected. 602 ''' 603 pass 604 605 # Dynamic control interface. 606 # The following methods are probably called on the GUI thread. Any driver 607 # that implements these methods must take pains to be thread safe, because 608 # the device_manager might be using the driver at the same time that one of 609 # these methods is called. 610 611 def is_dynamically_controllable(self): 612 ''' 613 Called by the device manager when starting plugins. If this method returns 614 a string, then a) it supports the device manager's dynamic control 615 interface, and b) that name is to be used when talking to the plugin. 616 617 This method can be called on the GUI thread. A driver that implements 618 this method must be thread safe. 619 ''' 620 return None 621 622 def start_plugin(self): 623 ''' 624 This method is called to start the plugin. The plugin should begin 625 to accept device connections however it does that. If the plugin is 626 already accepting connections, then do nothing. 627 628 This method can be called on the GUI thread. A driver that implements 629 this method must be thread safe. 630 ''' 631 pass 632 633 def stop_plugin(self): 634 ''' 635 This method is called to stop the plugin. The plugin should no longer 636 accept connections, and should cleanup behind itself. It is likely that 637 this method should call shutdown. If the plugin is already not accepting 638 connections, then do nothing. 639 640 This method can be called on the GUI thread. A driver that implements 641 this method must be thread safe. 642 ''' 643 pass 644 645 def get_option(self, opt_string, default=None): 646 ''' 647 Return the value of the option indicated by opt_string. This method can 648 be called when the plugin is not started. Return None if the option does 649 not exist. 650 651 This method can be called on the GUI thread. A driver that implements 652 this method must be thread safe. 653 ''' 654 return default 655 656 def set_option(self, opt_string, opt_value): 657 ''' 658 Set the value of the option indicated by opt_string. This method can 659 be called when the plugin is not started. 660 661 This method can be called on the GUI thread. A driver that implements 662 this method must be thread safe. 663 ''' 664 pass 665 666 def is_running(self): 667 ''' 668 Return True if the plugin is started, otherwise false 669 670 This method can be called on the GUI thread. A driver that implements 671 this method must be thread safe. 672 ''' 673 return False 674 675 def synchronize_with_db(self, db, book_id, book_metadata, first_call): 676 ''' 677 Called during book matching when a book on the device is matched with 678 a book in calibre's db. The method is responsible for synchronizing 679 data from the device to calibre's db (if needed). 680 681 The method must return a two-value tuple. The first value is a set of 682 calibre book ids changed if calibre's database was changed or None if the 683 database was not changed. If the first value is an empty set then the 684 metadata for the book on the device is updated with calibre's metadata 685 and given back to the device, but no GUI refresh of that book is done. 686 This is useful when the calibre data is correct but must be sent to the 687 device. 688 689 The second value is itself a 2-value tuple. The first value in the tuple 690 specifies whether a book format should be sent to the device. The intent 691 is to permit verifying that the book on the device is the same as the 692 book in calibre. This value must be None if no book is to be sent, 693 otherwise return the base file name on the device (a string like 694 foobar.epub). Be sure to include the extension in the name. The device 695 subsystem will construct a send_books job for all books with not- None 696 returned values. Note: other than to later retrieve the extension, the 697 name is ignored in cases where the device uses a template to generate 698 the file name, which most do. The second value in the returned tuple 699 indicated whether the format is future-dated. Return True if it is, 700 otherwise return False. calibre will display a dialog to the user 701 listing all future dated books. 702 703 Extremely important: this method is called on the GUI thread. It must 704 be threadsafe with respect to the device manager's thread. 705 706 book_id: the calibre id for the book in the database. 707 book_metadata: the Metadata object for the book coming from the device. 708 first_call: True if this is the first call during a sync, False otherwise 709 ''' 710 return (None, (None, False)) 711 712 713class BookList(list): 714 ''' 715 A list of books. Each Book object must have the fields 716 717 #. title 718 #. authors 719 #. size (file size of the book) 720 #. datetime (a UTC time tuple) 721 #. path (path on the device to the book) 722 #. thumbnail (can be None) thumbnail is either a str/bytes object with the 723 image data or it should have an attribute image_path that stores an 724 absolute (platform native) path to the image 725 #. tags (a list of strings, can be empty). 726 727 ''' 728 729 __getslice__ = None 730 __setslice__ = None 731 732 def __init__(self, oncard, prefix, settings): 733 pass 734 735 def supports_collections(self): 736 ''' Return True if the device supports collections for this book list. ''' 737 raise NotImplementedError() 738 739 def add_book(self, book, replace_metadata): 740 ''' 741 Add the book to the booklist. Intent is to maintain any device-internal 742 metadata. Return True if booklists must be sync'ed 743 ''' 744 raise NotImplementedError() 745 746 def remove_book(self, book): 747 ''' 748 Remove a book from the booklist. Correct any device metadata at the 749 same time 750 ''' 751 raise NotImplementedError() 752 753 def get_collections(self, collection_attributes): 754 ''' 755 Return a dictionary of collections created from collection_attributes. 756 Each entry in the dictionary is of the form collection name:[list of 757 books] 758 759 The list of books is sorted by book title, except for collections 760 created from series, in which case series_index is used. 761 762 :param collection_attributes: A list of attributes of the Book object 763 764 ''' 765 raise NotImplementedError() 766 767 768class CurrentlyConnectedDevice: 769 770 def __init__(self): 771 self._device = None 772 773 @property 774 def device(self): 775 return self._device 776 777 778# A device driver can check if a device is currently connected to calibre using 779# the following code:: 780# from calibre.device.interface import currently_connected_device 781# if currently_connected_device.device is None: 782# # no device connected 783# The device attribute will be either None or the device driver object 784# (DevicePlugin instance) for the currently connected device. 785currently_connected_device = CurrentlyConnectedDevice() 786