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:
@@ -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
|
||||
|
||||
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" \
|
||||
"$SLSKD_URL/api/v0/transfers/downloads/$USER/$FILE_ID" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY")
|
||||
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
|
||||
info "$ICON_TRASH Cleared transfers for: $USER"
|
||||
((SUCCESS++))
|
||||
((F_SUCCESS++))
|
||||
else
|
||||
error "Failed to clear: $USER (HTTP $RESULT)"
|
||||
((FAIL++))
|
||||
((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
|
||||
|
||||
Reference in New Issue
Block a user