diff --git a/Tools/downloaders_reset.sh b/Tools/downloaders_reset.sh new file mode 100644 index 0000000..65223e5 --- /dev/null +++ b/Tools/downloaders_reset.sh @@ -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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" \ No newline at end of file