fix: robust iOpenPod imports and sync error handling

_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/.
This commit is contained in:
Maksim Totmin 2026-05-31 21:20:42 +07:00
parent 4f7bd2709c
commit 0f12c4985f
2 changed files with 33 additions and 22 deletions

View File

@ -543,9 +543,10 @@ class IPodDevice:
except ImportError: except ImportError:
self.logger.warning("ipod_nano7_db not available, using simple file copy") self.logger.warning("ipod_nano7_db not available, using simple file copy")
except Exception as e: 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: if relative_path:
dest_dir = os.path.join(self.mount_point, relative_path) dest_dir = os.path.join(self.mount_point, relative_path)
else: else:

View File

@ -36,34 +36,43 @@ from mutagen import File as MutagenFile
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
_IOP_ROOT = "/tmp/iOpenPod" _IOP_ROOT = "/tmp/iOpenPod"
_IOP_CACHE: dict[str, Any] = {}
_IOP_READY = False
def _import_iop(module_name: str): 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 main.py imports our local ``ipod_device`` early, caching it in
``sys.modules``. When ipod_nano7_db later tries ``from ipod_device ``sys.modules``. On the FIRST call here we permanently evict the
import ChecksumType``, Python returns the cached *local* module local module so iOpenPod's ``ipod_device`` takes its place in
(which lacks ChecksumType). This helper temporarily evicts the ``sys.modules``. Our local classes (IPodDevice) were already
conflicting local modules, adds the iOpenPod root to sys.path, loads bound by ``from ipod_device import IPodDevice`` in main.py before
the requested module from there, then restores the path. 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. global _IOP_READY
_conflicting = {"ipod_device"} if module_name in _IOP_CACHE:
_saved = {} return _IOP_CACHE[module_name]
for name in _conflicting & set(sys.modules):
_saved[name] = sys.modules.pop(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 had_iop = _IOP_ROOT in sys.path
if not had_iop: if not had_iop:
sys.path.insert(0, _IOP_ROOT) sys.path.insert(0, _IOP_ROOT)
try: try:
return importlib.import_module(module_name) mod = importlib.import_module(module_name)
finally: finally:
if not had_iop: if not had_iop:
sys.path.remove(_IOP_ROOT) sys.path.remove(_IOP_ROOT)
sys.modules.update(_saved)
_IOP_CACHE[module_name] = mod
return mod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -463,12 +472,13 @@ class Nano7Database:
# ── sync: regenerate everything via iOpenPod ────────────────────── # ── sync: regenerate everything via iOpenPod ──────────────────────
def sync_itunescdb(self) -> None: 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, def _sync_itunescdb_impl(self) -> None:
writes SQLite databases (Library/Locations/Dynamic/Extras/Genius),
writes binary iTunesDB, writes iTunesCDB.ext and Locations.cbk.
"""
# 1. Scan all audio files on iPod # 1. Scan all audio files on iPod
scanned = self._scan_ipod_files() scanned = self._scan_ipod_files()
if not scanned: if not scanned: