498 lines
24 KiB
Bash
498 lines
24 KiB
Bash
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ================================= Downloaders Reset ==========================================
|
|
# ==============================================================================================
|
|
# Maintenance reset for all download clients on this server.
|
|
# Called every 15 minutes by critical_sync_maintenance.sh via CRITICAL_MAINTENANCE_SCRIPTS.
|
|
# Can also be run manually for ad hoc cleanup.
|
|
#
|
|
# ── DOWNLOADERS COVERED ───────────────────────────────────────────────────────────────────────
|
|
# slskd
|
|
# Stuck searches — clears Completed/Errored searches left by Soularr crashes
|
|
# prevents 409 Conflict on next Soularr startup
|
|
# Dead transfers — removes completed/errored/aborted transfer records per user
|
|
# prevents Soularr 404 loop when polling a user whose transfer is gone
|
|
# NEVER removes InProgress or Queued transfers
|
|
# Failed imports — purges albums Soularr downloaded but Lidarr rejected
|
|
# Soularr moves these to failed_imports/ and never cleans them up
|
|
#
|
|
# SABnzbd
|
|
# Completed history — removes completed download records older than DOWNLOADER_RETENTION_DAYS
|
|
# Failed history — removes failed download records older than DOWNLOADER_RETENTION_DAYS
|
|
# Stalled queue — removes Paused or Stuck queue items no longer progressing
|
|
# active downloading items are never touched
|
|
#
|
|
# qBittorrent
|
|
# Age failsafe — removes torrents older than QBIT_FAILSAFE_MIN_DAYS
|
|
# deleteFiles=false — removes from qBit, leaves files for arrs to manage
|
|
# optional ratio requirement via QBIT_FAILSAFE_MIN_RATIO
|
|
#
|
|
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
|
|
# detect_hosts() sets MY_ID and aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars.
|
|
# If a downloader URL is empty for this host — that section is skipped with a clear message.
|
|
# HOST2 currently has no downloaders configured — all sections skip cleanly on HOST2.
|
|
#
|
|
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
|
|
# slskd — skips users with InProgress or Queued transfers — never interrupts active downloads
|
|
# SABnzbd — age check before deletion — only removes items past retention threshold
|
|
# qBittorrent — age + optional ratio check — failsafe only removes old completed torrents
|
|
# All sections — skip gracefully if downloader is unreachable, no fatal exit
|
|
# acquire_lock "wait" — if previous run still active, waits briefly then exits cleanly
|
|
#
|
|
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
|
|
# HOST*_SLSKD_URL / HOST*_SLSKD_API_KEY / HOST*_SLSKD_FAILED_IMPORTS_DIR
|
|
# HOST*_SABNZBD_URL / HOST*_SABNZBD_API_KEY
|
|
# HOST*_QBIT_URL / HOST*_QBIT_USERNAME / HOST*_QBIT_PASSWORD
|
|
#
|
|
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
|
|
# DOWNLOADER_RETENTION_DAYS — days before history entries are purged
|
|
# QBIT_FAILSAFE_MIN_DAYS — minimum torrent age before failsafe deletion
|
|
# QBIT_FAILSAFE_MIN_RATIO — minimum ratio requirement (0 = age only)
|
|
#
|
|
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
|
|
# downloaders_reset.sh — normal reset
|
|
# downloaders_reset.sh --dry-run — preview without making changes
|
|
# downloaders_reset.sh --log — verbose output
|
|
# downloaders_reset.sh --status — show config and exit
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../load_config.sh"
|
|
|
|
parse_args "$@"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Setup ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Downloaders Reset ━━━"
|
|
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
error "Must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
# Lock first — wait mode since this runs every 15min and previous may still be finishing
|
|
acquire_lock "wait"
|
|
|
|
# detect_hosts() sets MY_ID and aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
|
|
detect_hosts
|
|
|
|
START_TIME=$(date +%s)
|
|
CUTOFF=$(( $(date +%s) - (DOWNLOADER_RETENTION_DAYS * 86400) ))
|
|
TOTAL_PASS=0
|
|
TOTAL_FAIL=0
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Status ━━━
|
|
# ==============================================================================================
|
|
if [[ "$SHOW_STATUS" == true ]]; then
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
|
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
|
echo "$ICON_GEAR slskd: ${SLSKD_URL:-not configured}"
|
|
echo "$ICON_GEAR SABnzbd: ${SABNZBD_URL:-not configured}"
|
|
echo "$ICON_GEAR qBittorrent: ${QBIT_URL:-not configured}"
|
|
echo "$ICON_TIME Retention: ${DOWNLOADER_RETENTION_DAYS} days"
|
|
echo "$ICON_GEAR qBit age: ${QBIT_FAILSAFE_MIN_DAYS} days"
|
|
echo "$ICON_GEAR qBit ratio: ${QBIT_FAILSAFE_MIN_RATIO} (0=age only)"
|
|
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
|
|
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
|
|
|
# Log which downloaders are active on this host
|
|
if [[ -z "$SLSKD_URL" ]] && [[ -z "$SABNZBD_URL" ]] && [[ -z "$QBIT_URL" ]]; then
|
|
warn "No downloaders configured for $MY_ID — nothing to reset"
|
|
exit 0
|
|
fi
|
|
[[ -n "$SLSKD_URL" ]] && log "slskd active on $MY_ID"
|
|
[[ -n "$SABNZBD_URL" ]] && log "SABnzbd active on $MY_ID"
|
|
[[ -n "$QBIT_URL" ]] && log "qBittorrent active on $MY_ID"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ slskd — Stuck Searches ━━━
|
|
# ==============================================================================================
|
|
# Clears searches in Completed/Errored state left by Soularr crashes.
|
|
# Prevents 409 Conflict error on next Soularr startup when it tries to
|
|
# create a search with the same ID that already exists in a terminal state.
|
|
|
|
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.
|
|
# Safety: NEVER removes transfers that are InProgress or Queued — active downloads protected.
|
|
|
|
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
|
|
# Skip users with any active or queued transfers — never interrupt downloads
|
|
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 rejected albums to failed_imports/ and never cleans them up.
|
|
# Purges directories older than DOWNLOADER_RETENTION_DAYS to prevent unbounded growth.
|
|
|
|
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.
|
|
# Keeps recent history for reference — only purges what's past the retention window.
|
|
|
|
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.
|
|
# Failed history is kept briefly for diagnosis but purged after the retention window.
|
|
|
|
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 queue items in Paused or Stuck state that are no longer progressing.
|
|
# Active downloading items (Downloading, Grabbing) are never touched.
|
|
# Paused items may be intentional pauses — but in an automated environment
|
|
# a Paused item sitting in the queue indefinitely is effectively stalled.
|
|
|
|
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/"//')
|
|
# Only remove Paused or Stuck items — Downloading/Grabbing are active
|
|
if [[ "$STATUS" != "Paused" ]] && [[ "$STATUS" != "Stuck" ]]; then
|
|
((SKIPPED++))
|
|
continue
|
|
fi
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would remove stalled item: $NZO_ID ($STATUS)"
|
|
((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 ($STATUS): $NZO_ID"
|
|
((DELETED++))
|
|
fi
|
|
done <<< "$STALLED_IDS"
|
|
success "Queue: $DELETED removed, $SKIPPED active (skipped)"
|
|
(( TOTAL_PASS += DELETED ))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ==============================================================================================
|
|
# ━━━ qBittorrent — Age Failsafe Cleanup ━━━
|
|
# ==============================================================================================
|
|
# Last-chance cleanup for torrents that have been sitting in qBit past their useful life.
|
|
# deleteFiles=false — removes the torrent record from qBit but leaves files on disk.
|
|
# Radarr/Sonarr manage actual files independently — this only cleans up the qBit entry.
|
|
#
|
|
# Safety checks before deletion:
|
|
# Age must exceed QBIT_FAILSAFE_MIN_DAYS
|
|
# Ratio must meet QBIT_FAILSAFE_MIN_RATIO (0 = age only, no ratio requirement)
|
|
|
|
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 — check QBIT_USERNAME/PASSWORD"
|
|
notify "qBittorrent auth failed on $(hostname) — check credentials in master_host*.conf" "Downloaders Reset" "warning"
|
|
((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 check — must be old enough
|
|
[[ "$AGE_DAYS" -lt "$QBIT_FAILSAFE_MIN_DAYS" ]] && ((SKIPPED++)) && continue
|
|
|
|
# Ratio check — if configured
|
|
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
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Summary ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY DOWNLOADERS RESET SUMMARY ━━━━━"
|
|
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
|
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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |