From 8a2b027e399d66b3afaf9341150ed1e40b9a3dad Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Mon, 14 Jul 2025 22:46:50 +0200 Subject: [PATCH 1/7] APNX generation for MTP devices --- src/calibre/devices/mtp/driver.py | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index cba7775468..b4af696d1f 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -544,6 +544,8 @@ class MTP_DEVICE(BASE): mtp_file = self.put_file(parent, path[-1], stream, sz) try: self.upload_cover(parent, relpath, storage, mi, stream) + # Upload the apnx file + self.upload_apnx(parent, fname, storage, mi, infile) except Exception: import traceback traceback.print_exc() @@ -632,6 +634,37 @@ class MTP_DEVICE(BASE): debug(f'Restored {count} cover thumbnails that were destroyed by Amazon') # }}} + def upload_apnx(self, parent, filename, storage, mi, filepath): + from calibre.devices.kindle.apnx import APNXBuilder + apnx_builder = APNXBuilder() + + name = filename.rpartition('.')[0] + apnx_filename = f'{name[:-2]}.apnx' + apnx_local_path = f'{os.path.join('/tmp', apnx_filename)}' + + try: + # TODO + method = None + custom_page_count = 100 + + apnx_builder.write_apnx(filepath, apnx_local_path, method=method, page_count=custom_page_count) + + apnx_size = os.path.getsize(apnx_local_path) + apnx_stream = open(apnx_local_path, 'rb') + + try: + apnx_path = parent.name, f'{name[:-2]}.sdr', apnx_filename + sdr_parent = self.ensure_parent(storage, apnx_path) + self.put_file(sdr_parent, apnx_filename, apnx_stream, apnx_size) + finally: + apnx_stream.close() + except: + print('Failed to generate APNX') + import traceback + traceback.print_exc() + finally: + os.remove(apnx_local_path) + def add_books_to_metadata(self, mtp_files, metadata, booklists): debug('add_books_to_metadata() called') from calibre.devices.mtp.books import Book From 35f701fb9f5a2e0312bd113c66f2f36e794bf62f Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Mon, 14 Jul 2025 23:01:19 +0200 Subject: [PATCH 2/7] Only upload APNX for Kindle devices --- src/calibre/devices/mtp/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index b4af696d1f..9568dd4e00 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -545,7 +545,8 @@ class MTP_DEVICE(BASE): try: self.upload_cover(parent, relpath, storage, mi, stream) # Upload the apnx file - self.upload_apnx(parent, fname, storage, mi, infile) + if self.is_kindle: + self.upload_apnx(parent, fname, storage, mi, infile) except Exception: import traceback traceback.print_exc() From 6d5c681a86ba26cdfc4389b3bde519cbcfdcad68 Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Mon, 14 Jul 2025 23:19:22 +0200 Subject: [PATCH 3/7] Options for APNX generation in MTP devices - No UI --- src/calibre/devices/mtp/driver.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 9568dd4e00..1903cc7335 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -86,6 +86,12 @@ class MTP_DEVICE(BASE): p.defaults['history'] = {} p.defaults['rules'] = [] p.defaults['ignored_folders'] = {} + p.defaults['apnx'] = { + 'send': True, + 'method': 'fast', + 'custom_column_page_count': None, + 'custom_column_method': None, + } return self._prefs @@ -545,7 +551,8 @@ class MTP_DEVICE(BASE): try: self.upload_cover(parent, relpath, storage, mi, stream) # Upload the apnx file - if self.is_kindle: + if self.is_kindle and self.get_pref('apnx').get('send', False): + debug('Uploading APNX file for', fname) self.upload_apnx(parent, fname, storage, mi, infile) except Exception: import traceback @@ -644,9 +651,25 @@ class MTP_DEVICE(BASE): apnx_local_path = f'{os.path.join('/tmp', apnx_filename)}' try: - # TODO - method = None - custom_page_count = 100 + custom_page_count = 0 + cust_col_name = self.get_pref('apnx').get('custom_column_page_count', None) + if cust_col_name: + try: + custom_page_count = int(mi.get(cust_col_name, 0)) + except: + pass + + method = self.get_pref('apnx').get('method', 'fast') + cust_col_method = self.get_pref('apnx').get('custom_column_method', None) + if cust_col_method: + try: + method = str(mi.get(cust_col_method)).lower() + if method is not None: + method = method.lower() + if method not in ('fast', 'accurate', 'pagebreak'): + method = None + except: + prints(f'Invalid custom column method: {cust_col_method}, ignoring') apnx_builder.write_apnx(filepath, apnx_local_path, method=method, page_count=custom_page_count) From 7d2afdcec2564ac0d54f83c661ddc83a07e368ed Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Tue, 15 Jul 2025 07:54:02 +0200 Subject: [PATCH 4/7] Use tempfile API to create local APNX file --- src/calibre/devices/mtp/driver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 1903cc7335..7837cd1de3 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -644,13 +644,13 @@ class MTP_DEVICE(BASE): def upload_apnx(self, parent, filename, storage, mi, filepath): from calibre.devices.kindle.apnx import APNXBuilder - apnx_builder = APNXBuilder() + import tempfile - name = filename.rpartition('.')[0] - apnx_filename = f'{name[:-2]}.apnx' - apnx_local_path = f'{os.path.join('/tmp', apnx_filename)}' + apnx_local_path = tempfile.NamedTemporaryFile(prefix='calibre-apnx-', suffix='.apnx', delete=False).name try: + apnx_builder = APNXBuilder() + custom_page_count = 0 cust_col_name = self.get_pref('apnx').get('custom_column_page_count', None) if cust_col_name: @@ -677,6 +677,8 @@ class MTP_DEVICE(BASE): apnx_stream = open(apnx_local_path, 'rb') try: + name = filename.rpartition('.')[0] + apnx_filename = f'{name[:-2]}.apnx' apnx_path = parent.name, f'{name[:-2]}.sdr', apnx_filename sdr_parent = self.ensure_parent(storage, apnx_path) self.put_file(sdr_parent, apnx_filename, apnx_stream, apnx_size) From 84a3f8818d9444f60cac7ff12c72c7e7525c0fd6 Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Tue, 15 Jul 2025 19:33:27 +0200 Subject: [PATCH 5/7] Feeback from forum applied --- src/calibre/devices/mtp/driver.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 7837cd1de3..9162572ffe 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -644,19 +644,19 @@ class MTP_DEVICE(BASE): def upload_apnx(self, parent, filename, storage, mi, filepath): from calibre.devices.kindle.apnx import APNXBuilder - import tempfile + from calibre.ptempfile import PersistentTemporaryFile - apnx_local_path = tempfile.NamedTemporaryFile(prefix='calibre-apnx-', suffix='.apnx', delete=False).name + apnx_local_file = PersistentTemporaryFile('.apnx') + apnx_local_path = apnx_local_file.name + apnx_local_file.close() try: - apnx_builder = APNXBuilder() - custom_page_count = 0 cust_col_name = self.get_pref('apnx').get('custom_column_page_count', None) if cust_col_name: try: custom_page_count = int(mi.get(cust_col_name, 0)) - except: + except Exception: pass method = self.get_pref('apnx').get('method', 'fast') @@ -668,23 +668,22 @@ class MTP_DEVICE(BASE): method = method.lower() if method not in ('fast', 'accurate', 'pagebreak'): method = None - except: + except Exception: prints(f'Invalid custom column method: {cust_col_method}, ignoring') + apnx_builder = APNXBuilder() apnx_builder.write_apnx(filepath, apnx_local_path, method=method, page_count=custom_page_count) apnx_size = os.path.getsize(apnx_local_path) - apnx_stream = open(apnx_local_path, 'rb') - try: + with open(apnx_local_path, 'rb') as apnx_stream: name = filename.rpartition('.')[0] apnx_filename = f'{name[:-2]}.apnx' apnx_path = parent.name, f'{name[:-2]}.sdr', apnx_filename sdr_parent = self.ensure_parent(storage, apnx_path) self.put_file(sdr_parent, apnx_filename, apnx_stream, apnx_size) - finally: - apnx_stream.close() - except: + + except Exception: print('Failed to generate APNX') import traceback traceback.print_exc() From b1f84b9b319f8324003f58b5e55b85d705452cd9 Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Tue, 15 Jul 2025 21:22:08 +0200 Subject: [PATCH 6/7] UI to configure APNX settings --- src/calibre/devices/mtp/defaults.py | 1 + src/calibre/devices/mtp/driver.py | 13 +++-- src/calibre/gui2/device_drivers/mtp_config.py | 53 +++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/mtp/defaults.py b/src/calibre/devices/mtp/defaults.py index db3c9400e0..f79c25dd4c 100644 --- a/src/calibre/devices/mtp/defaults.py +++ b/src/calibre/devices/mtp/defaults.py @@ -26,6 +26,7 @@ class DeviceDefaults: 'format_map': ['azw3', 'mobi', 'azw', 'azw1', 'azw4', 'kfx', 'pdf'], 'send_to': ['documents', 'kindle', 'books'], + 'apnx': {'send': True, 'method': 'fast', 'custom_column_page_count': None, 'custom_column_method': None} } ), # B&N devices diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 9162572ffe..406cf58f88 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -87,7 +87,7 @@ class MTP_DEVICE(BASE): p.defaults['rules'] = [] p.defaults['ignored_folders'] = {} p.defaults['apnx'] = { - 'send': True, + 'send': False, 'method': 'fast', 'custom_column_page_count': None, 'custom_column_method': None, @@ -643,6 +643,7 @@ class MTP_DEVICE(BASE): # }}} def upload_apnx(self, parent, filename, storage, mi, filepath): + debug('upload_apnx() called') from calibre.devices.kindle.apnx import APNXBuilder from calibre.ptempfile import PersistentTemporaryFile @@ -651,16 +652,19 @@ class MTP_DEVICE(BASE): apnx_local_file.close() try: + pref = self.get_pref('apnx') + custom_page_count = 0 - cust_col_name = self.get_pref('apnx').get('custom_column_page_count', None) + cust_col_name = pref.get('custom_column_page_count', None) if cust_col_name: try: custom_page_count = int(mi.get(cust_col_name, 0)) except Exception: pass - method = self.get_pref('apnx').get('method', 'fast') - cust_col_method = self.get_pref('apnx').get('custom_column_method', None) + method = pref.get('method', 'fast') + + cust_col_method = pref.get('custom_column_method', None) if cust_col_method: try: method = str(mi.get(cust_col_method)).lower() @@ -689,6 +693,7 @@ class MTP_DEVICE(BASE): traceback.print_exc() finally: os.remove(apnx_local_path) + debug('upload_apnx() ended') def add_books_to_metadata(self, mtp_files, metadata, booklists): debug('add_books_to_metadata() called') diff --git a/src/calibre/gui2/device_drivers/mtp_config.py b/src/calibre/gui2/device_drivers/mtp_config.py index 2800c3e8cd..5d00077942 100644 --- a/src/calibre/gui2/device_drivers/mtp_config.py +++ b/src/calibre/gui2/device_drivers/mtp_config.py @@ -31,6 +31,7 @@ from qt.core import ( QVBoxLayout, QWidget, pyqtSignal, + QCheckBox, ) from calibre.ebooks import BOOK_EXTENSIONS @@ -351,6 +352,51 @@ class FormatRules(QGroupBox): yield r # }}} +class APNX(QWidget): + def __init__(self, apnx): + QWidget.__init__(self) + self.layout = l = QVBoxLayout() + self.setLayout(l) + + self.layout.setAlignment(Qt.AlignTop) + + self.send = f1 = QCheckBox(_('Send page number information when sending books')) + f1.setChecked(apnx.get('send', False)) + l.addWidget(f1) + + label2 = QLabel('

