""" Unified metadata index for the Curator pool. Instead of one sidecar file per movie, the whole pool keeps a single JSON index at ``/.Curator.!index``. Curator owns the pool (it inserts/removes files itself), so files never move behind its back and a central index is safe. The index also carries **library-level settings** (``tag_schema``, ``copyasis_folders``) under a ``settings`` section, so everything describing a particular pool travels with the pool. The global config keeps only app data (window state, MRU) and the pointers to where the pool/output live. On-disk shape:: {"settings": {...}, "movies": {"Filmy/Matrix.mkv": {...}, ...}} Records are keyed by the file's path relative to the pool root (POSIX form), e.g. ``"Filmy/Matrix.mkv"`` — stable and portable across machines. """ import json from pathlib import Path INDEX_FILENAME = ".Curator.!index" class PoolIndex: """Single-file JSON metadata store for all files in a pool.""" def __init__(self, pool_dir: Path) -> None: self.pool_dir = Path(pool_dir) self.index_path = self.pool_dir / INDEX_FILENAME self.records: dict[str, dict] = {} self.settings: dict = {} self.load() def _key(self, file_path: Path) -> str: """Pool-relative POSIX key for a file path.""" path = Path(file_path) try: return path.relative_to(self.pool_dir).as_posix() except ValueError: return path.as_posix() def load(self) -> None: """Load the index from disk (missing/corrupt index = empty).""" if not self.index_path.exists(): self.records = {} self.settings = {} return try: with open(self.index_path, "r", encoding="utf-8") as f: data = json.load(f) self.records = data.get("movies", {}) self.settings = data.get("settings", {}) except (json.JSONDecodeError, OSError): self.records = {} self.settings = {} def save(self) -> None: """Persist the index to disk.""" self.pool_dir.mkdir(parents=True, exist_ok=True) with open(self.index_path, "w", encoding="utf-8") as f: json.dump( {"settings": self.settings, "movies": self.records}, f, indent=2, ensure_ascii=False, ) def get_setting(self, key: str, default=None): """Return a library-level setting stored in the index.""" return self.settings.get(key, default) def set_setting(self, key: str, value) -> None: """Upsert a library-level setting and persist the index.""" self.settings[key] = value self.save() def get(self, file_path: Path) -> dict | None: """Return the record for a file, or None if it is not indexed.""" return self.records.get(self._key(file_path)) def set(self, file_path: Path, data: dict) -> None: """Upsert a record and persist the index.""" self.records[self._key(file_path)] = data self.save() def delete(self, file_path: Path) -> None: """Remove a record (if present) and persist the index.""" if self.records.pop(self._key(file_path), None) is not None: self.save()