Files
Curator/src/core/pool_index.py
T

90 lines
3.2 KiB
Python

"""
Unified metadata index for the Curator pool.
Instead of one sidecar file per movie, the whole pool keeps a single JSON index
at ``<pool>/.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()