added and testin a downloar reset script

This commit is contained in:
2026-04-26 12:11:24 -04:00
parent a638035a2f
commit 516f6996bc
+425
View File
@@ -0,0 +1,425 @@
#!/bin/bash
# ==============================================================================================
# downloaders_reset.sh
# ==============================================================================================
# PURPOSE: 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 maintenance.
#
# DOWNLOADERS COVERED:
# slskd — clears stuck/errored searches, dead transfer records,
# purges expired failed imports
# 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
#
# USAGE:
# bash downloaders_reset.sh # live run
# bash downloaders_reset.sh --dry-run # preview only, no changes made
#
# SCHEDULE: Run before downloader restarts in daily_sync_maintenance.sh
# Ensures all downloaders start each day in a clean known state
#
# 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 RETENTION_DAYS
#
# TO ADD A NEW DOWNLOADER:
# Add URL, API key, and any specific variables in Configuration section
# Add a new section block following the same pattern as existing downloaders
# ==============================================================================================
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ Configuration ━━━
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Retention period in days — applies to ALL cleanup sections:
# slskd failed imports, SABnzbd completed history, SABnzbd failed history
RETENTION_DAYS=7
# ━━━ slskd ━━━
SLSKD_URL="http://localhost:8980"
SLSKD_API_KEY="4bF9kL2mNpQrT7vWxYz1A3dEgHjKoRsU"
# Path to Soularr failed imports folder
# Created by Soularr when Lidarr rejects a downloaded album
SLSKD_FAILED_IMPORTS_DIR="/mnt/user/Temp_Storage/Slskd/completed/failed_imports"
# ━━━ SABnzbd ━━━
SABNZBD_URL="http://localhost:8180"
SABNZBD_API_KEY="8bfefe41d83b4d50883e32859b55ca9a"
# ━━━ qBittorrent ━━━
QBIT_URL="http://localhost:8080"
QBIT_USERNAME="admin"
QBIT_PASSWORD="changeme"
# Last chance failsafe — deletes torrents older than this many days
# regardless of ratio. qBittorrent's own rules handle normal cleanup
# (ratio >= 1.25 OR 45 days inactive). This catches anything missed
# after an extended period. Set ratio gate to 0 to disable ratio check
# and use age as the only condition.
QBIT_FAILSAFE_MIN_DAYS=180
QBIT_FAILSAFE_MIN_RATIO=0
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRY_RUN=false
[[ "$1" == "--dry-run" ]] && DRY_RUN=true
$DRY_RUN && echo "🧪 DRY RUN MODE — no changes will be made"
echo ""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ slskd — Clear Stuck Searches ━━━
# Clears searches in Completed/Errored state left behind by Soularr crashes.
# Prevents 409 Conflict error on next Soularr startup.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 slskd — Stuck Searches"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
SEARCHES=$(curl -s -X GET "$SLSKD_URL/api/v0/searches" \
-H "X-Api-Key: $SLSKD_API_KEY")
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)
if [[ -z "$IDS" || "$COUNT" -eq 0 ]]; then
echo "✅ No stuck searches found"
else
echo "📋 Found $COUNT stuck search(es)"
if $DRY_RUN; then
echo "🧪 Would delete search IDs:"
echo "$IDS"
else
SUCCESS=0
FAIL=0
while IFS= read -r ID; do
[[ -z "$ID" ]] && continue
RESULT=$(curl -s -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
echo " 🗑️ Cleared search: $ID"
((SUCCESS++))
else
echo " ❌ Failed: $ID (HTTP $RESULT)"
((FAIL++))
fi
done <<< "$IDS"
echo "✅ Searches: $SUCCESS cleared, $FAIL failed"
fi
fi
echo ""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ slskd — Clear Dead Transfer Records ━━━
# Removes completed/errored/aborted transfer records per user.
# Prevents Soularr 404 loop when polling a user whose transfer
# record no longer exists after a crash or disconnect.
# NEVER removes transfers that are InProgress or Queued.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 slskd — Dead Transfer Records"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
TRANSFERS=$(curl -s -X GET "$SLSKD_URL/api/v0/transfers/downloads" \
-H "X-Api-Key: $SLSKD_API_KEY")
USERNAMES=$(echo "$TRANSFERS" | grep -o '"username":"[^"]*"' | \
sed 's/"username":"//;s/"//' | sort -u)
if [[ -z "$USERNAMES" ]]; then
echo "✅ No transfer records found"
else
SUCCESS=0
SKIPPED=0
FAIL=0
while IFS= read -r USER; do
[[ -z "$USER" ]] && continue
USER_STATES=$(echo "$TRANSFERS" | grep -o \
"\"username\":\"$USER\"[^}]*\"state\":\"[^\"]*\"" | \
grep -o '"state":"[^"]*"')
ACTIVE=$(echo "$USER_STATES" | grep -c "InProgress\|Queued")
if [[ "$ACTIVE" -gt 0 ]]; then
echo " ⏳ Skipping $USER$ACTIVE active/queued transfer(s)"
((SKIPPED++))
continue
fi
if $DRY_RUN; then
echo "🧪 Would clear transfers for: $USER"
((SUCCESS++))
continue
fi
RESULT=$(curl -s -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
echo " 🗑️ Cleared transfers for: $USER"
((SUCCESS++))
else
echo " ❌ Failed to clear: $USER (HTTP $RESULT)"
((FAIL++))
fi
done <<< "$USERNAMES"
echo "✅ Transfers: $SUCCESS cleared, $SKIPPED skipped (active), $FAIL failed"
fi
echo ""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ slskd — Purge Expired Failed Imports ━━━
# Removes albums Soularr downloaded but Lidarr rejected.
# Soularr moves these to failed_imports/ and never cleans them up.
# Retention controlled by RETENTION_DAYS variable.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 slskd — Failed Imports (older than ${RETENTION_DAYS} days)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ ! -d "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
echo "⚠️ Directory not found: $SLSKD_FAILED_IMPORTS_DIR"
else
OLD_IMPORTS=$(find "$SLSKD_FAILED_IMPORTS_DIR" \
-mindepth 1 -maxdepth 1 -mtime +${RETENTION_DAYS})
IMPORT_COUNT=$(echo "$OLD_IMPORTS" | grep -c . 2>/dev/null || echo 0)
if [[ -z "$OLD_IMPORTS" || "$IMPORT_COUNT" -eq 0 ]]; then
echo "✅ No expired failed imports found"
else
echo "📋 Found $IMPORT_COUNT expired failed import(s)"
if $DRY_RUN; then
echo "🧪 Would delete:"
echo "$OLD_IMPORTS"
else
find "$SLSKD_FAILED_IMPORTS_DIR" \
-mindepth 1 -maxdepth 1 -mtime +${RETENTION_DAYS} \
-exec rm -rf {} \;
echo "🗑️ Purged $IMPORT_COUNT expired failed import(s)"
fi
fi
fi
echo ""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ SABnzbd — Clear Completed History ━━━
# Removes completed download history older than RETENTION_DAYS.
# Keeps history clean without losing recent job records.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 SABnzbd — Completed History (older than ${RETENTION_DAYS} days)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
CUTOFF=$(( $(date +%s) - (RETENTION_DAYS * 86400) ))
HISTORY=$(curl -s \
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&apikey=$SABNZBD_API_KEY")
COMPLETED_IDS=$(echo "$HISTORY" | grep -o '"nzo_id":"[^"]*"' | \
sed 's/"nzo_id":"//;s/"//')
if [[ -z "$COMPLETED_IDS" ]]; then
echo "✅ 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; then
echo "🧪 Would delete completed job: $NZO_ID"
((DELETED++))
else
curl -s \
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
> /dev/null
echo " 🗑️ Deleted: $NZO_ID"
((DELETED++))
fi
done <<< "$COMPLETED_IDS"
echo "✅ Completed: $DELETED deleted, $SKIPPED within retention period"
fi
echo ""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ SABnzbd — Clear Failed History ━━━
# Removes failed download history older than RETENTION_DAYS.
# Failed jobs older than retention period serve no purpose.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 SABnzbd — Failed History (older than ${RETENTION_DAYS} days)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
FAILED=$(curl -s \
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&failed_only=1&apikey=$SABNZBD_API_KEY")
FAILED_IDS=$(echo "$FAILED" | grep -o '"nzo_id":"[^"]*"' | \
sed 's/"nzo_id":"//;s/"//')
if [[ -z "$FAILED_IDS" ]]; then
echo "✅ No failed history found"
else
DELETED=0
SKIPPED=0
while IFS= read -r NZO_ID; do
[[ -z "$NZO_ID" ]] && continue
JOB_TIME=$(echo "$FAILED" | 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; then
echo "🧪 Would delete failed job: $NZO_ID"
((DELETED++))
else
curl -s \
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
> /dev/null
echo " 🗑️ Deleted: $NZO_ID"
((DELETED++))
fi
done <<< "$FAILED_IDS"
echo "✅ Failed: $DELETED deleted, $SKIPPED within retention period"
fi
echo ""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ SABnzbd — Remove Stalled Queue Items ━━━
# Removes paused queue items that are no longer progressing.
# Active downloading items are never touched.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 SABnzbd — Stalled Queue Items"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
QUEUE=$(curl -s \
"$SABNZBD_URL/api?mode=queue&output=json&apikey=$SABNZBD_API_KEY")
STALLED_IDS=$(echo "$QUEUE" | grep -o '"nzo_id":"[^"]*"' | \
sed 's/"nzo_id":"//;s/"//')
if [[ -z "$STALLED_IDS" ]]; then
echo "✅ 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; then
echo "🧪 Would remove stalled item: $NZO_ID"
((DELETED++))
else
curl -s \
"$SABNZBD_URL/api?mode=queue&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
> /dev/null
echo " 🗑️ Removed: $NZO_ID"
((DELETED++))
fi
done <<< "$STALLED_IDS"
echo "✅ Queue: $DELETED removed, $SKIPPED active (skipped)"
fi
echo ""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━ qBittorrent — Last Chance Failsafe Cleanup ━━━
# Deletes torrents older than QBIT_FAILSAFE_MIN_DAYS.
# qBittorrent's own rules handle normal cleanup (ratio >= 1.25 OR 45 days
# inactive). This catches anything missed after extended time.
# QBIT_FAILSAFE_MIN_RATIO=0 disables ratio gate — age is the only condition.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 qBittorrent — Failsafe Cleanup"
echo " Age > ${QBIT_FAILSAFE_MIN_DAYS} days"
[[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]] && \
echo " Ratio >= ${QBIT_FAILSAFE_MIN_RATIO}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Login to qBittorrent and get session cookie
QBIT_COOKIE=$(curl -s -c - \
"$QBIT_URL/api/v2/auth/login" \
--data "username=$QBIT_USERNAME&password=$QBIT_PASSWORD" | \
grep SID | awk '{print "SID="$NF}')
if [[ -z "$QBIT_COOKIE" ]]; then
echo "❌ Failed to authenticate with qBittorrent — check URL, username, password"
else
TORRENTS=$(curl -s \
"$QBIT_URL/api/v2/torrents/info" \
-H "Cookie: $QBIT_COOKIE")
HASHES=$(echo "$TORRENTS" | grep -o '"hash":"[^"]*"' | \
sed 's/"hash":"//;s/"//')
NOW=$(date +%s)
CUTOFF_AGE=$(( NOW - (QBIT_FAILSAFE_MIN_DAYS * 86400) ))
DELETED=0
SKIPPED=0
while IFS= read -r HASH; do
[[ -z "$HASH" ]] && continue
# Get added_on timestamp and ratio for this torrent
ADDED=$(echo "$TORRENTS" | grep -A30 "\"hash\":\"$HASH\"" | \
grep -o '"added_on":[0-9]*' | grep -o '[0-9]*' | head -1)
RATIO=$(echo "$TORRENTS" | grep -A30 "\"hash\":\"$HASH\"" | \
grep -o '"ratio":[0-9.]*' | grep -o '[0-9.]*' | head -1)
NAME=$(echo "$TORRENTS" | grep -A30 "\"hash\":\"$HASH\"" | \
grep -o '"name":"[^"]*"' | sed 's/"name":"//;s/"//' | head -1)
[[ -z "$ADDED" ]] && continue
# Check age gate
[[ "$ADDED" -gt "$CUTOFF_AGE" ]] && ((SKIPPED++)) && continue
# Check ratio gate if enabled
if [[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]]; then
RATIO_OK=$(echo "$RATIO >= $QBIT_FAILSAFE_MIN_RATIO" | bc -l 2>/dev/null)
[[ "$RATIO_OK" != "1" ]] && ((SKIPPED++)) && continue
fi
AGE_DAYS=$(( (NOW - ADDED) / 86400 ))
if $DRY_RUN; then
echo "🧪 Would delete: $NAME (${AGE_DAYS} days old, ratio: $RATIO)"
((DELETED++))
else
curl -s -X POST \
"$QBIT_URL/api/v2/torrents/delete" \
-H "Cookie: $QBIT_COOKIE" \
--data "hashes=$HASH&deleteFiles=false"
echo " 🗑️ Deleted: $NAME (${AGE_DAYS} days old, ratio: $RATIO)"
((DELETED++))
fi
done <<< "$HASHES"
echo "✅ qBittorrent: $DELETED deleted, $SKIPPED skipped"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Downloaders reset complete"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"