Compare commits

...

2 Commits

View File

@@ -1,44 +1,10 @@
{ {
lib, lib,
pkgs, pkgs,
inputs,
... ...
}: }:
let let
# Mock qBittorrent - simple enough to keep mocked
mockQBittorrent = pkgs.writers.writePython3Bin "mock-qbt" { flakeIgnore = [ "E501" ]; } ''
import http.server
import socketserver
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
print(f"qbt: {fmt % args}")
def do_GET(self):
if self.path == "/api/v2/transfer/speedLimitsMode":
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(("1" if getattr(self.server, "alt_speed", False) else "0").encode())
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
if self.path == "/api/v2/transfer/toggleSpeedLimitsMode":
self.server.alt_speed = not getattr(self.server, "alt_speed", False)
self.send_response(200)
self.end_headers()
else:
self.send_response(404)
self.end_headers()
with socketserver.TCPServer(("0.0.0.0", 8080), Handler) as s:
print("Mock qBittorrent on port 8080")
s.serve_forever()
'';
payloads = { payloads = {
auth = pkgs.writeText "auth.json" (builtins.toJSON { Username = "jellyfin"; }); auth = pkgs.writeText "auth.json" (builtins.toJSON { Username = "jellyfin"; });
empty = pkgs.writeText "empty.json" (builtins.toJSON { }); empty = pkgs.writeText "empty.json" (builtins.toJSON { });
@@ -48,34 +14,76 @@ pkgs.testers.runNixOSTest {
name = "jellyfin-qbittorrent-monitor"; name = "jellyfin-qbittorrent-monitor";
nodes = { nodes = {
server = { server =
services.jellyfin.enable = true; { ... }:
environment.systemPackages = with pkgs; [ {
curl imports = [
ffmpeg inputs.vpn-confinement.nixosModules.default
]; ];
virtualisation.diskSize = 3 * 1024;
networking.firewall.allowedTCPPorts = [ services.jellyfin.enable = true;
8096
8080 # Real qBittorrent service
]; services.qbittorrent = {
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ enable = true;
{ webuiPort = 8080;
address = "192.168.1.1"; openFirewall = true;
prefixLength = 24;
} serverConfig.LegalNotice.Accepted = true;
];
networking.interfaces.eth1.ipv4.routes = [ serverConfig.Preferences = {
{ WebUI = {
address = "203.0.113.0"; # Disable authentication for testing
prefixLength = 24; AuthSubnetWhitelist = "0.0.0.0/0,::/0";
} AuthSubnetWhitelistEnabled = true;
]; LocalHostAuth = false;
systemd.services.mock-qbittorrent = { };
wantedBy = [ "multi-user.target" ];
serviceConfig.ExecStart = lib.getExe mockQBittorrent; Downloads = {
SavePath = "/var/lib/qbittorrent/downloads";
TempPath = "/var/lib/qbittorrent/incomplete";
};
};
serverConfig.BitTorrent.Session = {
# Normal speed - unlimited
GlobalUPSpeedLimit = 0;
GlobalDLSpeedLimit = 0;
# Alternate speed limits for when Jellyfin is streaming
AlternativeGlobalUPSpeedLimit = 100;
AlternativeGlobalDLSpeedLimit = 100;
};
};
environment.systemPackages = with pkgs; [
curl
ffmpeg
];
virtualisation.diskSize = 3 * 1024;
networking.firewall.allowedTCPPorts = [
8096
8080
];
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
{
address = "192.168.1.1";
prefixLength = 24;
}
];
networking.interfaces.eth1.ipv4.routes = [
{
address = "203.0.113.0";
prefixLength = 24;
}
];
# Create directories for qBittorrent
systemd.tmpfiles.rules = [
"d /var/lib/qbittorrent/downloads 0755 qbittorrent qbittorrent"
"d /var/lib/qbittorrent/incomplete 0755 qbittorrent qbittorrent"
];
}; };
};
# Public test IP (RFC 5737 TEST-NET-3) so Jellyfin sees it as external # Public test IP (RFC 5737 TEST-NET-3) so Jellyfin sees it as external
client = { client = {
@@ -122,9 +130,12 @@ pkgs.testers.runNixOSTest {
server.wait_for_unit("jellyfin.service") server.wait_for_unit("jellyfin.service")
server.wait_for_open_port(8096) server.wait_for_open_port(8096)
server.wait_until_succeeds("curl -sf http://localhost:8096/health | grep -q Healthy", timeout=60) server.wait_until_succeeds("curl -sf http://localhost:8096/health | grep -q Healthy", timeout=60)
server.wait_for_unit("mock-qbittorrent.service") server.wait_for_unit("qbittorrent.service")
server.wait_for_open_port(8080) server.wait_for_open_port(8080)
# Wait for qBittorrent WebUI to be responsive
server.wait_until_succeeds("curl -sf http://localhost:8080/api/v2/app/version", timeout=30)
with subtest("Complete Jellyfin setup wizard"): with subtest("Complete Jellyfin setup wizard"):
server.wait_until_succeeds(api_get("/Startup/Configuration")) server.wait_until_succeeds(api_get("/Startup/Configuration"))
server.succeed(api_get("/Startup/FirstUser")) server.succeed(api_get("/Startup/FirstUser"))
@@ -168,13 +179,13 @@ pkgs.testers.runNixOSTest {
python = "${pkgs.python3.withPackages (ps: [ ps.requests ])}/bin/python" python = "${pkgs.python3.withPackages (ps: [ ps.requests ])}/bin/python"
monitor = "${../services/jellyfin-qbittorrent-monitor.py}" monitor = "${../services/jellyfin-qbittorrent-monitor.py}"
server.succeed(f""" server.succeed(f"""
systemd-run --unit=monitor-test \\ systemd-run --unit=monitor-test \
--setenv=JELLYFIN_URL=http://localhost:8096 \\ --setenv=JELLYFIN_URL=http://localhost:8096 \
--setenv=JELLYFIN_API_KEY={token} \\ --setenv=JELLYFIN_API_KEY={token} \
--setenv=QBITTORRENT_URL=http://localhost:8080 \\ --setenv=QBITTORRENT_URL=http://localhost:8080 \
--setenv=CHECK_INTERVAL=1 \\ --setenv=CHECK_INTERVAL=1 \
--setenv=STREAMING_START_DELAY=1 \\ --setenv=STREAMING_START_DELAY=1 \
--setenv=STREAMING_STOP_DELAY=1 \\ --setenv=STREAMING_STOP_DELAY=1 \
{python} {monitor} {python} {monitor}
""") """)
time.sleep(2) time.sleep(2)
@@ -274,12 +285,13 @@ pkgs.testers.runNixOSTest {
time.sleep(2) time.sleep(2)
assert is_throttled(), "Should be throttled before qBittorrent restart" assert is_throttled(), "Should be throttled before qBittorrent restart"
# Restart mock-qbittorrent (this resets alt_speed to False) # Restart qBittorrent (this resets alt_speed to its config default - disabled)
server.succeed("systemctl restart mock-qbittorrent.service") server.succeed("systemctl restart qbittorrent.service")
server.wait_for_unit("mock-qbittorrent.service") server.wait_for_unit("qbittorrent.service")
server.wait_for_open_port(8080) server.wait_for_open_port(8080)
server.wait_until_succeeds("curl -sf http://localhost:8080/api/v2/app/version", timeout=30)
# qBittorrent restarted - alt_speed is now False (default) # qBittorrent restarted - alt_speed is now False (default on startup)
# The monitor should detect this and re-apply throttling # The monitor should detect this and re-apply throttling
time.sleep(3) # Give monitor time to detect and re-apply time.sleep(3) # Give monitor time to detect and re-apply
assert is_throttled(), "Monitor should re-apply throttling after qBittorrent restart" assert is_throttled(), "Monitor should re-apply throttling after qBittorrent restart"
@@ -299,10 +311,11 @@ pkgs.testers.runNixOSTest {
# Verify we're unthrottled (no active streams) # Verify we're unthrottled (no active streams)
assert not is_throttled(), "Should be unthrottled before test" assert not is_throttled(), "Should be unthrottled before test"
# Restart mock-qbittorrent # Restart qBittorrent
server.succeed("systemctl restart mock-qbittorrent.service") server.succeed("systemctl restart qbittorrent.service")
server.wait_for_unit("mock-qbittorrent.service") server.wait_for_unit("qbittorrent.service")
server.wait_for_open_port(8080) server.wait_for_open_port(8080)
server.wait_until_succeeds("curl -sf http://localhost:8080/api/v2/app/version", timeout=30)
# Give monitor time to check state # Give monitor time to check state
time.sleep(3) time.sleep(3)