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:
144
media_server/service/install_linux.sh
Normal file
144
media_server/service/install_linux.sh
Normal 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
|
||||
10
media_server/service/install_task_windows.ps1
Normal file
10
media_server/service/install_task_windows.ps1
Normal 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"
|
||||
151
media_server/service/install_windows.py
Normal file
151
media_server/service/install_windows.py
Normal 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()
|
||||
36
media_server/service/media-server.service
Normal file
36
media_server/service/media-server.service
Normal 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
|
||||
Reference in New Issue
Block a user