1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2010-2019, Timothy Legge <timlegge@gmail.com>, Kovid Goyal <kovid@kovidgoyal.net> and David Forrester <davidfor@internode.on.net>' 7__docformat__ = 'restructuredtext en' 8 9''' 10Driver for Kobo eReaders. Supports all e-ink devices. 11 12Originally developed by Timothy Legge <timlegge@gmail.com>. 13Extended to support Touch firmware 2.0.0 and later and newer devices by David Forrester <davidfor@internode.on.net> 14''' 15 16import os, time, shutil, re 17 18from contextlib import closing 19from datetime import datetime 20from calibre import strftime 21from calibre.utils.date import parse_date 22from calibre.devices.usbms.books import BookList 23from calibre.devices.usbms.books import CollectionsBookList 24from calibre.devices.kobo.books import KTCollectionsBookList 25from calibre.ebooks.metadata import authors_to_string 26from calibre.ebooks.metadata.book.base import Metadata 27from calibre.ebooks.metadata.utils import normalize_languages 28from calibre.devices.kobo.books import Book 29from calibre.devices.kobo.books import ImageWrapper 30from calibre.devices.mime import mime_type_ext 31from calibre.devices.usbms.driver import USBMS, debug_print 32from calibre import prints, fsync 33from calibre.ptempfile import PersistentTemporaryFile, better_mktemp 34from calibre.constants import DEBUG 35from calibre.utils.config_base import prefs 36from polyglot.builtins import iteritems, itervalues, string_or_bytes 37 38EPUB_EXT = '.epub' 39KEPUB_EXT = '.kepub' 40 41DEFAULT_COVER_LETTERBOX_COLOR = '#000000' 42 43# Implementation of QtQHash for strings. This doesn't seem to be in the Python implementation. 44 45 46def qhash(inputstr): 47 instr = b"" 48 if isinstance(inputstr, bytes): 49 instr = inputstr 50 elif isinstance(inputstr, str): 51 instr = inputstr.encode("utf8") 52 else: 53 return -1 54 55 h = 0x00000000 56 for x in bytearray(instr): 57 h = (h << 4) + x 58 h ^= (h & 0xf0000000) >> 23 59 h &= 0x0fffffff 60 61 return h 62 63 64def any_in(haystack, *needles): 65 for n in needles: 66 if n in haystack: 67 return True 68 return False 69 70 71class DummyCSSPreProcessor: 72 73 def __call__(self, data, add_namespace=False): 74 75 return data 76 77 78class KOBO(USBMS): 79 80 name = 'Kobo Reader Device Interface' 81 gui_name = 'Kobo Reader' 82 description = _('Communicate with the original Kobo Reader and the Kobo WiFi.') 83 author = 'Timothy Legge and David Forrester' 84 version = (2, 5, 1) 85 86 dbversion = 0 87 fwversion = (0,0,0) 88 # The firmware for these devices is not being updated. But the Kobo desktop application 89 # will update the database if the device is connected. The database structure is completely 90 # backwardly compatible. 91 supported_dbversion = 162 92 has_kepubs = False 93 94 supported_platforms = ['windows', 'osx', 'linux'] 95 96 booklist_class = CollectionsBookList 97 book_class = Book 98 99 # Ordered list of supported formats 100 FORMATS = ['kepub', 'epub', 'pdf', 'txt', 'cbz', 'cbr'] 101 CAN_SET_METADATA = ['collections'] 102 103 VENDOR_ID = [0x2237] 104 BCD = [0x0110, 0x0323, 0x0326] 105 ORIGINAL_PRODUCT_ID = [0x4165] 106 WIFI_PRODUCT_ID = [0x4161, 0x4162] 107 PRODUCT_ID = ORIGINAL_PRODUCT_ID + WIFI_PRODUCT_ID 108 109 VENDOR_NAME = ['KOBO_INC', 'KOBO'] 110 WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['.KOBOEREADER', 'EREADER'] 111 112 EBOOK_DIR_MAIN = '' 113 SUPPORTS_SUB_DIRS = True 114 SUPPORTS_ANNOTATIONS = True 115 116 # "kepubs" do not have an extension. The name looks like a GUID. Using an empty string seems to work. 117 VIRTUAL_BOOK_EXTENSIONS = frozenset(('kobo', '')) 118 119 EXTRA_CUSTOMIZATION_MESSAGE = [ 120 _('The Kobo supports several collections including ')+ 'Read, Closed, Im_Reading. ' + _( 121 'Create tags for automatic management'), 122 _('Upload covers for books (newer readers)') + ':::'+_( 123 'Normally, the Kobo readers get the cover image from the' 124 ' e-book file itself. With this option, calibre will send a ' 125 'separate cover image to the reader, useful if you ' 126 'have modified the cover.'), 127 _('Upload black and white covers'), 128 _('Show expired books') + ':::'+_( 129 'A bug in an earlier version left non kepubs book records' 130 ' in the database. With this option calibre will show the ' 131 'expired records and allow you to delete them with ' 132 'the new delete logic.'), 133 _('Show previews') + ':::'+_( 134 'Kobo previews are included on the Touch and some other versions' 135 ' by default they are no longer displayed as there is no good reason to ' 136 'see them. Enable if you wish to see/delete them.'), 137 _('Show recommendations') + ':::'+_( 138 'Kobo now shows recommendations on the device. In some cases these have ' 139 'files but in other cases they are just pointers to the web site to buy. ' 140 'Enable if you wish to see/delete them.'), 141 _('Attempt to support newer firmware') + ':::'+_( 142 'Kobo routinely updates the firmware and the ' 143 'database version. With this option calibre will attempt ' 144 'to perform full read-write functionality - Here be Dragons!! ' 145 'Enable only if you are comfortable with restoring your kobo ' 146 'to factory defaults and testing software'), 147 ] 148 149 EXTRA_CUSTOMIZATION_DEFAULT = [ 150 ', '.join(['tags']), 151 True, 152 True, 153 True, 154 False, 155 False, 156 False 157 ] 158 159 OPT_COLLECTIONS = 0 160 OPT_UPLOAD_COVERS = 1 161 OPT_UPLOAD_GRAYSCALE_COVERS = 2 162 OPT_SHOW_EXPIRED_BOOK_RECORDS = 3 163 OPT_SHOW_PREVIEWS = 4 164 OPT_SHOW_RECOMMENDATIONS = 5 165 OPT_SUPPORT_NEWER_FIRMWARE = 6 166 167 def __init__(self, *args, **kwargs): 168 USBMS.__init__(self, *args, **kwargs) 169 self.plugboards = self.plugboard_func = None 170 171 def initialize(self): 172 USBMS.initialize(self) 173 self.dbversion = 7 174 175 def device_database_path(self): 176 return self.normalize_path(self._main_prefix + '.kobo/KoboReader.sqlite') 177 178 def device_database_connection(self, use_row_factory=False): 179 import apsw 180 db_connection = apsw.Connection(self.device_database_path()) 181 182 if use_row_factory: 183 db_connection.setrowtrace(self.row_factory) 184 185 return db_connection 186 187 def row_factory(self, cursor, row): 188 return {k[0]: row[i] for i, k in enumerate(cursor.getdescription())} 189 190 def get_database_version(self, connection): 191 cursor = connection.cursor() 192 cursor.execute('SELECT version FROM dbversion') 193 try: 194 result = next(cursor) 195 dbversion = result['version'] 196 except StopIteration: 197 dbversion = 0 198 199 return dbversion 200 201 def get_firmware_version(self): 202 # Determine the firmware version 203 try: 204 with lopen(self.normalize_path(self._main_prefix + '.kobo/version'), 'rb') as f: 205 fwversion = f.readline().split(b',')[2] 206 fwversion = tuple(int(x) for x in fwversion.split(b'.')) 207 except Exception: 208 debug_print("Kobo::get_firmware_version - didn't get firmware version from file'") 209 fwversion = (0,0,0) 210 211 return fwversion 212 213 def sanitize_path_components(self, components): 214 invalid_filename_chars_re = re.compile(r'[\/\\\?%\*:;\|\"\'><\$!]', re.IGNORECASE | re.UNICODE) 215 return [invalid_filename_chars_re.sub('_', x) for x in components] 216 217 def books(self, oncard=None, end_session=True): 218 from calibre.ebooks.metadata.meta import path_to_ext 219 220 dummy_bl = BookList(None, None, None) 221 222 if oncard == 'carda' and not self._card_a_prefix: 223 self.report_progress(1.0, _('Getting list of books on device...')) 224 return dummy_bl 225 elif oncard == 'cardb' and not self._card_b_prefix: 226 self.report_progress(1.0, _('Getting list of books on device...')) 227 return dummy_bl 228 elif oncard and oncard != 'carda' and oncard != 'cardb': 229 self.report_progress(1.0, _('Getting list of books on device...')) 230 return dummy_bl 231 232 prefix = self._card_a_prefix if oncard == 'carda' else \ 233 self._card_b_prefix if oncard == 'cardb' \ 234 else self._main_prefix 235 236 self.fwversion = self.get_firmware_version() 237 238 if not (self.fwversion == (1,0) or self.fwversion == (1,4)): 239 self.has_kepubs = True 240 debug_print('Version of driver: ', self.version, 'Has kepubs:', self.has_kepubs) 241 debug_print('Version of firmware: ', self.fwversion, 'Has kepubs:', self.has_kepubs) 242 243 self.booklist_class.rebuild_collections = self.rebuild_collections 244 245 # get the metadata cache 246 bl = self.booklist_class(oncard, prefix, self.settings) 247 need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) 248 249 # make a dict cache of paths so the lookup in the loop below is faster. 250 bl_cache = {} 251 for idx,b in enumerate(bl): 252 bl_cache[b.lpath] = idx 253 254 def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType, expired, favouritesindex, accessibility): 255 changed = False 256 try: 257 lpath = path.partition(self.normalize_path(prefix))[2] 258 if lpath.startswith(os.sep): 259 lpath = lpath[len(os.sep):] 260 lpath = lpath.replace('\\', '/') 261 # debug_print("LPATH: ", lpath, " - Title: " , title) 262 263 playlist_map = {} 264 265 if lpath not in playlist_map: 266 playlist_map[lpath] = [] 267 268 if readstatus == 1: 269 playlist_map[lpath].append('Im_Reading') 270 elif readstatus == 2: 271 playlist_map[lpath].append('Read') 272 elif readstatus == 3: 273 playlist_map[lpath].append('Closed') 274 275 # Related to a bug in the Kobo firmware that leaves an expired row for deleted books 276 # this shows an expired Collection so the user can decide to delete the book 277 if expired == 3: 278 playlist_map[lpath].append('Expired') 279 # A SHORTLIST is supported on the touch but the data field is there on most earlier models 280 if favouritesindex == 1: 281 playlist_map[lpath].append('Shortlist') 282 283 # Label Previews 284 if accessibility == 6: 285 playlist_map[lpath].append('Preview') 286 elif accessibility == 4: 287 playlist_map[lpath].append('Recommendation') 288 289 path = self.normalize_path(path) 290 # print "Normalized FileName: " + path 291 292 idx = bl_cache.get(lpath, None) 293 if idx is not None: 294 bl_cache[lpath] = None 295 if ImageID is not None: 296 imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed') 297 if not os.path.exists(imagename): 298 # Try the Touch version if the image does not exist 299 imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - N3_LIBRARY_FULL.parsed') 300 301 # print "Image name Normalized: " + imagename 302 if not os.path.exists(imagename): 303 debug_print("Strange - The image name does not exist - title: ", title) 304 if imagename is not None: 305 bl[idx].thumbnail = ImageWrapper(imagename) 306 if (ContentType != '6' and MimeType != 'Shortcover'): 307 if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))): 308 if self.update_metadata_item(bl[idx]): 309 # print 'update_metadata_item returned true' 310 changed = True 311 else: 312 debug_print(" Strange: The file: ", prefix, lpath, " does not exist!") 313 if lpath in playlist_map and \ 314 playlist_map[lpath] not in bl[idx].device_collections: 315 bl[idx].device_collections = playlist_map.get(lpath,[]) 316 else: 317 if ContentType == '6' and MimeType == 'Shortcover': 318 book = self.book_class(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576) 319 else: 320 try: 321 if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))): 322 book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID) 323 else: 324 debug_print(" Strange: The file: ", prefix, lpath, " does not exist!") 325 title = "FILE MISSING: " + title 326 book = self.book_class(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576) 327 328 except: 329 debug_print("prefix: ", prefix, "lpath: ", lpath, "title: ", title, "authors: ", authors, 330 "mime: ", mime, "date: ", date, "ContentType: ", ContentType, "ImageID: ", ImageID) 331 raise 332 333 # print 'Update booklist' 334 book.device_collections = playlist_map.get(lpath,[]) # if lpath in playlist_map else [] 335 336 if bl.add_book(book, replace_metadata=False): 337 changed = True 338 except: # Probably a path encoding error 339 import traceback 340 traceback.print_exc() 341 return changed 342 343 with closing(self.device_database_connection(use_row_factory=True)) as connection: 344 345 self.dbversion = self.get_database_version(connection) 346 debug_print("Database Version: ", self.dbversion) 347 348 cursor = connection.cursor() 349 opts = self.settings() 350 if self.dbversion >= 33: 351 query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' 352 'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, Accessibility, IsDownloaded from content where ' 353 'BookID is Null %(previews)s %(recommendations)s and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict( 354 expiry=' and ContentType = 6)' if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')', 355 previews=' and Accessibility <> 6' if not self.show_previews else '', 356 recommendations=' and IsDownloaded in (\'true\', 1)' if opts.extra_customization[self.OPT_SHOW_RECOMMENDATIONS] is False else '') 357 elif self.dbversion >= 16 and self.dbversion < 33: 358 query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' 359 'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, Accessibility, "1" as IsDownloaded from content where ' 360 'BookID is Null and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(expiry=' and ContentType = 6)' 361 if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')') 362 elif self.dbversion < 16 and self.dbversion >= 14: 363 query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' 364 'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, "-1" as Accessibility, "1" as IsDownloaded from content where ' 365 'BookID is Null and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(expiry=' and ContentType = 6)' 366 if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')') 367 elif self.dbversion < 14 and self.dbversion >= 8: 368 query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' 369 'ImageID, ReadStatus, ___ExpirationStatus, "-1" as FavouritesIndex, "-1" as Accessibility, "1" as IsDownloaded from content where ' 370 'BookID is Null and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(expiry=' and ContentType = 6)' 371 if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')') 372 else: 373 query = ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' 374 'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as FavouritesIndex, ' 375 '"-1" as Accessibility, "1" as IsDownloaded from content where BookID is Null') 376 377 try: 378 cursor.execute(query) 379 except Exception as e: 380 err = str(e) 381 if not (any_in(err, '___ExpirationStatus', 'FavouritesIndex', 'Accessibility', 'IsDownloaded')): 382 raise 383 query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' 384 'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as ' 385 'FavouritesIndex, "-1" as Accessibility from content where ' 386 'BookID is Null') 387 cursor.execute(query) 388 389 changed = False 390 for row in cursor: 391 # self.report_progress((i+1) / float(numrows), _('Getting list of books on device...')) 392 if not hasattr(row['ContentID'], 'startswith') or row['ContentID'].startswith("file:///usr/local/Kobo/help/"): 393 # These are internal to the Kobo device and do not exist 394 continue 395 path = self.path_from_contentid(row['ContentID'], row['ContentType'], row['MimeType'], oncard) 396 mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip' 397 # debug_print("mime:", mime) 398 if oncard != 'carda' and oncard != 'cardb' and not row['ContentID'].startswith("file:///mnt/sd/"): 399 prefix = self._main_prefix 400 elif oncard == 'carda' and row['ContentID'].startswith("file:///mnt/sd/"): 401 prefix = self._card_a_prefix 402 changed = update_booklist(self._main_prefix, path, 403 row['Title'], row['Attribution'], mime, row['DateCreated'], row['ContentType'], 404 row['ImageId'], row['ReadStatus'], row['MimeType'], row['___ExpirationStatus'], 405 row['FavouritesIndex'], row['Accessibility'] 406 ) 407 408 if changed: 409 need_sync = True 410 411 cursor.close() 412 413 # Remove books that are no longer in the filesystem. Cache contains 414 # indices into the booklist if book not in filesystem, None otherwise 415 # Do the operation in reverse order so indices remain valid 416 for idx in sorted(itervalues(bl_cache), reverse=True, key=lambda x: x or -1): 417 if idx is not None: 418 need_sync = True 419 del bl[idx] 420 421 # print "count found in cache: %d, count of files in metadata: %d, need_sync: %s" % \ 422 # (len(bl_cache), len(bl), need_sync) 423 if need_sync: # self.count_found_in_bl != len(bl) or need_sync: 424 if oncard == 'cardb': 425 self.sync_booklists((None, None, bl)) 426 elif oncard == 'carda': 427 self.sync_booklists((None, bl, None)) 428 else: 429 self.sync_booklists((bl, None, None)) 430 431 self.report_progress(1.0, _('Getting list of books on device...')) 432 return bl 433 434 def filename_callback(self, path, mi): 435 # debug_print("Kobo:filename_callback:Path - {0}".format(path)) 436 437 idx = path.rfind('.') 438 ext = path[idx:] 439 if ext == KEPUB_EXT: 440 path = path + EPUB_EXT 441# debug_print("Kobo:filename_callback:New path - {0}".format(path)) 442 443 return path 444 445 def delete_via_sql(self, ContentID, ContentType): 446 # Delete Order: 447 # 1) shortcover_page 448 # 2) volume_shorcover 449 # 2) content 450 451 debug_print('delete_via_sql: ContentID: ', ContentID, 'ContentType: ', ContentType) 452 with closing(self.device_database_connection()) as connection: 453 454 cursor = connection.cursor() 455 t = (ContentID,) 456 cursor.execute('select ImageID from content where ContentID = ?', t) 457 458 ImageID = None 459 for row in cursor: 460 # First get the ImageID to delete the images 461 ImageID = row[0] 462 cursor.close() 463 464 cursor = connection.cursor() 465 if ContentType == 6 and self.dbversion < 8: 466 # Delete the shortcover_pages first 467 cursor.execute('delete from shortcover_page where shortcoverid in (select ContentID from content where BookID = ?)', t) 468 469 # Delete the volume_shortcovers second 470 cursor.execute('delete from volume_shortcovers where volumeid = ?', t) 471 472 # Delete the rows from content_keys 473 if self.dbversion >= 8: 474 cursor.execute('delete from content_keys where volumeid = ?', t) 475 476 # Delete the chapters associated with the book next 477 t = (ContentID,) 478 # Kobo does not delete the Book row (ie the row where the BookID is Null) 479 # The next server sync should remove the row 480 cursor.execute('delete from content where BookID = ?', t) 481 if ContentType == 6: 482 try: 483 cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0, ___ExpirationStatus=3 ' 484 'where BookID is Null and ContentID =?',t) 485 except Exception as e: 486 if 'no such column' not in str(e): 487 raise 488 try: 489 cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0 ' 490 'where BookID is Null and ContentID =?',t) 491 except Exception as e: 492 if 'no such column' not in str(e): 493 raise 494 cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\' ' 495 'where BookID is Null and ContentID =?',t) 496 else: 497 cursor.execute('delete from content where BookID is Null and ContentID =?',t) 498 499 cursor.close() 500 if ImageID is None: 501 print("Error condition ImageID was not found") 502 print("You likely tried to delete a book that the kobo has not yet added to the database") 503 504 # If all this succeeds we need to delete the images files via the ImageID 505 return ImageID 506 507 def delete_images(self, ImageID, book_path): 508 if ImageID is not None: 509 path_prefix = '.kobo/images/' 510 path = self._main_prefix + path_prefix + ImageID 511 512 file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed', ' - N3_LIBRARY_FULL.parsed', 513 ' - N3_LIBRARY_GRID.parsed', ' - N3_LIBRARY_LIST.parsed', ' - N3_SOCIAL_CURRENTREAD.parsed', ' - N3_FULL.parsed',) 514 515 for ending in file_endings: 516 fpath = path + ending 517 fpath = self.normalize_path(fpath) 518 519 if os.path.exists(fpath): 520 # print 'Image File Exists: ' + fpath 521 os.unlink(fpath) 522 523 def delete_books(self, paths, end_session=True): 524 if self.modify_database_check("delete_books") is False: 525 return 526 527 for i, path in enumerate(paths): 528 self.report_progress((i+1) / float(len(paths)), _('Removing books from device...')) 529 path = self.normalize_path(path) 530 # print "Delete file normalized path: " + path 531 extension = os.path.splitext(path)[1] 532 ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(path) 533 534 ContentID = self.contentid_from_path(path, ContentType) 535 536 ImageID = self.delete_via_sql(ContentID, ContentType) 537 # print " We would now delete the Images for" + ImageID 538 self.delete_images(ImageID, path) 539 540 if os.path.exists(path): 541 # Delete the ebook 542 # print "Delete the ebook: " + path 543 os.unlink(path) 544 545 filepath = os.path.splitext(path)[0] 546 for ext in self.DELETE_EXTS: 547 if os.path.exists(filepath + ext): 548 # print "Filename: " + filename 549 os.unlink(filepath + ext) 550 if os.path.exists(path + ext): 551 # print "Filename: " + filename 552 os.unlink(path + ext) 553 554 if self.SUPPORTS_SUB_DIRS: 555 try: 556 # print "removed" 557 os.removedirs(os.path.dirname(path)) 558 except Exception: 559 pass 560 self.report_progress(1.0, _('Removing books from device...')) 561 562 def remove_books_from_metadata(self, paths, booklists): 563 if self.modify_database_check("remove_books_from_metatata") is False: 564 return 565 566 for i, path in enumerate(paths): 567 self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...')) 568 for bl in booklists: 569 for book in bl: 570 # print "Book Path: " + book.path 571 if path.endswith(book.path): 572 # print " Remove: " + book.path 573 bl.remove_book(book) 574 self.report_progress(1.0, _('Removing books from device metadata listing...')) 575 576 def add_books_to_metadata(self, locations, metadata, booklists): 577 debug_print("KoboTouch::add_books_to_metadata - start. metadata=%s" % metadata[0]) 578 metadata = iter(metadata) 579 for i, location in enumerate(locations): 580 self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...')) 581 info = next(metadata) 582 debug_print("KoboTouch::add_books_to_metadata - info=%s" % info) 583 blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0 584 585 # Extract the correct prefix from the pathname. To do this correctly, 586 # we must ensure that both the prefix and the path are normalized 587 # so that the comparison will work. Book's __init__ will fix up 588 # lpath, so we don't need to worry about that here. 589 path = self.normalize_path(location[0]) 590 if self._main_prefix: 591 prefix = self._main_prefix if \ 592 path.startswith(self.normalize_path(self._main_prefix)) else None 593 if not prefix and self._card_a_prefix: 594 prefix = self._card_a_prefix if \ 595 path.startswith(self.normalize_path(self._card_a_prefix)) else None 596 if not prefix and self._card_b_prefix: 597 prefix = self._card_b_prefix if \ 598 path.startswith(self.normalize_path(self._card_b_prefix)) else None 599 if prefix is None: 600 prints('in add_books_to_metadata. Prefix is None!', path, 601 self._main_prefix) 602 continue 603 # print "Add book to metadata: " 604 # print "prefix: " + prefix 605 lpath = path.partition(prefix)[2] 606 if lpath.startswith('/') or lpath.startswith('\\'): 607 lpath = lpath[1:] 608 # print "path: " + lpath 609 book = self.book_class(prefix, lpath, info.title, other=info) 610 if book.size is None or book.size == 0: 611 book.size = os.stat(self.normalize_path(path)).st_size 612 b = booklists[blist].add_book(book, replace_metadata=True) 613 if b: 614 b._new_book = True 615 self.report_progress(1.0, _('Adding books to device metadata listing...')) 616 617 def contentid_from_path(self, path, ContentType): 618 if ContentType == 6: 619 extension = os.path.splitext(path)[1] 620 if extension == '.kobo': 621 ContentID = os.path.splitext(path)[0] 622 # Remove the prefix on the file. it could be either 623 ContentID = ContentID.replace(self._main_prefix, '') 624 else: 625 ContentID = path 626 ContentID = ContentID.replace(self._main_prefix + self.normalize_path('.kobo/kepub/'), '') 627 628 if self._card_a_prefix is not None: 629 ContentID = ContentID.replace(self._card_a_prefix, '') 630 elif ContentType == 999: # HTML Files 631 ContentID = path 632 ContentID = ContentID.replace(self._main_prefix, "/mnt/onboard/") 633 if self._card_a_prefix is not None: 634 ContentID = ContentID.replace(self._card_a_prefix, "/mnt/sd/") 635 else: # ContentType = 16 636 ContentID = path 637 ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/") 638 if self._card_a_prefix is not None: 639 ContentID = ContentID.replace(self._card_a_prefix, "file:///mnt/sd/") 640 ContentID = ContentID.replace("\\", '/') 641 return ContentID 642 643 def get_content_type_from_path(self, path): 644 # Strictly speaking the ContentType could be 6 or 10 645 # however newspapers have the same storage format 646 ContentType = 901 647 if path.find('kepub') >= 0: 648 ContentType = 6 649 return ContentType 650 651 def get_content_type_from_extension(self, extension): 652 if extension == '.kobo': 653 # Kobo books do not have book files. They do have some images though 654 # print "kobo book" 655 ContentType = 6 656 elif extension == '.pdf' or extension == '.epub': 657 # print "ePub or pdf" 658 ContentType = 16 659 elif extension == '.rtf' or extension == '.txt' or extension == '.htm' or extension == '.html': 660 # print "txt" 661 if self.fwversion == (1,0) or self.fwversion == (1,4) or self.fwversion == (1,7,4): 662 ContentType = 999 663 else: 664 ContentType = 901 665 else: # if extension == '.html' or extension == '.txt': 666 ContentType = 901 # Yet another hack: to get around Kobo changing how ContentID is stored 667 return ContentType 668 669 def path_from_contentid(self, ContentID, ContentType, MimeType, oncard): 670 path = ContentID 671 672 if oncard == 'cardb': 673 print('path from_contentid cardb') 674 elif oncard == 'carda': 675 path = path.replace("file:///mnt/sd/", self._card_a_prefix) 676 # print "SD Card: " + path 677 else: 678 if ContentType == "6" and MimeType == 'Shortcover': 679 # This is a hack as the kobo files do not exist 680 # but the path is required to make a unique id 681 # for calibre's reference 682 path = self._main_prefix + path + '.kobo' 683 # print "Path: " + path 684 elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip': 685 if path.startswith("file:///mnt/onboard/"): 686 path = self._main_prefix + path.replace("file:///mnt/onboard/", '') 687 else: 688 path = self._main_prefix + '.kobo/kepub/' + path 689 # print "Internal: " + path 690 else: 691 # if path.startswith("file:///mnt/onboard/"): 692 path = path.replace("file:///mnt/onboard/", self._main_prefix) 693 path = path.replace("/mnt/onboard/", self._main_prefix) 694 # print "Internal: " + path 695 696 return path 697 698 def modify_database_check(self, function): 699 # Checks to see whether the database version is supported 700 # and whether the user has chosen to support the firmware version 701 if self.dbversion > self.supported_dbversion: 702 # Unsupported database 703 opts = self.settings() 704 if not opts.extra_customization[self.OPT_SUPPORT_NEWER_FIRMWARE]: 705 debug_print('The database has been upgraded past supported version') 706 self.report_progress(1.0, _('Removing books from device...')) 707 from calibre.devices.errors import UserFeedback 708 raise UserFeedback(_("Kobo database version unsupported - See details"), 709 _('Your Kobo is running an updated firmware/database version.' 710 ' As calibre does not know about this updated firmware,' 711 ' database editing is disabled, to prevent corruption.' 712 ' You can still send books to your Kobo with calibre, ' 713 ' but deleting books and managing collections is disabled.' 714 ' If you are willing to experiment and know how to reset' 715 ' your Kobo to Factory defaults, you can override this' 716 ' check by right clicking the device icon in calibre and' 717 ' selecting "Configure this device" and then the ' 718 ' "Attempt to support newer firmware" option.' 719 ' Doing so may require you to perform a factory reset of' 720 ' your Kobo.') + (( 721 '\nDevice database version: %s.' 722 '\nDevice firmware version: %s') % (self.dbversion, self.fwversion)) 723 , UserFeedback.WARN) 724 725 return False 726 else: 727 # The user chose to edit the database anyway 728 return True 729 else: 730 # Supported database version 731 return True 732 733 def get_file(self, path, *args, **kwargs): 734 tpath = self.munge_path(path) 735 extension = os.path.splitext(tpath)[1] 736 if extension == '.kobo': 737 from calibre.devices.errors import UserFeedback 738 raise UserFeedback(_("Not Implemented"), 739 _('".kobo" files do not exist on the device as books; ' 740 'instead they are rows in the sqlite database. ' 741 'Currently they cannot be exported or viewed.'), 742 UserFeedback.WARN) 743 744 return USBMS.get_file(self, path, *args, **kwargs) 745 746 @classmethod 747 def book_from_path(cls, prefix, lpath, title, authors, mime, date, ContentType, ImageID): 748 # debug_print("KOBO:book_from_path - title=%s"%title) 749 from calibre.ebooks.metadata import MetaInformation 750 751 if cls.read_metadata or cls.MUST_READ_METADATA: 752 mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) 753 else: 754 from calibre.ebooks.metadata.meta import metadata_from_filename 755 mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)), 756 cls.build_template_regexp()) 757 if mi is None: 758 mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0], 759 [_('Unknown')]) 760 size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size 761 book = cls.book_class(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=size, other=mi) 762 763 return book 764 765 def get_device_paths(self): 766 paths = {} 767 for prefix, path, source_id in [ 768 ('main', 'metadata.calibre', 0), 769 ('card_a', 'metadata.calibre', 1), 770 ('card_b', 'metadata.calibre', 2) 771 ]: 772 prefix = getattr(self, '_%s_prefix'%prefix) 773 if prefix is not None and os.path.exists(prefix): 774 paths[source_id] = os.path.join(prefix, *(path.split('/'))) 775 return paths 776 777 def reset_readstatus(self, connection, oncard): 778 cursor = connection.cursor() 779 780 # Reset Im_Reading list in the database 781 if oncard == 'carda': 782 query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID like \'file:///mnt/sd/%\'' 783 elif oncard != 'carda' and oncard != 'cardb': 784 query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID not like \'file:///mnt/sd/%\'' 785 786 try: 787 cursor.execute(query) 788 except: 789 debug_print(' Database Exception: Unable to reset ReadStatus list') 790 raise 791 finally: 792 cursor.close() 793 794 def set_readstatus(self, connection, ContentID, ReadStatus): 795 debug_print("Kobo::set_readstatus - ContentID=%s, ReadStatus=%d" % (ContentID, ReadStatus)) 796 cursor = connection.cursor() 797 t = (ContentID,) 798 cursor.execute('select DateLastRead, ReadStatus from Content where BookID is Null and ContentID = ?', t) 799 try: 800 result = next(cursor) 801 datelastread = result['DateLastRead'] 802 current_ReadStatus = result['ReadStatus'] 803 except StopIteration: 804 datelastread = None 805 current_ReadStatus = 0 806 807 if not ReadStatus == current_ReadStatus: 808 if ReadStatus == 0: 809 datelastread = None 810 else: 811 datelastread = 'CURRENT_TIMESTAMP' if datelastread is None else datelastread 812 813 t = (ReadStatus, datelastread, ContentID,) 814 815 try: 816 debug_print("Kobo::set_readstatus - Making change - ContentID=%s, ReadStatus=%d, DateLastRead=%s" % (ContentID, ReadStatus, datelastread)) 817 cursor.execute('update content set ReadStatus=?,FirstTimeReading=\'false\',DateLastRead=? where BookID is Null and ContentID = ?', t) 818 except: 819 debug_print(' Database Exception: Unable to update ReadStatus') 820 raise 821 822 cursor.close() 823 824 def reset_favouritesindex(self, connection, oncard): 825 # Reset FavouritesIndex list in the database 826 if oncard == 'carda': 827 query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID like \'file:///mnt/sd/%\'' 828 elif oncard != 'carda' and oncard != 'cardb': 829 query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID not like \'file:///mnt/sd/%\'' 830 831 cursor = connection.cursor() 832 try: 833 cursor.execute(query) 834 except Exception as e: 835 debug_print(' Database Exception: Unable to reset Shortlist list') 836 if 'no such column' not in str(e): 837 raise 838 finally: 839 cursor.close() 840 841 def set_favouritesindex(self, connection, ContentID): 842 cursor = connection.cursor() 843 844 t = (ContentID,) 845 846 try: 847 cursor.execute('update content set FavouritesIndex=1 where BookID is Null and ContentID = ?', t) 848 except Exception as e: 849 debug_print(' Database Exception: Unable set book as Shortlist') 850 if 'no such column' not in str(e): 851 raise 852 finally: 853 cursor.close() 854 855 def update_device_database_collections(self, booklists, collections_attributes, oncard): 856 debug_print("Kobo:update_device_database_collections - oncard='%s'"%oncard) 857 if self.modify_database_check("update_device_database_collections") is False: 858 return 859 860 # Only process categories in this list 861 supportedcategories = { 862 "Im_Reading":1, 863 "Read":2, 864 "Closed":3, 865 "Shortlist":4, 866 # "Preview":99, # Unsupported as we don't want to change it 867 } 868 869 # Define lists for the ReadStatus 870 readstatuslist = { 871 "Im_Reading":1, 872 "Read":2, 873 "Closed":3, 874 } 875 876 accessibilitylist = { 877 "Preview":6, 878 "Recommendation":4, 879 } 880# debug_print('Starting update_device_database_collections', collections_attributes) 881 882 # Force collections_attributes to be 'tags' as no other is currently supported 883# debug_print('KOBO: overriding the provided collections_attributes:', collections_attributes) 884 collections_attributes = ['tags'] 885 886 collections = booklists.get_collections(collections_attributes) 887# debug_print('Kobo:update_device_database_collections - Collections:', collections) 888 889 # Create a connection to the sqlite database 890 # Needs to be outside books collection as in the case of removing 891 # the last book from the collection the list of books is empty 892 # and the removal of the last book would not occur 893 894 with closing(self.device_database_connection()) as connection: 895 896 if collections: 897 898 # Need to reset the collections outside the particular loops 899 # otherwise the last item will not be removed 900 self.reset_readstatus(connection, oncard) 901 if self.dbversion >= 14: 902 self.reset_favouritesindex(connection, oncard) 903 904 # Process any collections that exist 905 for category, books in collections.items(): 906 if category in supportedcategories: 907 # debug_print("Category: ", category, " id = ", readstatuslist.get(category)) 908 for book in books: 909 # debug_print(' Title:', book.title, 'category: ', category) 910 if category not in book.device_collections: 911 book.device_collections.append(category) 912 913 extension = os.path.splitext(book.path)[1] 914 ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(book.path) 915 916 ContentID = self.contentid_from_path(book.path, ContentType) 917 918 if category in tuple(readstatuslist): 919 # Manage ReadStatus 920 self.set_readstatus(connection, ContentID, readstatuslist.get(category)) 921 elif category == 'Shortlist' and self.dbversion >= 14: 922 # Manage FavouritesIndex/Shortlist 923 self.set_favouritesindex(connection, ContentID) 924 elif category in tuple(accessibilitylist): 925 # Do not manage the Accessibility List 926 pass 927 else: # No collections 928 # Since no collections exist the ReadStatus needs to be reset to 0 (Unread) 929 debug_print("No Collections - resetting ReadStatus") 930 self.reset_readstatus(connection, oncard) 931 if self.dbversion >= 14: 932 debug_print("No Collections - resetting FavouritesIndex") 933 self.reset_favouritesindex(connection, oncard) 934 935# debug_print('Finished update_device_database_collections', collections_attributes) 936 937 def get_collections_attributes(self): 938 collections = [x.lower().strip() for x in self.collections_columns.split(',')] 939 return collections 940 941 @property 942 def collections_columns(self): 943 opts = self.settings() 944 return opts.extra_customization[self.OPT_COLLECTIONS] 945 946 @property 947 def read_metadata(self): 948 return self.settings().read_metadata 949 950 @property 951 def show_previews(self): 952 opts = self.settings() 953 return opts.extra_customization[self.OPT_SHOW_PREVIEWS] is False 954 955 def sync_booklists(self, booklists, end_session=True): 956 debug_print('KOBO:sync_booklists - start') 957 paths = self.get_device_paths() 958# debug_print('KOBO:sync_booklists - booklists:', booklists) 959 960 blists = {} 961 for i in paths: 962 try: 963 if booklists[i] is not None: 964 # debug_print('Booklist: ', i) 965 blists[i] = booklists[i] 966 except IndexError: 967 pass 968 collections = self.get_collections_attributes() 969 970 # debug_print('KOBO: collection fields:', collections) 971 for i, blist in blists.items(): 972 if i == 0: 973 oncard = 'main' 974 else: 975 oncard = 'carda' 976 self.update_device_database_collections(blist, collections, oncard) 977 978 USBMS.sync_booklists(self, booklists, end_session=end_session) 979 debug_print('KOBO:sync_booklists - end') 980 981 def rebuild_collections(self, booklist, oncard): 982 collections_attributes = [] 983 self.update_device_database_collections(booklist, collections_attributes, oncard) 984 985 def upload_cover(self, path, filename, metadata, filepath): 986 ''' 987 Upload book cover to the device. Default implementation does nothing. 988 989 :param path: The full path to the folder where the associated book is located. 990 :param filename: The name of the book file without the extension. 991 :param metadata: metadata belonging to the book. Use metadata.thumbnail 992 for cover 993 :param filepath: The full path to the ebook file 994 995 ''' 996 997 opts = self.settings() 998 if not opts.extra_customization[self.OPT_UPLOAD_COVERS]: 999 # Building thumbnails disabled 1000 debug_print('KOBO: not uploading cover') 1001 return 1002 1003 if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]: 1004 uploadgrayscale = False 1005 else: 1006 uploadgrayscale = True 1007 1008 debug_print('KOBO: uploading cover') 1009 try: 1010 self._upload_cover(path, filename, metadata, filepath, uploadgrayscale) 1011 except: 1012 debug_print('FAILED to upload cover', filepath) 1013 1014 def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale): 1015 from calibre.utils.img import save_cover_data_to 1016 if metadata.cover: 1017 cover = self.normalize_path(metadata.cover.replace('/', os.sep)) 1018 1019 if os.path.exists(cover): 1020 # Get ContentID for Selected Book 1021 extension = os.path.splitext(filepath)[1] 1022 ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(filepath) 1023 ContentID = self.contentid_from_path(filepath, ContentType) 1024 1025 with closing(self.device_database_connection()) as connection: 1026 1027 cursor = connection.cursor() 1028 t = (ContentID,) 1029 cursor.execute('select ImageId from Content where BookID is Null and ContentID = ?', t) 1030 try: 1031 result = next(cursor) 1032# debug_print("ImageId: ", result[0]) 1033 ImageID = result[0] 1034 except StopIteration: 1035 debug_print("No rows exist in the database - cannot upload") 1036 return 1037 finally: 1038 cursor.close() 1039 1040 if ImageID is not None: 1041 path_prefix = '.kobo/images/' 1042 path = self._main_prefix + path_prefix + ImageID 1043 1044 file_endings = {' - iPhoneThumbnail.parsed':(103,150), 1045 ' - bbMediumGridList.parsed':(93,135), 1046 ' - NickelBookCover.parsed':(500,725), 1047 ' - N3_LIBRARY_FULL.parsed':(355,530), 1048 ' - N3_LIBRARY_GRID.parsed':(149,233), 1049 ' - N3_LIBRARY_LIST.parsed':(60,90), 1050 ' - N3_FULL.parsed':(600,800), 1051 ' - N3_SOCIAL_CURRENTREAD.parsed':(120,186)} 1052 1053 for ending, resize in file_endings.items(): 1054 fpath = path + ending 1055 fpath = self.normalize_path(fpath.replace('/', os.sep)) 1056 1057 if os.path.exists(fpath): 1058 with lopen(cover, 'rb') as f: 1059 data = f.read() 1060 1061 # Return the data resized and grayscaled if 1062 # required 1063 data = save_cover_data_to(data, grayscale=uploadgrayscale, resize_to=resize, minify_to=resize) 1064 1065 with lopen(fpath, 'wb') as f: 1066 f.write(data) 1067 fsync(f) 1068 1069 else: 1070 debug_print("ImageID could not be retrieved from the database") 1071 1072 def prepare_addable_books(self, paths): 1073 ''' 1074 The Kobo supports an encrypted epub referred to as a kepub 1075 Unfortunately Kobo decided to put the files on the device 1076 with no file extension. I just hope that decision causes 1077 them as much grief as it does me :-) 1078 1079 This has to make a temporary copy of the book files with a 1080 epub extension to allow calibre's normal processing to 1081 deal with the file appropriately 1082 ''' 1083 for idx, path in enumerate(paths): 1084 if path.find('kepub') >= 0: 1085 with closing(lopen(path, 'rb')) as r: 1086 tf = PersistentTemporaryFile(suffix='.epub') 1087 shutil.copyfileobj(r, tf) 1088# tf.write(r.read()) 1089 paths[idx] = tf.name 1090 return paths 1091 1092 @classmethod 1093 def config_widget(self): 1094 # TODO: Cleanup the following 1095 self.current_friendly_name = self.gui_name 1096 1097 from calibre.gui2.device_drivers.tabbed_device_config import TabbedDeviceConfig 1098 return TabbedDeviceConfig(self.settings(), self.FORMATS, self.SUPPORTS_SUB_DIRS, 1099 self.MUST_READ_METADATA, self.SUPPORTS_USE_AUTHOR_SORT, 1100 self.EXTRA_CUSTOMIZATION_MESSAGE, self, 1101 extra_customization_choices=self.EXTRA_CUSTOMIZATION_CHOICES) 1102 1103 def migrate_old_settings(self, old_settings): 1104 1105 OPT_COLLECTIONS = 0 1106 OPT_UPLOAD_COVERS = 1 1107 OPT_UPLOAD_GRAYSCALE_COVERS = 2 1108 OPT_SHOW_EXPIRED_BOOK_RECORDS = 3 1109 OPT_SHOW_PREVIEWS = 4 1110 OPT_SHOW_RECOMMENDATIONS = 5 1111 OPT_SUPPORT_NEWER_FIRMWARE = 6 1112 1113 p = {} 1114 p['format_map'] = old_settings.format_map 1115 p['save_template'] = old_settings.save_template 1116 p['use_subdirs'] = old_settings.use_subdirs 1117 p['read_metadata'] = old_settings.read_metadata 1118 p['use_author_sort'] = old_settings.use_author_sort 1119 p['extra_customization'] = old_settings.extra_customization 1120 1121 p['collections_columns'] = old_settings.extra_customization[OPT_COLLECTIONS] 1122 1123 p['upload_covers'] = old_settings.extra_customization[OPT_UPLOAD_COVERS] 1124 p['upload_grayscale'] = old_settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS] 1125 1126 p['show_expired_books'] = old_settings.extra_customization[OPT_SHOW_EXPIRED_BOOK_RECORDS] 1127 p['show_previews'] = old_settings.extra_customization[OPT_SHOW_PREVIEWS] 1128 p['show_recommendations'] = old_settings.extra_customization[OPT_SHOW_RECOMMENDATIONS] 1129 1130 p['support_newer_firmware'] = old_settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE] 1131 1132 return p 1133 1134 def create_annotations_path(self, mdata, device_path=None): 1135 if device_path: 1136 return device_path 1137 return USBMS.create_annotations_path(self, mdata) 1138 1139 def get_annotations(self, path_map): 1140 from calibre.devices.kobo.bookmark import Bookmark 1141 EPUB_FORMATS = ['epub'] 1142 epub_formats = set(EPUB_FORMATS) 1143 1144 def get_storage(): 1145 storage = [] 1146 if self._main_prefix: 1147 storage.append(os.path.join(self._main_prefix, self.EBOOK_DIR_MAIN)) 1148 if self._card_a_prefix: 1149 storage.append(os.path.join(self._card_a_prefix, self.EBOOK_DIR_CARD_A)) 1150 if self._card_b_prefix: 1151 storage.append(os.path.join(self._card_b_prefix, self.EBOOK_DIR_CARD_B)) 1152 return storage 1153 1154 def resolve_bookmark_paths(storage, path_map): 1155 pop_list = [] 1156 book_ext = {} 1157 for book_id in path_map: 1158 file_fmts = set() 1159 for fmt in path_map[book_id]['fmts']: 1160 file_fmts.add(fmt) 1161 bookmark_extension = None 1162 if file_fmts.intersection(epub_formats): 1163 book_extension = list(file_fmts.intersection(epub_formats))[0] 1164 bookmark_extension = 'epub' 1165 1166 if bookmark_extension: 1167 for vol in storage: 1168 bkmk_path = path_map[book_id]['path'] 1169 bkmk_path = bkmk_path 1170 if os.path.exists(bkmk_path): 1171 path_map[book_id] = bkmk_path 1172 book_ext[book_id] = book_extension 1173 break 1174 else: 1175 pop_list.append(book_id) 1176 else: 1177 pop_list.append(book_id) 1178 1179 # Remove non-existent bookmark templates 1180 for book_id in pop_list: 1181 path_map.pop(book_id) 1182 return path_map, book_ext 1183 1184 storage = get_storage() 1185 path_map, book_ext = resolve_bookmark_paths(storage, path_map) 1186 1187 bookmarked_books = {} 1188 with closing(self.device_database_connection(use_row_factory=True)) as connection: 1189 for book_id in path_map: 1190 extension = os.path.splitext(path_map[book_id])[1] 1191 ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(path_map[book_id]) 1192 ContentID = self.contentid_from_path(path_map[book_id], ContentType) 1193 debug_print("get_annotations - ContentID: ", ContentID, "ContentType: ", ContentType) 1194 1195 bookmark_ext = extension 1196 1197 myBookmark = Bookmark(connection, ContentID, path_map[book_id], book_id, book_ext[book_id], bookmark_ext) 1198 bookmarked_books[book_id] = self.UserAnnotation(type='kobo_bookmark', value=myBookmark) 1199 1200 # This returns as job.result in gui2.ui.annotations_fetched(self,job) 1201 return bookmarked_books 1202 1203 def generate_annotation_html(self, bookmark): 1204 import calendar 1205 from calibre.ebooks.BeautifulSoup import BeautifulSoup 1206 # Returns <div class="user_annotations"> ... </div> 1207 # last_read_location = bookmark.last_read_location 1208 # timestamp = bookmark.timestamp 1209 percent_read = bookmark.percent_read 1210 debug_print("Kobo::generate_annotation_html - last_read: ", bookmark.last_read) 1211 if bookmark.last_read is not None: 1212 try: 1213 last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(calendar.timegm(time.strptime(bookmark.last_read, "%Y-%m-%dT%H:%M:%S")))) 1214 except: 1215 try: 1216 last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(calendar.timegm(time.strptime(bookmark.last_read, "%Y-%m-%dT%H:%M:%S.%f")))) 1217 except: 1218 try: 1219 last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(calendar.timegm(time.strptime(bookmark.last_read, "%Y-%m-%dT%H:%M:%SZ")))) 1220 except: 1221 last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()) 1222 else: 1223 # self.datetime = time.gmtime() 1224 last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()) 1225 1226 # debug_print("Percent read: ", percent_read) 1227 ka_soup = BeautifulSoup() 1228 dtc = 0 1229 divTag = ka_soup.new_tag('div') 1230 divTag['class'] = 'user_annotations' 1231 1232 # Add the last-read location 1233 if bookmark.book_format == 'epub': 1234 markup = _("<hr /><b>Book last read:</b> %(time)s<br /><b>Percentage read:</b> %(pr)d%%<hr />") % dict( 1235 time=last_read, 1236 # loc=last_read_location, 1237 pr=percent_read) 1238 else: 1239 markup = _("<hr /><b>Book last read:</b> %(time)s<br /><b>Percentage read:</b> %(pr)d%%<hr />") % dict( 1240 time=last_read, 1241 # loc=last_read_location, 1242 pr=percent_read) 1243 spanTag = BeautifulSoup('<span style="font-weight:normal">' + markup + '</span>').find('span') 1244 1245 divTag.insert(dtc, spanTag) 1246 dtc += 1 1247 divTag.insert(dtc, ka_soup.new_tag('br')) 1248 dtc += 1 1249 1250 if bookmark.user_notes: 1251 user_notes = bookmark.user_notes 1252 annotations = [] 1253 1254 # Add the annotations sorted by location 1255 for location in sorted(user_notes): 1256 if user_notes[location]['type'] == 'Bookmark': 1257 annotations.append( 1258 _('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br /><b>%(typ)s</b>' 1259 '<br /><b>Chapter Progress:</b> %(chapter_progress)s%%<br />%(annotation)s<br /><hr />') % dict( 1260 chapter=user_notes[location]['chapter'], 1261 dl=user_notes[location]['displayed_location'], 1262 typ=user_notes[location]['type'], 1263 chapter_title=user_notes[location]['chapter_title'], 1264 chapter_progress=user_notes[location]['chapter_progress'], 1265 annotation=user_notes[location]['annotation'] if user_notes[location]['annotation'] is not None else "")) 1266 elif user_notes[location]['type'] == 'Highlight': 1267 annotations.append( 1268 _('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br /><b>%(typ)s</b><br />' 1269 '<b>Chapter progress:</b> %(chapter_progress)s%%<br /><b>Highlight:</b> %(text)s<br /><hr />') % dict( 1270 chapter=user_notes[location]['chapter'], 1271 dl=user_notes[location]['displayed_location'], 1272 typ=user_notes[location]['type'], 1273 chapter_title=user_notes[location]['chapter_title'], 1274 chapter_progress=user_notes[location]['chapter_progress'], 1275 text=user_notes[location]['text'])) 1276 elif user_notes[location]['type'] == 'Annotation': 1277 annotations.append( 1278 _('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br />' 1279 '<b>%(typ)s</b><br /><b>Chapter progress:</b> %(chapter_progress)s%%<br /><b>Highlight:</b> %(text)s<br />' 1280 '<b>Notes:</b> %(annotation)s<br /><hr />') % dict( 1281 chapter=user_notes[location]['chapter'], 1282 dl=user_notes[location]['displayed_location'], 1283 typ=user_notes[location]['type'], 1284 chapter_title=user_notes[location]['chapter_title'], 1285 chapter_progress=user_notes[location]['chapter_progress'], 1286 text=user_notes[location]['text'], 1287 annotation=user_notes[location]['annotation'])) 1288 else: 1289 annotations.append( 1290 _('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br />' 1291 '<b>%(typ)s</b><br /><b>Chapter progress:</b> %(chapter_progress)s%%<br /><b>Highlight:</b> %(text)s<br />' 1292 '<b>Notes:</b> %(annotation)s<br /><hr />') % dict( 1293 chapter=user_notes[location]['chapter'], 1294 dl=user_notes[location]['displayed_location'], 1295 typ=user_notes[location]['type'], 1296 chapter_title=user_notes[location]['chapter_title'], 1297 chapter_progress=user_notes[location]['chapter_progress'], 1298 text=user_notes[location]['text'], 1299 annotation=user_notes[location]['annotation'])) 1300 1301 for annotation in annotations: 1302 annot = BeautifulSoup('<span>' + annotation + '</span>').find('span') 1303 divTag.insert(dtc, annot) 1304 dtc += 1 1305 1306 ka_soup.insert(0,divTag) 1307 return ka_soup 1308 1309 def add_annotation_to_library(self, db, db_id, annotation): 1310 from calibre.ebooks.BeautifulSoup import prettify 1311 bm = annotation 1312 ignore_tags = {'Catalog', 'Clippings'} 1313 1314 if bm.type == 'kobo_bookmark' and bm.value.last_read: 1315 mi = db.get_metadata(db_id, index_is_id=True) 1316 debug_print("KOBO:add_annotation_to_library - Title: ", mi.title) 1317 user_notes_soup = self.generate_annotation_html(bm.value) 1318 if mi.comments: 1319 a_offset = mi.comments.find('<div class="user_annotations">') 1320 ad_offset = mi.comments.find('<hr class="annotations_divider" />') 1321 1322 if a_offset >= 0: 1323 mi.comments = mi.comments[:a_offset] 1324 if ad_offset >= 0: 1325 mi.comments = mi.comments[:ad_offset] 1326 if set(mi.tags).intersection(ignore_tags): 1327 return 1328 if mi.comments: 1329 hrTag = user_notes_soup.new_tag('hr') 1330 hrTag['class'] = 'annotations_divider' 1331 user_notes_soup.insert(0, hrTag) 1332 1333 mi.comments += prettify(user_notes_soup) 1334 else: 1335 mi.comments = prettify(user_notes_soup) 1336 # Update library comments 1337 db.set_comment(db_id, mi.comments) 1338 1339 # Add bookmark file to db_id 1340 # NOTE: As it is, this copied the book from the device back to the library. That meant it replaced the 1341 # existing file. Taking this out for that reason, but some books have a ANNOT file that could be 1342 # copied. 1343# db.add_format_with_hooks(db_id, bm.value.bookmark_extension, 1344# bm.value.path, index_is_id=True) 1345 1346 1347class KOBOTOUCH(KOBO): 1348 name = 'KoboTouch' 1349 gui_name = 'Kobo eReader' 1350 author = 'David Forrester' 1351 description = _( 1352 'Communicate with the Kobo Touch, Glo, Mini, Aura HD,' 1353 ' Aura H2O, Glo HD, Touch 2, Aura ONE, Aura Edition 2,' 1354 ' Aura H2O Edition 2, Clara HD, Forma, Libra H2O, Elipsa,' 1355 ' Sage and Libra 2 eReaders.' 1356 ' Based on the existing Kobo driver by %s.') % KOBO.author 1357# icon = I('devices/kobotouch.jpg') 1358 1359 supported_dbversion = 166 1360 min_supported_dbversion = 53 1361 min_dbversion_series = 65 1362 min_dbversion_externalid = 65 1363 min_dbversion_archive = 71 1364 min_dbversion_images_on_sdcard = 77 1365 min_dbversion_activity = 77 1366 min_dbversion_keywords = 82 1367 min_dbversion_seriesid = 136 1368 1369 # Starting with firmware version 3.19.x, the last number appears to be is a 1370 # build number. A number will be recorded here but it can be safely ignored 1371 # when testing the firmware version. 1372 max_supported_fwversion = (4, 30, 18838) 1373 # The following document firmware versions where new function or devices were added. 1374 # Not all are used, but this feels a good place to record it. 1375 min_fwversion_shelves = (2, 0, 0) 1376 min_fwversion_images_on_sdcard = (2, 4, 1) 1377 min_fwversion_images_tree = (2, 9, 0) # Cover images stored in tree under .kobo-images 1378 min_aurah2o_fwversion = (3, 7, 0) 1379 min_reviews_fwversion = (3, 12, 0) 1380 min_glohd_fwversion = (3, 14, 0) 1381 min_auraone_fwversion = (3, 20, 7280) 1382 min_fwversion_overdrive = (4, 0, 7523) 1383 min_clarahd_fwversion = (4, 8, 11090) 1384 min_forma_fwversion = (4, 11, 11879) 1385 min_librah20_fwversion = (4, 16, 13337) # "Reviewers" release. 1386 min_fwversion_epub_location = (4, 17, 13651) # ePub reading location without full contentid. 1387 min_fwversion_dropbox = (4, 18, 13737) # The Forma only at this point. 1388 min_fwversion_serieslist = (4, 20, 14601) # Series list needs the SeriesID to be set. 1389 min_nia_fwversion = (4, 22, 15202) 1390 min_elipsa_fwversion = (4, 28, 17820) 1391 min_libra2_fwversion = (4, 29, 18730) 1392 min_sage_fwversion = (4, 29, 18730) 1393 min_fwversion_audiobooks = (4, 29, 18730) 1394 1395 has_kepubs = True 1396 1397 booklist_class = KTCollectionsBookList 1398 book_class = Book 1399 kobo_series_dict = {} 1400 1401 MAX_PATH_LEN = 185 # 250 - (len(" - N3_LIBRARY_SHELF.parsed") + len("F:\.kobo\images\")) 1402 KOBO_EXTRA_CSSFILE = 'kobo_extra.css' 1403 1404 EXTRA_CUSTOMIZATION_MESSAGE = [] 1405 EXTRA_CUSTOMIZATION_DEFAULT = [] 1406 1407 OSX_MAIN_MEM_VOL_PAT = re.compile(r'/KOBOeReader') 1408 1409 opts = None 1410 1411 TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ" 1412 1413 AURA_PRODUCT_ID = [0x4203] 1414 AURA_EDITION2_PRODUCT_ID = [0x4226] 1415 AURA_HD_PRODUCT_ID = [0x4193] 1416 AURA_H2O_PRODUCT_ID = [0x4213] 1417 AURA_H2O_EDITION2_PRODUCT_ID = [0x4227] 1418 AURA_ONE_PRODUCT_ID = [0x4225] 1419 CLARA_HD_PRODUCT_ID = [0x4228] 1420 ELIPSA_PRODUCT_ID = [0x4233] 1421 FORMA_PRODUCT_ID = [0x4229] 1422 GLO_PRODUCT_ID = [0x4173] 1423 GLO_HD_PRODUCT_ID = [0x4223] 1424 LIBRA_H2O_PRODUCT_ID = [0x4232] 1425 LIBRA2_PRODUCT_ID = [0x4234] 1426 MINI_PRODUCT_ID = [0x4183] 1427 NIA_PRODUCT_ID = [0x4230] 1428 SAGE_PRODUCT_ID = [0x4231] 1429 TOUCH_PRODUCT_ID = [0x4163] 1430 TOUCH2_PRODUCT_ID = [0x4224] 1431 PRODUCT_ID = AURA_PRODUCT_ID + AURA_EDITION2_PRODUCT_ID + \ 1432 AURA_HD_PRODUCT_ID + AURA_H2O_PRODUCT_ID + AURA_H2O_EDITION2_PRODUCT_ID + \ 1433 GLO_PRODUCT_ID + GLO_HD_PRODUCT_ID + \ 1434 MINI_PRODUCT_ID + TOUCH_PRODUCT_ID + TOUCH2_PRODUCT_ID + \ 1435 AURA_ONE_PRODUCT_ID + CLARA_HD_PRODUCT_ID + FORMA_PRODUCT_ID + LIBRA_H2O_PRODUCT_ID + \ 1436 NIA_PRODUCT_ID + ELIPSA_PRODUCT_ID + \ 1437 SAGE_PRODUCT_ID + LIBRA2_PRODUCT_ID 1438 1439 BCD = [0x0110, 0x0326, 0x401, 0x409] 1440 1441 KOBO_AUDIOBOOKS_MIMETYPES = ['application/octet-stream', 'application/x-kobo-mp3z'] 1442 1443 # Image file name endings. Made up of: image size, min_dbversion, max_dbversion, isFullSize, 1444 # Note: "200" has been used just as a much larger number than the current versions. It is just a lazy 1445 # way of making it open ended. 1446 # NOTE: Values pulled from Nickel by @geek1011, 1447 # c.f., this handy recap: https://github.com/shermp/Kobo-UNCaGED/issues/16#issuecomment-494229994 1448 # Only the N3_FULL values differ, as they should match the screen's effective resolution. 1449 # Note that all Kobo devices share a common AR at roughly 0.75, 1450 # so results should be similar, no matter the exact device. 1451 # Common to all Kobo models 1452 COMMON_COVER_FILE_ENDINGS = { 1453 # Used for Details screen before FW2.8.1, then for current book tile on home screen 1454 ' - N3_LIBRARY_FULL.parsed': [(355,530),0, 200,False,], 1455 # Used for library lists 1456 ' - N3_LIBRARY_GRID.parsed': [(149,223),0, 200,False,], 1457 # Used for library lists 1458 ' - N3_LIBRARY_LIST.parsed': [(60,90),0, 53,False,], 1459 # Used for Details screen from FW2.8.1 1460 ' - AndroidBookLoadTablet_Aspect.parsed': [(355,530), 82, 100,False,], 1461 } 1462 # Legacy 6" devices 1463 LEGACY_COVER_FILE_ENDINGS = { 1464 # Used for screensaver, home screen 1465 ' - N3_FULL.parsed': [(600,800),0, 200,True,], 1466 } 1467 # Glo 1468 GLO_COVER_FILE_ENDINGS = { 1469 # Used for screensaver, home screen 1470 ' - N3_FULL.parsed': [(758,1024),0, 200,True,], 1471 } 1472 # Aura 1473 AURA_COVER_FILE_ENDINGS = { 1474 # Used for screensaver, home screen 1475 # NOTE: The Aura's bezel covers 10 pixels at the bottom. 1476 # Kobo officially advertised the screen resolution with those chopped off. 1477 ' - N3_FULL.parsed': [(758,1014),0, 200,True,], 1478 } 1479 # Glo HD and Clara HD share resolution, so the image sizes should be the same. 1480 GLO_HD_COVER_FILE_ENDINGS = { 1481 # Used for screensaver, home screen 1482 ' - N3_FULL.parsed': [(1072,1448), 0, 200,True,], 1483 } 1484 AURA_HD_COVER_FILE_ENDINGS = { 1485 # Used for screensaver, home screen 1486 ' - N3_FULL.parsed': [(1080,1440), 0, 200,True,], 1487 } 1488 AURA_H2O_COVER_FILE_ENDINGS = { 1489 # Used for screensaver, home screen 1490 # NOTE: The H2O's bezel covers 11 pixels at the top. 1491 # Unlike on the Aura, Nickel fails to account for this when generating covers. 1492 # c.f., https://github.com/shermp/Kobo-UNCaGED/pull/17#discussion_r286209827 1493 ' - N3_FULL.parsed': [(1080,1429), 0, 200,True,], 1494 } 1495 # Aura ONE and Elipsa have the same resolution. 1496 AURA_ONE_COVER_FILE_ENDINGS = { 1497 # Used for screensaver, home screen 1498 ' - N3_FULL.parsed': [(1404,1872), 0, 200,True,], 1499 } 1500 FORMA_COVER_FILE_ENDINGS = { 1501 # Used for screensaver, home screen 1502 # NOTE: Nickel currently fails to honor the real screen resolution when generating covers, 1503 # choosing instead to follow the Aura One codepath. 1504 ' - N3_FULL.parsed': [(1440,1920), 0, 200,True,], 1505 } 1506 LIBRA_H2O_COVER_FILE_ENDINGS = { 1507 # Used for screensaver, home screen 1508 ' - N3_FULL.parsed': [(1264,1680), 0, 200,True,], 1509 } 1510 # Following are the sizes used with pre2.1.4 firmware 1511# COVER_FILE_ENDINGS = { 1512# ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen 1513# ' - N3_LIBRARY_FULL.parsed':[(600,800),0, 99,], 1514# ' - N3_LIBRARY_GRID.parsed':[(149,223),0, 99,], # Used for library lists 1515# ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,], 1516# ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,], 1517# ' - N3_FULL.parsed':[(600,800),0, 99,], # Used for screensaver if "Full screen" is checked. 1518# } 1519 1520 def __init__(self, *args, **kwargs): 1521 KOBO.__init__(self, *args, **kwargs) 1522 self.plugboards = self.plugboard_func = None 1523 1524 def initialize(self): 1525 super().initialize() 1526 self.bookshelvelist = [] 1527 1528 def get_device_information(self, end_session=True): 1529 self.set_device_name() 1530 return super().get_device_information(end_session) 1531 1532 def open_linux(self): 1533 super().open_linux() 1534 1535 self.swap_drives_if_needed() 1536 1537 def open_osx(self): 1538 # Just dump some info to the logs. 1539 super().open_osx() 1540 1541 # Wrap some debugging output in a try/except so that it is unlikely to break things completely. 1542 try: 1543 if DEBUG: 1544 from calibre_extensions.usbobserver import get_mounted_filesystems 1545 mount_map = get_mounted_filesystems() 1546 debug_print('KoboTouch::open_osx - mount_map=', mount_map) 1547 debug_print('KoboTouch::open_osx - self._main_prefix=', self._main_prefix) 1548 debug_print('KoboTouch::open_osx - self._card_a_prefix=', self._card_a_prefix) 1549 debug_print('KoboTouch::open_osx - self._card_b_prefix=', self._card_b_prefix) 1550 except: 1551 pass 1552 1553 self.swap_drives_if_needed() 1554 1555 def swap_drives_if_needed(self): 1556 # Check the drives have been mounted as expected and swap if needed. 1557 if self._card_a_prefix is None: 1558 return 1559 1560 if not self.is_main_drive(self._main_prefix): 1561 temp_prefix = self._main_prefix 1562 self._main_prefix = self._card_a_prefix 1563 self._card_a_prefix = temp_prefix 1564 1565 def windows_sort_drives(self, drives): 1566 return self.sort_drives(drives) 1567 1568 def sort_drives(self, drives): 1569 if len(drives) < 2: 1570 return drives 1571 main = drives.get('main', None) 1572 carda = drives.get('carda', None) 1573 if main and carda and not self.is_main_drive(main): 1574 drives['main'] = carda 1575 drives['carda'] = main 1576 debug_print('KoboTouch::sort_drives - swapped drives - main=%s, carda=%s' % (drives['main'], drives['carda'])) 1577 return drives 1578 1579 def is_main_drive(self, drive): 1580 debug_print('KoboTouch::is_main_drive - drive=%s, path=%s' % (drive, os.path.join(drive, '.kobo'))) 1581 return os.path.exists(self.normalize_path(os.path.join(drive, '.kobo'))) 1582 1583 def books(self, oncard=None, end_session=True): 1584 debug_print("KoboTouch:books - oncard='%s'"%oncard) 1585 self.debugging_title = self.get_debugging_title() 1586 1587 dummy_bl = self.booklist_class(None, None, None) 1588 1589 if oncard == 'carda' and not self._card_a_prefix: 1590 self.report_progress(1.0, _('Getting list of books on device...')) 1591 debug_print("KoboTouch:books - Asked to process 'carda', but do not have one!") 1592 return dummy_bl 1593 elif oncard == 'cardb' and not self._card_b_prefix: 1594 self.report_progress(1.0, _('Getting list of books on device...')) 1595 debug_print("KoboTouch:books - Asked to process 'cardb', but do not have one!") 1596 return dummy_bl 1597 elif oncard and oncard != 'carda' and oncard != 'cardb': 1598 self.report_progress(1.0, _('Getting list of books on device...')) 1599 debug_print("KoboTouch:books - unknown card") 1600 return dummy_bl 1601 1602 prefix = self._card_a_prefix if oncard == 'carda' else \ 1603 self._card_b_prefix if oncard == 'cardb' \ 1604 else self._main_prefix 1605 debug_print("KoboTouch:books - oncard='%s', prefix='%s'"%(oncard, prefix)) 1606 1607 self.fwversion = self.get_firmware_version() 1608 1609 debug_print('Kobo device: %s' % self.gui_name) 1610 debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs) 1611 debug_print('Version of firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs) 1612 debug_print('Firmware supports cover image tree:', self.fwversion >= self.min_fwversion_images_tree) 1613 1614 self.booklist_class.rebuild_collections = self.rebuild_collections 1615 1616 # get the metadata cache 1617 bl = self.booklist_class(oncard, prefix, self.settings) 1618 1619 opts = self.settings() 1620 debug_print("KoboTouch:books - opts.extra_customization=", opts.extra_customization) 1621 debug_print("KoboTouch:books - driver options=", self) 1622 debug_print("KoboTouch:books - prefs['manage_device_metadata']=", prefs['manage_device_metadata']) 1623 debugging_title = self.debugging_title 1624 debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title) 1625 bl.set_debugging_title(debugging_title) 1626 debug_print("KoboTouch:books - length bl=%d"%len(bl)) 1627 need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) 1628 debug_print("KoboTouch:books - length bl after sync=%d"%len(bl)) 1629 1630 # make a dict cache of paths so the lookup in the loop below is faster. 1631 bl_cache = {} 1632 for idx,b in enumerate(bl): 1633 bl_cache[b.lpath] = idx 1634 1635 def update_booklist(prefix, path, ContentID, ContentType, MimeType, ImageID, 1636 title, authors, DateCreated, Description, Publisher, 1637 series, seriesnumber, SeriesID, SeriesNumberFloat, 1638 ISBN, Language, Subtitle, 1639 readstatus, expired, favouritesindex, accessibility, isdownloaded, 1640 userid, bookshelves 1641 ): 1642 show_debug = self.is_debugging_title(title) 1643# show_debug = authors == 'L. Frank Baum' 1644 if show_debug: 1645 debug_print("KoboTouch:update_booklist - title='%s'"%title, "ContentType=%s"%ContentType, "isdownloaded=", isdownloaded) 1646 debug_print( 1647 " prefix=%s, DateCreated=%s, readstatus=%d, MimeType=%s, expired=%d, favouritesindex=%d, accessibility=%d, isdownloaded=%s"% 1648 (prefix, DateCreated, readstatus, MimeType, expired, favouritesindex, accessibility, isdownloaded,)) 1649 changed = False 1650 try: 1651 lpath = path.partition(self.normalize_path(prefix))[2] 1652 if lpath.startswith(os.sep): 1653 lpath = lpath[len(os.sep):] 1654 lpath = lpath.replace('\\', '/') 1655# debug_print("KoboTouch:update_booklist - LPATH: ", lpath, " - Title: " , title) 1656 1657 playlist_map = {} 1658 1659 if lpath not in playlist_map: 1660 playlist_map[lpath] = [] 1661 1662 allow_shelves = True 1663 if readstatus == 1: 1664 playlist_map[lpath].append('Im_Reading') 1665 elif readstatus == 2: 1666 playlist_map[lpath].append('Read') 1667 elif readstatus == 3: 1668 playlist_map[lpath].append('Closed') 1669 1670 # Related to a bug in the Kobo firmware that leaves an expired row for deleted books 1671 # this shows an expired Collection so the user can decide to delete the book 1672 if expired == 3: 1673 playlist_map[lpath].append('Expired') 1674 allow_shelves = False 1675 # A SHORTLIST is supported on the touch but the data field is there on most earlier models 1676 if favouritesindex == 1: 1677 playlist_map[lpath].append('Shortlist') 1678 1679 # Audiobooks are identified by their MimeType 1680 if MimeType in self.KOBO_AUDIOBOOKS_MIMETYPES: 1681 playlist_map[lpath].append('Audiobook') 1682 1683 # The following is in flux: 1684 # - FW2.0.0, DBVersion 53,55 accessibility == 1 1685 # - FW2.1.2 beta, DBVersion == 56, accessibility == -1: 1686 # So, the following should be OK 1687 if isdownloaded == 'false': 1688 if self.dbversion < 56 and accessibility <= 1 or self.dbversion >= 56 and accessibility == -1: 1689 playlist_map[lpath].append('Deleted') 1690 allow_shelves = False 1691 if show_debug: 1692 debug_print("KoboTouch:update_booklist - have a deleted book") 1693 elif self.supports_kobo_archive() and (accessibility == 1 or accessibility == 2): 1694 playlist_map[lpath].append('Archived') 1695 allow_shelves = True 1696 1697 # Label Previews and Recommendations 1698 if accessibility == 6: 1699 if userid == '': 1700 playlist_map[lpath].append('Recommendation') 1701 allow_shelves = False 1702 else: 1703 playlist_map[lpath].append('Preview') 1704 allow_shelves = False 1705 elif accessibility == 4: # Pre 2.x.x firmware 1706 playlist_map[lpath].append('Recommendation') 1707 allow_shelves = False 1708 elif accessibility == 8: # From 4.22 but waa probably there earlier. 1709 playlist_map[lpath].append('Kobo Plus') 1710 allow_shelves = True 1711 elif accessibility == 9: # From 4.0 on Aura One 1712 playlist_map[lpath].append('OverDrive') 1713 allow_shelves = True 1714 1715 kobo_collections = playlist_map[lpath][:] 1716 1717 if allow_shelves: 1718 # debug_print('KoboTouch:update_booklist - allowing shelves - title=%s' % title) 1719 if len(bookshelves) > 0: 1720 playlist_map[lpath].extend(bookshelves) 1721 1722 if show_debug: 1723 debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map) 1724 1725 path = self.normalize_path(path) 1726 # print "Normalized FileName: " + path 1727 1728 # Collect the Kobo metadata 1729 authors_list = [a.strip() for a in authors.split("&")] if authors is not None else [_('Unknown')] 1730 kobo_metadata = Metadata(title, authors_list) 1731 kobo_metadata.series = series 1732 kobo_metadata.series_index = seriesnumber 1733 kobo_metadata.comments = Description 1734 kobo_metadata.publisher = Publisher 1735 kobo_metadata.language = Language 1736 kobo_metadata.isbn = ISBN 1737 if DateCreated is not None: 1738 try: 1739 kobo_metadata.pubdate = parse_date(DateCreated, assume_utc=True) 1740 except: 1741 try: 1742 kobo_metadata.pubdate = datetime.strptime(DateCreated, "%Y-%m-%dT%H:%M:%S.%fZ") 1743 except: 1744 debug_print("KoboTouch:update_booklist - Cannot convert date - DateCreated='%s'"%DateCreated) 1745 1746 idx = bl_cache.get(lpath, None) 1747 if idx is not None: # and not (accessibility == 1 and isdownloaded == 'false'): 1748 if show_debug: 1749 self.debug_index = idx 1750 debug_print("KoboTouch:update_booklist - idx=%d"%idx) 1751 debug_print("KoboTouch:update_booklist - lpath=%s"%lpath) 1752 debug_print('KoboTouch:update_booklist - bl[idx].device_collections=', bl[idx].device_collections) 1753 debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map) 1754 debug_print('KoboTouch:update_booklist - bookshelves=', bookshelves) 1755 debug_print('KoboTouch:update_booklist - kobo_collections=', kobo_collections) 1756 debug_print('KoboTouch:update_booklist - series="%s"' % bl[idx].series) 1757 debug_print('KoboTouch:update_booklist - the book=', bl[idx]) 1758 debug_print('KoboTouch:update_booklist - the authors=', bl[idx].authors) 1759 debug_print('KoboTouch:update_booklist - application_id=', bl[idx].application_id) 1760 debug_print('KoboTouch:update_booklist - size=', bl[idx].size) 1761 bl_cache[lpath] = None 1762 1763 if ImageID is not None: 1764 imagename = self.imagefilename_from_imageID(prefix, ImageID) 1765 if imagename is not None: 1766 bl[idx].thumbnail = ImageWrapper(imagename) 1767 if (ContentType == '6' and MimeType != 'application/x-kobo-epub+zip'): 1768 if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))): 1769 if self.update_metadata_item(bl[idx]): 1770 # debug_print("KoboTouch:update_booklist - update_metadata_item returned true") 1771 changed = True 1772 else: 1773 debug_print(" Strange: The file: ", prefix, lpath, " does not exist!") 1774 debug_print("KoboTouch:update_booklist - book size=", bl[idx].size) 1775 1776 if show_debug: 1777 debug_print("KoboTouch:update_booklist - ContentID='%s'"%ContentID) 1778 bl[idx].contentID = ContentID 1779 bl[idx].kobo_metadata = kobo_metadata 1780 bl[idx].kobo_series = series 1781 bl[idx].kobo_series_number = seriesnumber 1782 bl[idx].kobo_series_id = SeriesID 1783 bl[idx].kobo_series_number_float = SeriesNumberFloat 1784 bl[idx].kobo_subtitle = Subtitle 1785 bl[idx].can_put_on_shelves = allow_shelves 1786 bl[idx].mime = MimeType 1787 1788 if not bl[idx].is_sideloaded and bl[idx].has_kobo_series and SeriesID is not None: 1789 if show_debug: 1790 debug_print('KoboTouch:update_booklist - Have purchased kepub with series, saving SeriesID=', SeriesID) 1791 self.kobo_series_dict[series] = SeriesID 1792 1793 if lpath in playlist_map: 1794 bl[idx].device_collections = playlist_map.get(lpath,[]) 1795 bl[idx].current_shelves = bookshelves 1796 bl[idx].kobo_collections = kobo_collections 1797 1798 if show_debug: 1799 debug_print('KoboTouch:update_booklist - updated bl[idx].device_collections=', bl[idx].device_collections) 1800 debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map, 'changed=', changed) 1801# debug_print('KoboTouch:update_booklist - book=', bl[idx]) 1802 debug_print("KoboTouch:update_booklist - book class=%s"%bl[idx].__class__) 1803 debug_print("KoboTouch:update_booklist - book title=%s"%bl[idx].title) 1804 else: 1805 if show_debug: 1806 debug_print('KoboTouch:update_booklist - idx is none') 1807 try: 1808 if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))): 1809 book = self.book_from_path(prefix, lpath, title, authors, MimeType, DateCreated, ContentType, ImageID) 1810 else: 1811 if isdownloaded == 'true': # A recommendation or preview is OK to not have a file 1812 debug_print(" Strange: The file: ", prefix, lpath, " does not exist!") 1813 title = "FILE MISSING: " + title 1814 book = self.book_class(prefix, lpath, title, authors, MimeType, DateCreated, ContentType, ImageID, size=0) 1815 if show_debug: 1816 debug_print('KoboTouch:update_booklist - book file does not exist. ContentID="%s"'%ContentID) 1817 1818 except Exception as e: 1819 debug_print("KoboTouch:update_booklist - exception creating book: '%s'"%str(e)) 1820 debug_print(" prefix: ", prefix, "lpath: ", lpath, "title: ", title, "authors: ", authors, 1821 "MimeType: ", MimeType, "DateCreated: ", DateCreated, "ContentType: ", ContentType, "ImageID: ", ImageID) 1822 raise 1823 1824 if show_debug: 1825 debug_print('KoboTouch:update_booklist - class:', book.__class__) 1826# debug_print(' resolution:', book.__class__.__mro__) 1827 debug_print(" contentid: '%s'"%book.contentID) 1828 debug_print(" title:'%s'"%book.title) 1829 debug_print(" the book:", book) 1830 debug_print(" author_sort:'%s'"%book.author_sort) 1831 debug_print(" bookshelves:", bookshelves) 1832 debug_print(" kobo_collections:", kobo_collections) 1833 1834 # print 'Update booklist' 1835 book.device_collections = playlist_map.get(lpath,[]) # if lpath in playlist_map else [] 1836 book.current_shelves = bookshelves 1837 book.kobo_collections = kobo_collections 1838 book.contentID = ContentID 1839 book.kobo_metadata = kobo_metadata 1840 book.kobo_series = series 1841 book.kobo_series_number = seriesnumber 1842 book.kobo_series_id = SeriesID 1843 book.kobo_series_number_float = SeriesNumberFloat 1844 book.kobo_subtitle = Subtitle 1845 book.can_put_on_shelves = allow_shelves 1846# debug_print('KoboTouch:update_booklist - title=', title, 'book.device_collections', book.device_collections) 1847 1848 if not book.is_sideloaded and book.has_kobo_series and SeriesID is not None: 1849 if show_debug: 1850 debug_print('KoboTouch:update_booklist - Have purchased kepub with series, saving SeriesID=', SeriesID) 1851 self.kobo_series_dict[series] = SeriesID 1852 1853 if bl.add_book(book, replace_metadata=False): 1854 changed = True 1855 if show_debug: 1856 debug_print(' book.device_collections', book.device_collections) 1857 debug_print(' book.title', book.title) 1858 except: # Probably a path encoding error 1859 import traceback 1860 traceback.print_exc() 1861 return changed 1862 1863 def get_bookshelvesforbook(connection, ContentID): 1864 # debug_print("KoboTouch:get_bookshelvesforbook - " + ContentID) 1865 bookshelves = [] 1866 if not self.supports_bookshelves: 1867 return bookshelves 1868 1869 cursor = connection.cursor() 1870 query = "select ShelfName " \ 1871 "from ShelfContent " \ 1872 "where ContentId = ? " \ 1873 "and _IsDeleted = 'false' " \ 1874 "and ShelfName is not null" # This should never be null, but it is protection against an error cause by a sync to the Kobo server 1875 values = (ContentID, ) 1876 cursor.execute(query, values) 1877 for i, row in enumerate(cursor): 1878 bookshelves.append(row['ShelfName']) 1879 1880 cursor.close() 1881# debug_print("KoboTouch:get_bookshelvesforbook - count bookshelves=" + str(count_bookshelves)) 1882 return bookshelves 1883 1884 self.debug_index = 0 1885 1886 with closing(self.device_database_connection(use_row_factory=True)) as connection: 1887 debug_print("KoboTouch:books - reading device database") 1888 1889 self.dbversion = self.get_database_version(connection) 1890 debug_print("Database Version: ", self.dbversion) 1891 1892 self.bookshelvelist = self.get_bookshelflist(connection) 1893 debug_print("KoboTouch:books - shelf list:", self.bookshelvelist) 1894 1895 columns = 'Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ImageId, ReadStatus, Description, Publisher ' 1896 if self.dbversion >= 16: 1897 columns += ', ___ExpirationStatus, FavouritesIndex, Accessibility' 1898 else: 1899 columns += ', -1 as ___ExpirationStatus, -1 as FavouritesIndex, -1 as Accessibility' 1900 if self.dbversion >= 33: 1901 columns += ', Language, IsDownloaded' 1902 else: 1903 columns += ', NULL AS Language, "1" AS IsDownloaded' 1904 if self.dbversion >= 46: 1905 columns += ', ISBN' 1906 else: 1907 columns += ', NULL AS ISBN' 1908 if self.supports_series(): 1909 columns += ", Series, SeriesNumber, ___UserID, ExternalId, Subtitle" 1910 else: 1911 columns += ', null as Series, null as SeriesNumber, ___UserID, null as ExternalId, null as Subtitle' 1912 if self.supports_series_list: 1913 columns += ", SeriesID, SeriesNumberFloat" 1914 else: 1915 columns += ', null as SeriesID, null as SeriesNumberFloat' 1916 1917 where_clause = '' 1918 if self.supports_kobo_archive() or self.supports_overdrive(): 1919 where_clause = (" WHERE BookID IS NULL " 1920 " AND ((Accessibility = -1 AND IsDownloaded in ('true', 1 )) " # Sideloaded books 1921 " OR (Accessibility IN (%(downloaded_accessibility)s) %(expiry)s) " # Purchased books 1922 " %(previews)s %(recommendations)s ) " # Previews or Recommendations 1923 ) % \ 1924 dict( 1925 expiry="" if self.show_archived_books else "and IsDownloaded in ('true', 1)", 1926 previews=" OR (Accessibility in (6) AND ___UserID <> '')" if self.show_previews else "", 1927 recommendations=" OR (Accessibility IN (-1, 4, 6) AND ___UserId = '')" if self.show_recommendations else "", 1928 downloaded_accessibility="1,2,8,9" if self.supports_overdrive() else "1,2" 1929 ) 1930 elif self.supports_series(): 1931 where_clause = (" WHERE BookID IS NULL " 1932 " AND ((Accessibility = -1 AND IsDownloaded IN ('true', 1)) or (Accessibility IN (1,2)) %(previews)s %(recommendations)s )" 1933 " AND NOT ((___ExpirationStatus=3 OR ___ExpirationStatus is Null) %(expiry)s)" 1934 ) % \ 1935 dict( 1936 expiry=" AND ContentType = 6" if self.show_archived_books else "", 1937 previews=" or (Accessibility IN (6) AND ___UserID <> '')" if self.show_previews else "", 1938 recommendations=" or (Accessibility in (-1, 4, 6) AND ___UserId = '')" if self.show_recommendations else "" 1939 ) 1940 elif self.dbversion >= 33: 1941 where_clause = (' WHERE BookID IS NULL %(previews)s %(recommendations)s AND NOT' 1942 ' ((___ExpirationStatus=3 or ___ExpirationStatus IS NULL) %(expiry)s)' 1943 ) % \ 1944 dict( 1945 expiry=' AND ContentType = 6' if self.show_archived_books else '', 1946 previews=' AND Accessibility <> 6' if not self.show_previews else '', 1947 recommendations=' AND IsDownloaded IN (\'true\', 1)' if not self.show_recommendations else '' 1948 ) 1949 elif self.dbversion >= 16: 1950 where_clause = (' WHERE BookID IS NULL ' 1951 'AND NOT ((___ExpirationStatus=3 OR ___ExpirationStatus IS Null) %(expiry)s)' 1952 ) % \ 1953 dict(expiry=' and ContentType = 6' if self.show_archived_books else '') 1954 else: 1955 where_clause = ' WHERE BookID IS NULL' 1956 1957 # Note: The card condition should not need the contentId test for the SD 1958 # card. But the ExternalId does not get set for sideloaded kepubs on the 1959 # SD card. 1960 card_condition = '' 1961 if self.has_externalid(): 1962 card_condition = " AND (externalId IS NOT NULL AND externalId <> '' OR contentId LIKE 'file:///mnt/sd/%')" if oncard == 'carda' else ( 1963 " AND (externalId IS NULL OR externalId = '') AND contentId NOT LIKE 'file:///mnt/sd/%'") 1964 else: 1965 card_condition = " AND contentId LIKE 'file:///mnt/sd/%'" if oncard == 'carda' else " AND contentId NOT LIKE'file:///mnt/sd/%'" 1966 1967 query = 'SELECT ' + columns + ' FROM content ' + where_clause + card_condition 1968 debug_print("KoboTouch:books - query=", query) 1969 1970 cursor = connection.cursor() 1971 try: 1972 cursor.execute(query) 1973 except Exception as e: 1974 err = str(e) 1975 if not (any_in(err, '___ExpirationStatus', 'FavouritesIndex', 'Accessibility', 'IsDownloaded', 'Series', 'ExternalId')): 1976 raise 1977 query= ('SELECT Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' 1978 'ImageId, ReadStatus, -1 AS ___ExpirationStatus, "-1" AS FavouritesIndex, ' 1979 'null AS ISBN, NULL AS Language ' 1980 '-1 AS Accessibility, 1 AS IsDownloaded, NULL AS Series, NULL AS SeriesNumber, null as Subtitle ' 1981 'FROM content ' 1982 'WHERE BookID IS NULL' 1983 ) 1984 cursor.execute(query) 1985 1986 changed = False 1987 i = 0 1988 for row in cursor: 1989 i += 1 1990# self.report_progress((i) / float(books_on_device), _('Getting list of books on device...')) 1991 show_debug = self.is_debugging_title(row['Title']) 1992 if show_debug: 1993 debug_print("KoboTouch:books - looping on database - row=%d" % i) 1994 debug_print("KoboTouch:books - title='%s'"%row['Title'], "authors=", row['Attribution']) 1995 debug_print("KoboTouch:books - row=", row) 1996 if not hasattr(row['ContentID'], 'startswith') or row['ContentID'].lower().startswith( 1997 "file:///usr/local/kobo/help/") or row['ContentID'].lower().startswith("/usr/local/kobo/help/"): 1998 # These are internal to the Kobo device and do not exist 1999 continue 2000 externalId = None if row['ExternalId'] and len(row['ExternalId']) == 0 else row['ExternalId'] 2001 path = self.path_from_contentid(row['ContentID'], row['ContentType'], row['MimeType'], oncard, externalId) 2002 if show_debug: 2003 debug_print("KoboTouch:books - path='%s'"%path, " ContentID='%s'"%row['ContentID'], " externalId=%s" % externalId) 2004 2005 bookshelves = get_bookshelvesforbook(connection, row['ContentID']) 2006 2007 prefix = self._card_a_prefix if oncard == 'carda' else self._main_prefix 2008 changed = update_booklist(prefix, path, row['ContentID'], row['ContentType'], row['MimeType'], row['ImageId'], 2009 row['Title'], row['Attribution'], row['DateCreated'], row['Description'], row['Publisher'], 2010 row['Series'], row['SeriesNumber'], row['SeriesID'], row['SeriesNumberFloat'], 2011 row['ISBN'], row['Language'], row['Subtitle'], 2012 row['ReadStatus'], row['___ExpirationStatus'], 2013 int(row['FavouritesIndex']), row['Accessibility'], row['IsDownloaded'], 2014 row['___UserID'], bookshelves 2015 ) 2016 2017 if changed: 2018 need_sync = True 2019 2020 cursor.close() 2021 2022 if not prefs['manage_device_metadata'] == 'on_connect': 2023 self.dump_bookshelves(connection) 2024 else: 2025 debug_print("KoboTouch:books - automatically managing metadata") 2026 debug_print("KoboTouch:books - self.kobo_series_dict=", self.kobo_series_dict) 2027 # Remove books that are no longer in the filesystem. Cache contains 2028 # indices into the booklist if book not in filesystem, None otherwise 2029 # Do the operation in reverse order so indices remain valid 2030 for idx in sorted(itervalues(bl_cache), reverse=True, key=lambda x: x or -1): 2031 if idx is not None: 2032 if not os.path.exists(self.normalize_path(os.path.join(prefix, bl[idx].lpath))) or not bl[idx].contentID: 2033 need_sync = True 2034 del bl[idx] 2035 else: 2036 debug_print("KoboTouch:books - Book in mtadata.calibre, on file system but not database - bl[idx].title:'%s'"%bl[idx].title) 2037 2038 # print "count found in cache: %d, count of files in metadata: %d, need_sync: %s" % \ 2039 # (len(bl_cache), len(bl), need_sync) 2040 # Bypassing the KOBO sync_booklists as that does things we don't need to do 2041 # Also forcing sync to see if this solves issues with updating shelves and matching books. 2042 if need_sync or True: # self.count_found_in_bl != len(bl) or need_sync: 2043 debug_print("KoboTouch:books - about to sync_booklists") 2044 if oncard == 'cardb': 2045 USBMS.sync_booklists(self, (None, None, bl)) 2046 elif oncard == 'carda': 2047 USBMS.sync_booklists(self, (None, bl, None)) 2048 else: 2049 USBMS.sync_booklists(self, (bl, None, None)) 2050 debug_print("KoboTouch:books - have done sync_booklists") 2051 2052 self.report_progress(1.0, _('Getting list of books on device...')) 2053 debug_print("KoboTouch:books - end - oncard='%s'"%oncard) 2054 return bl 2055 2056 @classmethod 2057 def book_from_path(cls, prefix, lpath, title, authors, mime, date, ContentType, ImageID): 2058 debug_print("KoboTouch:book_from_path - title=%s"%title) 2059 book = super().book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID) 2060 2061 # Kobo Audiobooks are directories with files in them. 2062 if mime in cls.KOBO_AUDIOBOOKS_MIMETYPES and book.size == 0: 2063 audiobook_path = cls.normalize_path(os.path.join(prefix, lpath)) 2064 # debug_print("KoboTouch:book_from_path - audiobook=", audiobook_path) 2065 for audiofile in os.scandir(audiobook_path): 2066 # debug_print("KoboTouch:book_from_path - audiofile=", audiofile) 2067 if audiofile.is_file(): 2068 size = audiofile.stat().st_size 2069 # debug_print("KoboTouch:book_from_path - size=", size) 2070 book.size += size 2071 debug_print("KoboTouch:book_from_path - book.size=", book.size) 2072 2073 return book 2074 2075 def path_from_contentid(self, ContentID, ContentType, MimeType, oncard, externalId=None): 2076 path = ContentID 2077 2078 if not (externalId or MimeType == 'application/octet-stream'): 2079 return super().path_from_contentid(ContentID, ContentType, MimeType, oncard) 2080 2081 if oncard == 'cardb': 2082 print('path from_contentid cardb') 2083 else: 2084 if (ContentType == "6" or ContentType == "10"): 2085 if (MimeType == 'application/octet-stream'): # Audiobooks purchased from Kobo are in a different location. 2086 path = self._main_prefix + '.kobo/audiobook/' + path 2087 elif path.startswith("file:///mnt/onboard/"): 2088 path = self._main_prefix + path.replace("file:///mnt/onboard/", '') 2089 elif path.startswith("file:///mnt/sd/"): 2090 path = self._card_a_prefix + path.replace("file:///mnt/sd/", '') 2091 elif externalId: 2092 path = self._card_a_prefix + 'koboExtStorage/kepub/' + path 2093 else: 2094 path = self._main_prefix + '.kobo/kepub/' + path 2095 else: # Should never get here, but, just in case... 2096 # if path.startswith("file:///mnt/onboard/"): 2097 path = path.replace("file:///mnt/onboard/", self._main_prefix) 2098 path = path.replace("file:///mnt/sd/", self._card_a_prefix) 2099 path = path.replace("/mnt/onboard/", self._main_prefix) 2100 # print "Internal: " + path 2101 2102 return path 2103 2104 def imagefilename_from_imageID(self, prefix, ImageID): 2105 show_debug = self.is_debugging_title(ImageID) 2106 2107 if len(ImageID) > 0: 2108 path = self.images_path(prefix, ImageID) 2109 2110 for ending in self.cover_file_endings(): 2111 fpath = path + ending 2112 if os.path.exists(fpath): 2113 if show_debug: 2114 debug_print("KoboTouch:imagefilename_from_imageID - have cover image fpath=%s" % (fpath)) 2115 return fpath 2116 2117 if show_debug: 2118 debug_print("KoboTouch:imagefilename_from_imageID - no cover image found - ImageID=%s" % (ImageID)) 2119 return None 2120 2121 def get_extra_css(self): 2122 extra_sheet = None 2123 from css_parser.css import CSSRule 2124 2125 if self.modifying_css(): 2126 extra_css_path = os.path.join(self._main_prefix, self.KOBO_EXTRA_CSSFILE) 2127 if os.path.exists(extra_css_path): 2128 from css_parser import parseFile as cssparseFile 2129 try: 2130 extra_sheet = cssparseFile(extra_css_path) 2131 debug_print("KoboTouch:get_extra_css: Using extra CSS in {} ({} rules)".format(extra_css_path, len(extra_sheet.cssRules))) 2132 if len(extra_sheet.cssRules) ==0: 2133 debug_print("KoboTouch:get_extra_css: Extra CSS file has no valid rules. CSS will not be modified.") 2134 extra_sheet = None 2135 except Exception as e: 2136 debug_print("KoboTouch:get_extra_css: Problem parsing extra CSS file {}".format(extra_css_path)) 2137 debug_print("KoboTouch:get_extra_css: Exception {}".format(e)) 2138 2139 # create dictionary of features enabled in kobo extra css 2140 self.extra_css_options = {} 2141 if extra_sheet: 2142 # search extra_css for @page rule 2143 self.extra_css_options['has_atpage'] = len(self.get_extra_css_rules(extra_sheet, CSSRule.PAGE_RULE)) > 0 2144 2145 # search extra_css for style rule(s) containing widows or orphans 2146 self.extra_css_options['has_widows_orphans'] = len(self.get_extra_css_rules_widow_orphan(extra_sheet)) > 0 2147 debug_print('KoboTouch:get_extra_css - CSS options:', self.extra_css_options) 2148 2149 return extra_sheet 2150 2151 def get_extra_css_rules(self, sheet, css_rule): 2152 return [r for r in sheet.cssRules.rulesOfType(css_rule)] 2153 2154 def get_extra_css_rules_widow_orphan(self, sheet): 2155 from css_parser.css import CSSRule 2156 return [r for r in self.get_extra_css_rules(sheet, CSSRule.STYLE_RULE) 2157 if (r.style['widows'] or r.style['orphans'])] 2158 2159 def upload_books(self, files, names, on_card=None, end_session=True, 2160 metadata=None): 2161 debug_print('KoboTouch:upload_books - %d books'%(len(files))) 2162 debug_print('KoboTouch:upload_books - files=', files) 2163 2164 if self.modifying_epub(): 2165 self.extra_sheet = self.get_extra_css() 2166 i = 0 2167 for file, n, mi in zip(files, names, metadata): 2168 debug_print("KoboTouch:upload_books: Processing book: {} by {}".format(mi.title, " and ".join(mi.authors))) 2169 debug_print("KoboTouch:upload_books: file=%s, name=%s" % (file, n)) 2170 self.report_progress(i / float(len(files)), "Processing book: {} by {}".format(mi.title, " and ".join(mi.authors))) 2171 mi.kte_calibre_name = n 2172 self._modify_epub(file, mi) 2173 i += 1 2174 2175 self.report_progress(0, 'Working...') 2176 2177 result = super().upload_books(files, names, on_card, end_session, metadata) 2178# debug_print('KoboTouch:upload_books - result=', result) 2179 2180 if self.dbversion >= 53: 2181 try: 2182 with closing(self.device_database_connection()) as connection: 2183 cursor = connection.cursor() 2184 cleanup_query = "DELETE FROM content WHERE ContentID = ? AND Accessibility = 1 AND IsDownloaded = 'false'" 2185 2186 for fname, cycle in result: 2187 show_debug = self.is_debugging_title(fname) 2188 contentID = self.contentid_from_path(fname, 6) 2189 if show_debug: 2190 debug_print('KoboTouch:upload_books: fname=', fname) 2191 debug_print('KoboTouch:upload_books: contentID=', contentID) 2192 2193 cleanup_values = (contentID,) 2194# debug_print('KoboTouch:upload_books: Delete record left if deleted on Touch') 2195 cursor.execute(cleanup_query, cleanup_values) 2196 2197 if self.override_kobo_replace_existing: 2198 self.set_filesize_in_device_database(connection, contentID, fname) 2199 2200 if not self.upload_covers: 2201 imageID = self.imageid_from_contentid(contentID) 2202 self.delete_images(imageID, fname) 2203 2204 cursor.close() 2205 except Exception as e: 2206 debug_print('KoboTouch:upload_books - Exception: %s'%str(e)) 2207 2208 return result 2209 2210 def _modify_epub(self, book_file, metadata, container=None): 2211 debug_print("KoboTouch:_modify_epub:Processing {} - {}".format(metadata.author_sort, metadata.title)) 2212 2213 # Currently only modifying CSS, so if no stylesheet, don't do anything 2214 if not self.extra_sheet: 2215 debug_print("KoboTouch:_modify_epub: no CSS file") 2216 return True 2217 2218 container, commit_container = self.create_container(book_file, metadata, container) 2219 if not container: 2220 return False 2221 2222 from calibre.ebooks.oeb.base import OEB_STYLES 2223 2224 is_dirty = False 2225 for cssname, mt in iteritems(container.mime_map): 2226 if mt in OEB_STYLES: 2227 newsheet = container.parsed(cssname) 2228 oldrules = len(newsheet.cssRules) 2229 2230 # future css mods may be epub/kepub specific, so pass file extension arg 2231 fileext = os.path.splitext(book_file)[-1].lower() 2232 debug_print("KoboTouch:_modify_epub: Modifying {}".format(cssname)) 2233 if self._modify_stylesheet(newsheet, fileext): 2234 debug_print("KoboTouch:_modify_epub:CSS rules {} -> {} ({})".format(oldrules, len(newsheet.cssRules), cssname)) 2235 container.dirty(cssname) 2236 is_dirty = True 2237 2238 if commit_container: 2239 debug_print("KoboTouch:_modify_epub: committing container.") 2240 self.commit_container(container, is_dirty) 2241 2242 return True 2243 2244 def _modify_stylesheet(self, sheet, fileext, is_dirty=False): 2245 from css_parser.css import CSSRule 2246 2247 # if fileext in (EPUB_EXT, KEPUB_EXT): 2248 2249 # if kobo extra css contains a @page rule 2250 # remove any existing @page rules in epub css 2251 if self.extra_css_options.get('has_atpage', False): 2252 page_rules = self.get_extra_css_rules(sheet, CSSRule.PAGE_RULE) 2253 if len(page_rules) > 0: 2254 debug_print("KoboTouch:_modify_stylesheet: Removing existing @page rules") 2255 for rule in page_rules: 2256 rule.style = '' 2257 is_dirty = True 2258 2259 # if kobo extra css contains any widow/orphan style rules 2260 # remove any existing widow/orphan settings in epub css 2261 if self.extra_css_options.get('has_widows_orphans', False): 2262 widow_orphan_rules = self.get_extra_css_rules_widow_orphan(sheet) 2263 if len(widow_orphan_rules) > 0: 2264 debug_print("KoboTouch:_modify_stylesheet: Removing existing widows/orphans attribs") 2265 for rule in widow_orphan_rules: 2266 rule.style.removeProperty('widows') 2267 rule.style.removeProperty('orphans') 2268 is_dirty = True 2269 2270 # append all rules from kobo extra css 2271 debug_print("KoboTouch:_modify_stylesheet: Append all kobo extra css rules") 2272 for extra_rule in self.extra_sheet.cssRules: 2273 sheet.insertRule(extra_rule) 2274 is_dirty = True 2275 2276 return is_dirty 2277 2278 def create_container(self, book_file, metadata, container=None): 2279 # create new container if not received, else pass through 2280 if not container: 2281 commit_container = True 2282 try: 2283 from calibre.ebooks.oeb.polish.container import get_container 2284 debug_print("KoboTouch:create_container: try to create new container") 2285 container = get_container(book_file) 2286 container.css_preprocessor = DummyCSSPreProcessor() 2287 except Exception as e: 2288 debug_print("KoboTouch:create_container: exception from get_container {} - {}".format(metadata.author_sort, metadata.title)) 2289 debug_print("KoboTouch:create_container: exception is: {}".format(e)) 2290 else: 2291 commit_container = False 2292 debug_print("KoboTouch:create_container: received container") 2293 return container, commit_container 2294 2295 def commit_container(self, container, is_dirty=True): 2296 # commit container if changes have been made 2297 if is_dirty: 2298 debug_print("KoboTouch:commit_container: commit container.") 2299 container.commit() 2300 2301 # Clean-up-AYGO prevents build-up of TEMP exploded epub/kepub files 2302 debug_print("KoboTouch:commit_container: removing container temp files.") 2303 try: 2304 shutil.rmtree(container.root) 2305 except Exception: 2306 pass 2307 2308 def delete_via_sql(self, ContentID, ContentType): 2309 imageId = super().delete_via_sql(ContentID, ContentType) 2310 2311 if self.dbversion >= 53: 2312 debug_print('KoboTouch:delete_via_sql: ContentID="%s"'%ContentID, 'ContentType="%s"'%ContentType) 2313 try: 2314 with closing(self.device_database_connection()) as connection: 2315 debug_print('KoboTouch:delete_via_sql: have database connection') 2316 2317 cursor = connection.cursor() 2318 debug_print('KoboTouch:delete_via_sql: have cursor') 2319 t = (ContentID,) 2320 2321 # Delete the Bookmarks 2322 debug_print('KoboTouch:delete_via_sql: Delete from Bookmark') 2323 cursor.execute('DELETE FROM Bookmark WHERE VolumeID = ?', t) 2324 2325 # Delete from the Bookshelf 2326 debug_print('KoboTouch:delete_via_sql: Delete from the Bookshelf') 2327 cursor.execute('delete from ShelfContent where ContentID = ?', t) 2328 2329 # ContentType 6 is now for all books. 2330 debug_print('KoboTouch:delete_via_sql: BookID is Null') 2331 cursor.execute('delete from content where BookID is Null and ContentID =?',t) 2332 2333 # Remove the content_settings entry 2334 debug_print('KoboTouch:delete_via_sql: delete from content_settings') 2335 cursor.execute('delete from content_settings where ContentID =?',t) 2336 2337 # Remove the ratings entry 2338 debug_print('KoboTouch:delete_via_sql: delete from ratings') 2339 cursor.execute('delete from ratings where ContentID =?',t) 2340 2341 # Remove any entries for the Activity table - removes tile from new home page 2342 if self.has_activity_table(): 2343 debug_print('KoboTouch:delete_via_sql: delete from Activity') 2344 cursor.execute('delete from Activity where Id =?', t) 2345 2346 cursor.close() 2347 debug_print('KoboTouch:delete_via_sql: finished SQL') 2348 debug_print('KoboTouch:delete_via_sql: After SQL, no exception') 2349 except Exception as e: 2350 debug_print('KoboTouch:delete_via_sql - Database Exception: %s'%str(e)) 2351 2352 debug_print('KoboTouch:delete_via_sql: imageId="%s"'%imageId) 2353 if imageId is None: 2354 imageId = self.imageid_from_contentid(ContentID) 2355 2356 return imageId 2357 2358 def delete_images(self, ImageID, book_path): 2359 debug_print("KoboTouch:delete_images - ImageID=", ImageID) 2360 if ImageID is not None: 2361 path = self.images_path(book_path, ImageID) 2362 debug_print("KoboTouch:delete_images - path=%s" % path) 2363 2364 for ending in self.cover_file_endings().keys(): 2365 fpath = path + ending 2366 fpath = self.normalize_path(fpath) 2367 debug_print("KoboTouch:delete_images - fpath=%s" % fpath) 2368 2369 if os.path.exists(fpath): 2370 debug_print("KoboTouch:delete_images - Image File Exists") 2371 os.unlink(fpath) 2372 2373 try: 2374 os.removedirs(os.path.dirname(path)) 2375 except Exception: 2376 pass 2377 2378 def contentid_from_path(self, path, ContentType): 2379 show_debug = self.is_debugging_title(path) and True 2380 if show_debug: 2381 debug_print("KoboTouch:contentid_from_path - path='%s'"%path, "ContentType='%s'"%ContentType) 2382 debug_print("KoboTouch:contentid_from_path - self._main_prefix='%s'"%self._main_prefix, "self._card_a_prefix='%s'"%self._card_a_prefix) 2383 if ContentType == 6: 2384 extension = os.path.splitext(path)[1] 2385 if extension == '.kobo': 2386 ContentID = os.path.splitext(path)[0] 2387 # Remove the prefix on the file. it could be either 2388 ContentID = ContentID.replace(self._main_prefix, '') 2389 elif not extension: 2390 ContentID = path 2391 ContentID = ContentID.replace(self._main_prefix + self.normalize_path('.kobo/kepub/'), '') 2392 else: 2393 ContentID = path 2394 ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/") 2395 2396 if show_debug: 2397 debug_print("KoboTouch:contentid_from_path - 1 ContentID='%s'"%ContentID) 2398 2399 if self._card_a_prefix is not None: 2400 ContentID = ContentID.replace(self._card_a_prefix, "file:///mnt/sd/") 2401 else: # ContentType = 16 2402 debug_print("KoboTouch:contentid_from_path ContentType other than 6 - ContentType='%d'"%ContentType, "path='%s'"%path) 2403 ContentID = path 2404 ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/") 2405 if self._card_a_prefix is not None: 2406 ContentID = ContentID.replace(self._card_a_prefix, "file:///mnt/sd/") 2407 ContentID = ContentID.replace("\\", '/') 2408 if show_debug: 2409 debug_print("KoboTouch:contentid_from_path - end - ContentID='%s'"%ContentID) 2410 return ContentID 2411 2412 def get_content_type_from_path(self, path): 2413 ContentType = 6 2414 if self.fwversion < (1, 9, 17): 2415 ContentType = super().get_content_type_from_path(path) 2416 return ContentType 2417 2418 def get_content_type_from_extension(self, extension): 2419 debug_print("KoboTouch:get_content_type_from_extension - start") 2420 # With new firmware, ContentType appears to be 6 for all types of sideloaded books. 2421 ContentType = 6 2422 if self.fwversion < (1,9,17): 2423 ContentType = super().get_content_type_from_extension(extension) 2424 return ContentType 2425 2426 def set_plugboards(self, plugboards, pb_func): 2427 self.plugboards = plugboards 2428 self.plugboard_func = pb_func 2429 2430 def update_device_database_collections(self, booklists, collections_attributes, oncard): 2431 debug_print("KoboTouch:update_device_database_collections - oncard='%s'"%oncard) 2432 if self.modify_database_check("update_device_database_collections") is False: 2433 return 2434 2435 # Only process categories in this list 2436 supportedcategories = { 2437 "Im_Reading": 1, 2438 "Read": 2, 2439 "Closed": 3, 2440 "Shortlist": 4, 2441 "Archived": 5, 2442 } 2443 2444 # Define lists for the ReadStatus 2445 readstatuslist = { 2446 "Im_Reading":1, 2447 "Read":2, 2448 "Closed":3, 2449 } 2450 2451 accessibilitylist = { 2452 "Deleted":1, 2453 "OverDrive":9, 2454 "Preview":6, 2455 "Recommendation":4, 2456 } 2457# debug_print('KoboTouch:update_device_database_collections - collections_attributes=', collections_attributes) 2458 2459 create_collections = self.create_collections 2460 delete_empty_collections = self.delete_empty_collections 2461 update_series_details = self.update_series_details 2462 update_core_metadata = self.update_core_metadata 2463 update_purchased_kepubs = self.update_purchased_kepubs 2464 debugging_title = self.get_debugging_title() 2465 debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title) 2466 booklists.set_debugging_title(debugging_title) 2467 booklists.set_device_managed_collections(self.ignore_collections_names) 2468 2469 bookshelf_attribute = len(collections_attributes) > 0 2470 2471 collections = booklists.get_collections(collections_attributes) if bookshelf_attribute else None 2472# debug_print('KoboTouch:update_device_database_collections - Collections:', collections) 2473 2474 # Create a connection to the sqlite database 2475 # Needs to be outside books collection as in the case of removing 2476 # the last book from the collection the list of books is empty 2477 # and the removal of the last book would not occur 2478 2479 with closing(self.device_database_connection(use_row_factory=True)) as connection: 2480 2481 if self.manage_collections: 2482 if collections: 2483 # debug_print("KoboTouch:update_device_database_collections - length collections=" + str(len(collections))) 2484 2485 # Need to reset the collections outside the particular loops 2486 # otherwise the last item will not be removed 2487 if self.dbversion < 53: 2488 debug_print("KoboTouch:update_device_database_collections - calling reset_readstatus") 2489 self.reset_readstatus(connection, oncard) 2490 if self.dbversion >= 14 and self.fwversion < self.min_fwversion_shelves: 2491 debug_print("KoboTouch:update_device_database_collections - calling reset_favouritesindex") 2492 self.reset_favouritesindex(connection, oncard) 2493 2494# debug_print("KoboTouch:update_device_database_collections - length collections=", len(collections)) 2495# debug_print("KoboTouch:update_device_database_collections - self.bookshelvelist=", self.bookshelvelist) 2496 # Process any collections that exist 2497 for category, books in collections.items(): 2498 debug_print("KoboTouch:update_device_database_collections - category='%s' books=%d"%(category, len(books))) 2499 if create_collections and not (category in supportedcategories or category in readstatuslist or category in accessibilitylist): 2500 self.check_for_bookshelf(connection, category) 2501# if category in self.bookshelvelist: 2502# debug_print("Category: ", category, " id = ", readstatuslist.get(category)) 2503 for book in books: 2504 # debug_print(' Title:', book.title, 'category: ', category) 2505 show_debug = self.is_debugging_title(book.title) 2506 if show_debug: 2507 debug_print(' Title="%s"'%book.title, 'category="%s"'%category) 2508# debug_print(book) 2509 debug_print(' class=%s'%book.__class__) 2510 debug_print(' book.contentID="%s"'%book.contentID) 2511 debug_print(' book.application_id="%s"'%book.application_id) 2512 2513 if book.application_id is None: 2514 continue 2515 2516 category_added = False 2517 2518 if book.contentID is None: 2519 debug_print(' Do not know ContentID - Title="%s", Authors="%s", path="%s"'%(book.title, book.author, book.path)) 2520 extension = os.path.splitext(book.path)[1] 2521 ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(book.path) 2522 book.contentID = self.contentid_from_path(book.path, ContentType) 2523 2524 if category in self.ignore_collections_names: 2525 debug_print(' Ignoring collection=%s' % category) 2526 category_added = True 2527 elif category in self.bookshelvelist and self.supports_bookshelves: 2528 if show_debug: 2529 debug_print(' length book.device_collections=%d'%len(book.device_collections)) 2530 if category not in book.device_collections: 2531 if show_debug: 2532 debug_print(' Setting bookshelf on device') 2533 self.set_bookshelf(connection, book, category) 2534 category_added = True 2535 elif category in readstatuslist: 2536 debug_print("KoboTouch:update_device_database_collections - about to set_readstatus - category='%s'"%(category, )) 2537 # Manage ReadStatus 2538 self.set_readstatus(connection, book.contentID, readstatuslist.get(category)) 2539 category_added = True 2540 2541 elif category == 'Shortlist' and self.dbversion >= 14: 2542 if show_debug: 2543 debug_print(' Have an older version shortlist - %s'%book.title) 2544 # Manage FavouritesIndex/Shortlist 2545 if not self.supports_bookshelves: 2546 if show_debug: 2547 debug_print(' and about to set it - %s'%book.title) 2548 self.set_favouritesindex(connection, book.contentID) 2549 category_added = True 2550 elif category in accessibilitylist: 2551 # Do not manage the Accessibility List 2552 pass 2553 2554 if category_added and category not in book.device_collections: 2555 if show_debug: 2556 debug_print(' adding category to book.device_collections', book.device_collections) 2557 book.device_collections.append(category) 2558 else: 2559 if show_debug: 2560 debug_print(' category not added to book.device_collections', book.device_collections) 2561 debug_print("KoboTouch:update_device_database_collections - end for category='%s'"%category) 2562 2563 elif bookshelf_attribute: # No collections but have set the shelf option 2564 # Since no collections exist the ReadStatus needs to be reset to 0 (Unread) 2565 debug_print("No Collections - resetting ReadStatus") 2566 if self.dbversion < 53: 2567 self.reset_readstatus(connection, oncard) 2568 if self.dbversion >= 14 and self.fwversion < self.min_fwversion_shelves: 2569 debug_print("No Collections - resetting FavouritesIndex") 2570 self.reset_favouritesindex(connection, oncard) 2571 2572 # Set the series info and cleanup the bookshelves only if the firmware supports them and the user has set the options. 2573 if (self.supports_bookshelves and self.manage_collections or self.supports_series()) and ( 2574 bookshelf_attribute or update_series_details or update_core_metadata): 2575 debug_print("KoboTouch:update_device_database_collections - managing bookshelves and series.") 2576 2577 self.series_set = 0 2578 self.core_metadata_set = 0 2579 books_in_library = 0 2580 for book in booklists: 2581 # debug_print("KoboTouch:update_device_database_collections - book.title=%s, book.contentID=%s" % (book.title, book.contentID)) 2582 if book.application_id is not None and book.contentID is not None: 2583 books_in_library += 1 2584 show_debug = self.is_debugging_title(book.title) 2585 if show_debug: 2586 debug_print("KoboTouch:update_device_database_collections - book.title=%s" % book.title) 2587 debug_print( 2588 "KoboTouch:update_device_database_collections - contentId=%s," 2589 "update_core_metadata=%s,update_purchased_kepubs=%s, book.is_sideloaded=%s" % ( 2590 book.contentID, update_core_metadata, update_purchased_kepubs, book.is_sideloaded)) 2591 if update_core_metadata and (update_purchased_kepubs or book.is_sideloaded): 2592 if show_debug: 2593 debug_print("KoboTouch:update_device_database_collections - calling set_core_metadata") 2594 self.set_core_metadata(connection, book) 2595 elif update_series_details: 2596 if show_debug: 2597 debug_print("KoboTouch:update_device_database_collections - calling set_core_metadata - series only") 2598 self.set_core_metadata(connection, book, series_only=True) 2599 if self.manage_collections and bookshelf_attribute: 2600 if show_debug: 2601 debug_print("KoboTouch:update_device_database_collections - about to remove a book from shelves book.title=%s" % book.title) 2602 self.remove_book_from_device_bookshelves(connection, book) 2603 book.device_collections.extend(book.kobo_collections) 2604 if not prefs['manage_device_metadata'] == 'manual' and delete_empty_collections: 2605 debug_print("KoboTouch:update_device_database_collections - about to clear empty bookshelves") 2606 self.delete_empty_bookshelves(connection) 2607 debug_print("KoboTouch:update_device_database_collections - Number of series set=%d Number of books=%d" % (self.series_set, books_in_library)) 2608 debug_print("KoboTouch:update_device_database_collections - Number of core metadata set=%d Number of books=%d" % ( 2609 self.core_metadata_set, books_in_library)) 2610 2611 self.dump_bookshelves(connection) 2612 2613 debug_print('KoboTouch:update_device_database_collections - Finished ') 2614 2615 def rebuild_collections(self, booklist, oncard): 2616 debug_print("KoboTouch:rebuild_collections") 2617 collections_attributes = self.get_collections_attributes() 2618 2619 debug_print('KoboTouch:rebuild_collections: collection fields:', collections_attributes) 2620 self.update_device_database_collections(booklist, collections_attributes, oncard) 2621 2622 def upload_cover(self, path, filename, metadata, filepath): 2623 ''' 2624 Upload book cover to the device. Default implementation does nothing. 2625 2626 :param path: The full path to the folder where the associated book is located. 2627 :param filename: The name of the book file without the extension. 2628 :param metadata: metadata belonging to the book. Use metadata.thumbnail 2629 for cover 2630 :param filepath: The full path to the ebook file 2631 2632 ''' 2633 debug_print("KoboTouch:upload_cover - path='%s' filename='%s' "%(path, filename)) 2634 debug_print(" filepath='%s' "%(filepath)) 2635 2636 if not self.upload_covers: 2637 # Building thumbnails disabled 2638 # debug_print('KoboTouch: not uploading cover') 2639 return 2640 2641 # Only upload covers to SD card if that is supported 2642 if self._card_a_prefix and os.path.abspath(path).startswith(os.path.abspath(self._card_a_prefix)) and not self.supports_covers_on_sdcard(): 2643 return 2644 2645# debug_print('KoboTouch: uploading cover') 2646 try: 2647 self._upload_cover( 2648 path, filename, metadata, filepath, 2649 self.upload_grayscale, self.dithered_covers, 2650 self.keep_cover_aspect, self.letterbox_fs_covers, self.png_covers, 2651 letterbox_color=self.letterbox_fs_covers_color) 2652 except Exception as e: 2653 debug_print('KoboTouch: FAILED to upload cover=%s Exception=%s'%(filepath, str(e))) 2654 2655 def imageid_from_contentid(self, ContentID): 2656 ImageID = ContentID.replace('/', '_') 2657 ImageID = ImageID.replace(' ', '_') 2658 ImageID = ImageID.replace(':', '_') 2659 ImageID = ImageID.replace('.', '_') 2660 return ImageID 2661 2662 def images_path(self, path, imageId=None): 2663 if self._card_a_prefix and os.path.abspath(path).startswith(os.path.abspath(self._card_a_prefix)) and self.supports_covers_on_sdcard(): 2664 path_prefix = 'koboExtStorage/images-cache/' if self.supports_images_tree() else 'koboExtStorage/images/' 2665 path = os.path.join(self._card_a_prefix, path_prefix) 2666 else: 2667 path_prefix = '.kobo-images/' if self.supports_images_tree() else '.kobo/images/' 2668 path = os.path.join(self._main_prefix, path_prefix) 2669 2670 if self.supports_images_tree() and imageId: 2671 hash1 = qhash(imageId) 2672 dir1 = hash1 & (0xff * 1) 2673 dir2 = (hash1 & (0xff00 * 1)) >> 8 2674 path = os.path.join(path, "%s" % dir1, "%s" % dir2) 2675 2676 if imageId: 2677 path = os.path.join(path, imageId) 2678 return path 2679 2680 def _calculate_kobo_cover_size(self, library_size, kobo_size, expand, keep_cover_aspect, letterbox): 2681 # Remember the canvas size 2682 canvas_size = kobo_size 2683 2684 # NOTE: Loosely based on Qt's QSize::scaled implementation 2685 if keep_cover_aspect: 2686 # NOTE: Unlike Qt, we round to avoid accumulating errors, 2687 # as ImageOps will then floor via fit_image 2688 aspect_ratio = library_size[0] / library_size[1] 2689 rescaled_width = int(round(kobo_size[1] * aspect_ratio)) 2690 2691 if expand: 2692 use_height = (rescaled_width >= kobo_size[0]) 2693 else: 2694 use_height = (rescaled_width <= kobo_size[0]) 2695 2696 if use_height: 2697 kobo_size = (rescaled_width, kobo_size[1]) 2698 else: 2699 kobo_size = (kobo_size[0], int(round(kobo_size[0] / aspect_ratio))) 2700 2701 # Did we actually want to letterbox? 2702 if not letterbox: 2703 canvas_size = kobo_size 2704 return (kobo_size, canvas_size) 2705 2706 def _create_cover_data( 2707 self, cover_data, resize_to, minify_to, kobo_size, 2708 upload_grayscale=False, dithered_covers=False, keep_cover_aspect=False, is_full_size=False, letterbox=False, png_covers=False, quality=90, 2709 letterbox_color=DEFAULT_COVER_LETTERBOX_COLOR 2710 ): 2711 ''' 2712 This will generate the new cover image from the cover in the library. It is a wrapper 2713 for save_cover_data_to to allow it to be overridden in a subclass. For this reason, 2714 options are passed in that are not used by this implementation. 2715 2716 :param cover_data: original cover data 2717 :param resize_to: Size to resize the cover to (width, height). None means do not resize. 2718 :param minify_to: Maximum canvas size for the resized cover (width, height). 2719 :param kobo_size: Size of the cover image on the device. 2720 :param upload_grayscale: boolean True if driver configured to send grayscale thumbnails 2721 :param dithered_covers: boolean True if driver configured to quantize to 16-col grayscale 2722 :param keep_cover_aspect: boolean - True if the aspect ratio of the cover in the library is to be kept. 2723 :param is_full_size: True if this is the kobo_size is for the full size cover image 2724 Passed to allow ability to process screensaver differently 2725 to smaller thumbnails 2726 :param letterbox: True if we were asked to handle the letterboxing 2727 :param png_covers: True if we were asked to encode those images in PNG instead of JPG 2728 :param quality: 0-100 Output encoding quality (or compression level for PNG, àla IM) 2729 :param letterbox_color: Colour used for letterboxing. 2730 ''' 2731 2732 from calibre.utils.img import save_cover_data_to 2733 data = save_cover_data_to( 2734 cover_data, resize_to=resize_to, compression_quality=quality, minify_to=minify_to, grayscale=upload_grayscale, eink=dithered_covers, 2735 letterbox=letterbox, data_fmt="png" if png_covers else "jpeg", letterbox_color=letterbox_color) 2736 return data 2737 2738 def _upload_cover( 2739 self, path, filename, metadata, filepath, upload_grayscale, 2740 dithered_covers=False, keep_cover_aspect=False, letterbox_fs_covers=False, png_covers=False, 2741 letterbox_color=DEFAULT_COVER_LETTERBOX_COLOR 2742 ): 2743 from calibre.utils.imghdr import identify 2744 from calibre.utils.img import optimize_png 2745 debug_print("KoboTouch:_upload_cover - filename='%s' upload_grayscale='%s' dithered_covers='%s' "%(filename, upload_grayscale, dithered_covers)) 2746 2747 if not metadata.cover: 2748 return 2749 2750 show_debug = self.is_debugging_title(filename) 2751 if show_debug: 2752 debug_print("KoboTouch:_upload_cover - path='%s'"%path, "filename='%s'"%filename) 2753 debug_print(" filepath='%s'"%filepath) 2754 cover = self.normalize_path(metadata.cover.replace('/', os.sep)) 2755 2756 if not os.path.exists(cover): 2757 debug_print("KoboTouch:_upload_cover - Cover file does not exist in library") 2758 return 2759 2760 # Get ContentID for Selected Book 2761 extension = os.path.splitext(filepath)[1] 2762 ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(filepath) 2763 ContentID = self.contentid_from_path(filepath, ContentType) 2764 2765 try: 2766 with closing(self.device_database_connection()) as connection: 2767 2768 cursor = connection.cursor() 2769 t = (ContentID,) 2770 cursor.execute('select ImageId from Content where BookID is Null and ContentID = ?', t) 2771 try: 2772 result = next(cursor) 2773 ImageID = result[0] 2774 except StopIteration: 2775 ImageID = self.imageid_from_contentid(ContentID) 2776 debug_print("KoboTouch:_upload_cover - No rows exist in the database - generated ImageID='%s'" % ImageID) 2777 2778 cursor.close() 2779 2780 if ImageID is not None: 2781 path = self.images_path(path, ImageID) 2782 2783 if show_debug: 2784 debug_print("KoboTouch:_upload_cover - About to loop over cover endings") 2785 2786 image_dir = os.path.dirname(os.path.abspath(path)) 2787 if not os.path.exists(image_dir): 2788 debug_print("KoboTouch:_upload_cover - Image folder does not exist. Creating path='%s'" % (image_dir)) 2789 os.makedirs(image_dir) 2790 2791 with lopen(cover, 'rb') as f: 2792 cover_data = f.read() 2793 2794 fmt, width, height = identify(cover_data) 2795 library_cover_size = (width, height) 2796 2797 for ending, cover_options in self.cover_file_endings().items(): 2798 kobo_size, min_dbversion, max_dbversion, is_full_size = cover_options 2799 if show_debug: 2800 debug_print("KoboTouch:_upload_cover - library_cover_size=%s -> kobo_size=%s, min_dbversion=%d max_dbversion=%d, is_full_size=%s" % ( 2801 library_cover_size, kobo_size, min_dbversion, max_dbversion, is_full_size)) 2802 2803 if self.dbversion >= min_dbversion and self.dbversion <= max_dbversion: 2804 if show_debug: 2805 debug_print("KoboTouch:_upload_cover - creating cover for ending='%s'"%ending) # , "library_cover_size'%s'"%library_cover_size) 2806 fpath = path + ending 2807 fpath = self.normalize_path(fpath.replace('/', os.sep)) 2808 2809 # Never letterbox thumbnails, that's ugly. But for fullscreen covers, honor the setting. 2810 letterbox = letterbox_fs_covers and is_full_size 2811 2812 # NOTE: Full size means we have to fit *inside* the 2813 # given boundaries. Thumbnails, on the other hand, are 2814 # *expanded* around those boundaries. 2815 # In Qt, it'd mean full-screen covers are resized 2816 # using Qt::KeepAspectRatio, while thumbnails are 2817 # resized using Qt::KeepAspectRatioByExpanding 2818 # (i.e., QSize's boundedTo() vs. expandedTo(). See also IM's '^' geometry token, for the same "expand" behavior.) 2819 # Note that Nickel itself will generate bounded thumbnails, while it will download expanded thumbnails for store-bought KePubs... 2820 # We chose to emulate the KePub behavior. 2821 resize_to, expand_to = self._calculate_kobo_cover_size(library_cover_size, kobo_size, not is_full_size, keep_cover_aspect, letterbox) 2822 if show_debug: 2823 debug_print( 2824 "KoboTouch:_calculate_kobo_cover_size - expand_to=%s" 2825 " (vs. kobo_size=%s) & resize_to=%s, keep_cover_aspect=%s & letterbox_fs_covers=%s, png_covers=%s" % ( 2826 expand_to, kobo_size, resize_to, keep_cover_aspect, letterbox_fs_covers, png_covers)) 2827 2828 # NOTE: To speed things up, we enforce a lower 2829 # compression level for png_covers, as the final 2830 # optipng pass will then select a higher compression 2831 # level anyway, 2832 # so the compression level from that first pass 2833 # is irrelevant, and only takes up precious time 2834 # ;). 2835 quality = 10 if png_covers else 90 2836 2837 # Return the data resized and properly grayscaled/dithered/letterboxed if requested 2838 data = self._create_cover_data( 2839 cover_data, resize_to, expand_to, kobo_size, upload_grayscale, 2840 dithered_covers, keep_cover_aspect, is_full_size, letterbox, png_covers, quality, 2841 letterbox_color=letterbox_color) 2842 2843 # NOTE: If we're writing a PNG file, go through a quick 2844 # optipng pass to make sure it's encoded properly, as 2845 # Qt doesn't afford us enough control to do it right... 2846 # Unfortunately, optipng doesn't support reading 2847 # pipes, so this gets a bit clunky as we have go 2848 # through a temporary file... 2849 if png_covers: 2850 tmp_cover = better_mktemp() 2851 with lopen(tmp_cover, 'wb') as f: 2852 f.write(data) 2853 2854 optimize_png(tmp_cover, level=1) 2855 # Crossing FS boundaries, can't rename, have to copy + delete :/ 2856 shutil.copy2(tmp_cover, fpath) 2857 os.remove(tmp_cover) 2858 else: 2859 with lopen(fpath, 'wb') as f: 2860 f.write(data) 2861 fsync(f) 2862 except Exception as e: 2863 err = str(e) 2864 debug_print("KoboTouch:_upload_cover - Exception string: %s"%err) 2865 raise 2866 2867 def remove_book_from_device_bookshelves(self, connection, book): 2868 show_debug = self.is_debugging_title(book.title) # or True 2869 2870 remove_shelf_list = set(book.current_shelves) - set(book.device_collections) 2871 remove_shelf_list = remove_shelf_list - set(self.ignore_collections_names) 2872 2873 if show_debug: 2874 debug_print('KoboTouch:remove_book_from_device_bookshelves - book.application_id="%s"'%book.application_id) 2875 debug_print('KoboTouch:remove_book_from_device_bookshelves - book.contentID="%s"'%book.contentID) 2876 debug_print('KoboTouch:remove_book_from_device_bookshelves - book.device_collections=', book.device_collections) 2877 debug_print('KoboTouch:remove_book_from_device_bookshelves - book.current_shelves=', book.current_shelves) 2878 debug_print('KoboTouch:remove_book_from_device_bookshelves - remove_shelf_list=', remove_shelf_list) 2879 2880 if len(remove_shelf_list) == 0: 2881 return 2882 2883 query = 'DELETE FROM ShelfContent WHERE ContentId = ?' 2884 2885 values = [book.contentID,] 2886 2887 if book.device_collections: 2888 placeholder = '?' 2889 placeholders = ','.join(placeholder for unused in book.device_collections) 2890 query += ' and ShelfName not in (%s)' % placeholders 2891 values.extend(book.device_collections) 2892 2893 if show_debug: 2894 debug_print('KoboTouch:remove_book_from_device_bookshelves query="%s"'%query) 2895 debug_print('KoboTouch:remove_book_from_device_bookshelves values="%s"'%values) 2896 2897 cursor = connection.cursor() 2898 cursor.execute(query, values) 2899 cursor.close() 2900 2901 def set_filesize_in_device_database(self, connection, contentID, fpath): 2902 show_debug = self.is_debugging_title(fpath) 2903 if show_debug: 2904 debug_print('KoboTouch:set_filesize_in_device_database contentID="%s"'%contentID) 2905 2906 test_query = 'SELECT ___FileSize ' \ 2907 'FROM content ' \ 2908 'WHERE ContentID = ? ' \ 2909 ' AND ContentType = 6' 2910 test_values = (contentID, ) 2911 2912 updatequery = 'UPDATE content ' \ 2913 'SET ___FileSize = ? ' \ 2914 'WHERE ContentId = ? ' \ 2915 'AND ContentType = 6' 2916 2917 cursor = connection.cursor() 2918 cursor.execute(test_query, test_values) 2919 try: 2920 result = next(cursor) 2921 except StopIteration: 2922 result = None 2923 2924 if result is None: 2925 if show_debug: 2926 debug_print(' Did not find a record - new book on device') 2927 elif os.path.exists(fpath): 2928 file_size = os.stat(self.normalize_path(fpath)).st_size 2929 if show_debug: 2930 debug_print(' Found a record - will update - ___FileSize=', result[0], ' file_size=', file_size) 2931 if file_size != int(result[0]): 2932 update_values = (file_size, contentID, ) 2933 cursor.execute(updatequery, update_values) 2934 if show_debug: 2935 debug_print(' Size updated.') 2936 2937 cursor.close() 2938 2939# debug_print("KoboTouch:set_filesize_in_device_database - end") 2940 2941 def delete_empty_bookshelves(self, connection): 2942 debug_print("KoboTouch:delete_empty_bookshelves - start") 2943 2944 ignore_collections_placeholder = '' 2945 ignore_collections_values = [] 2946 if self.ignore_collections_names: 2947 placeholder = ',?' 2948 ignore_collections_placeholder = ''.join(placeholder for unused in self.ignore_collections_names) 2949 ignore_collections_values.extend(self.ignore_collections_names) 2950 debug_print("KoboTouch:delete_empty_bookshelves - ignore_collections_in=", ignore_collections_placeholder) 2951 debug_print("KoboTouch:delete_empty_bookshelves - ignore_collections=", ignore_collections_values) 2952 2953 delete_query = ("DELETE FROM Shelf " 2954 "WHERE Shelf._IsSynced = 'false' " 2955 "AND Shelf.InternalName not in ('Shortlist', 'Wishlist'" + ignore_collections_placeholder + ") " 2956 "AND (Type IS NULL OR Type <> 'SystemTag') " # Collections are created with Type of NULL and change after a sync. 2957 "AND NOT EXISTS " 2958 "(SELECT 1 FROM ShelfContent c " 2959 "WHERE Shelf.Name = C.ShelfName " 2960 "AND c._IsDeleted <> 'true')") 2961 debug_print("KoboTouch:delete_empty_bookshelves - delete_query=", delete_query) 2962 2963 update_query = ("UPDATE Shelf " 2964 "SET _IsDeleted = 'true' " 2965 "WHERE Shelf._IsSynced = 'true' " 2966 "AND Shelf.InternalName not in ('Shortlist', 'Wishlist'" + ignore_collections_placeholder + ") " 2967 "AND (Type IS NULL OR Type <> 'SystemTag') " 2968 "AND NOT EXISTS " 2969 "(SELECT 1 FROM ShelfContent C " 2970 "WHERE Shelf.Name = C.ShelfName " 2971 "AND c._IsDeleted <> 'true')") 2972 debug_print("KoboTouch:delete_empty_bookshelves - update_query=", update_query) 2973 2974 delete_activity_query = ("DELETE FROM Activity " 2975 "WHERE Type = 'Shelf' " 2976 "AND NOT EXISTS " 2977 "(SELECT 1 FROM Shelf " 2978 "WHERE Shelf.Name = Activity.Id " 2979 "AND Shelf._IsDeleted = 'false')" 2980 ) 2981 debug_print("KoboTouch:delete_empty_bookshelves - delete_activity_query=", delete_activity_query) 2982 2983 cursor = connection.cursor() 2984 cursor.execute(delete_query, ignore_collections_values) 2985 cursor.execute(update_query, ignore_collections_values) 2986 if self.has_activity_table(): 2987 cursor.execute(delete_activity_query) 2988 cursor.close() 2989 2990 debug_print("KoboTouch:delete_empty_bookshelves - end") 2991 2992 def get_bookshelflist(self, connection): 2993 # Retrieve the list of booksehelves 2994 # debug_print('KoboTouch:get_bookshelflist') 2995 bookshelves = [] 2996 2997 if not self.supports_bookshelves: 2998 return bookshelves 2999 3000 query = 'SELECT Name FROM Shelf WHERE _IsDeleted = "false"' 3001 3002 cursor = connection.cursor() 3003 cursor.execute(query) 3004# count_bookshelves = 0 3005 for row in cursor: 3006 bookshelves.append(row['Name']) 3007# count_bookshelves = i + 1 3008 3009 cursor.close() 3010# debug_print("KoboTouch:get_bookshelflist - count bookshelves=" + str(count_bookshelves)) 3011 3012 return bookshelves 3013 3014 def set_bookshelf(self, connection, book, shelfName): 3015 show_debug = self.is_debugging_title(book.title) 3016 if show_debug: 3017 debug_print('KoboTouch:set_bookshelf book.ContentID="%s"'%book.contentID) 3018 debug_print('KoboTouch:set_bookshelf book.current_shelves="%s"'%book.current_shelves) 3019 3020 if shelfName in book.current_shelves: 3021 if show_debug: 3022 debug_print(' book already on shelf.') 3023 return 3024 3025 test_query = 'SELECT _IsDeleted FROM ShelfContent WHERE ShelfName = ? and ContentId = ?' 3026 test_values = (shelfName, book.contentID, ) 3027 addquery = 'INSERT INTO ShelfContent ("ShelfName","ContentId","DateModified","_IsDeleted","_IsSynced") VALUES (?, ?, ?, "false", "false")' 3028 add_values = (shelfName, book.contentID, time.strftime(self.TIMESTAMP_STRING, time.gmtime()), ) 3029 updatequery = 'UPDATE ShelfContent SET _IsDeleted = "false" WHERE ShelfName = ? and ContentId = ?' 3030 update_values = (shelfName, book.contentID, ) 3031 3032 cursor = connection.cursor() 3033 cursor.execute(test_query, test_values) 3034 try: 3035 result = next(cursor) 3036 except StopIteration: 3037 result = None 3038 3039 if result is None: 3040 if show_debug: 3041 debug_print(' Did not find a record - adding') 3042 cursor.execute(addquery, add_values) 3043 elif result['_IsDeleted'] == 'true': 3044 if show_debug: 3045 debug_print(' Found a record - updating - result=', result) 3046 cursor.execute(updatequery, update_values) 3047 3048 cursor.close() 3049 3050# debug_print("KoboTouch:set_bookshelf - end") 3051 3052 def check_for_bookshelf(self, connection, bookshelf_name): 3053 show_debug = self.is_debugging_title(bookshelf_name) 3054 if show_debug: 3055 debug_print('KoboTouch:check_for_bookshelf bookshelf_name="%s"'%bookshelf_name) 3056 test_query = 'SELECT InternalName, Name, _IsDeleted FROM Shelf WHERE Name = ?' 3057 test_values = (bookshelf_name, ) 3058 addquery = 'INSERT INTO "main"."Shelf"' 3059 add_values = (time.strftime(self.TIMESTAMP_STRING, time.gmtime()), 3060 bookshelf_name, 3061 time.strftime(self.TIMESTAMP_STRING, time.gmtime()), 3062 bookshelf_name, 3063 "false", 3064 "true", 3065 "false", 3066 ) 3067 shelf_type = "UserTag" # if self.supports_reading_list else None 3068 if self.dbversion < 64: 3069 addquery += ' ("CreationDate","InternalName","LastModified","Name","_IsDeleted","_IsVisible","_IsSynced")'\ 3070 ' VALUES (?, ?, ?, ?, ?, ?, ?)' 3071 else: 3072 addquery += ' ("CreationDate", "InternalName","LastModified","Name","_IsDeleted","_IsVisible","_IsSynced", "Id", "Type")'\ 3073 ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' 3074 add_values = add_values +(bookshelf_name, shelf_type) 3075 3076 if show_debug: 3077 debug_print('KoboTouch:check_for_bookshelf addquery=', addquery) 3078 debug_print('KoboTouch:check_for_bookshelf add_values=', add_values) 3079 updatequery = 'UPDATE Shelf SET _IsDeleted = "false" WHERE Name = ?' 3080 3081 cursor = connection.cursor() 3082 cursor.execute(test_query, test_values) 3083 try: 3084 result = next(cursor) 3085 except StopIteration: 3086 result = None 3087 3088 if result is None: 3089 if show_debug: 3090 debug_print(' Did not find a record - adding shelf "%s"' % bookshelf_name) 3091 cursor.execute(addquery, add_values) 3092 elif result['_IsDeleted'] == 'true': 3093 debug_print("KoboTouch:check_for_bookshelf - Shelf '%s' is deleted - undeleting. result['_IsDeleted']='%s'" % ( 3094 bookshelf_name, str(result['_IsDeleted']))) 3095 cursor.execute(updatequery, test_values) 3096 3097 cursor.close() 3098 3099 # Update the bookshelf list. 3100 self.bookshelvelist = self.get_bookshelflist(connection) 3101 3102# debug_print("KoboTouch:set_bookshelf - end") 3103 3104 def remove_from_bookshelves(self, connection, oncard, ContentID=None, bookshelves=None): 3105 debug_print('KoboTouch:remove_from_bookshelf ContentID=', ContentID) 3106 if not self.supports_bookshelves: 3107 return 3108 query = 'DELETE FROM ShelfContent' 3109 3110 values = [] 3111 if ContentID is not None: 3112 query += ' WHERE ContentId = ?' 3113 values.append(ContentID) 3114 else: 3115 if oncard == 'carda': 3116 query += ' WHERE ContentID like \'file:///mnt/sd/%\'' 3117 elif oncard != 'carda' and oncard != 'cardb': 3118 query += ' WHERE ContentID not like \'file:///mnt/sd/%\'' 3119 3120 if bookshelves: 3121 placeholder = '?' 3122 placeholders = ','.join(placeholder for unused in bookshelves) 3123 query += ' and ShelfName in (%s)' % placeholders 3124 values.append(bookshelves) 3125 debug_print('KoboTouch:remove_from_bookshelf query=', query) 3126 debug_print('KoboTouch:remove_from_bookshelf values=', values) 3127 cursor = connection.cursor() 3128 cursor.execute(query, values) 3129 cursor.close() 3130 3131 debug_print("KoboTouch:remove_from_bookshelf - end") 3132 3133 # No longer used, but keep for a little bit. 3134 def set_series(self, connection, book): 3135 show_debug = self.is_debugging_title(book.title) 3136 if show_debug: 3137 debug_print('KoboTouch:set_series book.kobo_series="%s"'%book.kobo_series) 3138 debug_print('KoboTouch:set_series book.series="%s"'%book.series) 3139 debug_print('KoboTouch:set_series book.series_index=', book.series_index) 3140 3141 if book.series == book.kobo_series: 3142 kobo_series_number = None 3143 if book.kobo_series_number is not None: 3144 try: 3145 kobo_series_number = float(book.kobo_series_number) 3146 except: 3147 kobo_series_number = None 3148 if kobo_series_number == book.series_index: 3149 if show_debug: 3150 debug_print('KoboTouch:set_series - series info the same - not changing') 3151 return 3152 3153 update_query = 'UPDATE content SET Series=?, SeriesNumber==? where BookID is Null and ContentID = ?' 3154 if book.series is None: 3155 update_values = (None, None, book.contentID, ) 3156 elif book.series_index is None: # This should never happen, but... 3157 update_values = (book.series, None, book.contentID, ) 3158 else: 3159 update_values = (book.series, "%g"%book.series_index, book.contentID, ) 3160 3161 cursor = connection.cursor() 3162 try: 3163 if show_debug: 3164 debug_print('KoboTouch:set_series - about to set - parameters:', update_values) 3165 cursor.execute(update_query, update_values) 3166 self.series_set += 1 3167 except: 3168 debug_print(' Database Exception: Unable to set series info') 3169 raise 3170 finally: 3171 cursor.close() 3172 3173 if show_debug: 3174 debug_print("KoboTouch:set_series - end") 3175 3176 def set_core_metadata(self, connection, book, series_only=False): 3177 # debug_print('KoboTouch:set_core_metadata book="%s"' % book.title) 3178 show_debug = self.is_debugging_title(book.title) 3179 if show_debug: 3180 debug_print('KoboTouch:set_core_metadata book="%s", \nseries_only="%s"' % (book, series_only)) 3181 3182 plugboard = None 3183 if self.plugboard_func and not series_only: 3184 if book.contentID.endswith('.kepub.epub') or not os.path.splitext(book.contentID)[1]: 3185 extension = 'kepub' 3186 else: 3187 extension = os.path.splitext(book.contentID)[1][1:] 3188 plugboard = self.plugboard_func(self.__class__.__name__, extension, self.plugboards) 3189 3190 # If the book is a kepub, and there is no kepub plugboard, use the epub plugboard if it exists. 3191 if not plugboard and extension == 'kepub': 3192 plugboard = self.plugboard_func(self.__class__.__name__, 'epub', self.plugboards) 3193 3194 if plugboard is not None: 3195 newmi = book.deepcopy_metadata() 3196 newmi.template_to_attribute(book, plugboard) 3197 else: 3198 newmi = book 3199 3200 update_query = 'UPDATE content SET ' 3201 update_values = [] 3202 set_clause = '' 3203 changes_found = False 3204 kobo_metadata = book.kobo_metadata 3205 3206 if show_debug: 3207 debug_print('KoboTouch:set_core_metadata newmi.series="%s"' % (newmi.series, )) 3208 debug_print('KoboTouch:set_core_metadata kobo_metadata.series="%s"' % (kobo_metadata.series, )) 3209 debug_print('KoboTouch:set_core_metadata newmi.series_index="%s"' % (newmi.series_index, )) 3210 debug_print('KoboTouch:set_core_metadata kobo_metadata.series_index="%s"' % (kobo_metadata.series_index, )) 3211 debug_print('KoboTouch:set_core_metadata book.kobo_series_number="%s"' % (book.kobo_series_number, )) 3212 3213 if newmi.series is not None: 3214 new_series = newmi.series 3215 try: 3216 new_series_number = "%g" % newmi.series_index 3217 except: 3218 new_series_number = None 3219 else: 3220 new_series = None 3221 new_series_number = None 3222 3223 series_changed = not (new_series == kobo_metadata.series) 3224 series_number_changed = not (new_series_number == book.kobo_series_number) 3225 if show_debug: 3226 debug_print('KoboTouch:set_core_metadata new_series="%s"' % (new_series, )) 3227 debug_print('KoboTouch:set_core_metadata new_series_number="%s"' % (new_series_number, )) 3228 debug_print('KoboTouch:set_core_metadata series_number_changed="%s"' % (series_number_changed, )) 3229 debug_print('KoboTouch:set_core_metadata series_changed="%s"' % (series_changed, )) 3230 3231 if series_changed or series_number_changed: 3232 update_values.append(new_series) 3233 set_clause += ', Series = ? ' 3234 update_values.append(new_series_number) 3235 set_clause += ', SeriesNumber = ? ' 3236 if self.supports_series_list and book.is_sideloaded: 3237 series_id = self.kobo_series_dict.get(new_series, new_series) 3238 try: 3239 kobo_series_id = book.kobo_series_id 3240 kobo_series_number_float = book.kobo_series_number_float 3241 except Exception: # This should mean the book was sent to the device during the current session. 3242 kobo_series_id = None 3243 kobo_series_number_float = None 3244 3245 if series_changed or series_number_changed \ 3246 or not kobo_series_id == series_id \ 3247 or not kobo_series_number_float == newmi.series_index: 3248 update_values.append(series_id) 3249 set_clause += ', SeriesID = ? ' 3250 update_values.append(newmi.series_index) 3251 set_clause += ', SeriesNumberFloat = ? ' 3252 if show_debug: 3253 debug_print("KoboTouch:set_core_metadata Setting SeriesID - new_series='%s', series_id='%s'" % (new_series, series_id)) 3254 3255 if not series_only: 3256 if not (newmi.title == kobo_metadata.title): 3257 update_values.append(newmi.title) 3258 set_clause += ', Title = ? ' 3259 3260 if not (authors_to_string(newmi.authors) == authors_to_string(kobo_metadata.authors)): 3261 update_values.append(authors_to_string(newmi.authors)) 3262 set_clause += ', Attribution = ? ' 3263 3264 if not (newmi.publisher == kobo_metadata.publisher): 3265 update_values.append(newmi.publisher) 3266 set_clause += ', Publisher = ? ' 3267 3268 if not (newmi.pubdate == kobo_metadata.pubdate): 3269 pubdate_string = strftime(self.TIMESTAMP_STRING, newmi.pubdate) if newmi.pubdate else None 3270 update_values.append(pubdate_string) 3271 set_clause += ', DateCreated = ? ' 3272 3273 if not (newmi.comments == kobo_metadata.comments): 3274 update_values.append(newmi.comments) 3275 set_clause += ', Description = ? ' 3276 3277 if not (newmi.isbn == kobo_metadata.isbn): 3278 update_values.append(newmi.isbn) 3279 set_clause += ', ISBN = ? ' 3280 3281 library_language = normalize_languages(kobo_metadata.languages, newmi.languages) 3282 library_language = library_language[0] if library_language is not None and len(library_language) > 0 else None 3283 if not (library_language == kobo_metadata.language): 3284 update_values.append(library_language) 3285 set_clause += ', Language = ? ' 3286 3287 if self.update_subtitle: 3288 if self.subtitle_template is None or self.subtitle_template == '': 3289 new_subtitle = None 3290 else: 3291 pb = [(self.subtitle_template, 'subtitle')] 3292 book.template_to_attribute(book, pb) 3293 new_subtitle = book.subtitle if len(book.subtitle.strip()) else None 3294 if new_subtitle is not None and new_subtitle.startswith("PLUGBOARD TEMPLATE ERROR"): 3295 debug_print("KoboTouch:set_core_metadata subtitle template error - self.subtitle_template='%s'" % self.subtitle_template) 3296 debug_print("KoboTouch:set_core_metadata - new_subtitle=", new_subtitle) 3297 3298 if (new_subtitle is not None and (book.kobo_subtitle is None or book.subtitle != book.kobo_subtitle)) or \ 3299 (new_subtitle is None and book.kobo_subtitle is not None): 3300 update_values.append(new_subtitle) 3301 set_clause += ', Subtitle = ? ' 3302 3303 if len(set_clause) > 0: 3304 update_query += set_clause[1:] 3305 changes_found = True 3306 if show_debug: 3307 debug_print('KoboTouch:set_core_metadata set_clause="%s"' % set_clause) 3308 debug_print('KoboTouch:set_core_metadata update_values="%s"' % update_values) 3309 if changes_found: 3310 update_query += 'WHERE ContentID = ? AND BookID IS NULL' 3311 update_values.append(book.contentID) 3312 cursor = connection.cursor() 3313 try: 3314 if show_debug: 3315 debug_print('KoboTouch:set_core_metadata - about to set - parameters:', update_values) 3316 debug_print('KoboTouch:set_core_metadata - about to set - update_query:', update_query) 3317 cursor.execute(update_query, update_values) 3318 self.core_metadata_set += 1 3319 except: 3320 debug_print(' Database Exception: Unable to set the core metadata') 3321 raise 3322 finally: 3323 cursor.close() 3324 3325 if show_debug: 3326 debug_print("KoboTouch:set_core_metadata - end") 3327 3328 @classmethod 3329 def config_widget(cls): 3330 # TODO: Cleanup the following 3331 cls.current_friendly_name = cls.gui_name 3332 3333 from calibre.devices.kobo.kobotouch_config import KOBOTOUCHConfig 3334 return KOBOTOUCHConfig(cls.settings(), cls.FORMATS, 3335 cls.SUPPORTS_SUB_DIRS, cls.MUST_READ_METADATA, 3336 cls.SUPPORTS_USE_AUTHOR_SORT, cls.EXTRA_CUSTOMIZATION_MESSAGE, 3337 cls, extra_customization_choices=cls.EXTRA_CUSTOMIZATION_CHOICES 3338 ) 3339 3340 @classmethod 3341 def get_pref(cls, key): 3342 ''' Get the setting named key. First looks for a device specific setting. 3343 If that is not found looks for a device default and if that is not 3344 found uses the global default.''' 3345# debug_print("KoboTouch::get_prefs - key=", key, "cls=", cls) 3346 if not cls.opts: 3347 cls.opts = cls.settings() 3348 try: 3349 return getattr(cls.opts, key) 3350 except: 3351 debug_print("KoboTouch::get_prefs - probably an extra_customization:", key) 3352 return None 3353 3354 @classmethod 3355 def save_settings(cls, config_widget): 3356 cls.opts = None 3357 config_widget.commit() 3358 3359 @classmethod 3360 def save_template(cls): 3361 return cls.settings().save_template 3362 3363 @classmethod 3364 def _config(cls): 3365 c = super()._config() 3366 3367 c.add_opt('manage_collections', default=True) 3368 c.add_opt('collections_columns', default='') 3369 c.add_opt('create_collections', default=False) 3370 c.add_opt('delete_empty_collections', default=False) 3371 c.add_opt('ignore_collections_names', default='') 3372 3373 c.add_opt('upload_covers', default=False) 3374 c.add_opt('dithered_covers', default=False) 3375 c.add_opt('keep_cover_aspect', default=False) 3376 c.add_opt('upload_grayscale', default=False) 3377 c.add_opt('letterbox_fs_covers', default=False) 3378 c.add_opt('letterbox_fs_covers_color', default=DEFAULT_COVER_LETTERBOX_COLOR) 3379 c.add_opt('png_covers', default=False) 3380 3381 c.add_opt('show_archived_books', default=False) 3382 c.add_opt('show_previews', default=False) 3383 c.add_opt('show_recommendations', default=False) 3384 3385 c.add_opt('update_series', default=True) 3386 c.add_opt('update_core_metadata', default=False) 3387 c.add_opt('update_purchased_kepubs', default=False) 3388 c.add_opt('update_device_metadata', default=True) 3389 c.add_opt('update_subtitle', default=False) 3390 c.add_opt('subtitle_template', default=None) 3391 3392 c.add_opt('modify_css', default=False) 3393 c.add_opt('override_kobo_replace_existing', default=True) # Overriding the replace behaviour is how the driver has always worked. 3394 3395 c.add_opt('support_newer_firmware', default=False) 3396 c.add_opt('debugging_title', default='') 3397 c.add_opt('driver_version', default='') # Mainly for debugging purposes, but might use if need to migrate between versions. 3398 3399 return c 3400 3401 @classmethod 3402 def settings(cls): 3403 opts = cls._config().parse() 3404 if opts.extra_customization: 3405 opts = cls.migrate_old_settings(opts) 3406 3407 cls.opts = opts 3408 return opts 3409 3410 def isAura(self): 3411 return self.detected_device.idProduct in self.AURA_PRODUCT_ID 3412 3413 def isAuraEdition2(self): 3414 return self.detected_device.idProduct in self.AURA_EDITION2_PRODUCT_ID 3415 3416 def isAuraHD(self): 3417 return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID 3418 3419 def isAuraH2O(self): 3420 return self.detected_device.idProduct in self.AURA_H2O_PRODUCT_ID 3421 3422 def isAuraH2OEdition2(self): 3423 return self.detected_device.idProduct in self.AURA_H2O_EDITION2_PRODUCT_ID 3424 3425 def isAuraOne(self): 3426 return self.detected_device.idProduct in self.AURA_ONE_PRODUCT_ID 3427 3428 def isClaraHD(self): 3429 return self.detected_device.idProduct in self.CLARA_HD_PRODUCT_ID 3430 3431 def isElipsa(self): 3432 return self.detected_device.idProduct in self.ELIPSA_PRODUCT_ID 3433 3434 def isForma(self): 3435 return self.detected_device.idProduct in self.FORMA_PRODUCT_ID 3436 3437 def isGlo(self): 3438 return self.detected_device.idProduct in self.GLO_PRODUCT_ID 3439 3440 def isGloHD(self): 3441 return self.detected_device.idProduct in self.GLO_HD_PRODUCT_ID 3442 3443 def isLibraH2O(self): 3444 return self.detected_device.idProduct in self.LIBRA_H2O_PRODUCT_ID 3445 3446 def isLibra2(self): 3447 return self.detected_device.idProduct in self.LIBRA2_PRODUCT_ID 3448 3449 def isMini(self): 3450 return self.detected_device.idProduct in self.MINI_PRODUCT_ID 3451 3452 def isNia(self): 3453 return self.detected_device.idProduct in self.NIA_PRODUCT_ID 3454 3455 def isSage(self): 3456 return self.detected_device.idProduct in self.SAGE_PRODUCT_ID 3457 3458 def isTouch(self): 3459 return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID 3460 3461 def isTouch2(self): 3462 return self.detected_device.idProduct in self.TOUCH2_PRODUCT_ID 3463 3464 def cover_file_endings(self): 3465 if self.isAura(): 3466 _cover_file_endings = self.AURA_COVER_FILE_ENDINGS 3467 elif self.isAuraEdition2(): 3468 _cover_file_endings = self.GLO_COVER_FILE_ENDINGS 3469 elif self.isAuraHD(): 3470 _cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS 3471 elif self.isAuraH2O(): 3472 _cover_file_endings = self.AURA_H2O_COVER_FILE_ENDINGS 3473 elif self.isAuraH2OEdition2(): 3474 _cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS 3475 elif self.isAuraOne(): 3476 _cover_file_endings = self.AURA_ONE_COVER_FILE_ENDINGS 3477 elif self.isClaraHD(): 3478 _cover_file_endings = self.GLO_HD_COVER_FILE_ENDINGS 3479 elif self.isElipsa(): 3480 _cover_file_endings = self.AURA_ONE_COVER_FILE_ENDINGS 3481 elif self.isForma(): 3482 _cover_file_endings = self.FORMA_COVER_FILE_ENDINGS 3483 elif self.isGlo(): 3484 _cover_file_endings = self.GLO_COVER_FILE_ENDINGS 3485 elif self.isGloHD(): 3486 _cover_file_endings = self.GLO_HD_COVER_FILE_ENDINGS 3487 elif self.isLibraH2O(): 3488 _cover_file_endings = self.LIBRA_H2O_COVER_FILE_ENDINGS 3489 elif self.isLibra2(): 3490 _cover_file_endings = self.LIBRA_H2O_COVER_FILE_ENDINGS 3491 elif self.isMini(): 3492 _cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS 3493 elif self.isNia(): 3494 _cover_file_endings = self.GLO_COVER_FILE_ENDINGS 3495 elif self.isSage(): 3496 _cover_file_endings = self.FORMA_COVER_FILE_ENDINGS 3497 elif self.isTouch(): 3498 _cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS 3499 elif self.isTouch2(): 3500 _cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS 3501 else: 3502 _cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS 3503 3504 # Don't forget to merge that on top of the common dictionary (c.f., https://stackoverflow.com/q/38987) 3505 _all_cover_file_endings = self.COMMON_COVER_FILE_ENDINGS.copy() 3506 _all_cover_file_endings.update(_cover_file_endings) 3507 return _all_cover_file_endings 3508 3509 def set_device_name(self): 3510 device_name = self.gui_name 3511 if self.isAura(): 3512 device_name = 'Kobo Aura' 3513 elif self.isAuraEdition2(): 3514 device_name = 'Kobo Aura Edition 2' 3515 elif self.isAuraHD(): 3516 device_name = 'Kobo Aura HD' 3517 elif self.isAuraH2O(): 3518 device_name = 'Kobo Aura H2O' 3519 elif self.isAuraH2OEdition2(): 3520 device_name = 'Kobo Aura H2O Edition 2' 3521 elif self.isAuraOne(): 3522 device_name = 'Kobo Aura ONE' 3523 elif self.isClaraHD(): 3524 device_name = 'Kobo Clara HD' 3525 elif self.isElipsa(): 3526 device_name = 'Kobo Elipsa' 3527 elif self.isForma(): 3528 device_name = 'Kobo Forma' 3529 elif self.isGlo(): 3530 device_name = 'Kobo Glo' 3531 elif self.isGloHD(): 3532 device_name = 'Kobo Glo HD' 3533 elif self.isLibraH2O(): 3534 device_name = 'Kobo Libra H2O' 3535 elif self.isLibra2(): 3536 device_name = 'Kobo Libra 2' 3537 elif self.isMini(): 3538 device_name = 'Kobo Mini' 3539 elif self.isNia(): 3540 device_name = 'Kobo Nia' 3541 elif self.isSage(): 3542 device_name = 'Kobo Sage' 3543 elif self.isTouch(): 3544 device_name = 'Kobo Touch' 3545 elif self.isTouch2(): 3546 device_name = 'Kobo Touch 2' 3547 self.__class__.gui_name = device_name 3548 return device_name 3549 3550 @property 3551 def manage_collections(self): 3552 return self.get_pref('manage_collections') 3553 3554 @property 3555 def create_collections(self): 3556 return self.manage_collections and self.supports_bookshelves and self.get_pref('create_collections') and len(self.collections_columns) > 0 3557 3558 @property 3559 def collections_columns(self): 3560 return self.get_pref('collections_columns') if self.manage_collections else '' 3561 3562 def get_collections_attributes(self): 3563 collections_str = self.collections_columns 3564 collections = [x.lower().strip() for x in collections_str.split(',')] if collections_str else [] 3565 return collections 3566 3567 @property 3568 def delete_empty_collections(self): 3569 return self.manage_collections and self.get_pref('delete_empty_collections') 3570 3571 @property 3572 def ignore_collections_names(self): 3573 # Cache the collection from the options string. 3574 if not hasattr(self.opts, '_ignore_collections_names'): 3575 icn = self.get_pref('ignore_collections_names') 3576 self.opts._ignore_collections_names = [x.strip() for x in icn.split(',')] if icn else [] 3577 return self.opts._ignore_collections_names 3578 3579 @property 3580 def create_bookshelves(self): 3581 # Only for backwards compatibility 3582 return self.manage_collections 3583 3584 @property 3585 def delete_empty_shelves(self): 3586 # Only for backwards compatibility 3587 return self.delete_empty_collections 3588 3589 @property 3590 def upload_covers(self): 3591 return self.get_pref('upload_covers') 3592 3593 @property 3594 def keep_cover_aspect(self): 3595 return self.upload_covers and self.get_pref('keep_cover_aspect') 3596 3597 @property 3598 def upload_grayscale(self): 3599 return self.upload_covers and self.get_pref('upload_grayscale') 3600 3601 @property 3602 def dithered_covers(self): 3603 return self.upload_grayscale and self.get_pref('dithered_covers') 3604 3605 @property 3606 def letterbox_fs_covers(self): 3607 return self.keep_cover_aspect and self.get_pref('letterbox_fs_covers') 3608 3609 @property 3610 def letterbox_fs_covers_color(self): 3611 return self.get_pref('letterbox_fs_covers_color') 3612 3613 @property 3614 def png_covers(self): 3615 return self.upload_grayscale and self.get_pref('png_covers') 3616 3617 def modifying_epub(self): 3618 return self.modifying_css() 3619 3620 def modifying_css(self): 3621 return self.get_pref('modify_css') 3622 3623 @property 3624 def override_kobo_replace_existing(self): 3625 return self.get_pref('override_kobo_replace_existing') 3626 3627 @property 3628 def update_device_metadata(self): 3629 return self.get_pref('update_device_metadata') 3630 3631 @property 3632 def update_series_details(self): 3633 return self.update_device_metadata and self.get_pref('update_series') and self.supports_series() 3634 3635 @property 3636 def update_subtitle(self): 3637 # Subtitle was added to the database at the same time as the series support. 3638 return self.update_device_metadata and self.supports_series() and self.get_pref('update_subtitle') 3639 3640 @property 3641 def subtitle_template(self): 3642 if not self.update_subtitle: 3643 return None 3644 subtitle_template = self.get_pref('subtitle_template') 3645 subtitle_template = subtitle_template.strip() if subtitle_template is not None else None 3646 return subtitle_template 3647 3648 @property 3649 def update_core_metadata(self): 3650 return self.update_device_metadata and self.get_pref('update_core_metadata') 3651 3652 @property 3653 def update_purchased_kepubs(self): 3654 return self.update_device_metadata and self.get_pref('update_purchased_kepubs') 3655 3656 @classmethod 3657 def get_debugging_title(cls): 3658 debugging_title = cls.get_pref('debugging_title') 3659 if not debugging_title: # Make sure the value is set to prevent rereading the settings. 3660 debugging_title = '' 3661 return debugging_title 3662 3663 @property 3664 def supports_bookshelves(self): 3665 return self.dbversion >= self.min_supported_dbversion 3666 3667 @property 3668 def show_archived_books(self): 3669 return self.get_pref('show_archived_books') 3670 3671 @property 3672 def show_previews(self): 3673 return self.get_pref('show_previews') 3674 3675 @property 3676 def show_recommendations(self): 3677 return self.get_pref('show_recommendations') 3678 3679 @property 3680 def read_metadata(self): 3681 return self.get_pref('read_metadata') 3682 3683 def supports_series(self): 3684 return self.dbversion >= self.min_dbversion_series 3685 3686 @property 3687 def supports_series_list(self): 3688 return self.dbversion >= self.min_dbversion_seriesid and self.fwversion >= self.min_fwversion_serieslist 3689 3690 @property 3691 def supports_audiobooks(self): 3692 return self.fwversion >= self.min_fwversion_audiobooks 3693 3694 def supports_kobo_archive(self): 3695 return self.dbversion >= self.min_dbversion_archive 3696 3697 def supports_overdrive(self): 3698 return self.fwversion >= self.min_fwversion_overdrive 3699 3700 def supports_covers_on_sdcard(self): 3701 return self.dbversion >= self.min_dbversion_images_on_sdcard and self.fwversion >= self.min_fwversion_images_on_sdcard 3702 3703 def supports_images_tree(self): 3704 return self.fwversion >= self.min_fwversion_images_tree 3705 3706 def has_externalid(self): 3707 return self.dbversion >= self.min_dbversion_externalid 3708 3709 def has_activity_table(self): 3710 return self.dbversion >= self.min_dbversion_activity 3711 3712 def modify_database_check(self, function): 3713 # Checks to see whether the database version is supported 3714 # and whether the user has chosen to support the firmware version 3715 if self.dbversion > self.supported_dbversion or self.is_supported_fwversion: 3716 # Unsupported database 3717 if not self.get_pref('support_newer_firmware'): 3718 debug_print('The database has been upgraded past supported version') 3719 self.report_progress(1.0, _('Removing books from device...')) 3720 from calibre.devices.errors import UserFeedback 3721 raise UserFeedback(_("Kobo database version unsupported - See details"), 3722 _('Your Kobo is running an updated firmware/database version.' 3723 ' As calibre does not know about this updated firmware,' 3724 ' database editing is disabled, to prevent corruption.' 3725 ' You can still send books to your Kobo with calibre, ' 3726 ' but deleting books and managing collections is disabled.' 3727 ' If you are willing to experiment and know how to reset' 3728 ' your Kobo to Factory defaults, you can override this' 3729 ' check by right clicking the device icon in calibre and' 3730 ' selecting "Configure this device" and then the' 3731 ' "Attempt to support newer firmware" option.' 3732 ' Doing so may require you to perform a factory reset of' 3733 ' your Kobo.' 3734 ) + 3735 '\n\n' + 3736 _('Discussion of any new Kobo firmware can be found in the' 3737 ' Kobo forum at MobileRead. This is at %s.' 3738 ) % 'https://www.mobileread.com/forums/forumdisplay.php?f=223' + '\n' + 3739 ( 3740 '\nDevice database version: %s.' 3741 '\nDevice firmware version: %s' 3742 ) % (self.dbversion, self.fwversion), 3743 UserFeedback.WARN 3744 ) 3745 3746 return False 3747 else: 3748 # The user chose to edit the database anyway 3749 return True 3750 else: 3751 # Supported database version 3752 return True 3753 3754 @property 3755 def is_supported_fwversion(self): 3756 # Starting with firmware version 3.19.x, the last number appears to be is a 3757 # build number. It can be safely ignored when testing the firmware version. 3758 debug_print("KoboTouch::is_supported_fwversion - self.fwversion[:2]", self.fwversion[:2]) 3759 return self.fwversion[:2] > self.max_supported_fwversion 3760 3761 @classmethod 3762 def migrate_old_settings(cls, settings): 3763 debug_print("KoboTouch::migrate_old_settings - start") 3764 debug_print("KoboTouch::migrate_old_settings - settings.extra_customization=", settings.extra_customization) 3765 debug_print("KoboTouch::migrate_old_settings - For class=", cls.name) 3766 3767 count_options = 0 3768 OPT_COLLECTIONS = count_options 3769 count_options += 1 3770 OPT_CREATE_BOOKSHELVES = count_options 3771 count_options += 1 3772 OPT_DELETE_BOOKSHELVES = count_options 3773 count_options += 1 3774 OPT_UPLOAD_COVERS = count_options 3775 count_options += 1 3776 OPT_UPLOAD_GRAYSCALE_COVERS = count_options 3777 count_options += 1 3778 OPT_KEEP_COVER_ASPECT_RATIO = count_options 3779 count_options += 1 3780 OPT_SHOW_ARCHIVED_BOOK_RECORDS = count_options 3781 count_options += 1 3782 OPT_SHOW_PREVIEWS = count_options 3783 count_options += 1 3784 OPT_SHOW_RECOMMENDATIONS = count_options 3785 count_options += 1 3786 OPT_UPDATE_SERIES_DETAILS = count_options 3787 count_options += 1 3788 OPT_MODIFY_CSS = count_options 3789 count_options += 1 3790 OPT_SUPPORT_NEWER_FIRMWARE = count_options 3791 count_options += 1 3792 OPT_DEBUGGING_TITLE = count_options 3793 3794 # Always migrate options if for the KoboTouch class. 3795 # For a subclass, only migrate the KoboTouch options if they haven't already been migrated. This is based on 3796 # the total number of options. 3797 if cls == KOBOTOUCH or len(settings.extra_customization) >= count_options: 3798 config = cls._config() 3799 debug_print("KoboTouch::migrate_old_settings - config.preferences=", config.preferences) 3800 debug_print("KoboTouch::migrate_old_settings - settings need to be migrated") 3801 settings.manage_collections = True 3802 settings.collections_columns = settings.extra_customization[OPT_COLLECTIONS] 3803 debug_print("KoboTouch::migrate_old_settings - settings.collections_columns=", settings.collections_columns) 3804 settings.create_collections = settings.extra_customization[OPT_CREATE_BOOKSHELVES] 3805 settings.delete_empty_collections = settings.extra_customization[OPT_DELETE_BOOKSHELVES] 3806 3807 settings.upload_covers = settings.extra_customization[OPT_UPLOAD_COVERS] 3808 settings.keep_cover_aspect = settings.extra_customization[OPT_KEEP_COVER_ASPECT_RATIO] 3809 settings.upload_grayscale = settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS] 3810 3811 settings.show_archived_books = settings.extra_customization[OPT_SHOW_ARCHIVED_BOOK_RECORDS] 3812 settings.show_previews = settings.extra_customization[OPT_SHOW_PREVIEWS] 3813 settings.show_recommendations = settings.extra_customization[OPT_SHOW_RECOMMENDATIONS] 3814 3815 # If the configuration hasn't been change for a long time, the last few option will be out 3816 # of sync. The last two options are always the support newer firmware and the debugging 3817 # title. Set seties and Modify CSS were the last two new options. The debugging title is 3818 # a string, so looking for that. 3819 start_subclass_extra_options = OPT_MODIFY_CSS 3820 debugging_title = '' 3821 if isinstance(settings.extra_customization[OPT_MODIFY_CSS], string_or_bytes): 3822 debug_print("KoboTouch::migrate_old_settings - Don't have update_series option") 3823 settings.update_series = config.get_option('update_series').default 3824 settings.modify_css = config.get_option('modify_css').default 3825 settings.support_newer_firmware = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS] 3826 debugging_title = settings.extra_customization[OPT_MODIFY_CSS] 3827 start_subclass_extra_options = OPT_MODIFY_CSS + 1 3828 elif isinstance(settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE], string_or_bytes): 3829 debug_print("KoboTouch::migrate_old_settings - Don't have modify_css option") 3830 settings.update_series = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS] 3831 settings.modify_css = config.get_option('modify_css').default 3832 settings.support_newer_firmware = settings.extra_customization[OPT_MODIFY_CSS] 3833 debugging_title = settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE] 3834 start_subclass_extra_options = OPT_SUPPORT_NEWER_FIRMWARE + 1 3835 else: 3836 debug_print("KoboTouch::migrate_old_settings - Have all options") 3837 settings.update_series = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS] 3838 settings.modify_css = settings.extra_customization[OPT_MODIFY_CSS] 3839 settings.support_newer_firmware = settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE] 3840 debugging_title = settings.extra_customization[OPT_DEBUGGING_TITLE] 3841 start_subclass_extra_options = OPT_DEBUGGING_TITLE + 1 3842 3843 settings.debugging_title = debugging_title if isinstance(debugging_title, string_or_bytes) else '' 3844 settings.update_device_metadata = settings.update_series 3845 settings.extra_customization = settings.extra_customization[start_subclass_extra_options:] 3846 3847 return settings 3848 3849 def is_debugging_title(self, title): 3850 if not DEBUG: 3851 return False 3852# debug_print("KoboTouch:is_debugging - title=", title) 3853 3854 if not self.debugging_title and not self.debugging_title == '': 3855 self.debugging_title = self.get_debugging_title() 3856 try: 3857 is_debugging = len(self.debugging_title) > 0 and title.lower().find(self.debugging_title.lower()) >= 0 or len(title) == 0 3858 except: 3859 debug_print(("KoboTouch::is_debugging_title - Exception checking debugging title for title '{}'.").format(title)) 3860 is_debugging = False 3861 3862 return is_debugging 3863 3864 def dump_bookshelves(self, connection): 3865 if not (DEBUG and self.supports_bookshelves and False): 3866 return 3867 3868 debug_print('KoboTouch:dump_bookshelves - start') 3869 shelf_query = 'SELECT * FROM Shelf' 3870 shelfcontent_query = 'SELECT * FROM ShelfContent' 3871 placeholder = '%s' 3872 3873 cursor = connection.cursor() 3874 3875 prints('\nBookshelves on device:') 3876 cursor.execute(shelf_query) 3877 i = 0 3878 for row in cursor: 3879 placeholders = ', '.join(placeholder for unused in row) 3880 prints(placeholders%row) 3881 i += 1 3882 if i == 0: 3883 prints("No shelves found!!") 3884 else: 3885 prints("Number of shelves=%d"%i) 3886 3887 prints('\nBooks on shelves on device:') 3888 cursor.execute(shelfcontent_query) 3889 i = 0 3890 for row in cursor: 3891 placeholders = ', '.join(placeholder for unused in row) 3892 prints(placeholders%row) 3893 i += 1 3894 if i == 0: 3895 prints("No books are on any shelves!!") 3896 else: 3897 prints("Number of shelved books=%d"%i) 3898 3899 cursor.close() 3900 debug_print('KoboTouch:dump_bookshelves - end') 3901 3902 def __str__(self, *args, **kwargs): 3903 options = ', '.join(['%s: %s' % (x.name, self.get_pref(x.name)) for x in self._config().preferences]) 3904 return "Driver:%s, Options - %s" % (self.name, options) 3905 3906 3907if __name__ == '__main__': 3908 dev = KOBOTOUCH(None) 3909 dev.startup() 3910 try: 3911 dev.initialize() 3912 from calibre.devices.scanner import DeviceScanner 3913 scanner = DeviceScanner() 3914 scanner.scan() 3915 devs = scanner.devices 3916# debug_print("unit test: devs.__class__=", devs.__class__) 3917# debug_print("unit test: devs.__class__=", devs.__class__.__name__) 3918 debug_print("unit test: devs=", devs) 3919 debug_print("unit test: dev=", dev) 3920 # cd = dev.detect_managed_devices(devs) 3921 # if cd is None: 3922 # raise ValueError('Failed to detect KOBOTOUCH device') 3923 dev.set_progress_reporter(prints) 3924# dev.open(cd, None) 3925# dev.filesystem_cache.dump() 3926 print('Prefix for main memory:', dev.dbversion) 3927 finally: 3928 dev.shutdown() 3929