feat(jellyfin): Add uninstall, update, backup, HAProxy integration and LuCI actions

- 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>
This commit is contained in:
CyberMind-FR 2026-02-04 17:10:44 +01:00
parent b32a0c222b
commit 155f9d8005
5 changed files with 761 additions and 75 deletions

View File

@ -35,6 +35,30 @@ var callInstall = rpc.declare({
expect: {}
});
var callUninstall = rpc.declare({
object: 'luci.jellyfin',
method: 'uninstall',
expect: {}
});
var callUpdate = rpc.declare({
object: 'luci.jellyfin',
method: 'update',
expect: {}
});
var callConfigureHaproxy = rpc.declare({
object: 'luci.jellyfin',
method: 'configure_haproxy',
expect: {}
});
var callBackup = rpc.declare({
object: 'luci.jellyfin',
method: 'backup',
expect: {}
});
var callLogs = rpc.declare({
object: 'luci.jellyfin',
method: 'logs',
@ -89,19 +113,45 @@ return view.extend({
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Image:</td><td>' + (status.image || '-') + '</td></tr>';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Port:</td><td>' + port + '</td></tr>';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Data:</td><td>' + (status.data_path || '-') + '</td></tr>';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Domain:</td><td>' + (status.domain || '-') + '</td></tr>';
if (status.disk_usage)
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Disk:</td><td>' + status.disk_usage + '</td></tr>';
if (status.media_paths && status.media_paths.length > 0)
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Media:</td><td>' + status.media_paths.join('<br>') + '</td></tr>';
html += '</table>';
return html;
};
/* ---- Action Buttons ---- */
o = s.option(form.DummyValue, '_actions', _('Actions'));
/* ---- Integration Status ---- */
o = s.option(form.DummyValue, '_integrations', _('Integrations'));
o.rawhtml = true;
o.cfgvalue = function() {
return '';
var html = '<table style="border-collapse:collapse;">';
// HAProxy
var hc = '#8892b0', hl = 'Disabled';
if (status.haproxy_status === 'configured') {
hc = '#27ae60'; hl = 'Configured (' + (status.domain || '') + ')';
} else if (status.haproxy_status === 'pending') {
hc = '#f39c12'; hl = 'Enabled (not yet configured)';
}
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">HAProxy:</td><td style="color:' + hc + ';">' + hl + '</td></tr>';
// Mesh
var mc = status.mesh_enabled ? '#27ae60' : '#8892b0';
var ml = status.mesh_enabled ? 'Enabled' : 'Disabled';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Mesh P2P:</td><td style="color:' + mc + ';">' + ml + '</td></tr>';
// Firewall
var fc = status.firewall_wan ? '#27ae60' : '#8892b0';
var fl = status.firewall_wan ? 'WAN access on port ' + (status.port || 8096) : 'LAN only';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Firewall:</td><td style="color:' + fc + ';">' + fl + '</td></tr>';
html += '</table>';
return html;
};
/* ---- Action Buttons ---- */
var cs = status.container_status || 'not_installed';
if (cs === 'not_installed') {
@ -161,6 +211,57 @@ return view.extend({
window.open('http://' + window.location.hostname + ':' + port, '_blank');
};
}
o = s.option(form.Button, '_update', _('Update'));
o.inputtitle = _('Pull Latest Image');
o.inputstyle = 'action';
o.onclick = function() {
ui.showModal(_('Updating...'), [
E('p', { 'class': 'spinning' }, _('Pulling latest Docker image and restarting...'))
]);
return callUpdate().then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, _('Jellyfin updated successfully.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Update failed: ') + (res.output || 'Unknown error')), 'danger');
}
window.location.href = window.location.pathname + '?' + Date.now();
});
};
o = s.option(form.Button, '_backup', _('Backup'));
o.inputtitle = _('Create Backup');
o.inputstyle = 'action';
o.onclick = function() {
return callBackup().then(function(res) {
if (res && res.success) {
ui.addNotification(null, E('p', {}, _('Backup created: ') + (res.path || '')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Backup failed: ') + (res.output || 'Unknown error')), 'danger');
}
});
};
o = s.option(form.Button, '_uninstall', _('Uninstall'));
o.inputtitle = _('Uninstall');
o.inputstyle = 'remove';
o.onclick = function() {
if (!confirm(_('Are you sure you want to uninstall Jellyfin? Data will be preserved.')))
return;
ui.showModal(_('Uninstalling...'), [
E('p', { 'class': 'spinning' }, _('Removing container and integrations...'))
]);
return callUninstall().then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, _('Jellyfin uninstalled.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Uninstall failed: ') + (res.output || 'Unknown error')), 'danger');
}
window.location.href = window.location.pathname + '?' + Date.now();
});
};
}
/* ---- Configuration Section ---- */
@ -187,6 +288,50 @@ return view.extend({
o = s.option(form.Value, 'timezone', _('Timezone'));
o.placeholder = 'Europe/Paris';
/* ---- Network / Domain Section ---- */
s = m.section(form.NamedSection, 'network', 'jellyfin', _('Network & Domain'));
s.anonymous = true;
o = s.option(form.Value, 'domain', _('Domain'),
_('Domain name for accessing Jellyfin via HAProxy reverse proxy.'));
o.placeholder = 'jellyfin.secubox.local';
o = s.option(form.Value, 'public_url', _('Public URL'),
_('Full public URL if different from domain (e.g. https://media.example.com).'));
o.placeholder = 'https://media.example.com';
o = s.option(form.Flag, 'haproxy', _('HAProxy Integration'),
_('Register Jellyfin as an HAProxy vhost for reverse proxy access.'));
o.rmempty = false;
o = s.option(form.Flag, 'haproxy_ssl', _('SSL'),
_('Enable SSL for the HAProxy vhost.'));
o.rmempty = false;
o.depends('haproxy', '1');
o = s.option(form.Flag, 'haproxy_ssl_redirect', _('Force HTTPS'),
_('Redirect HTTP requests to HTTPS.'));
o.rmempty = false;
o.depends('haproxy', '1');
o = s.option(form.Flag, 'firewall_wan', _('WAN Access'),
_('Allow direct WAN access to the Jellyfin port (bypassing HAProxy).'));
o.rmempty = false;
o = s.option(form.Button, '_apply_haproxy', _('Apply HAProxy'));
o.inputtitle = _('Configure HAProxy Now');
o.inputstyle = 'action';
o.depends('haproxy', '1');
o.onclick = function() {
return callConfigureHaproxy().then(function(res) {
if (res && res.success) {
ui.addNotification(null, E('p', {}, _('HAProxy configured successfully.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('HAProxy configuration failed: ') + (res.output || 'Unknown error')), 'danger');
}
});
};
/* ---- Media Libraries ---- */
s = m.section(form.NamedSection, 'media', 'jellyfin', _('Media Libraries'));
s.anonymous = true;
@ -208,6 +353,19 @@ return view.extend({
o.placeholder = '/dev/dri';
o.depends('hw_accel', '1');
/* ---- Mesh P2P Section ---- */
s = m.section(form.NamedSection, 'mesh', 'jellyfin', _('Mesh P2P'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Mesh Integration'),
_('Register Jellyfin with the SecuBox P2P mesh network for discovery by other nodes.'));
o.rmempty = false;
o = s.option(form.Flag, 'announce_service', _('Announce Service'),
_('Announce this Jellyfin instance to mesh peers.'));
o.rmempty = false;
o.depends('enabled', '1');
/* ---- Logs Section ---- */
s = m.section(form.NamedSection, 'main', 'jellyfin', _('Logs'));
s.anonymous = true;

View File

@ -8,7 +8,7 @@ CONFIG="jellyfin"
case "$1" in
list)
echo '{"status":{},"start":{},"stop":{},"restart":{},"install":{},"logs":{"lines":"int"}}'
echo '{"status":{},"start":{},"stop":{},"restart":{},"install":{},"uninstall":{},"update":{},"configure_haproxy":{},"backup":{},"restore":{"path":"str"},"logs":{"lines":"int"}}'
;;
call)
case "$2" in
@ -22,12 +22,24 @@ case "$1" in
timezone=$(uci -q get ${CONFIG}.main.timezone)
hw_accel=$(uci -q get ${CONFIG}.transcoding.hw_accel)
# Network/domain config
domain=$(uci -q get ${CONFIG}.network.domain)
haproxy=$(uci -q get ${CONFIG}.network.haproxy)
firewall_wan=$(uci -q get ${CONFIG}.network.firewall_wan)
# Mesh config
mesh_enabled=$(uci -q get ${CONFIG}.mesh.enabled)
json_add_boolean "enabled" ${enabled:-0}
json_add_string "image" "${image:-jellyfin/jellyfin:latest}"
json_add_int "port" ${port:-8096}
json_add_string "data_path" "${data_path:-/srv/jellyfin}"
json_add_string "timezone" "${timezone:-Europe/Paris}"
json_add_boolean "hw_accel" ${hw_accel:-0}
json_add_string "domain" "${domain:-jellyfin.secubox.local}"
json_add_boolean "haproxy" ${haproxy:-0}
json_add_boolean "firewall_wan" ${firewall_wan:-0}
json_add_boolean "mesh_enabled" ${mesh_enabled:-0}
# Docker availability
if command -v docker >/dev/null 2>&1; then
@ -49,6 +61,27 @@ case "$1" in
json_add_string "container_uptime" ""
fi
# HAProxy vhost status
if [ "${haproxy:-0}" = "1" ]; then
vhost_exists=$(uci show haproxy 2>/dev/null | grep "\.domain='${domain:-jellyfin.secubox.local}'" | head -1)
if [ -n "$vhost_exists" ]; then
json_add_string "haproxy_status" "configured"
else
json_add_string "haproxy_status" "pending"
fi
else
json_add_string "haproxy_status" "disabled"
fi
# Disk usage
dp="${data_path:-/srv/jellyfin}"
if [ -d "$dp" ]; then
disk_usage=$(du -sh "$dp" 2>/dev/null | cut -f1)
json_add_string "disk_usage" "${disk_usage:-0}"
else
json_add_string "disk_usage" ""
fi
# Media paths
json_add_array "media_paths"
for mp in $(uci -q get ${CONFIG}.media.media_path); do
@ -83,6 +116,59 @@ case "$1" in
json_dump
;;
uninstall)
output=$(/usr/sbin/jellyfinctl uninstall 2>&1)
code=$?
json_init
json_add_boolean "success" $((code == 0))
json_add_string "output" "$output"
json_dump
;;
update)
output=$(/usr/sbin/jellyfinctl update 2>&1)
code=$?
json_init
json_add_boolean "success" $((code == 0))
json_add_string "output" "$output"
json_dump
;;
configure_haproxy)
output=$(/usr/sbin/jellyfinctl configure-haproxy 2>&1)
code=$?
json_init
json_add_boolean "success" $((code == 0))
json_add_string "output" "$output"
json_dump
;;
backup)
backup_file="/tmp/jellyfin-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
output=$(/usr/sbin/jellyfinctl backup "$backup_file" 2>&1)
code=$?
json_init
json_add_boolean "success" $((code == 0))
json_add_string "path" "$backup_file"
json_add_string "output" "$output"
json_dump
;;
restore)
read -r input
path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)
if [ -z "$path" ]; then
echo '{"success":false,"output":"No backup path specified"}'
else
output=$(/usr/sbin/jellyfinctl restore "$path" 2>&1)
code=$?
json_init
json_add_boolean "success" $((code == 0))
json_add_string "output" "$output"
json_dump
fi
;;
logs)
read -r input
lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)

