fix: repair slskd search and transfer cleanup in downloaders_reset

Searches: grep was potentially matching nested IDs from search result
objects. Switch to splitting JSON at { boundaries and requiring
"searchText" presence — only top-level search objects have that field.

Transfers: DELETE /api/v0/transfers/downloads/{username} returns 405
(endpoint does not exist). Replace with per-user GET then per-file
DELETE /api/v0/transfers/downloads/{username}/{id}, using the same
{-split + state grep approach to extract terminal-state file IDs.
This commit is contained in:
Gmer4Lfe
2026-05-19 15:48:47 -04:00
parent aa6d165725
commit 5cb16d4b18
+129 -41
View File
@@ -2,11 +2,21 @@
# ==============================================================================================
# ================================= 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 ───────────────────────────────────────────────────────────────────────
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Maintenance reset for all download clients on this server. Clears accumulated
# state that download clients generate but never clean up themselves — stuck
# searches, dead transfers, failed imports, stale queue entries, completed history.
#
# Called every 15 minutes by critical_sync_maintenance.sh via
# CRITICAL_MAINTENANCE_SCRIPTS. Can also be run manually for ad hoc cleanup.
# If a downloader is not configured for this host, that section skips cleanly.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# slskd
# Stuck searches — clears Completed/Errored searches left by Soularr crashes
# prevents 409 Conflict on next Soularr startup
@@ -27,33 +37,85 @@
# 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.
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# ── 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
# Never Interrupt Active Downloads
# Each downloader section checks for active state before any removal. slskd
# skips users with InProgress or Queued transfers. SABnzbd only removes items
# past the retention threshold. qBittorrent applies minimum age and optional
# ratio requirements. In-progress work is never touched.
#
# Graceful Skip on Unavailability
# If a downloader's URL is empty or the service is unreachable, that section
# skips cleanly with a log message. The script never exits fatally on a single
# unreachable downloader — the others still run.
#
# Host-Aware Configuration
# detect_hosts() aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
# to their unprefixed names. Downloaders not configured for this host are absent
# from the aliased vars and skip automatically.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Active Transfer Protection
# slskd: skips users with InProgress or Queued transfers before any removal.
# SABnzbd: age threshold enforced before deletion.
# qBittorrent: minimum age plus optional ratio gate before failsafe removal.
#
# Reachability Check
# Each section validates its downloader URL before API calls. Missing or
# unreachable downloaders skip without affecting other sections.
#
# Lock Acquisition
# acquire_lock "wait" — waits for previous run to finish since this runs every
# 15 minutes and prior execution may still be completing.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# HOST*_SLSKD_URL / HOST*_SLSKD_API_KEY / HOST*_SLSKD_FAILED_IMPORTS_DIR
# slskd connection and failed imports path. Aliased by detect_hosts()
#
# HOST*_SABNZBD_URL / HOST*_SABNZBD_API_KEY
# SABnzbd connection details. Aliased by detect_hosts()
#
# HOST*_QBIT_URL / HOST*_QBIT_USERNAME / HOST*_QBIT_PASSWORD
# qBittorrent connection details. Aliased by detect_hosts()
#
# ── 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)
# master.conf
#
# DOWNLOADER_RETENTION_DAYS
# Days before SABnzbd history entries (completed or failed) are removed
#
# QBIT_FAILSAFE_MIN_DAYS
# Minimum torrent age in days before failsafe removal is considered
#
# QBIT_FAILSAFE_MIN_RATIO
# Minimum seeding ratio required alongside age gate (0 = age only)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# downloaders_reset.sh
# Run maintenance reset for all configured download clients
#
# downloaders_reset.sh --dry-run
# Preview what would be removed without making any changes
#
# downloaders_reset.sh --status
# Show configured downloaders, current queue depths, and retention settings
#
# downloaders_reset.sh --log
# Verbose per-client per-item output
#
# ── 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)"
@@ -131,7 +193,8 @@ if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
if [[ -z "$SEARCHES" ]]; then
warn "slskd not reachable — skipping searches"
else
IDS=$(echo "$SEARCHES" | grep -o '"id":"[^"]*","isComplete":true' | \
IDS=$(echo "$SEARCHES" | tr '{' '\n' | \
grep '"isComplete":true' | grep '"searchText":' | \
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
COUNT=$(echo "$IDS" | grep -c . 2>/dev/null || echo 0)
COUNT="${COUNT//[^0-9]/}"; COUNT="${COUNT:-0}"
@@ -192,31 +255,56 @@ if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
SUCCESS=0; SKIPPED=0; FAIL=0
while IFS= read -r USER; do
[[ -z "$USER" ]] && continue
USER_DATA=$(curl -sf --max-time 10 \
"$SLSKD_URL/api/v0/transfers/downloads/$USER" \
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
# 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)"
ACTIVE=$(echo "$USER_DATA" | grep -c '"state":"InProgress"\|"state":"Queued"')
if [[ "${ACTIVE:-0}" -gt 0 ]]; then
info "$ICON_SKIP Skipping $USER — has active/queued transfer(s)"
((SKIPPED++))
continue
fi
# Extract IDs of terminal-state file transfers
# Split at { so each file object lands on its own line, then grep for state
FILE_IDS=$(echo "$USER_DATA" | tr '{' '\n' | \
grep '"state":"Completed"\|"state":"Errored"\|"state":"Aborted"\|"state":"Cancelled"' | \
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
if [[ -z "$FILE_IDS" ]]; then
info "$ICON_SKIP Skipping $USER — no terminal-state transfers"
((SKIPPED++))
continue
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would clear transfers for: $USER"
F_COUNT=$(echo "$FILE_IDS" | grep -c .)
warn "DRY RUN — would clear $F_COUNT transfer(s) 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
F_SUCCESS=0; F_FAIL=0
while IFS= read -r FILE_ID; do
[[ -z "$FILE_ID" ]] && continue
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
"$SLSKD_URL/api/v0/transfers/downloads/$USER/$FILE_ID" \
-H "X-Api-Key: $SLSKD_API_KEY")
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
((F_SUCCESS++))
else
((F_FAIL++))
fi
done <<< "$FILE_IDS"
info "$ICON_TRASH Cleared $F_SUCCESS transfer(s) for: $USER ($F_FAIL failed)"
((SUCCESS += F_SUCCESS))
((FAIL += F_FAIL))
done <<< "$USERNAMES"
success "Transfers: $SUCCESS cleared, $SKIPPED skipped (active), $FAIL failed"
success "Transfers: $SUCCESS cleared, $SKIPPED skipped (active/empty), $FAIL failed"
(( TOTAL_FAIL += FAIL ))
(( TOTAL_PASS += SUCCESS ))
fi