From 5cb16d4b189d7e5cca5ed35e1385cbe380d990cf Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Tue, 19 May 2026 15:48:47 -0400 Subject: [PATCH] fix: repair slskd search and transfer cleanup in downloaders_reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Docker_Essentials/downloaders_reset.sh | 170 +++++++++++++++++++------ 1 file changed, 129 insertions(+), 41 deletions(-) diff --git a/Docker_Essentials/downloaders_reset.sh b/Docker_Essentials/downloaders_reset.sh index ef1d54b..4cd2948 100644 --- a/Docker_Essentials/downloaders_reset.sh +++ b/Docker_Essentials/downloaders_reset.sh @@ -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