Move all watchdog scripts to a dedicated Watchdogs/ folder: Docker_Essentials/docker_watchdog.sh → Watchdogs/ unRAID_Essentials/system_watchdog.sh → Watchdogs/ unRAID_Essentials/resource_watchdog.sh → Watchdogs/ Orchestrators/watchdog_orchestrator.sh → Watchdogs/ Tools/watchdog_skip_list_manager.sh → Watchdogs/ Rename host config files: master_host1.conf → host1.conf master_host2.conf → host2.conf Update all references across the ecosystem: master.conf: WATCHDOG_ORCHESTRATOR_SCRIPTS paths → Watchdogs/ load_config.sh: host*.conf glob + all comments git_pull_execute.sh: sparse checkout glob + all comments Partnership/ssh_setup.sh: HOST_CONF path construction user_script_plug-in.sh: all script paths + per-host conf path common.sh, README.md, README-User_Script_Plug-in.md: comment refs All Partnership, Fallback, Monitors, Transcodes, Tools scripts: comment refs
269 lines
10 KiB
Bash
269 lines
10 KiB
Bash
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ================================= Media Cleaner ==============================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Remove scene debris, tool artifacts, and unsafe files from media shares
|
|
# before the orphan scan — so arr cleanup only encounters actual media files.
|
|
# Runs second in the daily window, after permissions and before arr cleanup.
|
|
# Two profiles — anime and media — each with their own folder list and patterns.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# Profiles:
|
|
# anime — cleans ANIME_CLEAN_FOLDERS using ANIME_FILE_PATTERNS
|
|
# media — cleans MEDIA_CLEAN_FOLDERS using MEDIA_FILE_PATTERNS (adds *.iso *.lrc)
|
|
#
|
|
# Scene debris — left by scene releases and download clients:
|
|
# *.sfv *.md5 *.sha1 — checksums — useless post-download
|
|
# *.nzb *.torrent — download files left by clients
|
|
# *.url *.lnk *.info *.diz — scene metadata
|
|
# *.sample* *.proof* — scene samples — never needed in library
|
|
# *sync-conflict* — Syncthing conflict copies
|
|
# *.rar *.zip *.7z *.ace — archives — source not needed after extraction
|
|
# *.r00-*.r09 *.srr — multi-part rar segments and repair files
|
|
# *.001 *.002 *.003 — split archive parts
|
|
# *.gz *.tar *.bz2 — linux archives
|
|
#
|
|
# Tool artifacts — incomplete or stale files from download clients:
|
|
# *.!ut *.!qB — uTorrent / qBittorrent incomplete markers
|
|
# *.crdownload *.opdownload — Chrome / Opera incomplete downloads
|
|
# *.part — partial download files
|
|
#
|
|
# Unsafe files — executables that should never appear in a media folder:
|
|
# *.exe *.scr *.com — Windows executables
|
|
# *.bat *.cmd *.vbs *.ps1 — Windows scripts
|
|
# *.msi *.dll *.sys — Windows system files
|
|
# *.sh — shell scripts in media folders = suspicious
|
|
#
|
|
# Media profile also removes: *.iso *.lrc
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# acquire_lock "wait" — wait if previous run still active
|
|
# detect_hosts() — correct folder lists per host via MY_ID aliases
|
|
# Empty array guards — warns and exits cleanly if no folders or patterns configured
|
|
# Folder existence — skips missing folders with warning, continues others
|
|
# validate_unraid_cmd — notify script validated before use
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# host*.conf
|
|
#
|
|
# HOST*_ANIME_CLEAN_FOLDERS — folders cleaned by the anime profile on this host
|
|
# HOST*_MEDIA_CLEAN_FOLDERS — folders cleaned by the media profile on this host
|
|
# Aliased by detect_hosts() — script uses ANIME_CLEAN_FOLDERS / MEDIA_CLEAN_FOLDERS
|
|
#
|
|
# master.conf
|
|
#
|
|
# ANIME_FILE_PATTERNS — file patterns removed by the anime profile
|
|
# MEDIA_FILE_PATTERNS — file patterns removed by the media profile
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# media_cleaner.sh anime — clean anime shares
|
|
# media_cleaner.sh media — clean media shares
|
|
# media_cleaner.sh anime --dry-run — preview anime clean, no deletions
|
|
# media_cleaner.sh media --dry-run — preview media clean, no deletions
|
|
# media_cleaner.sh anime --log — verbose output
|
|
# media_cleaner.sh anime --status — show config and exit
|
|
#
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../load_config.sh"
|
|
|
|
# ── Separate profile argument from flags ──────────────────────────────────────────────────────
|
|
# Profile (anime|media) is a positional arg — separate before parse_args sees flags
|
|
PROFILE=""
|
|
RAW_ARGS=()
|
|
|
|
for ARG in "$@"; do
|
|
case "$ARG" in
|
|
anime|media) PROFILE="$ARG" ;;
|
|
*) RAW_ARGS+=("$ARG") ;;
|
|
esac
|
|
done
|
|
|
|
parse_args "${RAW_ARGS[@]}"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Setup ━━━
|
|
# ==============================================================================================
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
error "Must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$PROFILE" ]]; then
|
|
error "No profile specified"
|
|
error "Usage: media_cleaner.sh <anime|media> [--dry-run] [--log] [--status]"
|
|
exit 1
|
|
fi
|
|
|
|
acquire_lock "wait"
|
|
|
|
# detect_hosts() sets MY_ID and aliases HOST*_ANIME/MEDIA_CLEAN_FOLDERS
|
|
detect_hosts
|
|
|
|
validate_unraid_cmd \
|
|
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
|
|
"" "" \
|
|
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
|
|
|
|
# Resolve profile folders and patterns
|
|
case "$PROFILE" in
|
|
anime)
|
|
CLEAN_FOLDERS=("${ANIME_CLEAN_FOLDERS[@]}")
|
|
FILE_PATTERNS=("${ANIME_FILE_PATTERNS[@]}")
|
|
;;
|
|
media)
|
|
CLEAN_FOLDERS=("${MEDIA_CLEAN_FOLDERS[@]}")
|
|
FILE_PATTERNS=("${MEDIA_FILE_PATTERNS[@]}")
|
|
;;
|
|
*)
|
|
error "Unknown profile: $PROFILE — must be anime or media"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# Empty array guards
|
|
if [[ ${#CLEAN_FOLDERS[@]} -eq 0 ]]; then
|
|
warn "No folders configured for profile '$PROFILE' on $MY_ID"
|
|
warn "Check HOST*_${PROFILE^^}_CLEAN_FOLDERS in host*.conf"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ ${#FILE_PATTERNS[@]} -eq 0 ]]; then
|
|
warn "No file patterns configured for profile '$PROFILE'"
|
|
warn "Check ${PROFILE^^}_FILE_PATTERNS in master.conf"
|
|
exit 0
|
|
fi
|
|
|
|
log "Profile: $PROFILE"
|
|
log "Folders: ${#CLEAN_FOLDERS[@]}"
|
|
log "Patterns: ${#FILE_PATTERNS[@]}"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Status ━━━
|
|
# ==============================================================================================
|
|
if [[ "$SHOW_STATUS" == true ]]; then
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
|
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
|
echo "$ICON_GEAR Profile: $PROFILE"
|
|
echo "$ICON_CLEAN Folders:"
|
|
for f in "${CLEAN_FOLDERS[@]}"; do
|
|
echo " $f"
|
|
done
|
|
echo "$ICON_TRASH Patterns: ${FILE_PATTERNS[*]}"
|
|
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Media Cleaner ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_CLEAN Media Cleaner — $PROFILE — $MY_ID ━━━"
|
|
echo ""
|
|
|
|
START=$(date +%s)
|
|
TOTAL_REMOVED=0
|
|
FAILED=()
|
|
SKIPPED=()
|
|
|
|
for FOLDER in "${CLEAN_FOLDERS[@]}"; do
|
|
FOLDER_NAME=$(basename "$FOLDER")
|
|
echo "━━━ $ICON_CLEAN $FOLDER_NAME ━━━"
|
|
|
|
if [[ ! -d "$FOLDER" ]]; then
|
|
warn "$FOLDER_NAME not found — skipping"
|
|
SKIPPED+=("$FOLDER_NAME")
|
|
echo ""
|
|
continue
|
|
fi
|
|
|
|
# Build find command dynamically from FILE_PATTERNS array
|
|
CMD=(find "$FOLDER" -type f \()
|
|
for (( i = 0; i < ${#FILE_PATTERNS[@]}; i++ )); do
|
|
CMD+=(-iname "${FILE_PATTERNS[i]}")
|
|
if [[ $i -lt $(( ${#FILE_PATTERNS[@]} - 1 )) ]]; then
|
|
CMD+=(-o)
|
|
fi
|
|
done
|
|
CMD+=(\))
|
|
|
|
# Count matching files before acting
|
|
FILE_COUNT=$("${CMD[@]}" 2>/dev/null | wc -l)
|
|
|
|
if [[ "$FILE_COUNT" -eq 0 ]]; then
|
|
log "$FOLDER_NAME — clean ✅"
|
|
echo ""
|
|
continue
|
|
fi
|
|
|
|
warn "$ICON_TRASH $FILE_COUNT file(s) to remove from $FOLDER_NAME"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — files that would be deleted:"
|
|
"${CMD[@]}" 2>/dev/null | while IFS= read -r f; do
|
|
echo " $ICON_TRASH $f"
|
|
done
|
|
else
|
|
CLEAN_CMD=("${CMD[@]}" -exec rm -f {} +)
|
|
if "${CLEAN_CMD[@]}" 2>/dev/null; then
|
|
log "$FOLDER_NAME — $FILE_COUNT file(s) removed"
|
|
TOTAL_REMOVED=$(( TOTAL_REMOVED + FILE_COUNT ))
|
|
else
|
|
error "$FOLDER_NAME — cleanup failed"
|
|
FAILED+=("$FOLDER_NAME")
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
done
|
|
|
|
END=$(date +%s)
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Summary ━━━
|
|
# ==============================================================================================
|
|
echo "━━━━━ $ICON_SUMMARY MEDIA CLEANER SUMMARY ━━━━━"
|
|
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
|
echo "$ICON_GEAR Profile: $PROFILE"
|
|
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
|
[[ ${#SKIPPED[@]} -gt 0 ]] && warn "Skipped: ${SKIPPED[*]} (folders not found)"
|
|
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — no files deleted"
|
|
elif [[ ${#FAILED[@]} -gt 0 ]]; then
|
|
echo "$ICON_ERROR Status: SOME FOLDERS FAILED"
|
|
notify "Media cleaner ($PROFILE) failed on $(hostname) — ${FAILED[*]}" \
|
|
"Media Cleaner" "warning"
|
|
elif [[ "$TOTAL_REMOVED" -eq 0 ]]; then
|
|
echo "$ICON_DONE Status: clean — nothing to remove"
|
|
else
|
|
warn "$ICON_TRASH Removed: $TOTAL_REMOVED file(s)"
|
|
notify "Media cleaner ($PROFILE) on $(hostname) — $TOTAL_REMOVED file(s) removed" \
|
|
"Media Cleaner" "warning"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
[[ ${#FAILED[@]} -gt 0 ]] && exit 1
|
|
exit 0 |