Files
media-player-server/media_server/config.py
alexei.dolgolyov 652f10fc4c Reduce visualizer latency, tighten UI paddings, fix mobile browser toolbar
- Visualizer: FPS 25→30, chunk_size 2048→1024, smoothing 0.65→0.15
- Beat effect: scale 0.03→0.04, glow range 0.5-0.8→0.4-0.8
- UI: reduce container/section paddings from 2rem to 1rem
- Source name: add ellipsis overflow for long names
- Mobile browser toolbar: use flex-wrap instead of column stack,
  hide "Items per page" label text on small screens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:35:23 +03:00

226 lines
7.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 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",
)
# Audio visualizer
visualizer_enabled: bool = Field(
default=True,
description="Enable audio spectrum visualizer (requires soundcard + numpy)",
)
visualizer_fps: int = Field(
default=30,
description="Visualizer update rate in frames per second",
ge=10,
le=60,
)
visualizer_bins: int = Field(
default=32,
description="Number of frequency bins for the visualizer",
ge=8,
le=128,
)
visualizer_device: Optional[str] = Field(
default=None,
description="Loopback audio device name for visualizer (None = auto-detect)",
)
@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()