#!/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 (searches, browsing, downloads) 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\]|search response|Folder contents|Browse response|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