jellyfin-qbittorrent-monitor: take into account soulseek
All checks were successful
Build and Deploy / mreow (push) Successful in 2m3s
Build and Deploy / yarn (push) Successful in 1m3s
Build and Deploy / muffin (push) Successful in 1m15s

This commit is contained in:
2026-05-15 02:42:13 -04:00
parent 9662745d6e
commit 293d85b0b5
7 changed files with 273 additions and 6 deletions

View File

@@ -808,5 +808,169 @@ pkgs.testers.runNixOSTest {
# After Jellyfin comes back, sessions are gone - should unthrottle
time.sleep(3)
assert not is_throttled(), "Should unthrottle after Jellyfin returns with no sessions"
# === SLSKD UPLOAD BANDWIDTH TESTS ===
# Spin up a mock slskd API server that simulates active uploads. The
# monitor should query it and subtract upload bandwidth from the torrent
# budget alongside Jellyfin streaming.
mock_slskd_port = 9999
# Start a mock slskd server. It checks the X-API-Key header to verify
# the monitor sends credentials correctly, and returns 401 if they mismatch.
mock_slskd_key = "test-slskd-api-key"
server.succeed(
f"{pkgs.python3}/bin/python -c \""
"import json, http.server, threading;"
f"EXPECTED_KEY='{mock_slskd_key}';"
"class H(http.server.BaseHTTPRequestHandler):"
" uploads_response=[];"
" def do_GET(s):"
" if s.path=='/api/v0/transfers/uploads':"
" if s.headers.get('X-API-Key')!=EXPECTED_KEY:"
" s.send_error(401);"
" return;"
" s.send_response(200);"
" s.send_header('Content-Type','application/json');"
" s.end_headers();"
" s.wfile.write(json.dumps(H.uploads_response).encode());"
" else: s.send_error(404);"
" def log_message(*a): pass;"
f"server=http.server.HTTPServer(('127.0.0.1',{mock_slskd_port}),H);"
"threading.Thread(target=server.serve_forever,daemon=True).start();"
"import signal; signal.pause()\" &"
)
server.wait_for_open_port(mock_slskd_port)
server.succeed(f"curl -sf http://127.0.0.1:{mock_slskd_port}/api/v0/transfers/uploads")
# Stop the normal monitor and start one pointed at the mock slskd
server.succeed("systemctl stop monitor-test || true")
time.sleep(1)
server.succeed(f"""
systemd-run --unit=monitor-slskd \
--setenv=JELLYFIN_URL=http://localhost:8096 \
--setenv=JELLYFIN_API_KEY={token} \
--setenv=QBITTORRENT_URL=http://localhost:8080 \
--setenv=CHECK_INTERVAL=1 \
--setenv=STREAMING_START_DELAY=1 \
--setenv=STREAMING_STOP_DELAY=1 \
--setenv=TOTAL_BANDWIDTH_BUDGET=50000000 \
--setenv=SERVICE_BUFFER=2000000 \
--setenv=DEFAULT_STREAM_BITRATE=10000000 \
--setenv=MIN_TORRENT_SPEED=100 \
--setenv=SLSKD_URL=http://127.0.0.1:{mock_slskd_port} \
--setenv=SLSKD_API_KEY={mock_slskd_key} \
{python} {monitor}
""")
time.sleep(2)
assert not is_throttled(), "Should start unthrottled (slskd reports no uploads)"
with subtest("Slskd uploads reduce torrent bandwidth budget"):
# Re-authenticate to get fresh token after previous tests
client_auth_result = json.loads(client.succeed(
f"curl -sf -X POST 'http://{server_ip}:8096/Users/AuthenticateByName' "
f"-d '@${jfLib.payloads.auth}' "
"-H 'Content-Type:application/json' "
f"-H 'X-Emby-Authorization:{client_auth}'"
))
client_token = client_auth_result["AccessToken"]
# Start a single Jellyfin stream to establish a baseline torrent limit
playback_start = {{
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-slskd",
"CanSeek": True,
"IsPaused": False,
}}
start_cmd = (
f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' "
f"-d '{json.dumps(playback_start)}' "
"-H 'Content-Type:application/json' "
f"-H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
)
client.succeed(start_cmd)
time.sleep(3)
assert is_throttled(), "Should throttle with streaming"
baseline_dl = get_alt_dl_limit()
# Now simulate active slskd uploads consuming 15 Mbps (1,875,000 bytes/s)
# by restarting the mock with two in-progress uploads at 937,500 bytes/s each.
server.succeed("pkill -f 'H(http' || true")
time.sleep(1)
# Restart the mock server with upload data; still validates the API key.
server.succeed(
f"{pkgs.python3}/bin/python -c \""
"import json, http.server, threading;"
f"EXPECTED_KEY='{mock_slskd_key}';"
"class H(http.server.BaseHTTPRequestHandler):"
" uploads_response=[{'username':'peer1','directories':[{'directory':'music',"
"'fileCount':2,'files':["
"{'averageSpeed':937500,'state':'InProgress','filename':'t1.flac','size':30000000,'username':'peer1'},"
"{'averageSpeed':937500,'state':'InProgress','filename':'t2.flac','size':25000000,'username':'peer1'}"
"]}]];"
" def do_GET(s):"
" if s.path=='/api/v0/transfers/uploads':"
" if s.headers.get('X-API-Key')!=EXPECTED_KEY:"
" s.send_error(401);"
" return;"
" s.send_response(200);"
" s.send_header('Content-Type','application/json');"
" s.end_headers();"
" s.wfile.write(json.dumps(H.uploads_response).encode());"
" else: s.send_error(404);"
" def log_message(*a): pass;"
f"server=http.server.HTTPServer(('127.0.0.1',{mock_slskd_port}),H);"
"threading.Thread(target=server.serve_forever,daemon=True).start();"
"import signal; signal.pause()\" &"
)
server.wait_for_open_port(mock_slskd_port)
time.sleep(4) # Let monitor poll and adjust
slskd_dl = get_alt_dl_limit()
# With 15 Mbps of slskd uploads consuming the shared budget, the
# remaining bandwidth for torrents must be lower than baseline.
assert slskd_dl < baseline_dl, \
f"Slskd uploads should reduce torrent budget. baseline={baseline_dl}, with_slskd={slskd_dl}"
# Stop Jellyfin playback
playback_stop = {{
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-slskd",
"PositionTicks": 50000000,
}}
stop_cmd = (
f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Stopped' "
f"-d '{json.dumps(playback_stop)}' "
"-H 'Content-Type:application/json' "
f"-H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
)
client.succeed(stop_cmd)
time.sleep(3)
# Clean up: stop mock slskd and restore normal monitor
server.succeed("pkill -f 'H(http' || true")
server.succeed("systemctl stop monitor-slskd || true")
time.sleep(1)
server.succeed(f"""
systemd-run --unit=monitor-test \
--setenv=JELLYFIN_URL=http://localhost:8096 \
--setenv=JELLYFIN_API_KEY={token} \
--setenv=QBITTORRENT_URL=http://localhost:8080 \
--setenv=CHECK_INTERVAL=1 \
--setenv=STREAMING_START_DELAY=1 \
--setenv=STREAMING_STOP_DELAY=1 \
--setenv=TOTAL_BANDWIDTH_BUDGET=50000000 \
--setenv=SERVICE_BUFFER=2000000 \
--setenv=DEFAULT_STREAM_BITRATE=10000000 \
--setenv=MIN_TORRENT_SPEED=100 \
{python} {monitor}
""")
time.sleep(2)
'';
}