90 lines
3.2 KiB
Python
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()
|