From 45d7c874622330eca22df508a8feb40d6bd59747 Mon Sep 17 00:00:00 2001 From: j-howell Date: Thu, 30 Jan 2025 08:01:29 -0500 Subject: [PATCH] Allow import of KFX files from MTP Kindle devices rev 2 --- src/calibre/devices/mtp/driver.py | 37 ++++++++++++++++++++- src/calibre/devices/mtp/filesystem_cache.py | 7 ++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 98ef067366..3ec925113c 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -114,8 +114,9 @@ class MTP_DEVICE(BASE): 'pictures', 'ringtones', 'samsung', 'sony', 'htc', 'bluetooth', 'fonts', 'games', 'lost.dir', 'video', 'whatsapp', 'image', 'com.zinio.mobile.android.reader'}: return True - if lpath[0].startswith('.') and lpath[0] != '.tolino': + if lpath[0].startswith('.') and lpath[0] != '.tolino' and lpath[0] != '.notebooks': # apparently the Tolino for some reason uses a hidden folder for its library, sigh. + # Kindle Scribe stores user notebooks in subfolders of '.notebooks' return True if lpath[0] == 'system' and not self.is_kindle: # on Kindles we need the system/thumbnails folder for the amazon cover bug workaround @@ -349,6 +350,21 @@ class MTP_DEVICE(BASE): from calibre.customize.ui import quick_metadata from calibre.ebooks.metadata.meta import get_metadata ext = mtp_file.name.rpartition('.')[-1].lower() + if self.is_kindle and ext == 'kfx' and mtp_file.name != 'metadata.kfx': + # locate the actual file containing KFX book metadata + stream = BytesIO() + try: + self.get_file_by_name(stream, mtp_file.parent, *(mtp_file.name[:-4] + '.sdr', 'assets', 'metadata.kfx')) + except Exception: + pass # expect failure for sideloaded books and personal documents + else: + stream.name = 'metadata.kfx' + with quick_metadata: + metadata = get_metadata(stream, stream_type=ext, + force_read_metadata=True, + pattern=self.build_template_regexp()) + if metadata.title != 'metadata' and metadata.title != 'Unknown': + return metadata stream = self.get_mtp_file(mtp_file) with quick_metadata: return get_metadata(stream, stream_type=ext, @@ -417,7 +433,26 @@ class MTP_DEVICE(BASE): ans.append((path, e, traceback.format_exc())) else: ans.append(out.name) + if self.is_kindle and path.endswith('.kfx'): + # copy additional KFX files from the associated .sdr folder + try: + sdr_folder_name = f.name[:-4] + '.sdr' + new_sdr_path = os.path.join(os.path.split(out.name)[0], sdr_folder_name) + os.mkdir(new_sdr_path) + self.scan_sdr_for_kfx_files(f.parent, (sdr_folder_name, 'assets'), new_sdr_path) + except Exception: + traceback.print_exc() return ans + + def scan_sdr_for_kfx_files(self, parent, folders, new_sdr_path): + for ff in self.list_folder_by_name(parent, *folders): + if ff.is_folder: + self.scan_sdr_for_kfx_files(parent, folders + (ff.name,), new_sdr_path) + elif ff.name.endswith('.kfx') or ff.name == 'voucher': + name = ''.join(shorten_components_to(245 - len(new_sdr_path), [ff.name])) if iswindows else ff.name + with open(os.path.join(new_sdr_path, name), 'wb') as out: + self.get_file_by_name(out, parent, *(folders + (ff.name,))) + # }}} # Sending files to the device {{{ diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 59d21f678d..19709b50c1 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -97,6 +97,13 @@ class FileOrFolder: self.is_ebook = (not self.is_folder and not self.is_storage and self.name.rpartition('.')[-1].lower() in bexts and not self.name.startswith('._')) + # allow Kindle Scribe notebooks to be imported and preserve the notebook name + if self.name == 'nbk' and not (self.is_folder or self.is_storage): + if len(self.full_path) >= 3 and self.full_path[-3] == '.notebooks': + nbk_name = self.full_path[-2].replace('!!notebook', '').replace('!!', ' ') + self.name = f'Scribe {nbk_name} Notebook.kfx' + self.is_ebook = True + def __repr__(self): if self.is_storage: name = 'Storage'