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 ==========================================
|
# ================================= 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
|
# slskd
|
||||||
# Stuck searches — clears Completed/Errored searches left by Soularr crashes
|
# Stuck searches — clears Completed/Errored searches left by Soularr crashes
|
||||||
# prevents 409 Conflict on next Soularr startup
|
# prevents 409 Conflict on next Soularr startup
|
||||||
@@ -27,33 +37,85 @@
|
|||||||
# deleteFiles=false — removes from qBit, leaves files for arrs to manage
|
# deleteFiles=false — removes from qBit, leaves files for arrs to manage
|
||||||
# optional ratio requirement via QBIT_FAILSAFE_MIN_RATIO
|
# 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.
|
# DESIGN PRINCIPLES
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
|
# Never Interrupt Active Downloads
|
||||||
# slskd — skips users with InProgress or Queued transfers — never interrupts active downloads
|
# Each downloader section checks for active state before any removal. slskd
|
||||||
# SABnzbd — age check before deletion — only removes items past retention threshold
|
# skips users with InProgress or Queued transfers. SABnzbd only removes items
|
||||||
# qBittorrent — age + optional ratio check — failsafe only removes old completed torrents
|
# past the retention threshold. qBittorrent applies minimum age and optional
|
||||||
# All sections — skip gracefully if downloader is unreachable, no fatal exit
|
# ratio requirements. In-progress work is never touched.
|
||||||
# acquire_lock "wait" — if previous run still active, waits briefly then exits cleanly
|
#
|
||||||
|
# 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
|
# 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
|
# HOST*_SABNZBD_URL / HOST*_SABNZBD_API_KEY
|
||||||
|
# SABnzbd connection details. Aliased by detect_hosts()
|
||||||
|
#
|
||||||
# HOST*_QBIT_URL / HOST*_QBIT_USERNAME / HOST*_QBIT_PASSWORD
|
# HOST*_QBIT_URL / HOST*_QBIT_USERNAME / HOST*_QBIT_PASSWORD
|
||||||
|
# qBittorrent connection details. Aliased by detect_hosts()
|
||||||
#
|
#
|
||||||
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
|
# master.conf
|
||||||
# DOWNLOADER_RETENTION_DAYS — days before history entries are purged
|
#
|
||||||
# QBIT_FAILSAFE_MIN_DAYS — minimum torrent age before failsafe deletion
|
# DOWNLOADER_RETENTION_DAYS
|
||||||
# QBIT_FAILSAFE_MIN_RATIO — minimum ratio requirement (0 = age only)
|
# 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)"
|
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
|
if [[ -z "$SEARCHES" ]]; then
|
||||||
warn "slskd not reachable — skipping searches"
|
warn "slskd not reachable — skipping searches"
|
||||||
else
|
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/"//')
|
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
|
||||||
COUNT=$(echo "$IDS" | grep -c . 2>/dev/null || echo 0)
|
COUNT=$(echo "$IDS" | grep -c . 2>/dev/null || echo 0)
|
||||||
COUNT="${COUNT//[^0-9]/}"; COUNT="${COUNT:-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
|
SUCCESS=0; SKIPPED=0; FAIL=0
|
||||||
while IFS= read -r USER; do
|
while IFS= read -r USER; do
|
||||||
[[ -z "$USER" ]] && continue
|
[[ -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
|
# Skip users with any active or queued transfers — never interrupt downloads
|
||||||
ACTIVE=$(echo "$TRANSFERS" | grep -o "\"username\":\"$USER\"[^}]*\"state\":\"[^\"]*\"" | \
|
ACTIVE=$(echo "$USER_DATA" | grep -c '"state":"InProgress"\|"state":"Queued"')
|
||||||
grep -c "InProgress\|Queued")
|
if [[ "${ACTIVE:-0}" -gt 0 ]]; then
|
||||||
if [[ "$ACTIVE" -gt 0 ]]; then
|
info "$ICON_SKIP Skipping $USER — has active/queued transfer(s)"
|
||||||
info "$ICON_SKIP Skipping $USER — $ACTIVE active/queued transfer(s)"
|
|
||||||
((SKIPPED++))
|
((SKIPPED++))
|
||||||
continue
|
continue
|
||||||
fi
|
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
|
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++))
|
((SUCCESS++))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
|
|
||||||
"$SLSKD_URL/api/v0/transfers/downloads/$USER" \
|
F_SUCCESS=0; F_FAIL=0
|
||||||
-H "X-Api-Key: $SLSKD_API_KEY")
|
while IFS= read -r FILE_ID; do
|
||||||
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
|
[[ -z "$FILE_ID" ]] && continue
|
||||||
info "$ICON_TRASH Cleared transfers for: $USER"
|
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
((SUCCESS++))
|
"$SLSKD_URL/api/v0/transfers/downloads/$USER/$FILE_ID" \
|
||||||
else
|
-H "X-Api-Key: $SLSKD_API_KEY")
|
||||||
error "Failed to clear: $USER (HTTP $RESULT)"
|
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
|
||||||
((FAIL++))
|
((F_SUCCESS++))
|
||||||
fi
|
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"
|
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_FAIL += FAIL ))
|
||||||
(( TOTAL_PASS += SUCCESS ))
|
(( TOTAL_PASS += SUCCESS ))
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user