Files
media-player-server/media_server/config.py
alexei.dolgolyov 7c631d09f6 Add media browser feature with UI improvements
- Refactored index.html: Split into separate HTML (309 lines), CSS (908 lines), and JS (1,286 lines) files
- Implemented media browser with folder configuration, recursive navigation, and thumbnail display
- Added metadata extraction using mutagen library (title, artist, album, duration, bitrate, codec)
- Implemented thumbnail generation and caching with SHA256 hash-based keys and LRU eviction
- Added platform-specific file playback (os.startfile on Windows, xdg-open on Linux, open on macOS)
- Implemented path validation security to prevent directory traversal attacks
- Added smooth thumbnail loading with fade-in animation and loading spinner
- Added i18n support for browser (English and Russian)
- Updated dependencies: mutagen>=1.47.0, pillow>=10.0.0
- Added comprehensive media browser documentation to README

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 21:31:02 +03:00

189 lines
6.6 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 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"',
)
@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()