jellyfin-qbittorrent-monitor: take into account soulseek
This commit is contained in:
@@ -68,6 +68,12 @@
|
|||||||
owner = "root";
|
owner = "root";
|
||||||
group = "root";
|
group = "root";
|
||||||
};
|
};
|
||||||
|
slskd-api-key = lib.mkIf config.services.slskd.enable {
|
||||||
|
file = ../secrets/server/slskd-api-key.age;
|
||||||
|
mode = "0400";
|
||||||
|
owner = "root";
|
||||||
|
group = "root";
|
||||||
|
};
|
||||||
|
|
||||||
slskd_env = {
|
slskd_env = {
|
||||||
file = ../secrets/server/slskd_env.age;
|
file = ../secrets/server/slskd_env.age;
|
||||||
|
|||||||
Binary file not shown.
BIN
secrets/server/slskd-api-key.age
Normal file
BIN
secrets/server/slskd-api-key.age
Normal file
Binary file not shown.
@@ -77,14 +77,21 @@ lib.mkIf config.services.jellyfin.enable {
|
|||||||
"jellyfin.service"
|
"jellyfin.service"
|
||||||
"qbittorrent.service"
|
"qbittorrent.service"
|
||||||
"jellyfin-webhook-configure.service"
|
"jellyfin-webhook-configure.service"
|
||||||
];
|
]
|
||||||
wants = [ "jellyfin-webhook-configure.service" ];
|
++ lib.optional config.services.slskd.enable "slskd.service";
|
||||||
|
wants = [
|
||||||
|
"jellyfin-webhook-configure.service"
|
||||||
|
]
|
||||||
|
++ lib.optional config.services.slskd.enable "slskd.service";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
ExecStart = pkgs.writeShellScript "jellyfin-monitor-start" ''
|
ExecStart = pkgs.writeShellScript "jellyfin-monitor-start" ''
|
||||||
export JELLYFIN_API_KEY=$(cat $CREDENTIALS_DIRECTORY/jellyfin-api-key)
|
export JELLYFIN_API_KEY=$(cat $CREDENTIALS_DIRECTORY/jellyfin-api-key)
|
||||||
|
${lib.optionalString config.services.slskd.enable ''
|
||||||
|
export SLSKD_API_KEY=$(cat $CREDENTIALS_DIRECTORY/slskd-api-key)
|
||||||
|
''}
|
||||||
exec ${
|
exec ${
|
||||||
pkgs.python3.withPackages (ps: with ps; [ requests ])
|
pkgs.python3.withPackages (ps: with ps; [ requests ])
|
||||||
}/bin/python ${./jellyfin-qbittorrent-monitor.py}
|
}/bin/python ${./jellyfin-qbittorrent-monitor.py}
|
||||||
@@ -106,7 +113,10 @@ lib.mkIf config.services.jellyfin.enable {
|
|||||||
RemoveIPC = true;
|
RemoveIPC = true;
|
||||||
|
|
||||||
# Load credentials from agenix secrets
|
# Load credentials from agenix secrets
|
||||||
LoadCredential = "jellyfin-api-key:${config.age.secrets.jellyfin-api-key.path}";
|
LoadCredential = [
|
||||||
|
"jellyfin-api-key:${config.age.secrets.jellyfin-api-key.path}"
|
||||||
|
]
|
||||||
|
++ lib.optional config.services.slskd.enable "slskd-api-key:${config.age.secrets.slskd-api-key.path}";
|
||||||
};
|
};
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
@@ -122,6 +132,9 @@ lib.mkIf config.services.jellyfin.enable {
|
|||||||
# Webhook receiver: Jellyfin Webhook plugin POSTs events here to throttle immediately.
|
# Webhook receiver: Jellyfin Webhook plugin POSTs events here to throttle immediately.
|
||||||
WEBHOOK_BIND = "127.0.0.1";
|
WEBHOOK_BIND = "127.0.0.1";
|
||||||
WEBHOOK_PORT = toString webhookPort;
|
WEBHOOK_PORT = toString webhookPort;
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs config.services.slskd.enable {
|
||||||
|
SLSKD_URL = "http://127.0.0.1:${builtins.toString service_configs.ports.private.soulseek_web.port}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class JellyfinQBittorrentMonitor:
|
|||||||
webhook_port=0,
|
webhook_port=0,
|
||||||
webhook_bind="127.0.0.1",
|
webhook_bind="127.0.0.1",
|
||||||
gateway_ip=None,
|
gateway_ip=None,
|
||||||
|
slskd_url=None,
|
||||||
|
slskd_api_key=None,
|
||||||
):
|
):
|
||||||
self.jellyfin_url = jellyfin_url
|
self.jellyfin_url = jellyfin_url
|
||||||
self.qbittorrent_url = qbittorrent_url
|
self.qbittorrent_url = qbittorrent_url
|
||||||
@@ -69,6 +71,11 @@ class JellyfinQBittorrentMonitor:
|
|||||||
self.wake_event = threading.Event()
|
self.wake_event = threading.Event()
|
||||||
self.webhook_server = None
|
self.webhook_server = None
|
||||||
|
|
||||||
|
# Soulseek (slskd) upload monitoring — optional.
|
||||||
|
# When slskd is present, active upload bandwidth is subtracted from the
|
||||||
|
# torrent budget alongside Jellyfin streaming. No soulseek URL → no-op.
|
||||||
|
self.slskd_url = slskd_url
|
||||||
|
self.slskd_api_key = slskd_api_key
|
||||||
# Local network ranges (RFC 1918 private networks + localhost)
|
# Local network ranges (RFC 1918 private networks + localhost)
|
||||||
self.local_networks = [
|
self.local_networks = [
|
||||||
ipaddress.ip_network("10.0.0.0/8"),
|
ipaddress.ip_network("10.0.0.0/8"),
|
||||||
@@ -232,6 +239,66 @@ class JellyfinQBittorrentMonitor:
|
|||||||
active_streams.append({"name": stream_name, "bitrate_bps": bitrate})
|
active_streams.append({"name": stream_name, "bitrate_bps": bitrate})
|
||||||
|
|
||||||
return active_streams
|
return active_streams
|
||||||
|
def check_soulseek_uploads(self) -> int:
|
||||||
|
"""Return total active upload bandwidth from slskd in bits per second.
|
||||||
|
|
||||||
|
Returns 0 when slskd is not configured, unreachable, or has no in-
|
||||||
|
progress uploads. The slskd REST API returns averageSpeed in bytes/s
|
||||||
|
per Transfer; we sum across all InProgress uploads and convert to bps.
|
||||||
|
"""
|
||||||
|
if not self.slskd_url:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if self.slskd_api_key:
|
||||||
|
headers["X-API-Key"] = self.slskd_api_key
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.session.get(
|
||||||
|
f"{self.slskd_url}/api/v0/transfers/uploads",
|
||||||
|
headers=headers,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
status = e.response.status_code if e.response is not None else "?"
|
||||||
|
# 401/403 mean our key is wrong or missing — a configuration error
|
||||||
|
# that needs operator attention. Log loudly so it's not silently ignored.
|
||||||
|
if status in (401, 403):
|
||||||
|
logger.error(
|
||||||
|
f"slskd auth rejected (HTTP {status}). "
|
||||||
|
f"Verify slskd-api-key agenix secret matches "
|
||||||
|
f"services.slskd.settings.web.authentication.api_keys.monitor.key"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(f"slskd HTTP error {status}: {e}")
|
||||||
|
return 0
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning(f"Failed to query slskd uploads: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
uploads = response.json()
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.warning(f"Failed to parse slskd uploads response: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_bps = 0
|
||||||
|
for user in uploads:
|
||||||
|
for directory in user.get("directories", []):
|
||||||
|
for transfer in directory.get("files", []):
|
||||||
|
if transfer.get("state") == "InProgress":
|
||||||
|
# averageSpeed is in bytes/s
|
||||||
|
avg_speed = transfer.get("averageSpeed", 0) or 0
|
||||||
|
total_bps += int(avg_speed * 8)
|
||||||
|
|
||||||
|
if total_bps > 0:
|
||||||
|
logger.debug(
|
||||||
|
f"Soulseek uploads: {total_bps} bps "
|
||||||
|
f"({total_bps / 8 / 1024:.0f} KB/s)"
|
||||||
|
)
|
||||||
|
return total_bps
|
||||||
|
|
||||||
def check_qbittorrent_alternate_limits(self) -> bool:
|
def check_qbittorrent_alternate_limits(self) -> bool:
|
||||||
try:
|
try:
|
||||||
@@ -415,6 +482,8 @@ class JellyfinQBittorrentMonitor:
|
|||||||
)
|
)
|
||||||
if self.webhook_port:
|
if self.webhook_port:
|
||||||
logger.info(f"Webhook receiver: {self.webhook_bind}:{self.webhook_port}")
|
logger.info(f"Webhook receiver: {self.webhook_bind}:{self.webhook_port}")
|
||||||
|
if self.slskd_url:
|
||||||
|
logger.info(f"Soulseek (slskd) URL: {self.slskd_url}")
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||||
@@ -458,13 +527,18 @@ class JellyfinQBittorrentMonitor:
|
|||||||
total_streaming_bps = sum(
|
total_streaming_bps = sum(
|
||||||
stream["bitrate_bps"] for stream in active_streams
|
stream["bitrate_bps"] for stream in active_streams
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Soulseek uploads also consume WAN bandwidth — subtract from
|
||||||
|
# the torrent budget alongside Jellyfin streams.
|
||||||
|
soulseek_upload_bps = self.check_soulseek_uploads()
|
||||||
|
total_consumer_bps = total_streaming_bps + soulseek_upload_bps
|
||||||
|
|
||||||
remaining_bps = (
|
remaining_bps = (
|
||||||
self.total_bandwidth_budget
|
self.total_bandwidth_budget
|
||||||
- self.service_buffer
|
- self.service_buffer
|
||||||
- total_streaming_bps
|
- total_consumer_bps
|
||||||
)
|
)
|
||||||
remaining_kbs = max(0, remaining_bps) / 8 / 1024
|
remaining_kbs = max(0, remaining_bps) / 8 / 1024
|
||||||
|
|
||||||
if not streaming_state:
|
if not streaming_state:
|
||||||
desired_state = "unlimited"
|
desired_state = "unlimited"
|
||||||
elif streaming_active:
|
elif streaming_active:
|
||||||
@@ -487,11 +561,12 @@ class JellyfinQBittorrentMonitor:
|
|||||||
action = "pause torrents"
|
action = "pause torrents"
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"State change %s -> %s | streams=%d total_bps=%d remaining_bps=%d action=%s",
|
"State change %s -> %s | streams=%d jellyfin_bps=%d slskd_bps=%d remaining_bps=%d action=%s",
|
||||||
self.current_state,
|
self.current_state,
|
||||||
desired_state,
|
desired_state,
|
||||||
len(active_streams),
|
len(active_streams),
|
||||||
total_streaming_bps,
|
total_streaming_bps,
|
||||||
|
soulseek_upload_bps,
|
||||||
remaining_bps,
|
remaining_bps,
|
||||||
action,
|
action,
|
||||||
)
|
)
|
||||||
@@ -544,6 +619,8 @@ if __name__ == "__main__":
|
|||||||
webhook_port = int(os.getenv("WEBHOOK_PORT", "0"))
|
webhook_port = int(os.getenv("WEBHOOK_PORT", "0"))
|
||||||
webhook_bind = os.getenv("WEBHOOK_BIND", "127.0.0.1")
|
webhook_bind = os.getenv("WEBHOOK_BIND", "127.0.0.1")
|
||||||
gateway_ip = os.getenv("LAN_GATEWAY_IP") or None
|
gateway_ip = os.getenv("LAN_GATEWAY_IP") or None
|
||||||
|
slskd_url = os.getenv("SLSKD_URL") or None
|
||||||
|
slskd_api_key = os.getenv("SLSKD_API_KEY") or None
|
||||||
|
|
||||||
monitor = JellyfinQBittorrentMonitor(
|
monitor = JellyfinQBittorrentMonitor(
|
||||||
jellyfin_url=jellyfin_url,
|
jellyfin_url=jellyfin_url,
|
||||||
@@ -560,6 +637,8 @@ if __name__ == "__main__":
|
|||||||
webhook_port=webhook_port,
|
webhook_port=webhook_port,
|
||||||
webhook_bind=webhook_bind,
|
webhook_bind=webhook_bind,
|
||||||
gateway_ip=gateway_ip,
|
gateway_ip=gateway_ip,
|
||||||
|
slskd_url=slskd_url,
|
||||||
|
slskd_api_key=slskd_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
monitor.run()
|
monitor.run()
|
||||||
|
|||||||
@@ -35,6 +35,11 @@
|
|||||||
settings = {
|
settings = {
|
||||||
web = {
|
web = {
|
||||||
port = service_configs.ports.private.soulseek_web.port;
|
port = service_configs.ports.private.soulseek_web.port;
|
||||||
|
authentication.api_keys.monitor = {
|
||||||
|
key = "slskd-monitor-readonly-key-localhost";
|
||||||
|
role = "readonly";
|
||||||
|
cidr = "127.0.0.1/32";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
soulseek = {
|
soulseek = {
|
||||||
# description = "smth idk";
|
# description = "smth idk";
|
||||||
|
|||||||
@@ -808,5 +808,169 @@ pkgs.testers.runNixOSTest {
|
|||||||
# After Jellyfin comes back, sessions are gone - should unthrottle
|
# After Jellyfin comes back, sessions are gone - should unthrottle
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
assert not is_throttled(), "Should unthrottle after Jellyfin returns with no sessions"
|
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)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user