- Add jellyfinctl commands: uninstall, update, backup, configure-haproxy - Add RPCD methods: uninstall, update, backup, configure_haproxy - Add domain and disk_usage to status display - Add action buttons in LuCI overview: Update, Backup, Configure HAProxy, Uninstall - Add UCI options: domain, backup_path, haproxy_enabled Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
608 lines
16 KiB
Bash
608 lines
16 KiB
Bash
#!/bin/sh
|
|
# SecuBox Jellyfin Media Server manager
|
|
# Full integration: Docker, HAProxy, Firewall, Mesh P2P, Backup/Restore
|
|
|
|
VERSION="2.0.0"
|
|
CONFIG="jellyfin"
|
|
CONTAINER="secbx-jellyfin"
|
|
OPKG_UPDATED=0
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}[JELLYFIN]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
|
|
# ============================================================================
|
|
# Configuration Helpers
|
|
# ============================================================================
|
|
|
|
uci_get() { uci -q get ${CONFIG}.$1; }
|
|
uci_set() { uci -q set ${CONFIG}.$1="$2"; }
|
|
|
|
require_root() {
|
|
[ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; }
|
|
}
|
|
|
|
defaults() {
|
|
image="$(uci_get main.image)"
|
|
[ -z "$image" ] && image="jellyfin/jellyfin:latest"
|
|
data_path="$(uci_get main.data_path)"
|
|
[ -z "$data_path" ] && data_path="/srv/jellyfin"
|
|
port="$(uci_get main.port)"
|
|
[ -z "$port" ] && port="8096"
|
|
timezone="$(uci_get main.timezone)"
|
|
[ -z "$timezone" ] && timezone="Europe/Paris"
|
|
hw_accel="$(uci_get transcoding.hw_accel)"
|
|
[ -z "$hw_accel" ] && hw_accel="0"
|
|
gpu_device="$(uci_get transcoding.gpu_device)"
|
|
domain="$(uci_get network.domain)"
|
|
[ -z "$domain" ] && domain="jellyfin.secubox.local"
|
|
}
|
|
|
|
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
|
|
|
ensure_packages() {
|
|
for pkg in "$@"; do
|
|
if ! opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then
|
|
if [ "$OPKG_UPDATED" -eq 0 ]; then
|
|
opkg update || return 1
|
|
OPKG_UPDATED=1
|
|
fi
|
|
opkg install "$pkg" || return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ============================================================================
|
|
# Prerequisite Checks
|
|
# ============================================================================
|
|
|
|
check_prereqs() {
|
|
defaults
|
|
ensure_dir "$data_path"
|
|
[ -d /sys/fs/cgroup ] || { error "/sys/fs/cgroup missing"; return 1; }
|
|
ensure_packages dockerd docker containerd
|
|
/etc/init.d/dockerd enable >/dev/null 2>&1
|
|
/etc/init.d/dockerd start >/dev/null 2>&1
|
|
}
|
|
|
|
# ============================================================================
|
|
# Docker Helpers
|
|
# ============================================================================
|
|
|
|
pull_image() {
|
|
defaults
|
|
docker pull "$image"
|
|
}
|
|
|
|
stop_container() {
|
|
docker stop "$CONTAINER" >/dev/null 2>&1 || true
|
|
docker rm "$CONTAINER" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
build_media_mounts() {
|
|
local mounts=""
|
|
local paths
|
|
paths=$(uci -q get ${CONFIG}.media.media_path)
|
|
if [ -n "$paths" ]; then
|
|
for p in $paths; do
|
|
[ -d "$p" ] && mounts="$mounts -v ${p}:${p}:ro"
|
|
done
|
|
fi
|
|
echo "$mounts"
|
|
}
|
|
|
|
build_gpu_args() {
|
|
if [ "$hw_accel" = "1" ]; then
|
|
local dev="${gpu_device:-/dev/dri}"
|
|
if [ -e "$dev" ]; then
|
|
echo "--device=${dev}:${dev}"
|
|
else
|
|
echo ""
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# HAProxy Integration
|
|
# ============================================================================
|
|
|
|
configure_haproxy() {
|
|
local haproxy_enabled=$(uci_get network.haproxy)
|
|
[ "$haproxy_enabled" != "1" ] && { log "HAProxy integration disabled in UCI"; return 0; }
|
|
|
|
if ! command -v haproxyctl >/dev/null 2>&1; then
|
|
warn "haproxyctl not found, skipping HAProxy configuration"
|
|
return 0
|
|
fi
|
|
|
|
defaults
|
|
|
|
local ssl=$(uci_get network.haproxy_ssl)
|
|
local ssl_redirect=$(uci_get network.haproxy_ssl_redirect)
|
|
|
|
# Check if vhost already exists (idempotent)
|
|
local existing=$(uci show haproxy 2>/dev/null | grep "\.domain='$domain'" | head -1)
|
|
if [ -n "$existing" ]; then
|
|
log "HAProxy vhost for $domain already configured"
|
|
return 0
|
|
fi
|
|
|
|
log "Configuring HAProxy for $domain..."
|
|
|
|
# Add backend
|
|
uci -q add haproxy backend
|
|
uci -q set haproxy.@backend[-1].name='jellyfin_web'
|
|
uci -q set haproxy.@backend[-1].mode='http'
|
|
uci -q add_list haproxy.@backend[-1].server="jellyfin 127.0.0.1:$port check"
|
|
|
|
# Add vhost
|
|
uci -q add haproxy vhost
|
|
uci -q set haproxy.@vhost[-1].enabled='1'
|
|
uci -q set haproxy.@vhost[-1].domain="$domain"
|
|
uci -q set haproxy.@vhost[-1].backend='jellyfin_web'
|
|
uci -q set haproxy.@vhost[-1].ssl="${ssl:-1}"
|
|
uci -q set haproxy.@vhost[-1].ssl_redirect="${ssl_redirect:-1}"
|
|
uci -q set haproxy.@vhost[-1].websocket='1'
|
|
|
|
uci commit haproxy
|
|
/etc/init.d/haproxy reload 2>/dev/null
|
|
|
|
log "HAProxy configured for $domain"
|
|
}
|
|
|
|
remove_haproxy() {
|
|
if ! command -v haproxyctl >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
defaults
|
|
|
|
log "Removing HAProxy configuration for $domain..."
|
|
|
|
# Find and remove backend
|
|
local idx=0
|
|
while uci -q get haproxy.@backend[$idx] >/dev/null 2>&1; do
|
|
local name=$(uci -q get haproxy.@backend[$idx].name)
|
|
if [ "$name" = "jellyfin_web" ]; then
|
|
uci delete haproxy.@backend[$idx]
|
|
break
|
|
fi
|
|
idx=$((idx + 1))
|
|
done
|
|
|
|
# Find and remove vhost
|
|
idx=0
|
|
while uci -q get haproxy.@vhost[$idx] >/dev/null 2>&1; do
|
|
local vdomain=$(uci -q get haproxy.@vhost[$idx].domain)
|
|
if [ "$vdomain" = "$domain" ]; then
|
|
uci delete haproxy.@vhost[$idx]
|
|
break
|
|
fi
|
|
idx=$((idx + 1))
|
|
done
|
|
|
|
uci commit haproxy
|
|
/etc/init.d/haproxy reload 2>/dev/null
|
|
|
|
log "HAProxy configuration removed"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Firewall
|
|
# ============================================================================
|
|
|
|
configure_firewall() {
|
|
local fw_wan=$(uci_get network.firewall_wan)
|
|
[ "$fw_wan" != "1" ] && { log "WAN firewall rule disabled in UCI"; return 0; }
|
|
|
|
defaults
|
|
|
|
# Idempotent: check if rule already exists
|
|
if uci show firewall 2>/dev/null | grep -q "Jellyfin-HTTP"; then
|
|
log "Firewall rule for Jellyfin already exists"
|
|
return 0
|
|
fi
|
|
|
|
log "Configuring firewall for port $port..."
|
|
|
|
uci add firewall rule
|
|
uci set firewall.@rule[-1].name='Jellyfin-HTTP'
|
|
uci set firewall.@rule[-1].src='wan'
|
|
uci set firewall.@rule[-1].dest_port="$port"
|
|
uci set firewall.@rule[-1].proto='tcp'
|
|
uci set firewall.@rule[-1].target='ACCEPT'
|
|
uci set firewall.@rule[-1].enabled='1'
|
|
|
|
uci commit firewall
|
|
/etc/init.d/firewall reload 2>/dev/null
|
|
|
|
log "Firewall configured"
|
|
}
|
|
|
|
remove_firewall() {
|
|
log "Removing firewall rules..."
|
|
|
|
local idx=0
|
|
while uci -q get firewall.@rule[$idx] >/dev/null 2>&1; do
|
|
local name=$(uci -q get firewall.@rule[$idx].name)
|
|
if [ "$name" = "Jellyfin-HTTP" ]; then
|
|
uci delete firewall.@rule[$idx]
|
|
uci commit firewall
|
|
/etc/init.d/firewall reload 2>/dev/null
|
|
log "Firewall rule removed"
|
|
return 0
|
|
fi
|
|
idx=$((idx + 1))
|
|
done
|
|
}
|
|
|
|
# ============================================================================
|
|
# Mesh Integration
|
|
# ============================================================================
|
|
|
|
register_mesh_service() {
|
|
local mesh_enabled=$(uci_get mesh.enabled)
|
|
[ "$mesh_enabled" != "1" ] && return 0
|
|
|
|
defaults
|
|
|
|
if [ -x /usr/sbin/secubox-p2p ]; then
|
|
/usr/sbin/secubox-p2p register-service jellyfin "$port" 2>/dev/null
|
|
log "Registered Jellyfin with mesh network"
|
|
else
|
|
warn "secubox-p2p not found, skipping mesh registration"
|
|
fi
|
|
|
|
local dns_enabled=$(uci -q get secubox-p2p.dns.enabled || echo "0")
|
|
if [ "$dns_enabled" = "1" ]; then
|
|
local dns_domain=$(uci -q get secubox-p2p.dns.base_domain || echo "mesh.local")
|
|
local hostname=$(echo "$domain" | cut -d'.' -f1)
|
|
log "Mesh DNS: $hostname.$dns_domain"
|
|
fi
|
|
}
|
|
|
|
unregister_mesh_service() {
|
|
if [ -x /usr/sbin/secubox-p2p ]; then
|
|
/usr/sbin/secubox-p2p unregister-service jellyfin 2>/dev/null
|
|
log "Unregistered Jellyfin from mesh network"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Installation
|
|
# ============================================================================
|
|
|
|
cmd_install() {
|
|
require_root
|
|
log "Installing Jellyfin Media Server..."
|
|
|
|
check_prereqs || exit 1
|
|
defaults
|
|
|
|
ensure_dir "$data_path/config"
|
|
ensure_dir "$data_path/cache"
|
|
|
|
log "Pulling Docker image..."
|
|
pull_image || exit 1
|
|
|
|
uci_set main.enabled '1'
|
|
uci commit ${CONFIG}
|
|
/etc/init.d/jellyfin enable
|
|
|
|
# Integrate with HAProxy if configured
|
|
configure_haproxy
|
|
|
|
# Configure firewall if WAN access requested
|
|
configure_firewall
|
|
|
|
# Register with mesh if enabled
|
|
register_mesh_service
|
|
|
|
log "Jellyfin installed successfully!"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Add media: uci add_list jellyfin.media.media_path='/path/to/media'"
|
|
echo " 2. Set domain: uci set jellyfin.network.domain='media.example.com'"
|
|
echo " 3. Commit: uci commit jellyfin"
|
|
echo " 4. Start: /etc/init.d/jellyfin start"
|
|
echo " Web UI: http://<device-ip>:${port}"
|
|
echo ""
|
|
}
|
|
|
|
cmd_uninstall() {
|
|
require_root
|
|
log "Uninstalling Jellyfin..."
|
|
|
|
/etc/init.d/jellyfin stop 2>/dev/null
|
|
/etc/init.d/jellyfin disable 2>/dev/null
|
|
stop_container
|
|
|
|
# Remove integrations
|
|
remove_haproxy
|
|
remove_firewall
|
|
unregister_mesh_service
|
|
|
|
# Remove image
|
|
defaults
|
|
docker rmi "$image" 2>/dev/null
|
|
|
|
uci_set main.enabled '0'
|
|
uci commit ${CONFIG}
|
|
|
|
log "Jellyfin uninstalled. Data preserved at $data_path"
|
|
log "To remove data: rm -rf $data_path"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Check & Update
|
|
# ============================================================================
|
|
|
|
cmd_check() {
|
|
check_prereqs
|
|
echo "Prerequisite check completed."
|
|
}
|
|
|
|
cmd_update() {
|
|
require_root
|
|
log "Pulling latest image..."
|
|
pull_image || exit 1
|
|
log "Restarting service..."
|
|
/etc/init.d/jellyfin restart
|
|
# Prune old images
|
|
docker image prune -f 2>/dev/null
|
|
log "Update complete"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Status
|
|
# ============================================================================
|
|
|
|
cmd_status() {
|
|
defaults
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo " Jellyfin Media Server v$VERSION"
|
|
echo "========================================"
|
|
echo ""
|
|
|
|
local enabled=$(uci_get main.enabled)
|
|
echo "Configuration:"
|
|
echo " Enabled: $([ "$enabled" = "1" ] && echo -e "${GREEN}Yes${NC}" || echo -e "${RED}No${NC}")"
|
|
echo " Image: $image"
|
|
echo " Port: $port"
|
|
echo " Data: $data_path"
|
|
echo " Domain: $domain"
|
|
echo ""
|
|
|
|
# Docker check
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo -e "Docker: ${RED}Not installed${NC}"
|
|
return
|
|
fi
|
|
|
|
# Container status
|
|
echo "Container:"
|
|
local state=$(docker inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null)
|
|
if [ "$state" = "running" ]; then
|
|
echo -e " Status: ${GREEN}Running${NC}"
|
|
local uptime=$(docker ps --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null)
|
|
echo " Uptime: $uptime"
|
|
elif [ -n "$state" ]; then
|
|
echo -e " Status: ${YELLOW}$state${NC}"
|
|
else
|
|
echo -e " Status: ${RED}Not installed${NC}"
|
|
fi
|
|
echo ""
|
|
|
|
# Media paths
|
|
local paths=$(uci -q get ${CONFIG}.media.media_path)
|
|
if [ -n "$paths" ]; then
|
|
echo "Media Libraries:"
|
|
for p in $paths; do
|
|
if [ -d "$p" ]; then
|
|
local count=$(ls -1 "$p" 2>/dev/null | wc -l)
|
|
echo " $p ($count items)"
|
|
else
|
|
echo -e " $p ${RED}(not found)${NC}"
|
|
fi
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
# Integration status
|
|
echo "Integrations:"
|
|
local haproxy_enabled=$(uci_get network.haproxy)
|
|
if [ "$haproxy_enabled" = "1" ]; then
|
|
local vhost_exists=$(uci show haproxy 2>/dev/null | grep "\.domain='$domain'" | head -1)
|
|
if [ -n "$vhost_exists" ]; then
|
|
echo -e " HAProxy: ${GREEN}Configured${NC} ($domain)"
|
|
else
|
|
echo -e " HAProxy: ${YELLOW}Enabled but not configured${NC}"
|
|
fi
|
|
else
|
|
echo " HAProxy: Disabled"
|
|
fi
|
|
|
|
local mesh_enabled=$(uci_get mesh.enabled)
|
|
if [ "$mesh_enabled" = "1" ]; then
|
|
echo -e " Mesh P2P: ${GREEN}Enabled${NC}"
|
|
else
|
|
echo " Mesh P2P: Disabled"
|
|
fi
|
|
|
|
local fw_wan=$(uci_get network.firewall_wan)
|
|
if [ "$fw_wan" = "1" ]; then
|
|
echo -e " Firewall: ${GREEN}WAN access on port $port${NC}"
|
|
else
|
|
echo " Firewall: LAN only"
|
|
fi
|
|
|
|
# Disk usage
|
|
if [ -d "$data_path" ]; then
|
|
local disk=$(du -sh "$data_path" 2>/dev/null | cut -f1)
|
|
echo ""
|
|
echo "Storage:"
|
|
echo " Data size: ${disk:-unknown}"
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Logs & Shell
|
|
# ============================================================================
|
|
|
|
cmd_logs() { docker logs "$@" "$CONTAINER" 2>&1; }
|
|
|
|
cmd_shell() {
|
|
docker exec -it "$CONTAINER" /bin/bash 2>/dev/null || docker exec -it "$CONTAINER" /bin/sh
|
|
}
|
|
|
|
# ============================================================================
|
|
# Backup / Restore
|
|
# ============================================================================
|
|
|
|
cmd_backup() {
|
|
local backup_file="${1:-/tmp/jellyfin-backup-$(date +%Y%m%d-%H%M%S).tar.gz}"
|
|
|
|
defaults
|
|
log "Creating backup..."
|
|
|
|
tar -czf "$backup_file" \
|
|
-C / \
|
|
etc/config/jellyfin \
|
|
"${data_path#/}/config" \
|
|
2>/dev/null
|
|
|
|
if [ -f "$backup_file" ]; then
|
|
local size=$(ls -lh "$backup_file" | awk '{print $5}')
|
|
log "Backup created: $backup_file ($size)"
|
|
else
|
|
error "Backup failed"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
cmd_restore() {
|
|
local backup_file="$1"
|
|
|
|
if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
|
|
echo "Usage: jellyfinctl restore <backup_file>"
|
|
return 1
|
|
fi
|
|
|
|
require_root
|
|
log "Restoring from $backup_file..."
|
|
|
|
/etc/init.d/jellyfin stop 2>/dev/null
|
|
tar -xzf "$backup_file" -C /
|
|
/etc/init.d/jellyfin start
|
|
|
|
log "Restore complete"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Service Run (procd integration)
|
|
# ============================================================================
|
|
|
|
cmd_service_run() {
|
|
require_root
|
|
check_prereqs || exit 1
|
|
defaults
|
|
stop_container
|
|
|
|
local media_mounts
|
|
media_mounts=$(build_media_mounts)
|
|
|
|
local gpu_args
|
|
gpu_args=$(build_gpu_args)
|
|
|
|
local docker_args="--name $CONTAINER"
|
|
docker_args="$docker_args -p ${port}:8096"
|
|
docker_args="$docker_args -v ${data_path}/config:/config"
|
|
docker_args="$docker_args -v ${data_path}/cache:/cache"
|
|
docker_args="$docker_args -e TZ=${timezone}"
|
|
|
|
# shellcheck disable=SC2086
|
|
exec docker run --rm $docker_args $media_mounts $gpu_args "$image"
|
|
}
|
|
|
|
cmd_service_stop() {
|
|
require_root
|
|
stop_container
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Jellyfin Media Server Control v$VERSION
|
|
|
|
Usage: jellyfinctl <command> [options]
|
|
|
|
Commands:
|
|
install Install prerequisites, pull image, configure integrations
|
|
uninstall Stop service, remove container and integrations
|
|
check Run prerequisite checks
|
|
update Pull latest image and restart
|
|
status Show service and integration status
|
|
|
|
logs [-f] [--tail N] Show container logs
|
|
shell Open shell inside container
|
|
|
|
configure-haproxy Configure/update HAProxy vhost
|
|
remove-haproxy Remove HAProxy configuration
|
|
configure-fw Configure firewall rules
|
|
remove-fw Remove firewall rules
|
|
register-mesh Register with mesh P2P network
|
|
unregister-mesh Unregister from mesh network
|
|
|
|
backup [file] Create configuration backup
|
|
restore <file> Restore from backup
|
|
|
|
service-run Internal: run container via procd
|
|
service-stop Internal: stop container
|
|
|
|
Examples:
|
|
jellyfinctl install
|
|
jellyfinctl status
|
|
jellyfinctl logs --tail 100
|
|
jellyfinctl backup /tmp/jellyfin.tar.gz
|
|
jellyfinctl configure-haproxy
|
|
|
|
EOF
|
|
}
|
|
|
|
case "${1:-}" in
|
|
install) shift; cmd_install "$@" ;;
|
|
uninstall) shift; cmd_uninstall "$@" ;;
|
|
check) shift; cmd_check "$@" ;;
|
|
update) shift; cmd_update "$@" ;;
|
|
status) shift; cmd_status "$@" ;;
|
|
logs) shift; cmd_logs "$@" ;;
|
|
shell) shift; cmd_shell "$@" ;;
|
|
configure-haproxy) configure_haproxy ;;
|
|
remove-haproxy) remove_haproxy ;;
|
|
configure-fw) configure_firewall ;;
|
|
remove-fw) remove_firewall ;;
|
|
register-mesh) register_mesh_service ;;
|
|
unregister-mesh) unregister_mesh_service ;;
|
|
backup) shift; cmd_backup "$@" ;;
|
|
restore) shift; cmd_restore "$@" ;;
|
|
service-run) shift; cmd_service_run "$@" ;;
|
|
service-stop) shift; cmd_service_stop "$@" ;;
|
|
help|--help|-h|'') show_help ;;
|
|
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
|
|
esac
|
|
|
|
exit 0
|