commit d0f98baaa3e866e96add118302885e3e95ba0796 Author: AJ Isaacs Date: Sat Feb 14 22:21:35 2026 -0500 Initial commit: slskd auto-disconnect script Cron script that disconnects slskd from the Soulseek network after a configurable period of inactivity to avoid sharing files 24/7. Co-Authored-By: Claude Opus 4.6 diff --git a/auto-disconnect.sh b/auto-disconnect.sh new file mode 100644 index 0000000..9d8f77f --- /dev/null +++ b/auto-disconnect.sh @@ -0,0 +1,151 @@ +#!/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 + +# --- Configuration --- +SLSKD_URL="http://localhost:5030" +SLSKD_USER="slskd" +SLSKD_PASS="slskd" +IDLE_TIMEOUT_MIN="${SLSKD_IDLE_TIMEOUT:-30}" +LOG_CHECK_MIN=5 +CONTAINER_NAME="slskd" + +SCRIPT_DIR="/opt/arr/slskd/scripts" +STATE_FILE="$SCRIPT_DIR/.last_active" +LOG_FILE="$SCRIPT_DIR/auto-disconnect.log" +LOG_MAX_LINES=500 + +GOTIFY_URL="https://notify.thecozycat.net" +GOTIFY_TOKEN="${SLSKD_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" ]; 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