From 7274b86ec191bf1f84e6b3cc95413064abfb56c2 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 11 Sep 2025 17:46:08 -0400 Subject: [PATCH] claude'd jellyfin auto limit qbt upload --- secrets/jellyfin-api-key | Bin 0 -> 55 bytes services/jellyfin-qbittorrent-monitor.py | 326 +++++++++++++++++++++++ services/qbittorrent.nix | 4 + services/wg.nix | 81 +++--- 4 files changed, 367 insertions(+), 44 deletions(-) create mode 100644 secrets/jellyfin-api-key create mode 100644 services/jellyfin-qbittorrent-monitor.py diff --git a/secrets/jellyfin-api-key b/secrets/jellyfin-api-key new file mode 100644 index 0000000000000000000000000000000000000000..a91e5c31435b72d5cbac7dc2a8d25d0ad4b84183 GIT binary patch literal 55 zcmZQ@_Y83kiVO&0Sn=qE&=SM6b~ZzsVs`N(DbCGHxR$MJij= self.streaming_start_delay: + self.last_state_change = now + return True + + # If we want to stop throttling (streaming stopped) + elif not new_streaming_state and self.last_streaming_state: + if time_since_change >= self.streaming_stop_delay: + self.last_state_change = now + return True + + return False + + def run(self): + """Main monitoring loop""" + logger.info("Starting Jellyfin-qBittorrent monitor") + logger.info(f"Jellyfin URL: {self.jellyfin_url}") + logger.info(f"qBittorrent URL: {self.qbittorrent_url}") + logger.info(f"Check interval: {self.check_interval}s") + + # Set up signal handlers + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + + while self.running: + try: + # Check for active streaming + active_streams = self.check_jellyfin_sessions() + streaming_active = len(active_streams) > 0 + + # Log current status + if streaming_active: + logger.info( + f"Active streams ({len(active_streams)}): {', '.join(active_streams)}" + ) + else: + logger.debug("No active streaming sessions") + + # Apply hysteresis and change state if needed + if self.should_change_state(streaming_active): + self.last_streaming_state = streaming_active + self.toggle_qbittorrent_limits(streaming_active) + + time.sleep(self.check_interval) + + except KeyboardInterrupt: + break + except Exception as e: + logger.error(f"Unexpected error in monitoring loop: {e}") + time.sleep(self.check_interval) + + self.restore_normal_limits() + logger.info("Monitor stopped") + + +if __name__ == "__main__": + import os + + # Configuration from environment variables + jellyfin_url = os.getenv("JELLYFIN_URL", "http://localhost:8096") + qbittorrent_url = os.getenv("QBITTORRENT_URL", "http://localhost:8080") + check_interval = int(os.getenv("CHECK_INTERVAL", "30")) + jellyfin_api_key = os.getenv("JELLYFIN_API_KEY") + + monitor = JellyfinQBittorrentMonitor( + jellyfin_url=jellyfin_url, + qbittorrent_url=qbittorrent_url, + check_interval=check_interval, + jellyfin_api_key=jellyfin_api_key, + ) + + monitor.run() diff --git a/services/qbittorrent.nix b/services/qbittorrent.nix index 42a7c7e..f6a0589 100644 --- a/services/qbittorrent.nix +++ b/services/qbittorrent.nix @@ -56,6 +56,10 @@ GlobalUPSpeedLimit = 1000; GlobalDLSpeedLimit = 1000; + + # Alternate speed limits for when Jellyfin is streaming + AlternativeGlobalUPSpeedLimit = 500; # 500 KB/s when throttled + AlternativeGlobalDLSpeedLimit = 800; # 800 KB/s when throttled IncludeOverheadInLimits = true; GlobalMaxRatio = 6.0; diff --git a/services/wg.nix b/services/wg.nix index 3f22593..540ec58 100644 --- a/services/wg.nix +++ b/services/wg.nix @@ -19,51 +19,44 @@ nload ]; - networking.firewall.extraCommands = '' - # Exempt local traffic from marking - iptables -t mangle -A POSTROUTING -s ${service_configs.https.wg_ip}/24 -d 192.168.1.0/24 -j RETURN + systemd.services."jellyfin-qbittorrent-monitor" = { + description = "Monitor Jellyfin streaming and control qBittorrent rate limits"; + after = [ + "network.target" + "jellyfin.service" + "qbittorrent.service" + ]; + wantedBy = [ "multi-user.target" ]; - # Mark all other traffic from the VPN namespace - iptables -t mangle -A POSTROUTING -s ${service_configs.https.wg_ip}/24 -j MARK --set-mark 1 - ''; + serviceConfig = { + Type = "simple"; + ExecStart = pkgs.writeShellScript "jellyfin-monitor-start" '' + export JELLYFIN_API_KEY=$(cat ${../secrets/jellyfin-api-key}) + exec ${ + pkgs.python3.withPackages (ps: with ps; [ requests ]) + }/bin/python ${./jellyfin-qbittorrent-monitor.py} + ''; + Restart = "always"; + RestartSec = "10s"; - systemd.services."traffic-shaping" = - let - upload_pipe = 44; - high_prio = 40; - low_prio = 4; - in - { - description = "Apply QoS to prioritize non-VPN traffic"; - after = [ - "network.target" - "vpn-wg.service" - ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = pkgs.writeShellScript "tc-setup" '' - # Add HTB qdisc to physical interface - ${pkgs.iproute2}/bin/tc qdisc add dev ${eth_interface} root handle 1: htb default 10 - - # Define classes: - # - Class 1:10 (high priority, unmarked) - # - Class 1:20 (low priority, marked VPN traffic) - ${pkgs.iproute2}/bin/tc class add dev ${eth_interface} parent 1: classid 1:1 htb rate ${builtins.toString upload_pipe}mbit ceil ${builtins.toString upload_pipe}mbit - ${pkgs.iproute2}/bin/tc class add dev ${eth_interface} parent 1:1 classid 1:10 htb rate ${builtins.toString high_prio}mbit ceil ${builtins.toString upload_pipe}mbit prio 1 - ${pkgs.iproute2}/bin/tc class add dev ${eth_interface} parent 1:1 classid 1:20 htb rate ${builtins.toString low_prio}mbit ceil ${builtins.toString upload_pipe}mbit prio 2 - - # Direct marked packets to low-priority class - ${pkgs.iproute2}/bin/tc filter add dev ${eth_interface} parent 1: protocol ip prio 1 handle 1 fw flowid 1:20 - ''; - - ExecStop = pkgs.writeShellScript "tc-stop" '' - ${pkgs.iproute2}/bin/tc filter del dev ${eth_interface} parent 1: - ${pkgs.iproute2}/bin/tc class del dev ${eth_interface} parent 1: classid 1:20 - ${pkgs.iproute2}/bin/tc class del dev ${eth_interface} parent 1: classid 1:10 - ${pkgs.iproute2}/bin/tc class del dev ${eth_interface} parent 1: classid 1:1 - ${pkgs.iproute2}/bin/tc qdisc del dev ${eth_interface} root - ''; - }; + # Security hardening + DynamicUser = true; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; }; + + environment = { + JELLYFIN_URL = "http://localhost:${builtins.toString service_configs.ports.jellyfin}"; + QBITTORRENT_URL = "http://${service_configs.https.wg_ip}:${builtins.toString service_configs.ports.torrent}"; + CHECK_INTERVAL = "30"; + }; + }; }