' + _('Page count calculation method') + '

') + label2.setWordWrap(True) + l.addWidget(label2) + self.method = f2 = QComboBox(self) + f2.addItem('fast', 'fast') + f2.addItem('accurate', 'accurate') + f2.addItem('pagebrek', 'pagebreak') + f2.setCurrentIndex(f2.findText(apnx.get('method', 'fast'))) + l.addWidget(f2) + + label3 = QLabel('

' + _('Custom column name to retrieve page counts from') + '

') + label3.setWordWrap(True) + l.addWidget(label3) + self.column_page_count = f3 = QLineEdit(self) + f3.setText(apnx.get('custom_column_page_count', '')) + l.addWidget(f3) + + label4 = QLabel('

' + _('Custom column name to retrieve calculation method from') + '

') + label4.setWordWrap(True) + l.addWidget(label4) + self.column_method = f4 = QLineEdit(self) + f4.setText(apnx.get('custom_column_method', '')) + l.addWidget(f4) + + @property + def apnx(self): + result = { + 'send': self.send.isChecked(), + 'method': str(self.method.currentData()).strip(), + 'custom_column_page_count': str(self.column_page_count.text()).strip(), + 'custom_column_method': str(self.column_method.text()).strip(), + } + return result class MTPConfig(QTabWidget): @@ -421,6 +467,10 @@ class MTPConfig(QTabWidget): l.addWidget(r, 7, 0, 1, 2) l.setRowStretch(7, 100) + if self.device.is_kindle: + self.apnxTab = APNX(self.get_pref('apnx')) + self.addTab(self.apnxTab, _('Page numbering (APNX)')) + self.igntab = IgnoredDevices(self.device.prefs['history'], self.device.prefs['blacklist']) self.addTab(self.igntab, _('Ignored devices')) @@ -509,6 +559,9 @@ class MTPConfig(QTabWidget): if self.current_ignored_folders != self.initial_ignored_folders: p['ignored_folders'] = self.current_ignored_folders + p.pop('apnx', None) + p['apnx'] = self.apnxTab.apnx + if self.current_device_key is not None: self.device.prefs[self.current_device_key] = p From 2c6084700a44ef1b5151291d975007263255ab58 Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Tue, 15 Jul 2025 22:28:12 +0200 Subject: [PATCH 7/7] Fix for APNX file name --- src/calibre/devices/mtp/driver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 406cf58f88..a92ad011df 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -552,8 +552,9 @@ class MTP_DEVICE(BASE): self.upload_cover(parent, relpath, storage, mi, stream) # Upload the apnx file if self.is_kindle and self.get_pref('apnx').get('send', False): - debug('Uploading APNX file for', fname) - self.upload_apnx(parent, fname, storage, mi, infile) + name = path[-1].rpartition('.')[0] + debug('Uploading APNX file for', name) + self.upload_apnx(parent, name, storage, mi, infile) except Exception: import traceback traceback.print_exc() @@ -642,7 +643,7 @@ class MTP_DEVICE(BASE): debug(f'Restored {count} cover thumbnails that were destroyed by Amazon') # }}} - def upload_apnx(self, parent, filename, storage, mi, filepath): + def upload_apnx(self, parent, name, storage, mi, filepath): debug('upload_apnx() called') from calibre.devices.kindle.apnx import APNXBuilder from calibre.ptempfile import PersistentTemporaryFile @@ -681,9 +682,8 @@ class MTP_DEVICE(BASE): apnx_size = os.path.getsize(apnx_local_path) with open(apnx_local_path, 'rb') as apnx_stream: - name = filename.rpartition('.')[0] - apnx_filename = f'{name[:-2]}.apnx' - apnx_path = parent.name, f'{name[:-2]}.sdr', apnx_filename + apnx_filename = f'{name}.apnx' + apnx_path = parent.name, f'{name}.sdr', apnx_filename sdr_parent = self.ensure_parent(storage, apnx_path) self.put_file(sdr_parent, apnx_filename, apnx_stream, apnx_size)