Files
media-player-server/media_server/config.py
alexei.dolgolyov 1a1cfbaafb Add callbacks support for all media actions
- Add CallbackConfig model for callback scripts
- Add callbacks section to config for optional command execution
- Add turn_on/turn_off/toggle endpoints (callback-only)
- Add callbacks for all media actions (play, pause, stop, next, previous, volume, mute, seek)
- Update README with callbacks documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 03:44:18 +03:00

167 lines
5.8 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 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 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_token: str = Field(
default_factory=lambda: secrets.token_urlsafe(32),
description="API authentication token",
)
# 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)",
)
@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_token": 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()