Files
Varaverk/Docker_Essentials/downloaders_reset.sh
T

433 lines
19 KiB
Bash

#!/bin/bash
# -----------------------------------------------------------------------------------------------
# --------------------------------- Downloaders Reset ------------------------------------------
# -----------------------------------------------------------------------------------------------
# Daily maintenance reset for all download clients.
# Clears stuck states, purges old history, and prepares downloaders for a clean daily cycle.
# Run before container restarts in daily_sync_maintenance.sh via MEDIA_MANAGEMENT_JOBS.
#
# Downloaders covered:
# slskd — clears stuck/errored searches, dead transfer records,
# purges expired failed imports (albums Lidarr rejected)
# SABnzbd — clears completed and failed history older than retention period,
# removes stalled/paused queue items
# qBittorrent — last chance failsafe delete for torrents older than
# QBIT_FAILSAFE_MIN_DAYS regardless of ratio
#
# Safety:
# Always --dry-run first before scheduling
# slskd transfer cleanup skips any user with InProgress or Queued transfers
# qBittorrent only deletes if torrent age exceeds QBIT_FAILSAFE_MIN_DAYS
# SABnzbd only deletes history older than DOWNLOADER_RETENTION_DAYS
# qBittorrent deleteFiles=false — removes from qBit, leaves files for arrs to manage
#
# Configuration in Master.conf under Docker Essentials — Downloaders Reset.
# Supports --dry-run to preview without making changes.
# -----------------------------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../Master.conf"
source "$SCRIPT_DIR/../common.sh"
parse_args "$@"
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_GEAR Setup ━━━
# -----------------------------------------------------------------------------------------------
echo ""
echo "━━━ $ICON_GEAR Downloaders Reset ━━━"
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
detect_hosts
acquire_lock "wait"
START_TIME=$(date +%s)
# Select host-specific config
if [[ "$LOCAL_SERVER_NAME" == "$HOST1" ]]; then
SLSKD_URL="$HOST1_SLSKD_URL"
SLSKD_API_KEY="$HOST1_SLSKD_API_KEY"
SLSKD_FAILED_IMPORTS_DIR="$HOST1_SLSKD_FAILED_IMPORTS_DIR"
SABNZBD_URL="$HOST1_SABNZBD_URL"
SABNZBD_API_KEY="$HOST1_SABNZBD_API_KEY"
QBIT_URL="$HOST1_QBIT_URL"
QBIT_USERNAME="$HOST1_QBIT_USERNAME"
QBIT_PASSWORD="$HOST1_QBIT_PASSWORD"
else
# HOST2 placeholders — fill in when HOST2 is back online
SLSKD_URL="${HOST2_SLSKD_URL:-}"
SLSKD_API_KEY="${HOST2_SLSKD_API_KEY:-}"
SLSKD_FAILED_IMPORTS_DIR="${HOST2_SLSKD_FAILED_IMPORTS_DIR:-}"
SABNZBD_URL="${HOST2_SABNZBD_URL:-}"
SABNZBD_API_KEY="${HOST2_SABNZBD_API_KEY:-}"
QBIT_URL="${HOST2_QBIT_URL:-}"
QBIT_USERNAME="${HOST2_QBIT_USERNAME:-}"
QBIT_PASSWORD="${HOST2_QBIT_PASSWORD:-}"
fi
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
CUTOFF=$(( $(date +%s) - (DOWNLOADER_RETENTION_DAYS * 86400) ))
TOTAL_PASS=0
TOTAL_FAIL=0
# -----------------------------------------------------------------------------------------------
# ━━━ slskd — Stuck Searches ━━━
# Clears searches in Completed/Errored state left by Soularr crashes
# Prevents 409 Conflict error on next Soularr startup
# -----------------------------------------------------------------------------------------------
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
echo ""
echo "━━━ 🔍 slskd — Stuck Searches ━━━"
SEARCHES=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/searches" \
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
if [[ -z "$SEARCHES" ]]; then
warn "slskd not reachable — skipping searches"
else
IDS=$(echo "$SEARCHES" | grep -o '"id":"[^"]*","isComplete":true' | \
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
COUNT=$(echo "$IDS" | grep -c . 2>/dev/null || echo 0)
COUNT="${COUNT//[^0-9]/}"; COUNT="${COUNT:-0}"
if [[ "$COUNT" -eq 0 ]]; then
success "No stuck searches found ✅"
else
info "Found $COUNT stuck search(es)"
SUCCESS=0; FAIL=0
while IFS= read -r ID; do
[[ -z "$ID" ]] && continue
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would delete search: $ID"
((SUCCESS++))
continue
fi
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
"$SLSKD_URL/api/v0/searches/$ID" \
-H "X-Api-Key: $SLSKD_API_KEY")
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
info "$ICON_TRASH Cleared search: $ID"
((SUCCESS++))
else
error "Failed: $ID (HTTP $RESULT)"
((FAIL++))
fi
done <<< "$IDS"
success "Searches: $SUCCESS cleared, $FAIL failed"
(( TOTAL_FAIL += FAIL ))
(( TOTAL_PASS += SUCCESS ))
fi
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ slskd — Dead Transfer Records ━━━
# Removes completed/errored/aborted transfer records per user
# Prevents Soularr 404 loop when polling a user whose transfer no longer exists
# NEVER removes transfers that are InProgress or Queued
# -----------------------------------------------------------------------------------------------
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
echo ""
echo "━━━ 🔍 slskd — Dead Transfer Records ━━━"
TRANSFERS=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/transfers/downloads" \
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
if [[ -z "$TRANSFERS" ]]; then
warn "slskd not reachable — skipping transfers"
else
USERNAMES=$(echo "$TRANSFERS" | grep -o '"username":"[^"]*"' | \
sed 's/"username":"//;s/"//' | sort -u)
if [[ -z "$USERNAMES" ]]; then
success "No transfer records found ✅"
else
SUCCESS=0; SKIPPED=0; FAIL=0
while IFS= read -r USER; do
[[ -z "$USER" ]] && continue
ACTIVE=$(echo "$TRANSFERS" | grep -o "\"username\":\"$USER\"[^}]*\"state\":\"[^\"]*\"" | \
grep -c "InProgress\|Queued")
if [[ "$ACTIVE" -gt 0 ]]; then
info "$ICON_SKIP Skipping $USER$ACTIVE active/queued transfer(s)"
((SKIPPED++))
continue
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would clear transfers for: $USER"
((SUCCESS++))
continue
fi
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
"$SLSKD_URL/api/v0/transfers/downloads/$USER" \
-H "X-Api-Key: $SLSKD_API_KEY")
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
info "$ICON_TRASH Cleared transfers for: $USER"
((SUCCESS++))
else
error "Failed to clear: $USER (HTTP $RESULT)"
((FAIL++))
fi
done <<< "$USERNAMES"
success "Transfers: $SUCCESS cleared, $SKIPPED skipped (active), $FAIL failed"
(( TOTAL_FAIL += FAIL ))
(( TOTAL_PASS += SUCCESS ))
fi
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ slskd — Purge Expired Failed Imports ━━━
# Removes albums Soularr downloaded but Lidarr rejected
# Soularr moves these to failed_imports/ and never cleans them up
# -----------------------------------------------------------------------------------------------
if [[ -n "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
echo ""
echo "━━━ 🔍 slskd — Failed Imports (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
if [[ ! -d "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
warn "Directory not found: $SLSKD_FAILED_IMPORTS_DIR — skipping"
else
OLD_IMPORTS=$(find "$SLSKD_FAILED_IMPORTS_DIR" \
-mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}")
IMPORT_COUNT=$(echo "$OLD_IMPORTS" | grep -c . 2>/dev/null || echo 0)
IMPORT_COUNT="${IMPORT_COUNT//[^0-9]/}"; IMPORT_COUNT="${IMPORT_COUNT:-0}"
if [[ "$IMPORT_COUNT" -eq 0 ]]; then
success "No expired failed imports found ✅"
else
info "Found $IMPORT_COUNT expired failed import(s)"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would delete:"
echo "$OLD_IMPORTS"
else
find "$SLSKD_FAILED_IMPORTS_DIR" \
-mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}" \
-exec rm -rf {} \;
success "$ICON_TRASH Purged $IMPORT_COUNT expired failed import(s)"
(( TOTAL_PASS += IMPORT_COUNT ))
fi
fi
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ SABnzbd — Clear Completed History ━━━
# Removes completed download history older than DOWNLOADER_RETENTION_DAYS
# -----------------------------------------------------------------------------------------------
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
echo ""
echo "━━━ 🔍 SABnzbd — Completed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
HISTORY=$(curl -sf --max-time 15 \
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&apikey=$SABNZBD_API_KEY" 2>/dev/null)
if [[ -z "$HISTORY" ]]; then
warn "SABnzbd not reachable — skipping completed history"
else
COMPLETED_IDS=$(echo "$HISTORY" | grep -o '"nzo_id":"[^"]*"' | \
sed 's/"nzo_id":"//;s/"//')
if [[ -z "$COMPLETED_IDS" ]]; then
success "No completed history found ✅"
else
DELETED=0; SKIPPED=0
while IFS= read -r NZO_ID; do
[[ -z "$NZO_ID" ]] && continue
JOB_TIME=$(echo "$HISTORY" | grep -A5 "$NZO_ID" | \
grep -o '"completed":[0-9]*' | grep -o '[0-9]*')
[[ -z "$JOB_TIME" ]] && continue
[[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would delete completed job: $NZO_ID"
((DELETED++))
else
curl -sf --max-time 10 \
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
>/dev/null
info "$ICON_TRASH Deleted: $NZO_ID"
((DELETED++))
fi
done <<< "$COMPLETED_IDS"
success "Completed: $DELETED deleted, $SKIPPED within retention"
(( TOTAL_PASS += DELETED ))
fi
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ SABnzbd — Clear Failed History ━━━
# Removes failed download history older than DOWNLOADER_RETENTION_DAYS
# -----------------------------------------------------------------------------------------------
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
echo ""
echo "━━━ 🔍 SABnzbd — Failed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
FAILED_HIST=$(curl -sf --max-time 15 \
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&failed_only=1&apikey=$SABNZBD_API_KEY" 2>/dev/null)
if [[ -z "$FAILED_HIST" ]]; then
warn "SABnzbd not reachable — skipping failed history"
else
FAILED_IDS=$(echo "$FAILED_HIST" | grep -o '"nzo_id":"[^"]*"' | \
sed 's/"nzo_id":"//;s/"//')
if [[ -z "$FAILED_IDS" ]]; then
success "No failed history found ✅"
else
DELETED=0; SKIPPED=0
while IFS= read -r NZO_ID; do
[[ -z "$NZO_ID" ]] && continue
JOB_TIME=$(echo "$FAILED_HIST" | grep -A5 "$NZO_ID" | \
grep -o '"completed":[0-9]*' | grep -o '[0-9]*')
[[ -z "$JOB_TIME" ]] && continue
[[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would delete failed job: $NZO_ID"
((DELETED++))
else
curl -sf --max-time 10 \
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
>/dev/null
info "$ICON_TRASH Deleted: $NZO_ID"
((DELETED++))
fi
done <<< "$FAILED_IDS"
success "Failed: $DELETED deleted, $SKIPPED within retention"
(( TOTAL_PASS += DELETED ))
fi
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ SABnzbd — Remove Stalled Queue Items ━━━
# Removes paused queue items no longer progressing
# Active downloading items are never touched
# -----------------------------------------------------------------------------------------------
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
echo ""
echo "━━━ 🔍 SABnzbd — Stalled Queue Items ━━━"
QUEUE=$(curl -sf --max-time 10 \
"$SABNZBD_URL/api?mode=queue&output=json&apikey=$SABNZBD_API_KEY" 2>/dev/null)
if [[ -z "$QUEUE" ]]; then
warn "SABnzbd not reachable — skipping queue"
else
STALLED_IDS=$(echo "$QUEUE" | grep -o '"nzo_id":"[^"]*"' | \
sed 's/"nzo_id":"//;s/"//')
if [[ -z "$STALLED_IDS" ]]; then
success "No stalled queue items found ✅"
else
DELETED=0; SKIPPED=0
while IFS= read -r NZO_ID; do
[[ -z "$NZO_ID" ]] && continue
STATUS=$(echo "$QUEUE" | grep -A10 "$NZO_ID" | \
grep -o '"status":"[^"]*"' | sed 's/"status":"//;s/"//')
[[ "$STATUS" != "Paused" ]] && ((SKIPPED++)) && continue
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would remove stalled item: $NZO_ID"
((DELETED++))
else
curl -sf --max-time 10 \
"$SABNZBD_URL/api?mode=queue&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
>/dev/null
info "$ICON_TRASH Removed stalled: $NZO_ID"
((DELETED++))
fi
done <<< "$STALLED_IDS"
success "Queue: $DELETED removed, $SKIPPED active (skipped)"
(( TOTAL_PASS += DELETED ))
fi
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ qBittorrent — Last Chance Failsafe Cleanup ━━━
# Deletes torrents older than QBIT_FAILSAFE_MIN_DAYS
# deleteFiles=false — removes from qBit, leaves files for arrs to manage
# -----------------------------------------------------------------------------------------------
if [[ -n "$QBIT_URL" ]] && [[ -n "$QBIT_USERNAME" ]]; then
echo ""
echo "━━━ 🔍 qBittorrent — Failsafe (older than ${QBIT_FAILSAFE_MIN_DAYS} days) ━━━"
[[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]] && \
info "Ratio requirement: >= ${QBIT_FAILSAFE_MIN_RATIO}"
QBIT_COOKIE=$(curl -sf --max-time 10 -c - \
"$QBIT_URL/api/v2/auth/login" \
--data "username=$QBIT_USERNAME&password=$QBIT_PASSWORD" 2>/dev/null | \
grep SID | awk '{print "SID="$NF}')
if [[ -z "$QBIT_COOKIE" ]]; then
error "Failed to authenticate with qBittorrent"
((TOTAL_FAIL++))
else
TORRENTS=$(curl -sf --max-time 15 \
"$QBIT_URL/api/v2/torrents/info" \
-H "Cookie: $QBIT_COOKIE" 2>/dev/null)
NOW=$(date +%s)
DELETED=0; SKIPPED=0
while read -r TORRENT; do
[[ -z "$TORRENT" ]] && continue
HASH=$(echo "$TORRENT" | grep -o '"hash":"[^"]*"' | sed 's/"hash":"//;s/"//')
NAME=$(echo "$TORRENT" | grep -o '"name":"[^"]*"' | sed 's/"name":"//;s/"//')
ADDED=$(echo "$TORRENT" | grep -o '"added_on":[0-9]*' | grep -o '[0-9]*')
RATIO=$(echo "$TORRENT" | grep -o '"ratio":[0-9.]*' | grep -o '[0-9.]*')
[[ -z "$HASH" || -z "$ADDED" ]] && continue
AGE_DAYS=$(( (NOW - ADDED) / 86400 ))
[[ "$AGE_DAYS" -lt "$QBIT_FAILSAFE_MIN_DAYS" ]] && ((SKIPPED++)) && continue
if [[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]]; then
RATIO_INT="${RATIO%.*}"
MIN_RATIO_INT="${QBIT_FAILSAFE_MIN_RATIO%.*}"
[[ "$RATIO_INT" -lt "$MIN_RATIO_INT" ]] && ((SKIPPED++)) && continue
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would delete: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
((DELETED++))
else
curl -sf --max-time 10 -X POST \
"$QBIT_URL/api/v2/torrents/delete" \
-H "Cookie: $QBIT_COOKIE" \
--data "hashes=$HASH&deleteFiles=false" >/dev/null
info "$ICON_TRASH Deleted: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
((DELETED++))
fi
done < <(echo "$TORRENTS" | tr '}' '\n')
success "qBittorrent: $DELETED deleted, $SKIPPED skipped (under threshold)"
(( TOTAL_PASS += DELETED ))
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_SUMMARY Summary ━━━
# -----------------------------------------------------------------------------------------------
echo ""
echo "━━━━━ $ICON_SUMMARY DOWNLOADERS RESET SUMMARY ━━━━━"
echo "$ICON_TIME Duration: $(format_duration $(( $(date +%s) - START_TIME )))"
echo "$ICON_SUCCESS Actions: $TOTAL_PASS"
echo "$ICON_ERROR Failures: $TOTAL_FAIL"
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo "$ICON_WARN Status: DRY RUN — no changes made"
elif [[ "$TOTAL_FAIL" -gt 0 ]]; then
echo "$ICON_ERROR Status: $TOTAL_FAIL failure(s) — check logs"
notify "Downloaders reset completed with failures on $(hostname)" "Downloaders Reset" "warning"
exit 1
else
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"