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:
144
media-server/media_server/service/install_linux.sh
Normal file
144
media-server/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/media_server/service/install_task_windows.ps1
Normal file
10
media-server/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/media_server/service/install_windows.py
Normal file
151
media-server/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/media_server/service/media-server.service
Normal file
36
media-server/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