From 0f12c4985f6649840b6361b9e37b257c3121d284 Mon Sep 17 00:00:00 2001 From: Maksim Totmin Date: Sun, 31 May 2026 21:20:42 +0700 Subject: [PATCH] fix: robust iOpenPod imports and sync error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _import_iop() now permanently evicts local ipod_device from sys.modules on first call (instead of restoring it), caches all iOpenPod modules, and avoids the spurious ModuleNotFoundError cascade on rapid calls. sync_itunescdb() wrapped in try/except — no longer propagates import/sync errors to add_track() callers. ipod_device.py fallback no longer duplicates files to /Music/ when add_track() already copied them to F00/. --- src/ipod_device.py | 5 +++-- src/ipod_nano7_db.py | 50 ++++++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/ipod_device.py b/src/ipod_device.py index 83d174f..933d799 100644 --- a/src/ipod_device.py +++ b/src/ipod_device.py @@ -543,9 +543,10 @@ class IPodDevice: except ImportError: self.logger.warning("ipod_nano7_db not available, using simple file copy") except Exception as e: - self.logger.error(f"Nano7 database transfer failed: {e}, falling back to simple copy") + self.logger.error(f"Nano7 database transfer failed: {e}") + return False # Don't fall back — file was already copied to F00/ - # Fallback: simple file copy (won't show in iPod interface) + # Fallback: simple file copy — only on ImportError if relative_path: dest_dir = os.path.join(self.mount_point, relative_path) else: diff --git a/src/ipod_nano7_db.py b/src/ipod_nano7_db.py index e84452a..1f635c3 100644 --- a/src/ipod_nano7_db.py +++ b/src/ipod_nano7_db.py @@ -36,34 +36,43 @@ from mutagen import File as MutagenFile from mutagen.easyid3 import EasyID3 _IOP_ROOT = "/tmp/iOpenPod" +_IOP_CACHE: dict[str, Any] = {} +_IOP_READY = False def _import_iop(module_name: str): - """Import a module from iOpenPod, bypassing any locally-cached conflict. + """Import a module from iOpenPod, evicting our local ipod_device once. - main.py imports our local ``ipod_device`` first, which caches it in - ``sys.modules``. When ipod_nano7_db later tries ``from ipod_device - import ChecksumType``, Python returns the cached *local* module - (which lacks ChecksumType). This helper temporarily evicts the - conflicting local modules, adds the iOpenPod root to sys.path, loads - the requested module from there, then restores the path. + main.py imports our local ``ipod_device`` early, caching it in + ``sys.modules``. On the FIRST call here we permanently evict the + local module so iOpenPod's ``ipod_device`` takes its place in + ``sys.modules``. Our local classes (IPodDevice) were already + bound by ``from ipod_device import IPodDevice`` in main.py before + the eviction, so they stay alive through their own references. + + Subsequent calls are served from a simple module cache. """ - # Modules that may be cached as our local copies and need eviction. - _conflicting = {"ipod_device"} - _saved = {} - for name in _conflicting & set(sys.modules): - _saved[name] = sys.modules.pop(name) + global _IOP_READY + if module_name in _IOP_CACHE: + return _IOP_CACHE[module_name] + + if not _IOP_READY: + # Evict local shadows ONCE so iOpenPod modules resolve correctly. + sys.modules.pop("ipod_device", None) + _IOP_READY = True + logger.debug("iOpenPod boot: evicted local ipod_device") had_iop = _IOP_ROOT in sys.path if not had_iop: sys.path.insert(0, _IOP_ROOT) - try: - return importlib.import_module(module_name) + mod = importlib.import_module(module_name) finally: if not had_iop: sys.path.remove(_IOP_ROOT) - sys.modules.update(_saved) + + _IOP_CACHE[module_name] = mod + return mod logger = logging.getLogger(__name__) @@ -463,12 +472,13 @@ class Nano7Database: # ── sync: regenerate everything via iOpenPod ────────────────────── def sync_itunescdb(self) -> None: - """Regenerate ALL databases using iOpenPod's writers. + """Regenerate ALL databases using iOpenPod's writers.""" + try: + self._sync_itunescdb_impl() + except Exception as e: + logger.exception("iTunesDB sync failed: %s", e) - Scans all audio files on iPod, builds correct TrackInfo list, - writes SQLite databases (Library/Locations/Dynamic/Extras/Genius), - writes binary iTunesDB, writes iTunesCDB.ext and Locations.cbk. - """ + def _sync_itunescdb_impl(self) -> None: # 1. Scan all audio files on iPod scanned = self._scan_ipod_files() if not scanned: