Files
slskd-auto-disconnect/auto-disconnect.sh
AJ Isaacs 15c7640432 Fix idle timer never expiring due to remote user activity
The log activity check matched "Folder contents", "Browse response",
and "search response" patterns which are triggered by remote users
browsing the shared library — not local user activity. This constant
passive traffic kept resetting the idle timer, preventing disconnect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:01:20 -05:00

160 lines
4.2 KiB
Bash

#!/bin/bash
# slskd auto-disconnect
# Disconnects from Soulseek after a period of inactivity to avoid
# sharing files 24/7. Runs via cron every 5 minutes.
#
# Activity is detected by:
# 1. Active downloads in progress via the API
# 2. Recent log activity (downloads, fresh connections) in docker logs
#
# Uploads do NOT reset the idle timer, but an in-progress upload will
# delay disconnection until it finishes.
#
# Environment overrides:
# SLSKD_IDLE_TIMEOUT - minutes before disconnect (default: 30)
# SLSKD_GOTIFY_TOKEN - Gotify app token for notifications (optional)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Load .env if present
if [ -f "$SCRIPT_DIR/.env" ]; then
set -a
source "$SCRIPT_DIR/.env"
set +a
fi
# --- Configuration ---
SLSKD_URL="${SLSKD_URL:-http://localhost:5030}"
SLSKD_USER="${SLSKD_USER:-slskd}"
SLSKD_PASS="${SLSKD_PASS:-slskd}"
IDLE_TIMEOUT_MIN="${SLSKD_IDLE_TIMEOUT:-30}"
LOG_CHECK_MIN=5
CONTAINER_NAME="slskd"
STATE_FILE="$SCRIPT_DIR/.last_active"
LOG_FILE="$SCRIPT_DIR/auto-disconnect.log"
LOG_MAX_LINES=500
GOTIFY_URL="${GOTIFY_URL:-}"
GOTIFY_TOKEN="${GOTIFY_TOKEN:-}"
# --- Functions ---
log_msg() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
if [ -f "$LOG_FILE" ] && [ "$(wc -l < "$LOG_FILE")" -gt "$LOG_MAX_LINES" ]; then
tail -n "$((LOG_MAX_LINES / 2))" "$LOG_FILE" > "$LOG_FILE.tmp"
mv "$LOG_FILE.tmp" "$LOG_FILE"
fi
}
get_token() {
curl -sf --max-time 10 "$SLSKD_URL/api/v0/session" -X POST \
-H "Content-Type: application/json" \
-d "{\"username\":\"$SLSKD_USER\",\"password\":\"$SLSKD_PASS\"}" \
| jq -r '.token // empty'
}
api_get() {
curl -sf --max-time 10 "$SLSKD_URL$1" -H "Authorization: Bearer $TOKEN"
}
is_connected() {
local connected
connected=$(api_get "/api/v0/server" | jq -r '.isConnected')
[ "$connected" = "true" ]
}
has_active_downloads() {
local count
count=$(api_get "/api/v0/transfers/downloads" \
| jq '[.[].directories[]?.files[]? | select(.state | test("InProgress|Initializing|Queued, Locally"))] | length')
[ "${count:-0}" -gt 0 ]
}
has_active_uploads() {
local count
count=$(api_get "/api/v0/transfers/uploads" \
| jq '[.[]?.directories[]?.files[]? | select(.state | test("InProgress|Initializing"))] | length')
[ "${count:-0}" -gt 0 ]
}
has_recent_log_activity() {
local count
count=$(docker logs "$CONTAINER_NAME" --since "${LOG_CHECK_MIN}m" 2>&1 \
| grep -cE '\[DOWNLOAD\]|Connected to the Soulseek' || true)
[ "${count:-0}" -gt 0 ]
}
disconnect() {
curl -sf --max-time 10 -X DELETE "$SLSKD_URL/api/v0/server" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}' > /dev/null
}
touch_active() {
date +%s > "$STATE_FILE"
}
seconds_since_active() {
if [ ! -f "$STATE_FILE" ]; then
touch_active
echo 0
return
fi
local last now
last=$(cat "$STATE_FILE")
now=$(date +%s)
echo $((now - last))
}
notify() {
local msg="$1"
if [ -n "$GOTIFY_TOKEN" ] && [ -n "$GOTIFY_URL" ]; then
curl -sf --max-time 10 "$GOTIFY_URL/message" -X POST \
-H "X-Gotify-Key: $GOTIFY_TOKEN" \
-F "title=slskd" \
-F "message=$msg" \
-F "priority=3" > /dev/null 2>&1 || true
fi
}
# --- Main ---
TOKEN=$(get_token)
if [ -z "$TOKEN" ]; then
log_msg "ERROR: Failed to authenticate with slskd API"
exit 1
fi
if ! is_connected; then
rm -f "$STATE_FILE"
exit 0
fi
# Downloads and user activity reset the idle timer
if has_active_downloads || has_recent_log_activity; then
touch_active
exit 0
fi
# Past idle threshold?
idle_sec=$(seconds_since_active)
threshold_sec=$((IDLE_TIMEOUT_MIN * 60))
if [ "$idle_sec" -ge "$threshold_sec" ]; then
# Let any in-progress upload finish before pulling the plug
if has_active_uploads; then
log_msg "Idle threshold reached but upload in progress, waiting"
exit 0
fi
disconnect
idle_min=$((idle_sec / 60))
log_msg "Disconnected after ${idle_min}m idle (threshold: ${IDLE_TIMEOUT_MIN}m)"
notify "Disconnected from Soulseek after ${idle_min} minutes of inactivity"
fi