View File

@ -1,3 +1,5 @@
# Jellyfin Media Server Configuration
config jellyfin 'main'
option enabled '0'
option image 'jellyfin/jellyfin:latest'
@ -5,6 +7,14 @@ config jellyfin 'main'
option port '8096'
option timezone 'Europe/Paris'
config jellyfin 'network'
option domain 'jellyfin.secubox.local'
option public_url ''
option haproxy '0'
option haproxy_ssl '1'
option haproxy_ssl_redirect '1'
option firewall_wan '0'
config jellyfin 'media'
# list media_path '/mnt/media/movies'
# list media_path '/mnt/media/music'
@ -13,3 +23,7 @@ config jellyfin 'media'
config jellyfin 'transcoding'
option hw_accel '0'
option gpu_device ''
config jellyfin 'mesh'
option enabled '0'
option announce_service '1'

View File

@ -22,7 +22,15 @@ stop_service() {
"$SERVICE_BIN" service-stop >/dev/null 2>&1
}
restart_service() {
reload_service() {
stop_service
start_service
}
service_triggers() {
procd_add_reload_trigger "jellyfin"
}
status_service() {
"$SERVICE_BIN" status
}

View File

@ -1,29 +1,32 @@
#!/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
usage() {
cat <<'USAGE'
Usage: jellyfinctl <command>
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
Commands:
install Install prerequisites, prepare directories, pull image
check Run prerequisite checks
update Pull new image and restart
status Show container status
logs Show container logs (use -f to follow)
shell Open shell inside container
service-run Internal: run container via procd
service-stop Stop container
USAGE
}
log() { echo -e "${GREEN}[JELLYFIN]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
require_root() { [ "$(id -u)" -eq 0 ]; }
# ============================================================================
# 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)"
@ -37,6 +40,8 @@ defaults() {
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"; }
@ -53,16 +58,27 @@ ensure_packages() {
done
}
# ============================================================================
# Prerequisite Checks
# ============================================================================
check_prereqs() {
defaults
ensure_dir "$data_path"
[ -d /sys/fs/cgroup ] || { echo "[ERROR] /sys/fs/cgroup missing" >&2; return 1; }
[ -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
}
pull_image() { defaults; docker pull "$image"; }
# ============================================================================
# Docker Helpers
# ============================================================================
pull_image() {
defaults
docker pull "$image"
}
stop_container() {
docker stop "$CONTAINER" >/dev/null 2>&1 || true
@ -92,60 +108,412 @@ build_gpu_args() {
fi
}
cmd_install() {
require_root || { echo "Root required" >&2; exit 1; }
echo "[Jellyfin] Installing prerequisites..."
check_prereqs || exit 1
ensure_dir "$data_path/config"
ensure_dir "$data_path/cache"
echo "[Jellyfin] Pulling Docker image..."
pull_image || exit 1
uci set ${CONFIG}.main.enabled='1'
uci commit ${CONFIG}
/etc/init.d/jellyfin enable
echo ""
echo "Jellyfin installed successfully."
echo "Configure media paths:"
echo " uci add_list jellyfin.media.media_path='/path/to/media'"
echo " uci commit jellyfin"
echo "Start with: /etc/init.d/jellyfin start"
echo "Web UI: http://<device-ip>:${port}"
}
# ============================================================================
# HAProxy Integration
# ============================================================================
cmd_check() { check_prereqs; echo "Prerequisite check completed."; }
configure_haproxy() {
local haproxy_enabled=$(uci_get network.haproxy)
[ "$haproxy_enabled" != "1" ] && { log "HAProxy integration disabled in UCI"; return 0; }
cmd_update() {
require_root || { echo "Root required" >&2; exit 1; }
echo "[Jellyfin] Pulling latest image..."
pull_image || exit 1
echo "[Jellyfin] Restarting..."
/etc/init.d/jellyfin restart
}
if ! command -v haproxyctl >/dev/null 2>&1; then
warn "haproxyctl not found, skipping HAProxy configuration"
return 0
fi
cmd_status() {
defaults
echo "Jellyfin Media Server"
echo "====================="
echo " Image: $image"
echo " Port: $port"
echo " Data: $data_path"
echo ""
if docker ps --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null | grep -q .; then
echo " Container: RUNNING"
docker ps --filter "name=$CONTAINER" --format ' Uptime: {{.Status}}'
elif docker ps -a --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null | grep -q .; then
echo " Container: STOPPED"
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
echo " Container: NOT INSTALLED"
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; }
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 || { echo "Root required" >&2; exit 1; }
require_root
check_prereqs || exit 1
defaults
stop_container
@ -167,21 +535,73 @@ cmd_service_run() {
}
cmd_service_stop() {
require_root || { echo "Root required" >&2; exit 1; }
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 "$@" ;;
check) shift; cmd_check "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
help|--help|-h|'') usage ;;
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
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