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>
160 lines
4.2 KiB
Bash
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
|