The JSON key stays 'mpris' for backwards compatibility, but it now also governs the macOS Now Playing session and the media keys.
127 lines
3.9 KiB
Python
127 lines
3.9 KiB
Python
"""Paths, persistent settings and tiny cache helpers.
|
|
|
|
Everything lives in the usual XDG places:
|
|
|
|
~/.config/somafm-tui/config.json favourites, volume, last station
|
|
~/.cache/somafm-tui/channels.json last good copy of somafm.com/channels.json
|
|
~/.cache/somafm-tui/playlists.json resolved .pls -> stream URLs
|
|
~/.cache/somafm-tui/songs/<id>.json last good copy of the song history
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
from dataclasses import asdict, dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
APP_NAME = "somafm-tui"
|
|
|
|
|
|
def _xdg(env_var: str, fallback: str) -> Path:
|
|
value = os.environ.get(env_var)
|
|
if value:
|
|
return Path(value).expanduser()
|
|
return Path.home() / fallback
|
|
|
|
|
|
CONFIG_DIR = _xdg("XDG_CONFIG_HOME", ".config") / APP_NAME
|
|
CACHE_DIR = _xdg("XDG_CACHE_HOME", ".cache") / APP_NAME
|
|
RUNTIME_DIR = Path(os.environ.get("XDG_RUNTIME_DIR") or "/tmp") / APP_NAME
|
|
|
|
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
CHANNELS_CACHE = CACHE_DIR / "channels.json"
|
|
PLAYLIST_CACHE = CACHE_DIR / "playlists.json"
|
|
SONGS_CACHE_DIR = CACHE_DIR / "songs"
|
|
|
|
VISUALIZERS = ("both", "bars", "wave")
|
|
|
|
|
|
def ensure_dirs() -> None:
|
|
for directory in (CONFIG_DIR, CACHE_DIR, SONGS_CACHE_DIR):
|
|
with contextlib.suppress(OSError):
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def atomic_write_text(path: Path, text: str) -> None:
|
|
"""Write ``text`` to ``path`` without ever leaving a half-written file."""
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = path.with_name(f".{path.name}.tmp")
|
|
tmp.write_text(text, encoding="utf-8")
|
|
os.replace(tmp, path)
|
|
|
|
|
|
def read_json(path: Path) -> Any:
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def write_json(path: Path, payload: Any) -> None:
|
|
with contextlib.suppress(OSError):
|
|
atomic_write_text(path, json.dumps(payload, indent=2, ensure_ascii=False))
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Config:
|
|
favorites: list[str] = field(default_factory=list)
|
|
last_station: str = "groovesalad"
|
|
volume: int = 70
|
|
visualizer: str = "both"
|
|
autoplay: bool = False
|
|
#: Publish what is playing to the desktop media session (MPRIS on Linux,
|
|
#: macOS Now Playing via mpv) and honour the media keys. The JSON key is
|
|
#: spelled ``mpris`` for backwards compatibility.
|
|
mpris: bool = True
|
|
|
|
@classmethod
|
|
def load(cls) -> Config:
|
|
raw = read_json(CONFIG_PATH)
|
|
config = cls()
|
|
if not isinstance(raw, dict):
|
|
return config
|
|
|
|
favorites = raw.get("favorites")
|
|
if isinstance(favorites, list):
|
|
config.favorites = [str(item) for item in favorites if isinstance(item, str)]
|
|
|
|
last_station = raw.get("last_station")
|
|
if isinstance(last_station, str) and last_station:
|
|
config.last_station = last_station
|
|
|
|
volume = raw.get("volume")
|
|
if isinstance(volume, (int, float)):
|
|
config.volume = max(0, min(100, int(volume)))
|
|
|
|
visualizer = raw.get("visualizer")
|
|
if visualizer in VISUALIZERS:
|
|
config.visualizer = visualizer
|
|
|
|
if isinstance(raw.get("autoplay"), bool):
|
|
config.autoplay = raw["autoplay"]
|
|
if isinstance(raw.get("mpris"), bool):
|
|
config.mpris = raw["mpris"]
|
|
return config
|
|
|
|
def save(self) -> None:
|
|
try:
|
|
ensure_dirs()
|
|
atomic_write_text(
|
|
CONFIG_PATH, json.dumps(asdict(self), indent=2, ensure_ascii=False)
|
|
)
|
|
except OSError:
|
|
pass
|
|
|
|
def is_favorite(self, station_id: str) -> bool:
|
|
return station_id in self.favorites
|
|
|
|
def toggle_favorite(self, station_id: str) -> bool:
|
|
if station_id in self.favorites:
|
|
self.favorites.remove(station_id)
|
|
return False
|
|
self.favorites.append(station_id)
|
|
return True
|