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