diff --git a/services/jellyfin-qbittorrent-monitor.py b/services/jellyfin-qbittorrent-monitor.py index f363c28..d7f401b 100644 --- a/services/jellyfin-qbittorrent-monitor.py +++ b/services/jellyfin-qbittorrent-monitor.py @@ -32,8 +32,8 @@ class JellyfinQBittorrentMonitor: self.session = requests.Session() # Use session for cookies # Hysteresis settings to prevent rapid switching - self.streaming_start_delay = 10 # seconds to wait before throttling - self.streaming_stop_delay = 60 # seconds to wait before removing throttle + self.streaming_start_delay = 10 + self.streaming_stop_delay = 60 self.last_state_change = 0 # Try to authenticate with qBittorrent @@ -50,18 +50,11 @@ class JellyfinQBittorrentMonitor: logger.info("Attempting to authenticate with qBittorrent...") try: - # First, try to access a simple endpoint to see if auth is needed test_response = self.session.get( f"{self.qbittorrent_url}/api/v2/app/version", timeout=5 ) - logger.info( - f"Version endpoint test: HTTP {test_response.status_code}, Response: {test_response.text}" - ) - if test_response.status_code == 200: - logger.info( - "qBittorrent accessible without explicit login - subnet whitelist working" - ) + logger.info("qBittorrent accessible without explicit login - subnet whitelist working") return True except Exception as e: @@ -75,7 +68,6 @@ class JellyfinQBittorrentMonitor: "Content-Type": "application/x-www-form-urlencoded", } - logger.info(f"Attempting login to {self.qbittorrent_url}/login") response = self.session.post( f"{self.qbittorrent_url}/login", data=login_data, @@ -83,22 +75,13 @@ class JellyfinQBittorrentMonitor: timeout=10, ) - logger.info( - f"Login response: HTTP {response.status_code}, Response: '{response.text}'" - ) - - if response.status_code == 200: - if "Ok." in response.text or response.text.strip() == "Ok.": - logger.info("Successfully authenticated with qBittorrent") - return True - elif "Fails." in response.text: - logger.warning( - "qBittorrent login failed - authentication may be required" - ) - else: - logger.info(f"Unexpected login response: '{response.text}'") + if response.status_code == 200 and ("Ok." in response.text or response.text.strip() == "Ok."): + logger.info("Successfully authenticated with qBittorrent") + return True + elif "Fails." in response.text: + logger.warning("qBittorrent login failed - authentication may be required") else: - logger.warning(f"Login request failed with HTTP {response.status_code}") + logger.warning(f"Login failed: HTTP {response.status_code}") except Exception as e: logger.error(f"Could not authenticate with qBittorrent: {e}") @@ -140,97 +123,65 @@ class JellyfinQBittorrentMonitor: def check_qbittorrent_alternate_limits(self): """Check if alternate speed limits are currently enabled""" - # For qBittorrent v5.1.0, use API v2 with GET requests try: - # Try the transfer info endpoint first (more reliable) + response = self.session.get( + f"{self.qbittorrent_url}/api/v2/transfer/speedLimitsMode", timeout=10 + ) + if response.status_code == 200: + return response.text.strip() == "1" + else: + logger.warning(f"SpeedLimitsMode endpoint returned HTTP {response.status_code}") + + except requests.exceptions.RequestException as e: + logger.error(f"SpeedLimitsMode endpoint failed: {e}") + except Exception as e: + logger.error(f"Failed to parse speedLimitsMode response: {e}") + + # Fallback: try transfer info endpoint + try: response = self.session.get( f"{self.qbittorrent_url}/api/v2/transfer/info", timeout=10 ) - logger.info(f"Transfer info endpoint: HTTP {response.status_code}") - if response.status_code == 200: data = response.json() - logger.info(f"Transfer info keys: {list(data.keys())}") - - # Check for alternative speed limit status in the response if "use_alt_speed_limits" in data: - is_enabled = data["use_alt_speed_limits"] - logger.info(f"Alternative speed limits enabled: {is_enabled}") - return is_enabled - - response.raise_for_status() - - except requests.exceptions.RequestException as e: - logger.error(f"Transfer info endpoint failed: {e}") - except json.JSONDecodeError as e: - logger.error(f"Failed to parse transfer info JSON: {e}") - - # Fallback: try app preferences endpoint - try: - response = self.session.get( - f"{self.qbittorrent_url}/api/v2/app/preferences", timeout=10 - ) - logger.info(f"Preferences endpoint: HTTP {response.status_code}") - - if response.status_code == 200: - data = response.json() - # Look for alternative speed settings - if "alt_up_limit" in data or "scheduler_enabled" in data: - # Check if alternative speeds are currently active - # This is a bit indirect but should work - logger.info( - "Found preferences data, assuming alt speeds not active by default" - ) - return False + return data["use_alt_speed_limits"] except Exception as e: - logger.error(f"Preferences endpoint failed: {e}") + logger.error(f"Transfer info fallback failed: {e}") - logger.error( - "Failed to check qBittorrent alternate limits status: all endpoints failed" - ) - return False + logger.warning("Could not determine qBittorrent alternate limits status, using tracked state") + return self.throttle_active def toggle_qbittorrent_limits(self, enable_throttle): """Toggle qBittorrent alternate speed limits""" try: - # Check current state current_throttle = self.check_qbittorrent_alternate_limits() + + if current_throttle == enable_throttle: + action = "enabled" if enable_throttle else "disabled" + logger.info(f"Alternate speed limits already {action}, no action needed") + return - if enable_throttle and not current_throttle: - try: - # Use API v2 POST endpoint to toggle alternative speed limits - response = self.session.post( - f"{self.qbittorrent_url}/api/v2/transfer/toggleSpeedLimitsMode", - timeout=10, - ) - logger.info( - f"Toggle enable response: HTTP {response.status_code}, {response.text[:100]}" - ) - response.raise_for_status() - self.throttle_active = True - logger.info("✓ Enabled alternate speed limits (throttling)") - return - except requests.exceptions.RequestException as e: - logger.error(f"Failed to enable alternate speed limits: {e}") - - elif not enable_throttle and current_throttle: - try: - # Use API v2 POST endpoint to toggle alternative speed limits - response = self.session.post( - f"{self.qbittorrent_url}/api/v2/transfer/toggleSpeedLimitsMode", - timeout=10, - ) - logger.info( - f"Toggle disable response: HTTP {response.status_code}, {response.text[:100]}" - ) - response.raise_for_status() - self.throttle_active = False - logger.info("✓ Disabled alternate speed limits (normal)") - return - except requests.exceptions.RequestException as e: - logger.error(f"Failed to disable alternate speed limits: {e}") - + response = self.session.post( + f"{self.qbittorrent_url}/api/v2/transfer/toggleSpeedLimitsMode", + timeout=10, + ) + response.raise_for_status() + + self.throttle_active = enable_throttle + + # Verify the change took effect + new_state = self.check_qbittorrent_alternate_limits() + if new_state == enable_throttle: + action = "enabled" if enable_throttle else "disabled" + logger.info(f"✓ Successfully {action} alternate speed limits") + else: + logger.warning(f"Toggle may have failed: expected {enable_throttle}, got {new_state}") + + except requests.exceptions.RequestException as e: + action = "enable" if enable_throttle else "disable" + logger.error(f"Failed to {action} alternate speed limits: {e}") except Exception as e: logger.error(f"Failed to toggle qBittorrent limits: {e}") @@ -244,24 +195,28 @@ class JellyfinQBittorrentMonitor: """Apply hysteresis to prevent rapid state changes""" now = time.time() - # If state hasn't changed, no action needed if new_streaming_state == self.last_streaming_state: return False - # Calculate time since last state change time_since_change = now - self.last_state_change - # If we want to start throttling (streaming started) + # Start throttling (streaming started) if new_streaming_state and not self.last_streaming_state: if time_since_change >= self.streaming_start_delay: self.last_state_change = now return True + else: + remaining = self.streaming_start_delay - time_since_change + logger.info(f"Streaming started - waiting {remaining:.1f}s before enabling throttling") - # If we want to stop throttling (streaming stopped) + # 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 + else: + remaining = self.streaming_stop_delay - time_since_change + logger.info(f"Streaming stopped - waiting {remaining:.1f}s before disabling throttling") return False @@ -287,8 +242,8 @@ class JellyfinQBittorrentMonitor: logger.info( f"Active streams ({len(active_streams)}): {', '.join(active_streams)}" ) - else: - logger.debug("No active streaming sessions") + elif len(active_streams) == 0 and self.last_streaming_state: + logger.info("No active streaming sessions") # Apply hysteresis and change state if needed if self.should_change_state(streaming_active):