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:
parent
b32a0c222b
commit
155f9d8005
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user