diff --git a/package/secubox/luci-app-jellyfin/htdocs/luci-static/resources/view/jellyfin/overview.js b/package/secubox/luci-app-jellyfin/htdocs/luci-static/resources/view/jellyfin/overview.js
index 45e2848f..90f5918a 100644
--- a/package/secubox/luci-app-jellyfin/htdocs/luci-static/resources/view/jellyfin/overview.js
+++ b/package/secubox/luci-app-jellyfin/htdocs/luci-static/resources/view/jellyfin/overview.js
@@ -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 += '
| Image: | ' + (status.image || '-') + ' |
';
html += '| Port: | ' + port + ' |
';
html += '| Data: | ' + (status.data_path || '-') + ' |
';
+ html += '| Domain: | ' + (status.domain || '-') + ' |
';
+ if (status.disk_usage)
+ html += '| Disk: | ' + status.disk_usage + ' |
';
if (status.media_paths && status.media_paths.length > 0)
html += '| Media: | ' + status.media_paths.join(' ') + ' |
';
html += '';
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 = '';
+
+ // 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 += '| HAProxy: | ' + hl + ' |
';
+
+ // Mesh
+ var mc = status.mesh_enabled ? '#27ae60' : '#8892b0';
+ var ml = status.mesh_enabled ? 'Enabled' : 'Disabled';
+ html += '| Mesh P2P: | ' + ml + ' |
';
+
+ // Firewall
+ var fc = status.firewall_wan ? '#27ae60' : '#8892b0';
+ var fl = status.firewall_wan ? 'WAN access on port ' + (status.port || 8096) : 'LAN only';
+ html += '| Firewall: | ' + fl + ' |
';
+
+ html += '
';
+ 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;
diff --git a/package/secubox/luci-app-jellyfin/root/usr/libexec/rpcd/luci.jellyfin b/package/secubox/luci-app-jellyfin/root/usr/libexec/rpcd/luci.jellyfin
index 98ec2f68..6df1e18e 100644
--- a/package/secubox/luci-app-jellyfin/root/usr/libexec/rpcd/luci.jellyfin
+++ b/package/secubox/luci-app-jellyfin/root/usr/libexec/rpcd/luci.jellyfin
@@ -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)
diff --git a/package/secubox/secubox-app-jellyfin/files/etc/config/jellyfin b/package/secubox/secubox-app-jellyfin/files/etc/config/jellyfin
index b8bc8c0d..c139e137 100644
--- a/package/secubox/secubox-app-jellyfin/files/etc/config/jellyfin
+++ b/package/secubox/secubox-app-jellyfin/files/etc/config/jellyfin
@@ -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'
diff --git a/package/secubox/secubox-app-jellyfin/files/etc/init.d/jellyfin b/package/secubox/secubox-app-jellyfin/files/etc/init.d/jellyfin
index 3938fe66..01928d12 100644
--- a/package/secubox/secubox-app-jellyfin/files/etc/init.d/jellyfin
+++ b/package/secubox/secubox-app-jellyfin/files/etc/init.d/jellyfin
@@ -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
+}
diff --git a/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl b/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl
index e99e5825..91223288 100644
--- a/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl
+++ b/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl
@@ -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
+# 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://:${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://:${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 "
+ 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 [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 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