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:
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:

View File

@ -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: