Initial commit: Media server and Home Assistant integration

- FastAPI server for Windows media control via WinRT/SMTC
- Home Assistant custom integration with media player entity
- Script button entities for system commands
- Position tracking with grace period for track skip handling
- Server availability detection in HA entity

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 13:08:40 +03:00
commit 67a89e8349
37 changed files with 5058 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
#!/bin/bash
# Linux service installation script for Media Server
set -e
SERVICE_NAME="media-server"
INSTALL_DIR="/opt/media-server"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}@.service"
CURRENT_USER=$(whoami)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
echo_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
echo_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
check_root() {
if [[ $EUID -ne 0 ]]; then
echo_error "This script must be run as root (use sudo)"
exit 1
fi
}
install_dependencies() {
echo_info "Installing system dependencies..."
if command -v apt-get &> /dev/null; then
apt-get update
apt-get install -y python3 python3-pip python3-venv python3-dbus python3-gi libdbus-1-dev libglib2.0-dev
elif command -v dnf &> /dev/null; then
dnf install -y python3 python3-pip python3-dbus python3-gobject dbus-devel glib2-devel
elif command -v pacman &> /dev/null; then
pacman -S --noconfirm python python-pip python-dbus python-gobject
else
echo_warn "Unknown package manager. Please install dependencies manually:"
echo " - python3, python3-pip, python3-venv"
echo " - python3-dbus, python3-gi"
echo " - libdbus-1-dev, libglib2.0-dev"
fi
}
install_service() {
echo_info "Installing Media Server..."
# Create installation directory
mkdir -p "$INSTALL_DIR"
# Copy source files
cp -r "$(dirname "$0")/../"* "$INSTALL_DIR/"
# Create virtual environment
echo_info "Creating Python virtual environment..."
python3 -m venv "$INSTALL_DIR/venv"
# Install Python dependencies
echo_info "Installing Python dependencies..."
"$INSTALL_DIR/venv/bin/pip" install --upgrade pip
"$INSTALL_DIR/venv/bin/pip" install -r "$INSTALL_DIR/requirements.txt"
# Install systemd service file
echo_info "Installing systemd service..."
cp "$INSTALL_DIR/service/media-server.service" "$SERVICE_FILE"
# Reload systemd
systemctl daemon-reload
# Generate config if not exists
if [[ ! -f "/home/$SUDO_USER/.config/media-server/config.yaml" ]]; then
echo_info "Generating configuration file..."
sudo -u "$SUDO_USER" "$INSTALL_DIR/venv/bin/python" -m media_server.main --generate-config
fi
echo_info "Installation complete!"
echo ""
echo "To enable and start the service for user '$SUDO_USER':"
echo " sudo systemctl enable ${SERVICE_NAME}@${SUDO_USER}"
echo " sudo systemctl start ${SERVICE_NAME}@${SUDO_USER}"
echo ""
echo "To view the API token:"
echo " cat ~/.config/media-server/config.yaml"
echo ""
echo "To view logs:"
echo " journalctl -u ${SERVICE_NAME}@${SUDO_USER} -f"
}
uninstall_service() {
echo_info "Uninstalling Media Server..."
# Stop and disable service
systemctl stop "${SERVICE_NAME}@*" 2>/dev/null || true
systemctl disable "${SERVICE_NAME}@*" 2>/dev/null || true
# Remove service file
rm -f "$SERVICE_FILE"
systemctl daemon-reload
# Remove installation directory
rm -rf "$INSTALL_DIR"
echo_info "Uninstallation complete!"
echo "Note: Configuration files in ~/.config/media-server were not removed."
}
show_usage() {
echo "Usage: $0 [install|uninstall|deps]"
echo ""
echo "Commands:"
echo " install Install the Media Server as a systemd service"
echo " uninstall Remove the Media Server service"
echo " deps Install system dependencies only"
}
# Main
case "${1:-}" in
install)
check_root
install_dependencies
install_service
;;
uninstall)
check_root
uninstall_service
;;
deps)
check_root
install_dependencies
;;
*)
show_usage
exit 1
;;
esac

View File

@@ -0,0 +1,10 @@
# Get the project root directory (two levels up from this script)
$projectRoot = (Get-Item $PSScriptRoot).Parent.Parent.FullName
$action = New-ScheduledTaskAction -Execute "python" -Argument "-m media_server.main" -WorkingDirectory $projectRoot
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERNAME" -LogonType S4U -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "MediaServer" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Media Server for Home Assistant"
Write-Host "Scheduled task 'MediaServer' created with working directory: $projectRoot"

