#!/bin/bash set -e # Color codes for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Configuration MAX_BACKUP_AGE_DAYS=30 MAX_BACKUPS_TO_KEEP=10 BACKUP_ROOT="/mnt/share/media/backups/plex" LOG_ROOT="/mnt/share/media/backups/logs" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" JSON_LOG_FILE="${SCRIPT_DIR}/logs/plex-backup.json" # Create necessary directories mkdir -p "${LOG_ROOT}" "${SCRIPT_DIR}/logs" # Date variables CURRENT_DATE=$(date +%Y%m%d) CURRENT_DATETIME=$(date +%Y%m%d_%H%M%S) LOG_FILE="${LOG_ROOT}/plex_backup_${CURRENT_DATETIME}.log" BACKUP_PATH="${BACKUP_ROOT}/${CURRENT_DATE}" # Plex files to backup with their nicknames for easier handling declare -A PLEX_FILES=( ["library_db"]="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db" ["blobs_db"]="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.blobs.db" ["preferences"]="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Preferences.xml" ) # Logging functions log_message() { local message="$1" local timestamp=$(date '+%H:%M:%S') echo "${timestamp} ${message}" | tee -a "${LOG_FILE}" } log_error() { local message="$1" log_message "${RED}ERROR: ${message}${NC}" } log_success() { local message="$1" log_message "${GREEN}SUCCESS: ${message}${NC}" } log_warning() { local message="$1" log_message "${YELLOW}WARNING: ${message}${NC}" } # Initialize JSON log file initialize_json_log() { if [ ! -f "${JSON_LOG_FILE}" ] || ! jq empty "${JSON_LOG_FILE}" 2>/dev/null; then echo "{}" > "${JSON_LOG_FILE}" log_message "Initialized JSON log file" fi } # Check if file needs backup based on modification time needs_backup() { local file="$1" if [ ! -f "$file" ]; then log_warning "File not found: $file" return 1 fi local current_mod_date=$(stat -c %Y "$file") local last_backup_date=$(jq -r --arg file "$file" '.[$file] // 0' "${JSON_LOG_FILE}") if [ "$last_backup_date" == "null" ] || [ "$last_backup_date" == "0" ]; then log_message "File has never been backed up: $(basename "$file")" return 0 fi if [ "$current_mod_date" -gt "$last_backup_date" ]; then log_message "File modified since last backup: $(basename "$file")" return 0 fi log_message "File unchanged since last backup: $(basename "$file")" return 1 } # Update JSON log with successful backup update_json_log() { local file="$1" local mod_date=$(stat -c %Y "$file") jq -c --arg file "$file" --argjson mod_date "$mod_date" '.[$file] = $mod_date' "${JSON_LOG_FILE}" > "${JSON_LOG_FILE}.tmp" if [ $? -eq 0 ]; then mv "${JSON_LOG_FILE}.tmp" "${JSON_LOG_FILE}" log_message "Updated backup log for: $(basename "$file")" else log_error "Failed to update JSON log file" rm -f "${JSON_LOG_FILE}.tmp" return 1 fi } # Calculate MD5 checksum calculate_checksum() { local file="$1" md5sum "$file" | cut -d' ' -f1 } # Verify file integrity after copy verify_backup() { local src="$1" local dest="$2" if [ ! -f "$dest" ]; then log_error "Backup file not found: $dest" return 1 fi local src_checksum=$(calculate_checksum "$src") local dest_checksum=$(calculate_checksum "$dest") if [ "$src_checksum" = "$dest_checksum" ]; then log_success "Backup verified: $(basename "$dest")" return 0 else log_error "Backup verification failed: $(basename "$dest")" return 1 fi } # Manage Plex service manage_plex_service() { local action="$1" log_message "Attempting to $action Plex Media Server..." if systemctl is-active --quiet plexmediaserver.service; then case "$action" in "stop") sudo systemctl stop plexmediaserver.service sleep 3 # Give it time to stop cleanly log_success "Plex Media Server stopped" ;; "start") sudo systemctl start plexmediaserver.service sleep 3 # Give it time to start log_success "Plex Media Server started" ;; esac else case "$action" in "stop") log_warning "Plex Media Server was not running" ;; "start") log_warning "Plex Media Server failed to start or was already stopped" ;; esac fi } # Check available disk space check_disk_space() { local backup_dir="$1" local required_space_mb="$2" local available_space_kb=$(df "$backup_dir" | awk 'NR==2 {print $4}') local available_space_mb=$((available_space_kb / 1024)) if [ "$available_space_mb" -lt "$required_space_mb" ]; then log_error "Insufficient disk space. Required: ${required_space_mb}MB, Available: ${available_space_mb}MB" return 1 fi log_message "Disk space check passed. Available: ${available_space_mb}MB" return 0 } # Estimate backup size estimate_backup_size() { local total_size=0 for nickname in "${!PLEX_FILES[@]}"; do local file="${PLEX_FILES[$nickname]}" if [ -f "$file" ] && needs_backup "$file"; then local size_kb=$(du -k "$file" | cut -f1) total_size=$((total_size + size_kb)) fi done echo $((total_size / 1024)) # Return size in MB } # Clean old backups cleanup_old_backups() { log_message "Cleaning up old backups..." # Remove backups older than MAX_BACKUP_AGE_DAYS find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "????????" -mtime +${MAX_BACKUP_AGE_DAYS} -exec rm -rf {} \; 2>/dev/null || true # Keep only MAX_BACKUPS_TO_KEEP most recent backups local backup_count=$(find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "????????" | wc -l) if [ "$backup_count" -gt "$MAX_BACKUPS_TO_KEEP" ]; then local excess=$((backup_count - MAX_BACKUPS_TO_KEEP)) find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "????????" -printf '%T@ %p\n' | sort -n | head -n "$excess" | cut -d' ' -f2- | xargs rm -rf log_message "Removed $excess old backup directories" fi # Clean old log files find "${LOG_ROOT}" -name "plex_backup_*.log" -mtime +${MAX_BACKUP_AGE_DAYS} -delete 2>/dev/null || true } # Create backup with verification backup_file() { local nickname="$1" local src_file="${PLEX_FILES[$nickname]}" local filename=$(basename "$src_file") local dest_file="${BACKUP_PATH}/${filename}" log_message "Backing up $nickname: $filename" # Copy with sudo if needed if sudo cp "$src_file" "$dest_file"; then # Verify the backup if verify_backup "$src_file" "$dest_file"; then update_json_log "$src_file" local size=$(du -h "$dest_file" | cut -f1) log_success "Successfully backed up $filename ($size)" return 0 else rm -f "$dest_file" return 1 fi else log_error "Failed to copy $filename" return 1 fi } # Send notification send_notification() { local files_backed_up="$1" local message="🎬 Plex backup completed successfully! 📦 $files_backed_up files backed up on $(hostname) ✅" if command -v curl >/dev/null 2>&1; then curl -s \ -H "tags:popcorn,backup,plex,$(hostname)" \ -d "$message" \ https://notify.peterwood.rocks/lab || log_warning "Failed to send notification" else log_warning "curl not available, skipping notification" fi } # Main backup function main() { log_message "Starting enhanced Plex backup process at $(date)" # Initialize initialize_json_log # Estimate backup size and check disk space local estimated_size_mb=$(estimate_backup_size) local required_space_mb=$((estimated_size_mb + 100)) # Add 100MB buffer if ! check_disk_space "$(dirname "$BACKUP_PATH")" "$required_space_mb"; then log_error "Backup aborted due to insufficient disk space" exit 1 fi # Stop Plex service manage_plex_service stop # Create backup directory mkdir -p "${BACKUP_PATH}" local files_backed_up=0 local backup_errors=0 # Backup each file for nickname in "${!PLEX_FILES[@]}"; do local file="${PLEX_FILES[$nickname]}" if [ ! -f "$file" ]; then log_warning "File not found: $file" continue fi if needs_backup "$file"; then if backup_file "$nickname"; then files_backed_up=$((files_backed_up + 1)) else backup_errors=$((backup_errors + 1)) fi fi done # Start Plex service manage_plex_service start # Create compressed archive if files were backed up if [ "$files_backed_up" -gt 0 ]; then local archive_file="${SCRIPT_DIR}/plex_backup_${CURRENT_DATE}.tar.gz" log_message "Creating compressed archive..." if tar -czf "$archive_file" -C "$BACKUP_PATH" .; then log_success "Archive created: $(basename "$archive_file")" # Verify archive if tar -tzf "$archive_file" >/dev/null 2>&1; then log_success "Archive verification passed" rm -rf "$BACKUP_PATH" log_message "Temporary backup directory removed" else log_error "Archive verification failed" backup_errors=$((backup_errors + 1)) fi else log_error "Failed to create archive" backup_errors=$((backup_errors + 1)) fi # Send notification send_notification "$files_backed_up" else log_message "No files needed backup, removing empty backup directory" rmdir "$BACKUP_PATH" 2>/dev/null || true fi # Cleanup old backups cleanup_old_backups # Final summary log_message "Backup process completed at $(date)" log_message "Files backed up: $files_backed_up" log_message "Errors encountered: $backup_errors" if [ "$backup_errors" -gt 0 ]; then log_error "Backup completed with errors" exit 1 else log_success "Backup completed successfully" fi } # Trap to ensure Plex is restarted on script exit trap 'manage_plex_service start' EXIT # Run main function main "$@"