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>
3.9 KiB
Qobuz pacing ("budget gate") — design
Date: 2026-07-15
Problem
Bulk hi-res downloading got the Qobuz account blocked (403 USER_BLOCKED). We need to keep
using Qobuz for hi-res but at a volume and cadence that looks like a human listener, so the
account isn't flagged again — while a large wanted-list still drains promptly via other sources.
Approach: a per-job Qobuz eligibility gate
One gate decides, per album, whether Qobuz is an eligible source right now. If the gate is closed, Qobuz candidates are dropped before ranking, so the worker falls through to the best non-Qobuz source (Soulseek/YouTube) and keeps moving — no idling. Monitored albums grabbed that way are upgraded to Qobuz hi-res later, automatically, via the existing quality-upgrade window, once Qobuz has budget again.
Qobuz is eligible for a job only if ALL hold:
- Active hours — current hour ∈ [activeStart, activeEnd) (a daytime window; start < end).
- Under today's cap — Qobuz downloads counted today < today's cap.
- Spacing elapsed — now ≥ the randomized "next allowed" time set after the last Qobuz download.
After each successful Qobuz download the worker bumps today's counter and sets the next-allowed time. The counter resets at the day boundary (tracked by a stored date).
Today's cap = warm-up ramp of the steady cap, by account age (days since warmupStartDate):
| Account age (days) | Cap |
|---|---|
| 0–2 | min(5, steady) |
| 3–6 | min(10, steady) |
| 7–13 | min(20, steady) |
| 14+ | steady |
If warmupStartDate is unset, no ramp (steady cap applies).
Spacing / jitter (combined): the gap to the next Qobuz download is randomized around
activeWindowSeconds / todayCap (so the day's budget spreads evenly across active hours), ±30%,
with a hard floor (≥ 60 s). This is the jitter — never a fixed metronome — and it prevents
front-loading the whole daily cap into the first hour.
Configuration (Config keys; editable in Settings → Qobuz → Pacing)
Settings:
qobuz.pacing.enabled(defaulttrue)qobuz.pacing.activeStartHour(default8)qobuz.pacing.activeEndHour(default23)qobuz.pacing.dailyCap(steady cap, default40)qobuz.pacing.warmupStartDate(ISO date; set to the new account's start day)
Worker-managed state (not user-edited):
qobuz.pacing.countDay(date the counter is for)qobuz.pacing.countToday(int)qobuz.pacing.nextAllowedAt(epoch seconds)
Current hour / day come from the DB clock (now()), avoiding container-timezone ambiguity.
Config is read fresh each worker loop, so changes take effect with no restart.
Worker integration
- New module
worker/lyra_worker/qobuz_gate.py: aQobuzPacingconfig dataclass (from_config), a puretoday_cap(...), a pureis_allowed(...)decision, and thin DB helpersgate_open(conn, now)(reads state + decides) andrecord_download(conn, pacing, now)(bumps counter + sets next-allowed). pipeline.py: after searching sources, ifgate_openis false AND at least one non-Qobuz candidate exists, drop Qobuz candidates before ranking (fall-through). If Qobuz is the only source available, keep it (a Qobuz-only setup isn't gated — holding would strand the album).- After a successful download where
winner.source == "qobuz", callrecord_download.
Testing
Pure-logic unit tests for is_allowed (each gate condition), today_cap (ramp boundaries), and
the spacing computation; pipeline tests that a gated Qobuz candidate is dropped in favour of a
non-Qobuz source, that an open gate keeps Qobuz, and that a successful Qobuz download records the
counter/next-allowed.
Out of scope
- Holding/parking an album for later Qobuz when Qobuz is the only source (fails/last-source as today). Multi-source setups fall through, which is the common case.
- Timezone-aware active-hours windows that wrap past midnight (start < end only).