Files
media-player-server/media_server/config.py
alexei.dolgolyov adf2d936da Consolidate tabs, Quick Access links, mini player nav, link descriptions
- Merge Scripts/Callbacks/Links tabs into single Settings tab with collapsible sections
- Rename Actions tab to Quick Access showing both scripts and configured links
- Add prev/next buttons to mini (secondary) player
- Add optional description field to links (backend + frontend)
- Add CSS chevron indicators on collapsible settings sections
- Persist section collapse/expand state in localStorage
- Fix race condition in Quick Access rendering with generation counter
- Order settings sections: Scripts, Links, Callbacks

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 15:08:09 +03:00

204 lines
7.1 KiB
Python

"""Configuration management for the media server."""
import os
import secrets
from pathlib import Path
from typing import Optional
import yaml
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class MediaFolderConfig(BaseModel):
"""Configuration for a media folder."""
path: str = Field(..., description="Absolute path to media folder")
label: str = Field(..., description="Human-readable display label")
enabled: bool = Field(default=True, description="Whether this folder is active")
class CallbackConfig(BaseModel):
"""Configuration for a callback script (no label/description needed)."""
command: str = Field(..., description="Command or script to execute")
timeout: int = Field(default=30, description="Execution timeout in seconds", ge=1, le=300)
working_dir: Optional[str] = Field(default=None, description="Working directory")
shell: bool = Field(default=True, description="Run command in shell")
class ScriptConfig(BaseModel):
"""Configuration for a custom script."""
command: str = Field(..., description="Command or script to execute")
label: Optional[str] = Field(default=None, description="User-friendly display label")
description: str = Field(default="", description="Script description")
icon: Optional[str] = Field(default=None, description="Custom icon (e.g., 'mdi:power')")
timeout: int = Field(default=30, description="Execution timeout in seconds", ge=1, le=300)
working_dir: Optional[str] = Field(default=None, description="Working directory")
shell: bool = Field(default=True, description="Run command in shell")
class LinkConfig(BaseModel):
"""Configuration for a header quick link."""
url: str = Field(..., description="URL to open")
icon: str = Field(default="mdi:link", description="MDI icon name (e.g., 'mdi:led-strip-variant')")
label: str = Field(default="", description="Tooltip text")
description: str = Field(default="", description="Optional description")
class Settings(BaseSettings):
"""Application settings loaded from environment or config file."""
model_config = SettingsConfigDict(
env_prefix="MEDIA_SERVER_",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
# Server settings
host: str = Field(default="0.0.0.0", description="Server bind address")
port: int = Field(default=8765, description="Server port")
# Authentication
api_tokens: dict[str, str] = Field(
default_factory=lambda: {"default": secrets.token_urlsafe(32)},
description="Named API tokens for access control (label: token pairs)",
)
# Media controller settings
poll_interval: float = Field(
default=1.0, description="Media status poll interval in seconds"
)
# Audio device settings
audio_device: Optional[str] = Field(
default=None,
description="Audio device name to control (None = default device). Use /api/audio/devices to list available devices.",
)
# Logging
log_level: str = Field(default="INFO", description="Logging level")
# Custom scripts (loaded separately from YAML)
scripts: dict[str, ScriptConfig] = Field(
default_factory=dict,
description="Custom scripts that can be executed via API",
)
# Callback scripts (executed by integration events, not shown in UI)
callbacks: dict[str, CallbackConfig] = Field(
default_factory=dict,
description="Callback scripts executed by integration events (on_turn_on, on_turn_off, on_toggle)",
)
# Media folders for browsing
media_folders: dict[str, MediaFolderConfig] = Field(
default_factory=dict,
description="Media folders available for browsing in the media browser",
)
# Thumbnail settings
thumbnail_size: str = Field(
default="medium",
description='Thumbnail size: "small" (150x150), "medium" (300x300), or "both"',
)
# Header quick links
links: dict[str, LinkConfig] = Field(
default_factory=dict,
description="Quick links displayed as icons in the header",
)
@classmethod
def load_from_yaml(cls, path: Optional[Path] = None) -> "Settings":
"""Load settings from a YAML configuration file."""
if path is None:
# Look for config in standard locations
search_paths = [
Path("config.yaml"),
Path("config.yml"),
]
# Add platform-specific config directory
if os.name == "nt": # Windows
appdata = os.environ.get("APPDATA", "")
if appdata:
search_paths.append(Path(appdata) / "media-server" / "config.yaml")
else: # Linux/Unix/macOS
search_paths.append(Path.home() / ".config" / "media-server" / "config.yaml")
search_paths.append(Path("/etc/media-server/config.yaml"))
for search_path in search_paths:
if search_path.exists():
path = search_path
break
if path and path.exists():
with open(path, "r", encoding="utf-8") as f:
config_data = yaml.safe_load(f) or {}
return cls(**config_data)
return cls()
def get_config_dir() -> Path:
"""Get the configuration directory path."""
if os.name == "nt": # Windows
config_dir = Path(os.environ.get("APPDATA", "")) / "media-server"
else: # Linux/Unix
config_dir = Path.home() / ".config" / "media-server"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir
def generate_default_config(path: Optional[Path] = None) -> Path:
"""Generate a default configuration file with a new API token."""
if path is None:
path = get_config_dir() / "config.yaml"
config = {
"host": "0.0.0.0",
"port": 8765,
"api_tokens": {
"default": secrets.token_urlsafe(32),
},
"poll_interval": 1.0,
"log_level": "INFO",
# Audio device to control (use GET /api/audio/devices to list available devices)
# Set to null or remove to use default device
# "audio_device": "Speakers (Realtek",
"scripts": {
"example_script": {
"command": "echo Hello from Media Server!",
"description": "Example script - echoes a message",
"timeout": 10,
"shell": True,
},
# Add your custom scripts here:
# "shutdown": {
# "command": "shutdown /s /t 60",
# "description": "Shutdown computer in 60 seconds",
# "timeout": 5,
# },
# "lock_screen": {
# "command": "rundll32.exe user32.dll,LockWorkStation",
# "description": "Lock the workstation",
# "timeout": 5,
# },
},
}
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
return path
# Global settings instance
settings = Settings.load_from_yaml()