neo-pod-desktop/src/session_store.py
Maksim Totmin ba7144c88f feat: migrate LibraryCache to SQLite, add SessionStore + splash overlay
- Rewrite LibraryCache from JSON (cache dir) to SQLite (data dir with WAL)
- Each track gets a stable UUID so play stats survive file renames/re-encodes
- Thread-safe via PRAGMA busy_timeout + RLock
- Automatic backup rotation (.backup, .bak) on every write
- One-shot migration from old ~/.cache/library_cache.json
- Add SessionStore (~/.local/share/session.json) for playback state
- Add SplashOverlay with spinning animation on first (empty-DB) launch
- _scan_library() now accepts on_progress callback for the overlay
- Fix _mtime → mtime SQLite column name lookup in recently-added filter
- PlaylistManager path migrated to ~/.local/share/ with auto-migration
- Metadata sync: remove dead consume_play_deltas(), use library API
2026-06-01 22:12:56 +07:00

89 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""
Session state store — persistent playback state.
Stored as JSON in ``~/.local/share/neo-pod-desktop/session.json``.
Replaces the ``[Playback]`` section of config.ini — session state
(user's last track, position, volume, playback mode) is user data,
not configuration.
"""
import json
import logging
import os
from typing import Any
from xdg_base import xdg_data_path
logger = logging.getLogger(__name__)
_SESSION_FILE = "session.json"
class SessionStore:
def __init__(self, path: str | None = None):
self._path = path or os.path.join(xdg_data_path(), _SESSION_FILE)
self._data: dict[str, Any] = {}
self._load()
# ── load / save ────────────────────────────────────────────────────
def _load(self) -> None:
if not os.path.exists(self._path):
return
try:
with open(self._path, "r", encoding="utf-8") as f:
self._data = json.load(f)
except (json.JSONDecodeError, OSError) as exc:
logger.warning("Failed to load session: %s", exc)
self._data = {}
def save(self) -> None:
tmp = self._path + ".tmp"
try:
os.makedirs(os.path.dirname(self._path), exist_ok=True)
with open(tmp, "w", encoding="utf-8") as f:
json.dump(self._data, f, ensure_ascii=False)
os.replace(tmp, self._path)
except OSError as exc:
logger.warning("Failed to save session: %s", exc)
# ── typed accessors ────────────────────────────────────────────────
def get_str(self, key: str, fallback: str = "") -> str:
return str(self._data.get(key, fallback))
def get_int(self, key: str, fallback: int = 0) -> int:
return int(self._data.get(key, fallback))
def get_bool(self, key: str, fallback: bool = False) -> bool:
v = self._data.get(key, fallback)
if isinstance(v, bool):
return v
return str(v).lower() in ("true", "1", "yes")
def set(self, key: str, value: Any) -> None:
self._data[key] = value
# ── migrations ─────────────────────────────────────────────────────
def migrate_from_config(self, config_loader: Any) -> None:
"""Import ``[Playback]`` section from the old config loader."""
keys = [
"last_track_path", "last_position_ms", "last_volume",
"last_playing", "shuffle_active", "repeat_mode",
]
changed = False
for k in keys:
if k not in self._data:
v = config_loader.get("Playback", k, fallback="")
self.set(k, v)
changed = True
if changed:
self.save()
# Wipe old Playback section so it isn't read again.
if config_loader.config.has_section("Playback"):
config_loader.config.remove_section("Playback")
config_loader.save()
logger.info("Migrated playback state from config.ini to session.json")