Files
Lyra/worker/tests/test_qobuz_gate_pipeline.py
T
Jonathan 67f374c470 feat: Qobuz pacing budget gate (human-like volume + cadence)
Avoid re-triggering a USER_BLOCKED account by capping Qobuz to a human-looking
volume and cadence. A per-album gate (worker/lyra_worker/qobuz_gate.py) allows
Qobuz only within active hours, under today's cap (a warm-up ramp of a steady
cap), and past a randomized spacing since the last Qobuz download; the gap spreads
the day's budget across active hours with jitter. When the gate is closed the
pipeline drops Qobuz candidates so the job falls through to another source (and
monitored albums upgrade to Qobuz later); a Qobuz-only setup is never gated.
Config keys read live each loop; counters reset at the day boundary via the DB
clock. Tunable in Settings -> Qobuz -> Pacing (new /api/qobuz/pacing route).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:45:56 +02:00

86 lines
3.3 KiB
Python

"""The Qobuz budget gate, wired into the pipeline: when the gate is closed the job falls through
to another source; when open it uses Qobuz and records the download; a Qobuz-only setup is never
gated (nothing to fall through to)."""
from lyra_worker.adapters.fakes import FakeQobuz, FakeSoulseek
from lyra_worker.claim import claim_next
from lyra_worker.pipeline import run_pipeline
from tests.conftest import insert_request
def _cfg(conn, key, value):
with conn.cursor() as cur:
cur.execute(
'INSERT INTO "Config"(key,value,secret,"updatedAt") VALUES (%s,%s,false,now()) '
'ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value, "updatedAt"=now()',
(key, value),
)
conn.commit()
def _open_gate(conn):
_cfg(conn, "qobuz.pacing.activeStartHour", "0")
_cfg(conn, "qobuz.pacing.activeEndHour", "24")
def _closed_gate(conn):
_cfg(conn, "qobuz.pacing.activeStartHour", "5") # empty window (5..5) → always closed
_cfg(conn, "qobuz.pacing.activeEndHour", "5")
def _state(conn, job_id):
with conn.cursor() as cur:
cur.execute('SELECT state FROM "Job" WHERE id = %s', (job_id,))
return cur.fetchone()[0]
def _chosen(conn, job_id):
with conn.cursor() as cur:
cur.execute('SELECT source FROM "Candidate" WHERE "jobId" = %s AND chosen = true', (job_id,))
row = cur.fetchone()
return row[0] if row else None
def _cfg_get(conn, key):
with conn.cursor() as cur:
cur.execute('SELECT value FROM "Config" WHERE key = %s', (key,))
row = cur.fetchone()
return row[0] if row else None
def test_closed_gate_drops_qobuz_and_falls_through(conn):
_closed_gate(conn)
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
claim_next(conn)
run_pipeline(conn, job_id, [FakeQobuz(), FakeSoulseek()], dest_root="/tmp/lib-gate")
assert _state(conn, job_id) == "imported"
assert _chosen(conn, job_id) == "soulseek" # Qobuz dropped by the closed gate
def test_open_gate_uses_qobuz_and_records_the_download(conn):
_open_gate(conn)
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
claim_next(conn)
run_pipeline(conn, job_id, [FakeQobuz(), FakeSoulseek()], dest_root="/tmp/lib-gate")
assert _state(conn, job_id) == "imported"
assert _chosen(conn, job_id) == "qobuz"
assert _cfg_get(conn, "qobuz.pacing.countToday") == "1" # recorded against today's budget
assert _cfg_get(conn, "qobuz.pacing.nextAllowedAt") is not None
def test_qobuz_only_setup_is_never_gated(conn):
_closed_gate(conn)
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
claim_next(conn)
run_pipeline(conn, job_id, [FakeQobuz()], dest_root="/tmp/lib-gate")
assert _state(conn, job_id) == "imported"
assert _chosen(conn, job_id) == "qobuz" # not dropped — nothing to fall through to
def test_non_qobuz_download_does_not_touch_the_counter(conn):
_closed_gate(conn) # forces Soulseek
job_id = insert_request(conn, artist="Radiohead", album="In Rainbows")
claim_next(conn)
run_pipeline(conn, job_id, [FakeQobuz(), FakeSoulseek()], dest_root="/tmp/lib-gate")
assert _chosen(conn, job_id) == "soulseek"
assert _cfg_get(conn, "qobuz.pacing.countToday") is None # only Qobuz downloads count