View File

@@ -0,0 +1,151 @@
"""Windows service installer for Media Server.
This module allows the media server to be installed as a Windows service
that starts automatically on boot.
Usage:
Install: python -m media_server.service.install_windows install
Start: python -m media_server.service.install_windows start
Stop: python -m media_server.service.install_windows stop
Remove: python -m media_server.service.install_windows remove
Debug: python -m media_server.service.install_windows debug
"""
import os
import sys
import socket
import logging
try:
import win32serviceutil
import win32service
import win32event
import servicemanager
import win32api
WIN32_AVAILABLE = True
except ImportError:
WIN32_AVAILABLE = False
print("pywin32 not installed. Install with: pip install pywin32")
class MediaServerService:
"""Windows service wrapper for the Media Server."""
_svc_name_ = "MediaServer"
_svc_display_name_ = "Media Server"
_svc_description_ = "REST API server for controlling system media playback"
def __init__(self, args=None):
if WIN32_AVAILABLE:
win32serviceutil.ServiceFramework.__init__(self, args)
self.stop_event = win32event.CreateEvent(None, 0, 0, None)
self.is_running = False
self.server = None
def SvcStop(self):
"""Stop the service."""
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
win32event.SetEvent(self.stop_event)
self.is_running = False
if self.server:
self.server.should_exit = True
def SvcDoRun(self):
"""Run the service."""
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, ""),
)
self.is_running = True
self.main()
def main(self):
"""Main service loop."""
import uvicorn
from media_server.main import app
from media_server.config import settings
config = uvicorn.Config(
app,
host=settings.host,
port=settings.port,
log_level=settings.log_level.lower(),
)
self.server = uvicorn.Server(config)
self.server.run()
if WIN32_AVAILABLE:
# Dynamically inherit from ServiceFramework when available
MediaServerService = type(
"MediaServerService",
(win32serviceutil.ServiceFramework,),
dict(MediaServerService.__dict__),
)
def install_service():
"""Install the Windows service."""
if not WIN32_AVAILABLE:
print("Error: pywin32 is required for Windows service installation")
print("Install with: pip install pywin32")
return False
try:
# Get the path to the Python executable
python_exe = sys.executable
# Get the path to this module
module_path = os.path.abspath(__file__)
win32serviceutil.InstallService(
MediaServerService._svc_name_,
MediaServerService._svc_name_,
MediaServerService._svc_display_name_,
startType=win32service.SERVICE_AUTO_START,
description=MediaServerService._svc_description_,
)
print(f"Service '{MediaServerService._svc_display_name_}' installed successfully")
print("Start the service with: sc start MediaServer")
return True
except Exception as e:
print(f"Failed to install service: {e}")
return False
def remove_service():
"""Remove the Windows service."""
if not WIN32_AVAILABLE:
print("Error: pywin32 is required")
return False
try:
win32serviceutil.RemoveService(MediaServerService._svc_name_)
print(f"Service '{MediaServerService._svc_display_name_}' removed successfully")
return True
except Exception as e:
print(f"Failed to remove service: {e}")
return False
def main():
"""Main entry point for service management."""
if not WIN32_AVAILABLE:
print("Error: pywin32 is required for Windows service support")
print("Install with: pip install pywin32")
sys.exit(1)
if len(sys.argv) == 1:
# Running as a service
servicemanager.Initialize()
servicemanager.PrepareToHostSingle(MediaServerService)
servicemanager.StartServiceCtrlDispatcher()
else:
# Command line management
win32serviceutil.HandleCommandLine(MediaServerService)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,36 @@
[Unit]
Description=Media Server - REST API for controlling system media playback
After=network.target sound.target
Wants=sound.target
[Service]
Type=simple
User=%i
Group=%i
# Environment variables (optional - can also use config file)
# Environment=MEDIA_SERVER_HOST=0.0.0.0
# Environment=MEDIA_SERVER_PORT=8765
# Environment=MEDIA_SERVER_API_TOKEN=your-secret-token
# Working directory
WorkingDirectory=/opt/media-server
# Start command - adjust path to your Python environment
ExecStart=/opt/media-server/venv/bin/python -m media_server.main
# Restart policy
Restart=always
RestartSec=10
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
# Required for D-Bus access (MPRIS)
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%U/bus
[Install]
WantedBy=multi-user.target