jellyfin-qbittorrent-monitor: improve testing infra
This commit is contained in:
parent
73379efe40
commit
f40f9748a4
@ -21,6 +21,8 @@ class JellyfinQBittorrentMonitor:
|
||||
qbittorrent_url="http://localhost:8080",
|
||||
check_interval=30,
|
||||
jellyfin_api_key=None,
|
||||
streaming_start_delay=10,
|
||||
streaming_stop_delay=60,
|
||||
):
|
||||
self.jellyfin_url = jellyfin_url
|
||||
self.qbittorrent_url = qbittorrent_url
|
||||
@ -33,8 +35,8 @@ class JellyfinQBittorrentMonitor:
|
||||
self.last_active_streams = []
|
||||
|
||||
# Hysteresis settings to prevent rapid switching
|
||||
self.streaming_start_delay = 10
|
||||
self.streaming_stop_delay = 60
|
||||
self.streaming_start_delay = streaming_start_delay
|
||||
self.streaming_stop_delay = streaming_stop_delay
|
||||
self.last_state_change = 0
|
||||
|
||||
# Local network ranges (RFC 1918 private networks + localhost)
|
||||
@ -80,7 +82,7 @@ class JellyfinQBittorrentMonitor:
|
||||
for session in sessions:
|
||||
if (
|
||||
"NowPlayingItem" in session
|
||||
and session.get("PlayState", {}).get("IsPaused", True) == False
|
||||
and not session.get("PlayState", {}).get("IsPaused", True)
|
||||
):
|
||||
# Check if session is from external network
|
||||
remote_endpoint = session.get("RemoteEndPoint", "")
|
||||
@ -249,12 +251,16 @@ if __name__ == "__main__":
|
||||
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")
|
||||
streaming_start_delay = int(os.getenv("STREAMING_START_DELAY", "10"))
|
||||
streaming_stop_delay = int(os.getenv("STREAMING_STOP_DELAY", "60"))
|
||||
|
||||
monitor = JellyfinQBittorrentMonitor(
|
||||
jellyfin_url=jellyfin_url,
|
||||
qbittorrent_url=qbittorrent_url,
|
||||
check_interval=check_interval,
|
||||
jellyfin_api_key=jellyfin_api_key,
|
||||
streaming_start_delay=streaming_start_delay,
|
||||
streaming_stop_delay=streaming_stop_delay,
|
||||
)
|
||||
|
||||
monitor.run()
|
||||
|
||||
@ -69,10 +69,9 @@ pkgs.testers.runNixOSTest {
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = lib.getExe (
|
||||
pkgs.writers.writePython3Bin "mock-jellyfin" { } ''
|
||||
pkgs.writers.writePython3Bin "mock-jellyfin" { flakeIgnore = [ "E501" ]; } ''
|
||||
import http.server
|
||||
import socketserver
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
@ -83,24 +82,42 @@ pkgs.testers.runNixOSTest {
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
|
||||
# Check if we should simulate streaming
|
||||
streaming_file = '/tmp/jellyfin_streaming'
|
||||
if os.path.exists(streaming_file):
|
||||
# Simulate external streaming session
|
||||
state = getattr(self.server, 'test_state', {
|
||||
'streaming': False,
|
||||
'paused': False,
|
||||
'local': False,
|
||||
'media_type': 'Movie'
|
||||
})
|
||||
|
||||
if state['streaming']:
|
||||
if state['local']:
|
||||
remote_ip = '192.168.1.100'
|
||||
else:
|
||||
remote_ip = '203.0.113.42'
|
||||
|
||||
# Map media types to names
|
||||
type_names = {
|
||||
'Movie': 'Test Movie',
|
||||
'Episode': 'Test Episode S01E01',
|
||||
'Video': 'Test Video',
|
||||
'Audio': 'Test Song'
|
||||
}
|
||||
|
||||
sessions = [{
|
||||
'Id': 'test-session-1',
|
||||
'UserName': 'ExternalUser',
|
||||
'RemoteEndPoint': '203.0.113.42', # External IP
|
||||
'RemoteEndPoint': remote_ip,
|
||||
'NowPlayingItem': {
|
||||
'Name': 'Test Movie',
|
||||
'Type': 'Movie'
|
||||
'Name': type_names.get(
|
||||
state['media_type'], 'Test Content'
|
||||
),
|
||||
'Type': state['media_type']
|
||||
},
|
||||
'PlayState': {
|
||||
'IsPaused': False
|
||||
'IsPaused': state['paused']
|
||||
}
|
||||
}]
|
||||
else:
|
||||
# No streaming sessions
|
||||
sessions = []
|
||||
|
||||
self.wfile.write(json.dumps(sessions).encode())
|
||||
@ -108,6 +125,52 @@ pkgs.testers.runNixOSTest {
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
if self.path.startswith('/control/'):
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
post_data = self.rfile.read(content_length)
|
||||
data = json.loads(post_data.decode()) if post_data else {}
|
||||
|
||||
if not hasattr(self.server, 'test_state'):
|
||||
self.server.test_state = {
|
||||
'streaming': False,
|
||||
'paused': False,
|
||||
'local': False,
|
||||
'media_type': 'Movie'
|
||||
}
|
||||
|
||||
if self.path == '/control/state':
|
||||
# Set complete state
|
||||
self.server.test_state.update(data)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'OK')
|
||||
state_str = str(self.server.test_state)
|
||||
print(f'Jellyfin Mock: State updated to {state_str}')
|
||||
elif self.path == '/control/reset':
|
||||
# Reset to default state
|
||||
self.server.test_state = {
|
||||
'streaming': False,
|
||||
'paused': False,
|
||||
'local': False,
|
||||
'media_type': 'Movie'
|
||||
}
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'OK')
|
||||
print('Jellyfin Mock: State reset')
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
except Exception as e:
|
||||
print(f'Jellyfin Mock: Control error: {e}')
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
print(f'Jellyfin Mock: {format % args}')
|
||||
|
||||
@ -147,18 +210,33 @@ pkgs.testers.runNixOSTest {
|
||||
server.wait_for_open_port(8080) # Mock qBittorrent
|
||||
|
||||
import time
|
||||
import json
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
# TEST 1: Verify initial state - no streaming, normal speed limits
|
||||
qbt_result = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||
assert qbt_result.strip() == "0", "qBittorrent should start with normal speed limits"
|
||||
# Helper function to set mock server state
|
||||
def set_jellyfin_state(streaming=False, paused=False, local=False, media_type="Movie"):
|
||||
state = {
|
||||
"streaming": streaming,
|
||||
"paused": paused,
|
||||
"local": local,
|
||||
"media_type": media_type
|
||||
}
|
||||
server.succeed(f"curl -s -X POST -H 'Content-Type: application/json' -d '{json.dumps(state)}' http://localhost:8096/control/state")
|
||||
|
||||
# Helper function to get current qBittorrent throttling state
|
||||
def get_throttling_state():
|
||||
result = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||
return result.strip() == "1"
|
||||
|
||||
print("\\nTesting initial state...")
|
||||
assert not get_throttling_state(), "qBittorrent should start with normal speed limits"
|
||||
|
||||
# Verify no streaming sessions initially
|
||||
sessions_result = server.succeed("curl -s http://localhost:8096/Sessions")
|
||||
print(f"Initial Jellyfin sessions: {sessions_result}")
|
||||
assert "[]" in sessions_result, "Should be no streaming sessions initially"
|
||||
|
||||
# Start the monitor
|
||||
# Start the monitor with fast delays for testing
|
||||
python_path = "${pkgs.python3.withPackages (ps: with ps; [ requests ])}/bin/python"
|
||||
monitor_path = "${../services/jellyfin-qbittorrent-monitor.py}"
|
||||
server.succeed(f"""
|
||||
@ -166,36 +244,65 @@ pkgs.testers.runNixOSTest {
|
||||
--setenv=JELLYFIN_URL=http://localhost:8096 \\
|
||||
--setenv=QBITTORRENT_URL=http://localhost:8080 \\
|
||||
--setenv=CHECK_INTERVAL=2 \\
|
||||
--setenv=STREAMING_START_DELAY=2 \\
|
||||
--setenv=STREAMING_STOP_DELAY=3 \\
|
||||
{python_path} {monitor_path}
|
||||
""")
|
||||
|
||||
# Wait for monitor to start
|
||||
time.sleep(3)
|
||||
|
||||
# TEST 2: Verify monitor runs and keeps normal limits with no streaming
|
||||
qbt_result = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||
assert qbt_result.strip() == "0", "Should maintain normal speed limits with no streaming"
|
||||
# Define test scenarios
|
||||
media_types = ["Movie", "Episode", "Video", "Audio"]
|
||||
playback_states = [False, True] # False=playing, True=paused
|
||||
network_locations = [False, True] # False=external, True=local
|
||||
|
||||
# TEST 3: Simulate external streaming and verify throttling
|
||||
print("\\nSimulating external streaming session...")
|
||||
server.succeed("touch /tmp/jellyfin_streaming") # Signal mock to return streaming session
|
||||
test_count = 0
|
||||
for media_type in media_types:
|
||||
for is_paused in playback_states:
|
||||
for is_local in network_locations:
|
||||
test_count += 1
|
||||
print(f"\\nTest {test_count}: {media_type}, {'paused' if is_paused else 'playing'}, {'local' if is_local else 'external'}")
|
||||
|
||||
# Wait for monitor to detect streaming and apply throttling (includes hysteresis delay)
|
||||
time.sleep(15)
|
||||
# Set streaming state
|
||||
set_jellyfin_state(streaming=True, paused=is_paused, local=is_local, media_type=media_type)
|
||||
time.sleep(6) # Wait for monitor to detect and apply changes
|
||||
|
||||
qbt_result_streaming = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||
throttling_active = get_throttling_state()
|
||||
|
||||
# Check if throttling was enabled
|
||||
assert qbt_result_streaming.strip() == "1", "External streaming should enable qBittorrent throttling"
|
||||
# Determine expected behavior:
|
||||
# Throttling should be active only if:
|
||||
# - Not paused AND
|
||||
# - Not local AND
|
||||
# - Media type is video (Movie, Episode, Video) - NOT Audio
|
||||
should_throttle = (
|
||||
not is_paused and
|
||||
not is_local and
|
||||
media_type in ["Movie", "Episode", "Video"]
|
||||
)
|
||||
|
||||
# TEST 4: Stop streaming and verify throttling is removed
|
||||
print("\\nStopping streaming session...")
|
||||
server.succeed("rm -f /tmp/jellyfin_streaming") # Signal mock to return no sessions
|
||||
if should_throttle:
|
||||
assert throttling_active, f"Expected throttling for {media_type}, {'paused' if is_paused else 'playing'}, {'local' if is_local else 'external'}"
|
||||
else:
|
||||
assert not throttling_active, f"Expected no throttling for {media_type}, {'paused' if is_paused else 'playing'}, {'local' if is_local else 'external'}"
|
||||
|
||||
# Wait for monitor to detect no streaming and remove throttling (includes hysteresis delay)
|
||||
time.sleep(65)
|
||||
set_jellyfin_state(streaming=False)
|
||||
time.sleep(7) # Wait for stop delay
|
||||
|
||||
qbt_result_no_streaming = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||
assert qbt_result_no_streaming.strip() == "0", "No streaming should disable qBittorrent throttling"
|
||||
assert not get_throttling_state(), "No streaming should disable throttling"
|
||||
|
||||
# Start with throttling-enabled state
|
||||
set_jellyfin_state(streaming=True, paused=False, local=False, media_type="Movie")
|
||||
time.sleep(6)
|
||||
assert get_throttling_state(), "Should enable throttling for external Movie"
|
||||
|
||||
# Switch to paused (should disable throttling)
|
||||
set_jellyfin_state(streaming=True, paused=True, local=False, media_type="Movie")
|
||||
time.sleep(6)
|
||||
assert not get_throttling_state(), "Should disable throttling when paused"
|
||||
|
||||
# Switch back to playing (should re-enable throttling)
|
||||
set_jellyfin_state(streaming=True, paused=False, local=False, media_type="Movie")
|
||||
time.sleep(6)
|
||||
assert get_throttling_state(), "Should re-enable throttling when unpaused"
|
||||
'';
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user