titan-iac/scripts/jellyfin_manual_load.fish

257 lines
7.7 KiB
Fish
Executable File

#!/usr/bin/env fish
# Copy local files/folders into Jellyfin's RWX PVC reliably using rsync.
# Primary path: NodePort -> rsync direct to node (no apiserver).
# Fallback: kubectl port-forward to service (OK for small/medium files).
#
# Usage:
# scripts/jellyfin_manual_load.fish <LOCAL_PATH> [REMOTE_SUBDIR] [JELLYFIN_API_KEY]
# Examples:
# scripts/jellyfin_manual_load.fish "$HOME/Downloads/Avatar - The Last Airbender (2005 - 2008) [1080p]" kids_tv "$JELLYFIN_API_TOKEN"
# scripts/jellyfin_manual_load.fish "$HOME/Movies/." movies # copy contents-only into /media/movies
function usage
echo "Usage: "(basename (status filename))" <LOCAL_PATH> [REMOTE_SUBDIR] [JELLYFIN_API_KEY]"
echo " LOCAL_PATH: file or directory. Use '/.' to copy contents-only."
echo " REMOTE_SUBDIR: subdir under /media on the PVC (optional)."
end
# --- sanity checks ---
set -g KCTL (command -v kubectl)
if test -z "$KCTL"
echo "ERROR: kubectl not found in PATH."; exit 1
end
if not command -sq rsync
echo "ERROR: rsync not found. Install rsync and re-run."; exit 1
end
function kc --description 'kubectl with unlimited timeout'
command "$KCTL" --request-timeout=0 $argv
end
# --- constants ---
set -g NS jellyfin
set -g POD loader
set -g CTN toolbox
set -g YAML services/jellyfin/loader.yaml
set -g RSYNC_SVC loader-rsync
set -g RSYNC_NODEPORT 30873
set -g DEST_BASE /media
# --- args ---
if test (count $argv) -lt 1
usage; exit 1
end
# expand ~ in first arg even when quoted
set -l LOCAL_RAW $argv[1]
set -l LOCAL (string replace -r '^~(?=/|$)' -- $HOME $LOCAL_RAW)
if not test -e "$LOCAL"
echo "ERROR: '$LOCAL' does not exist."; exit 1
end
set -l REMOTE_SUBDIR ""
if test (count $argv) -ge 2
set REMOTE_SUBDIR (string replace -r '^/+|/+$' '' -- $argv[2])
end
set -l API_TOKEN ""
if test (count $argv) -ge 3
set API_TOKEN $argv[3]
end
# contents-only mode if LOCAL ended with '/.'
set -l contents_only 0
if string match -r '/\.\s*$' -- "$LOCAL_RAW" >/dev/null
set contents_only 1
set LOCAL (string replace -r '/\.\s*$' '' -- "$LOCAL")
end
# --- create/refresh loader pod ---
if kc -n $NS get pod $POD -o name >/dev/null 2>&1
echo "Found existing $NS/$POD; deleting it first..."
kc -n $NS delete pod $POD --wait >/dev/null
end
echo "Creating $NS/$POD from $YAML ..."
if not kc -n $NS apply -f "$YAML" >/dev/null
echo "ERROR: failed to apply $YAML"; exit 1
end
echo "Waiting for $NS/$POD to be Ready..."
if not kc -n $NS wait --for=condition=Ready pod/$POD --timeout=600s >/dev/null
echo "ERROR: $POD did not become Ready."; exit 1
end
# ensure base dir & perms
kc -n $NS exec $POD -c $CTN -- sh -lc "mkdir -p -- \"$DEST_BASE\" && chmod 0777 \"$DEST_BASE\"" >/dev/null
if test -n "$REMOTE_SUBDIR"
kc -n $NS exec $POD -c $CTN -- sh -lc "mkdir -p -- \"$DEST_BASE/$REMOTE_SUBDIR\" && chmod 0777 \"$DEST_BASE/$REMOTE_SUBDIR\"" >/dev/null
end
# label so a Service can select it
kc -n $NS label pod $POD app=loader --overwrite >/dev/null
# --- start rsync daemon inside the loader container ---
echo "Starting rsync daemon inside $NS/$POD ..."
set -l RSYNCD_CFG '
pid file = /var/run/rsyncd.pid
use chroot = no
log file = /dev/stdout
max connections = 4
[media]
path = /media
read only = false
uid = 0
gid = 0
hosts allow = 0.0.0.0/0
'
# Install rsync (if needed), write config, stop any old rsync, then start daemon (detaches by itself).
if not kc -n $NS exec $POD -c $CTN -- sh -lc "
(rsync --version >/dev/null 2>&1) || apk add --no-cache rsync >/dev/null 2>&1
cat > /etc/rsyncd.conf <<'EOF'
$RSYNCD_CFG
EOF
pkill rsync >/dev/null 2>&1 || true
rsync --daemon --config=/etc/rsyncd.conf --port=873
# quick presence check
pgrep rsync >/dev/null
"
echo "ERROR: failed to start rsyncd in the loader pod."; exit 1
end
# --- (re)create NodePort service to reach rsyncd ---
kc -n $NS delete svc $RSYNC_SVC --ignore-not-found >/dev/null
printf "%s\n" \
"apiVersion: v1
kind: Service
metadata:
name: $RSYNC_SVC
namespace: $NS
spec:
type: NodePort
selector:
app: loader
ports:
- name: rsync
port: 873
targetPort: 873
nodePort: $RSYNC_NODEPORT" | kc apply -f - >/dev/null
# wait for endpoints to be populated
for i in (seq 1 30)
set -l ep (kc -n $NS get endpoints $RSYNC_SVC -o jsonpath='{range .subsets[*].addresses[*]}{.ip}{" "}{end}')
if test -n "$ep"
break
end
sleep 1
end
# Which node is the pod on?
set -l NODE (kc -n $NS get pod $POD -o jsonpath='{.spec.nodeName}')
set -l HOST (kc get node $NODE -o jsonpath='{range .status.addresses[?(@.type=="InternalIP")]}{.address}{end}')
# Try NodePort reachability; if blocked, fall back to port-forward.
set -l DEST_URL ""
set -l VIA "nodeport"
echo "Waiting for rsync on $HOST:$RSYNC_NODEPORT ..."
for i in (seq 1 10)
if rsync "rsync://$HOST:$RSYNC_NODEPORT/" >/dev/null 2>&1
set DEST_URL "rsync://$HOST:$RSYNC_NODEPORT/media"
break
end
sleep 1
end
if test -z "$DEST_URL"
set VIA "port-forward"
set -l PF_LOCAL 3873
echo "NodePort not reachable; falling back to kubectl port-forward on 127.0.0.1:$PF_LOCAL ..."
# background port-forward; capture PID
kc -n $NS port-forward svc/$RSYNC_SVC 127.0.0.1:$PF_LOCAL:873 >/dev/null 2>&1 &
set -l PF_PID $last_pid
# wait until local rsync answers
for i in (seq 1 30)
if rsync "rsync://127.0.0.1:$PF_LOCAL/" >/dev/null 2>&1
set DEST_URL "rsync://127.0.0.1:$PF_LOCAL/media"
break
end
sleep 1
end
if test -z "$DEST_URL"
echo "ERROR: rsync daemon not reachable via NodePort or port-forward."
if test -n "$PF_PID"
command kill $PF_PID >/dev/null 2>&1
end
exit 1
end
end
if test -n "$REMOTE_SUBDIR"
set DEST_URL "$DEST_URL/$REMOTE_SUBDIR"
end
# --- rsync flags (robust/resumable/overwrite) ---
set -l RSYNC_FLAGS -a --progress --human-readable \
--partial --partial-dir=.rsync-partial --delay-updates \
--chmod=Du=rwx,Dgo=rwx,Fu=rw,Fgo=rw \
--timeout=600 --contimeout=30 \
--exclude='.nfs*'
# --- perform copy ---
set -l copy_ok 0
if test -f "$LOCAL"
set -l base (basename "$LOCAL")
echo "Copying file '$base' -> $DEST_URL/ ... ($VIA)"
if rsync $RSYNC_FLAGS "$LOCAL" "$DEST_URL/"
set copy_ok 1
end
else if test -d "$LOCAL"
set -l base (basename "$LOCAL")
if test $contents_only -eq 1
echo "Copying contents of '$base/' -> $DEST_URL/ ... ($VIA)"
if rsync $RSYNC_FLAGS "$LOCAL/." "$DEST_URL/"
set copy_ok 1
end
else
echo "Copying folder '$base' -> $DEST_URL/ ... ($VIA)"
if rsync $RSYNC_FLAGS "$LOCAL" "$DEST_URL/"
set copy_ok 1
end
end
else
echo "ERROR: '$LOCAL' is neither file nor directory."
end
# --- verify & optionally refresh Jellyfin ---
echo "Verifying on the pod (top level of "(test -n "$REMOTE_SUBDIR"; and echo "$DEST_BASE/$REMOTE_SUBDIR"; or echo "$DEST_BASE")") ..."
kc -n $NS exec $POD -c $CTN -- sh -lc "du -sh -- \"$DEST_BASE\"; ls -lah -- \"$DEST_BASE\" | sed -n '1,200p'"
if test $copy_ok -eq 1
if test -n "$API_TOKEN"
if command -sq curl
echo "Triggering Jellyfin library refresh..."
if curl -fsS -X POST -H "X-Emby-Token: $API_TOKEN" "https://stream.bstein.dev/Library/Refresh" >/dev/null
echo "Jellyfin library refresh triggered."
else
echo "WARNING: Jellyfin library refresh HTTP call failed."
end
else
echo "NOTE: 'curl' not found; skipping library refresh."
end
end
echo "Cleaning up $NS/$POD and $RSYNC_SVC ..."
if test "$VIA" = "port-forward" -a -n "$PF_PID"
command kill $PF_PID >/dev/null 2>&1
end
kc -n $NS delete svc/$RSYNC_SVC --wait=false >/dev/null
kc -n $NS delete pod/$POD --wait >/dev/null
echo "Done."
else
echo "Copy encountered errors; leaving $POD and $RSYNC_SVC running for inspection."
echo "Tip: check rsyncd in the pod: kubectl -n $NS exec $POD -c $CTN -- pgrep -a rsync || true"
exit 1
end