472 lines
22 KiB
Bash
472 lines
22 KiB
Bash
|
|
#!/bin/bash
|
||
|
|
# =============================================================
|
||
|
|
# PLEX32 — Config Backup Script
|
||
|
|
# Creates tar archives of Plex and Tautulli configs
|
||
|
|
# Copies to NAS08 via NFS as single files
|
||
|
|
# Keeps last 3 backups, deletes older ones
|
||
|
|
# Sends HTML email report in KingDezigns style
|
||
|
|
# Location: /usr/scripts/plex32/plex32_backup.sh
|
||
|
|
# Schedule: Every 3 days at 2:00 AM (cron)
|
||
|
|
# =============================================================
|
||
|
|
|
||
|
|
# --- Config ---
|
||
|
|
HOSTNAME="plex32"
|
||
|
|
PLEX_SRC="/opt/plex/config"
|
||
|
|
TAUTULLI_SRC="/opt/tautulli/config"
|
||
|
|
NFS_DEST="/mnt/plex/config"
|
||
|
|
TAUTULLI_LOC="backups/tautulli"
|
||
|
|
PLEX_LOC="backups/plex"
|
||
|
|
|
||
|
|
TMP_DIR="/tmp/plex32-backup"
|
||
|
|
LOG_DIR="/var/log/plex32-backup"
|
||
|
|
LOG_FILE="$LOG_DIR/backup-$(date +%Y%m%d).log"
|
||
|
|
RETENTION_COUNT=3
|
||
|
|
COMPOSE_DIR="/home/rufusking/docker/plex"
|
||
|
|
|
||
|
|
DATESTAMP=$(date '+%Y-%m-%d')
|
||
|
|
TIMESTAMP=$(date '+%B %d, %Y at %I:%M %p')
|
||
|
|
|
||
|
|
PLEX_ARCHIVE="plex-config-${DATESTAMP}.tar.gz"
|
||
|
|
TAUTULLI_ARCHIVE="tautulli-config-${DATESTAMP}.tar.gz"
|
||
|
|
|
||
|
|
SMTP_SERVER="smtppro.zoho.com"
|
||
|
|
SMTP_PORT=465
|
||
|
|
SMTP_USER="rufus.king@kingdezigns.com"
|
||
|
|
SMTP_PASS="$(cat /etc/plex32-smtp-pass 2>/dev/null)"
|
||
|
|
EMAIL_TO="rufus.king@kingdezigns.com"
|
||
|
|
EMAIL_FROM="rufus.king@kingdezigns.com"
|
||
|
|
|
||
|
|
# --- Setup ---
|
||
|
|
mkdir -p "$LOG_DIR" "$TMP_DIR"
|
||
|
|
START_TIME=$(date +%s)
|
||
|
|
|
||
|
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] PLEX32 backup started"
|
||
|
|
|
||
|
|
# --- Check NFS mount ---
|
||
|
|
if ! mountpoint -q "$NFS_DEST"; then
|
||
|
|
echo "[ERROR] NFS mount not available: $NFS_DEST"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Stop containers ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Stopping Plex and Tautulli containers..."
|
||
|
|
cd "$COMPOSE_DIR" && docker compose down
|
||
|
|
STOP_STATUS=$?
|
||
|
|
if [ $STOP_STATUS -ne 0 ]; then
|
||
|
|
echo "[ERROR] Failed to stop containers cleanly"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Tar Plex config ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Plex config archive..."
|
||
|
|
PLEX_START=$(date +%s)
|
||
|
|
tar -czf "$TMP_DIR/$PLEX_ARCHIVE" \
|
||
|
|
--exclude="config/Library/Application Support/Plex Media Server/Cache" \
|
||
|
|
--exclude="config/Library/Application Support/Plex Media Server/Logs" \
|
||
|
|
--exclude="config/Library/Application Support/Plex Media Server/Crash Reports" \
|
||
|
|
-C /opt/plex config 2>&1
|
||
|
|
PLEX_TAR_STATUS=$?
|
||
|
|
PLEX_END=$(date +%s)
|
||
|
|
PLEX_DURATION=$((PLEX_END - PLEX_START))
|
||
|
|
|
||
|
|
if [ $PLEX_TAR_STATUS -eq 0 ]; then
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive created in ${PLEX_DURATION}s"
|
||
|
|
else
|
||
|
|
echo "[ERROR] Plex tar failed with status $PLEX_TAR_STATUS"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Tar Tautulli config ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Tautulli config archive..."
|
||
|
|
TAUTULLI_START=$(date +%s)
|
||
|
|
tar -czf "$TMP_DIR/$TAUTULLI_ARCHIVE" \
|
||
|
|
-C /opt/tautulli config 2>&1
|
||
|
|
TAUTULLI_TAR_STATUS=$?
|
||
|
|
TAUTULLI_END=$(date +%s)
|
||
|
|
TAUTULLI_DURATION=$((TAUTULLI_END - TAUTULLI_START))
|
||
|
|
|
||
|
|
if [ $TAUTULLI_TAR_STATUS -eq 0 ]; then
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive created in ${TAUTULLI_DURATION}s"
|
||
|
|
else
|
||
|
|
echo "[ERROR] Tautulli tar failed with status $TAUTULLI_TAR_STATUS"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Restart containers ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restarting containers..."
|
||
|
|
cd "$COMPOSE_DIR" && docker compose up -d
|
||
|
|
START_STATUS=$?
|
||
|
|
if [ $START_STATUS -eq 0 ]; then
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Containers restarted successfully"
|
||
|
|
else
|
||
|
|
echo "[ERROR] Failed to restart containers on first attempt — retrying in 15 seconds..."
|
||
|
|
sleep 15
|
||
|
|
cd "$COMPOSE_DIR" && docker compose up -d
|
||
|
|
START_STATUS=$?
|
||
|
|
if [ $START_STATUS -eq 0 ]; then
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Containers restarted successfully on retry"
|
||
|
|
else
|
||
|
|
echo "[ERROR] Containers failed to restart after retry — manual intervention required"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Verify containers are actually running ---
|
||
|
|
sleep 5
|
||
|
|
PLEX_RUNNING=$(docker ps --filter "name=plex" --filter "status=running" --format "{{.Names}}" | grep -c "^plex$")
|
||
|
|
TAUTULLI_RUNNING=$(docker ps --filter "name=tautulli" --filter "status=running" --format "{{.Names}}" | grep -c "^tautulli$")
|
||
|
|
|
||
|
|
if [ "$PLEX_RUNNING" -eq 1 ]; then
|
||
|
|
PLEX_CONTAINER_STATUS="✅ RUNNING"
|
||
|
|
PLEX_CONTAINER_COLOR="#166534"
|
||
|
|
PLEX_CONTAINER_BG="#f0fdf4"
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex container verified running"
|
||
|
|
else
|
||
|
|
PLEX_CONTAINER_STATUS="❌ NOT RUNNING — MANUAL INTERVENTION REQUIRED"
|
||
|
|
PLEX_CONTAINER_COLOR="#b91c1c"
|
||
|
|
PLEX_CONTAINER_BG="#fff1f2"
|
||
|
|
echo "[ERROR] Plex container is NOT running after backup"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "$TAUTULLI_RUNNING" -eq 1 ]; then
|
||
|
|
TAUTULLI_CONTAINER_STATUS="✅ RUNNING"
|
||
|
|
TAUTULLI_CONTAINER_COLOR="#166534"
|
||
|
|
TAUTULLI_CONTAINER_BG="#f0fdf4"
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli container verified running"
|
||
|
|
else
|
||
|
|
TAUTULLI_CONTAINER_STATUS="❌ NOT RUNNING — MANUAL INTERVENTION REQUIRED"
|
||
|
|
TAUTULLI_CONTAINER_COLOR="#b91c1c"
|
||
|
|
TAUTULLI_CONTAINER_BG="#fff1f2"
|
||
|
|
echo "[ERROR] Tautulli container is NOT running after backup"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Copy Plex archive to NAS08 ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Plex archive to NAS08..."
|
||
|
|
COPY_PLEX_START=$(date +%s)
|
||
|
|
cp "$TMP_DIR/$PLEX_ARCHIVE" "$NFS_DEST/$PLEX_LOC/"
|
||
|
|
PLEX_COPY_STATUS=$?
|
||
|
|
COPY_PLEX_END=$(date +%s)
|
||
|
|
PLEX_COPY_DURATION=$((COPY_PLEX_END - COPY_PLEX_START))
|
||
|
|
|
||
|
|
if [ $PLEX_COPY_STATUS -eq 0 ]; then
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive copied in ${PLEX_COPY_DURATION}s"
|
||
|
|
else
|
||
|
|
echo "[ERROR] Plex archive copy failed"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Copy Tautulli archive to NAS08 ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Tautulli archive to NAS08..."
|
||
|
|
COPY_TAUTULLI_START=$(date +%s)
|
||
|
|
cp "$TMP_DIR/$TAUTULLI_ARCHIVE" "$NFS_DEST/$TAUTULLI_LOC/"
|
||
|
|
TAUTULLI_COPY_STATUS=$?
|
||
|
|
COPY_TAUTULLI_END=$(date +%s)
|
||
|
|
TAUTULLI_COPY_DURATION=$((COPY_TAUTULLI_END - COPY_TAUTULLI_START))
|
||
|
|
|
||
|
|
if [ $TAUTULLI_COPY_STATUS -eq 0 ]; then
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive copied in ${TAUTULLI_COPY_DURATION}s"
|
||
|
|
else
|
||
|
|
echo "[ERROR] Tautulli archive copy failed"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Rotate old backups — keep last 3 ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Rotating old backups..."
|
||
|
|
ls -t "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f
|
||
|
|
ls -t "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f
|
||
|
|
|
||
|
|
# --- Cleanup temp files ---
|
||
|
|
rm -f "$TMP_DIR/$PLEX_ARCHIVE" "$TMP_DIR/$TAUTULLI_ARCHIVE"
|
||
|
|
|
||
|
|
# --- Sizes and storage ---
|
||
|
|
PLEX_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$PLEX_LOC/$PLEX_ARCHIVE" 2>/dev/null | awk '{print $5}')
|
||
|
|
TAUTULLI_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC/$TAUTULLI_ARCHIVE" 2>/dev/null | awk '{print $5}')
|
||
|
|
PLEX_BACKUP_COUNT=$(ls "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | wc -l)
|
||
|
|
TAUTULLI_BACKUP_COUNT=$(ls "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | wc -l)
|
||
|
|
|
||
|
|
# Get accurate NAS08 disk stats via SSH (avoids NFS dataset quota confusion)
|
||
|
|
NAS08_DISK_INFO=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 rufusking@192.168.150.35 \
|
||
|
|
"df -h /export/kingdezigns-all | awk 'NR==2{print \$3\" used / \"\$2\" total / \"\$4\" free\"}'" 2>/dev/null)
|
||
|
|
if [ -z "$NAS08_DISK_INFO" ]; then
|
||
|
|
# Fallback to NFS mount stats if SSH fails
|
||
|
|
NAS08_DISK_INFO=$(df -h "$NFS_DEST" 2>/dev/null | awk 'NR==2{print $3" used / "$2" total / "$4" free"}')
|
||
|
|
fi
|
||
|
|
|
||
|
|
# List of current backups on NAS08
|
||
|
|
PLEX_BACKUP_LIST=$(ls -lh "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | \
|
||
|
|
awk '{print $NF" ("$5")"}' | xargs -I{} basename {} 2>/dev/null | \
|
||
|
|
while read -r line; do echo "$line"; done)
|
||
|
|
TAUTULLI_BACKUP_LIST=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | \
|
||
|
|
awk '{print $NF" ("$5")"}' | xargs -I{} basename {} 2>/dev/null | \
|
||
|
|
while read -r line; do echo "$line"; done)
|
||
|
|
|
||
|
|
# Build backup list HTML rows
|
||
|
|
PLEX_LIST_ROWS=""
|
||
|
|
while IFS= read -r f; do
|
||
|
|
SIZE=$(ls -lh "$NFS_DEST/$PLEX_LOC/$f" 2>/dev/null | awk '{print $5}')
|
||
|
|
PLEX_LIST_ROWS="${PLEX_LIST_ROWS}<tr><td style='font-size:12px;font-family:monospace;color:#374151;padding:6px 18px;border-top:1px solid #e5e7eb;'>$f</td><td style='font-size:12px;color:#6b7280;padding:6px 18px;border-top:1px solid #e5e7eb;'>$SIZE</td></tr>"
|
||
|
|
done < <(ls "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | xargs -I{} basename {})
|
||
|
|
|
||
|
|
TAUTULLI_LIST_ROWS=""
|
||
|
|
while IFS= read -r f; do
|
||
|
|
SIZE=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC/$f" 2>/dev/null | awk '{print $5}')
|
||
|
|
TAUTULLI_LIST_ROWS="${TAUTULLI_LIST_ROWS}<tr><td style='font-size:12px;font-family:monospace;color:#374151;padding:6px 18px;border-top:1px solid #e5e7eb;'>$f</td><td style='font-size:12px;color:#6b7280;padding:6px 18px;border-top:1px solid #e5e7eb;'>$SIZE</td></tr>"
|
||
|
|
done < <(ls "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | xargs -I{} basename {})
|
||
|
|
|
||
|
|
# --- Total duration ---
|
||
|
|
END_TIME=$(date +%s)
|
||
|
|
TOTAL_DURATION=$((END_TIME - START_TIME))
|
||
|
|
TOTAL_MIN=$((TOTAL_DURATION / 60))
|
||
|
|
TOTAL_SEC=$((TOTAL_DURATION % 60))
|
||
|
|
|
||
|
|
# --- Overall status ---
|
||
|
|
PLEX_STATUS=$((PLEX_TAR_STATUS + PLEX_COPY_STATUS))
|
||
|
|
TAUTULLI_STATUS=$((TAUTULLI_TAR_STATUS + TAUTULLI_COPY_STATUS))
|
||
|
|
CONTAINER_STATUS=$((PLEX_RUNNING == 1 && TAUTULLI_RUNNING == 1 ? 0 : 1))
|
||
|
|
|
||
|
|
if [ $PLEX_STATUS -eq 0 ] && [ $TAUTULLI_STATUS -eq 0 ] && [ $CONTAINER_STATUS -eq 0 ]; then
|
||
|
|
OVERALL_STATUS="SUCCESS"
|
||
|
|
STATUS_COLOR="#1a7f4b"
|
||
|
|
BANNER_ICON="✅"
|
||
|
|
BANNER_MSG="Backup completed successfully — Plex and Tautulli configs archived to NAS08"
|
||
|
|
BADGE_COLOR="#1a7f4b"
|
||
|
|
elif [ $PLEX_STATUS -ne 0 ] && [ $TAUTULLI_STATUS -ne 0 ]; then
|
||
|
|
OVERALL_STATUS="FAILED"
|
||
|
|
STATUS_COLOR="#b91c1c"
|
||
|
|
BANNER_ICON="🔴"
|
||
|
|
BANNER_MSG="Backup failed — both archive operations encountered errors"
|
||
|
|
BADGE_COLOR="#b91c1c"
|
||
|
|
else
|
||
|
|
OVERALL_STATUS="PARTIAL"
|
||
|
|
STATUS_COLOR="#b45309"
|
||
|
|
BANNER_ICON="⚠️"
|
||
|
|
BANNER_MSG="Backup partially completed — review individual results below"
|
||
|
|
BADGE_COLOR="#b45309"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Status labels ---
|
||
|
|
if [ $PLEX_STATUS -eq 0 ]; then
|
||
|
|
PLEX_LABEL="✅ SUCCESS"
|
||
|
|
PLEX_ROW_BG="#f0fdf4"
|
||
|
|
PLEX_TEXT_COLOR="#166534"
|
||
|
|
else
|
||
|
|
PLEX_LABEL="❌ FAILED"
|
||
|
|
PLEX_ROW_BG="#fff1f2"
|
||
|
|
PLEX_TEXT_COLOR="#b91c1c"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ $TAUTULLI_STATUS -eq 0 ]; then
|
||
|
|
TAUTULLI_LABEL="✅ SUCCESS"
|
||
|
|
TAUTULLI_ROW_BG="#f0fdf4"
|
||
|
|
TAUTULLI_TEXT_COLOR="#166534"
|
||
|
|
else
|
||
|
|
TAUTULLI_LABEL="❌ FAILED"
|
||
|
|
TAUTULLI_ROW_BG="#fff1f2"
|
||
|
|
TAUTULLI_TEXT_COLOR="#b91c1c"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Send email ---
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sending email report..."
|
||
|
|
|
||
|
|
python3 << PYEOF
|
||
|
|
import smtplib, ssl
|
||
|
|
from email.mime.multipart import MIMEMultipart
|
||
|
|
from email.mime.text import MIMEText
|
||
|
|
|
||
|
|
smtp_server = "$SMTP_SERVER"
|
||
|
|
smtp_port = $SMTP_PORT
|
||
|
|
smtp_user = "$SMTP_USER"
|
||
|
|
smtp_pass = "$SMTP_PASS"
|
||
|
|
email_to = "$EMAIL_TO"
|
||
|
|
email_from = "$EMAIL_FROM"
|
||
|
|
|
||
|
|
subject = "PLEX32 Backup — $OVERALL_STATUS — $DATESTAMP"
|
||
|
|
|
||
|
|
html = """
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<body style="margin:0;padding:20px;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr><td align="center">
|
||
|
|
<table width="720" cellspacing="0" cellpadding="0" style="max-width:720px;width:100%;">
|
||
|
|
|
||
|
|
<!-- HEADER -->
|
||
|
|
<tr><td style="background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;">
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0"><tr>
|
||
|
|
<td>
|
||
|
|
<div style="font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;">Config Backup</div>
|
||
|
|
<div style="font-size:26px;font-weight:800;color:white;margin-top:4px;">🎬 plex32</div>
|
||
|
|
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">$TIMESTAMP | VLAN 50 — Lab/Servers</div>
|
||
|
|
</td>
|
||
|
|
<td align="right">
|
||
|
|
<div style="background:$BADGE_COLOR;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;">$OVERALL_STATUS</div>
|
||
|
|
</td>
|
||
|
|
</tr></table>
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
<!-- STATUS BANNER -->
|
||
|
|
<tr><td style="background:$STATUS_COLOR;padding:12px 32px;">
|
||
|
|
<span style="color:white;font-size:14px;font-weight:600;">$BANNER_ICON $BANNER_MSG</span>
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
<!-- BODY -->
|
||
|
|
<tr><td style="background:white;border:1px solid #e5e7eb;padding:28px 32px;">
|
||
|
|
|
||
|
|
<!-- STAT CARDS -->
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr>
|
||
|
|
<td style="padding:4px;">
|
||
|
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Duration</div>
|
||
|
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">${TOTAL_MIN}m ${TOTAL_SEC}s</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td style="padding:4px;">
|
||
|
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Plex Archive</div>
|
||
|
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$PLEX_ARCHIVE_SIZE</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td style="padding:4px;">
|
||
|
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Tautulli Archive</div>
|
||
|
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$TAUTULLI_ARCHIVE_SIZE</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td style="padding:4px;">
|
||
|
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Plex Copies</div>
|
||
|
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$PLEX_BACKUP_COUNT / $RETENTION_COUNT</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
|
||
|
|
<!-- RESULTS TABLE -->
|
||
|
|
<div style="margin-top:24px;background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
|
||
|
|
<div style="font-size:13px;font-weight:700;color:#374151;padding:14px 18px;border-bottom:1px solid #e5e7eb;text-transform:uppercase;letter-spacing:.5px;">📋 Backup Results</div>
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr style="background:#f9fafb;">
|
||
|
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Component</th>
|
||
|
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Archive</th>
|
||
|
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Duration</th>
|
||
|
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Status</th>
|
||
|
|
</tr>
|
||
|
|
<tr style="background:$PLEX_ROW_BG;">
|
||
|
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">🎬 Plex Config</td>
|
||
|
|
<td style="font-size:12px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;font-family:monospace;">$PLEX_ARCHIVE</td>
|
||
|
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">${PLEX_DURATION}s tar + ${PLEX_COPY_DURATION}s copy</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$PLEX_TEXT_COLOR;">$PLEX_LABEL</td>
|
||
|
|
</tr>
|
||
|
|
<tr style="background:$TAUTULLI_ROW_BG;">
|
||
|
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">📊 Tautulli Config</td>
|
||
|
|
<td style="font-size:12px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;font-family:monospace;">$TAUTULLI_ARCHIVE</td>
|
||
|
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">${TAUTULLI_DURATION}s tar + ${TAUTULLI_COPY_DURATION}s copy</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$TAUTULLI_TEXT_COLOR;">$TAUTULLI_LABEL</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- CONTAINER STATUS -->
|
||
|
|
<div style="margin-top:20px;background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
|
||
|
|
<div style="font-size:13px;font-weight:700;color:#374151;padding:14px 18px;border-bottom:1px solid #e5e7eb;text-transform:uppercase;letter-spacing:.5px;">🐳 Container Status After Backup</div>
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr style="background:$PLEX_CONTAINER_BG;">
|
||
|
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">🎬 Plex</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$PLEX_CONTAINER_COLOR;">$PLEX_CONTAINER_STATUS</td>
|
||
|
|
</tr>
|
||
|
|
<tr style="background:$TAUTULLI_CONTAINER_BG;">
|
||
|
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">📊 Tautulli</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$TAUTULLI_CONTAINER_COLOR;">$TAUTULLI_CONTAINER_STATUS</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- CURRENT BACKUPS ON NAS08 -->
|
||
|
|
<div style="margin-top:20px;background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
|
||
|
|
<div style="font-size:13px;font-weight:700;color:#374151;padding:14px 18px;border-bottom:1px solid #e5e7eb;text-transform:uppercase;letter-spacing:.5px;">🗄️ Archives on NAS08</div>
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr style="background:#f9fafb;">
|
||
|
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;" colspan="2">Plex Backups (max $RETENTION_COUNT kept)</th>
|
||
|
|
</tr>
|
||
|
|
$PLEX_LIST_ROWS
|
||
|
|
<tr style="background:#f9fafb;">
|
||
|
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;border-top:2px solid #e5e7eb;" colspan="2">Tautulli Backups (max $RETENTION_COUNT kept)</th>
|
||
|
|
</tr>
|
||
|
|
$TAUTULLI_LIST_ROWS
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- DESTINATION INFO -->
|
||
|
|
<div style="margin-top:16px;padding:12px 14px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb;">
|
||
|
|
<span style="font-size:12px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.5px;">Backup Destination</span>
|
||
|
|
<span style="margin-left:10px;font-size:13px;color:#374151;">NAS08 (192.168.150.35) — $NAS08_DISK_INFO</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- RESTORE INSTRUCTIONS -->
|
||
|
|
<div style="margin-top:16px;background:white;border-radius:10px;border:1px solid #fac775;overflow:hidden;">
|
||
|
|
<div style="font-size:13px;font-weight:700;color:#633806;padding:14px 18px;border-bottom:1px solid #fac775;background:#fffbeb;text-transform:uppercase;letter-spacing:.5px;">💡 How to Restore Plex Config</div>
|
||
|
|
<div style="padding:16px 18px;">
|
||
|
|
|
||
|
|
<div style="font-size:12px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">Machine: plex32 (192.168.150.45)</div>
|
||
|
|
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 1 — Stop Plex and Tautulli</div>
|
||
|
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">cd ~/docker/plex && docker compose down</pre>
|
||
|
|
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 2 — Copy archive from NAS08 to PLEX32</div>
|
||
|
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">cp /mnt/plex/config/$PLEX_LOC/$PLEX_ARCHIVE /tmp/</pre>
|
||
|
|
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 3 — Clear current Plex config</div>
|
||
|
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">sudo rm -rf /opt/plex/config/Library</pre>
|
||
|
|
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 4 — Extract the backup</div>
|
||
|
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">sudo tar -xzf /tmp/$PLEX_ARCHIVE -C /opt/plex
|
||
|
|
sudo chown -R rufusking:ncshare /opt/plex/config</pre>
|
||
|
|
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 5 — Restart containers</div>
|
||
|
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">cd ~/docker/plex && docker compose up -d</pre>
|
||
|
|
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 6 — Verify both containers are running</div>
|
||
|
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">docker ps</pre>
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-top:6px;">Both <code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;">plex</code> and <code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;">tautulli</code> should show status <strong>Up</strong>. Then open <code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;">http://192.168.150.45:32400/web</code> and confirm your library and watch history are intact.</div>
|
||
|
|
|
||
|
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:16px;">Step 7 — Cleanup temp file</div>
|
||
|
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">rm /tmp/$PLEX_ARCHIVE</pre>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
<!-- FOOTER -->
|
||
|
|
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
|
||
|
|
<span style="font-size:11px;color:#64748b;">Config Backup | plex32 | KingDezigns Infrastructure</span>
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
</table>
|
||
|
|
</td></tr>
|
||
|
|
</table>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
"""
|
||
|
|
|
||
|
|
msg = MIMEMultipart("alternative")
|
||
|
|
msg["Subject"] = subject
|
||
|
|
msg["From"] = email_from
|
||
|
|
msg["To"] = email_to
|
||
|
|
msg.attach(MIMEText(html, "html"))
|
||
|
|
|
||
|
|
ctx = ssl.create_default_context()
|
||
|
|
with smtplib.SMTP_SSL(smtp_server, smtp_port, context=ctx, timeout=15) as s:
|
||
|
|
s.login(smtp_user, smtp_pass)
|
||
|
|
s.sendmail(email_from, [email_to], msg.as_string())
|
||
|
|
print("Email sent successfully")
|
||
|
|
PYEOF
|
||
|
|
|
||
|
|
EMAIL_STATUS=$?
|
||
|
|
if [ $EMAIL_STATUS -eq 0 ]; then
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Email sent successfully"
|
||
|
|
else
|
||
|
|
echo "[ERROR] Email send failed"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Cleanup old logs ---
|
||
|
|
find "$LOG_DIR" -name "backup-*.log" -mtime +90 -delete 2>/dev/null
|
||
|
|
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup complete — Status: $OVERALL_STATUS | Duration: ${TOTAL_MIN}m ${TOTAL_SEC}s"
|