Refactor project into two standalone components

Split monorepo into separate units for future independent repositories:
- media-server/: Standalone FastAPI server with own README, requirements,
  config example, and CLAUDE.md
- haos-integration/: HACS-ready Home Assistant integration with hacs.json,
  own README, and CLAUDE.md

Both components now have their own .gitignore files and can be easily
extracted into separate repositories.

Also adds custom icon support for scripts configuration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 14:36:23 +03:00
parent 5519e449cd
commit e26df64e4b
44 changed files with 367 additions and 105 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