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 += ''; + + // Mesh + var mc = status.mesh_enabled ? '#27ae60' : '#8892b0'; + var ml = status.mesh_enabled ? 'Enabled' : 'Disabled'; + html += ''; + + // Firewall + var fc = status.firewall_wan ? '#27ae60' : '#8892b0'; + var fl = status.firewall_wan ? 'WAN access on port ' + (status.port || 8096) : 'LAN only'; + html += ''; + + html += '
HAProxy:' + hl + '
Mesh P2P:' + ml + '
Firewall:' + fl + '
'; + 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