- 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
89 lines
3.3 KiB
Python
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")
|