From bf123fed63c3072f965eea5d93e40530b5072f65 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:41:48 -0700 Subject: [PATCH] Reject Qobuz 30-second sample/preview downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-layer detection: (1) check the Qobuz API response for sample=True before downloading, and (2) validate actual file duration with mutagen after download — if under 35 seconds, delete and return None. Qobuz returns valid audio files for previews (~2-5MB FLAC) that pass the existing 100KB size check, so duration is the reliable signal. --- core/qobuz_client.py | 23 +++++++++++++++++++++++ webui/static/helper.js | 1 + 2 files changed, 24 insertions(+) diff --git a/core/qobuz_client.py b/core/qobuz_client.py index 61a550d8..ed5dfaa1 100644 --- a/core/qobuz_client.py +++ b/core/qobuz_client.py @@ -974,6 +974,12 @@ class QobuzClient: logger.error("No Qobuz stream available at any quality") return None + # Qobuz returns sample=True for 30-second previews (no subscription or region-restricted) + if stream_data.get('sample', False): + logger.warning(f"Qobuz returned a 30s sample for '{display_name}' — " + f"track may require a Qobuz subscription or is region-restricted. Skipping.") + return None + download_url = stream_data['url'] # Determine file extension from stream response @@ -1065,6 +1071,23 @@ class QobuzClient: out_path.unlink(missing_ok=True) return None + # Safety net: detect 30-second samples by checking actual file duration. + # Qobuz previews are valid audio files (~2-5MB) that pass the size check above. + try: + from mutagen import File as MutagenFile + audio = MutagenFile(str(out_path)) + if audio and audio.info and audio.info.length: + duration_s = audio.info.length + if duration_s < 35: + logger.warning( + f"Qobuz download is only {duration_s:.0f}s — likely a 30s sample/preview " + f"for '{display_name}'. Deleting." + ) + out_path.unlink(missing_ok=True) + return None + except Exception as e: + logger.debug(f"Could not check audio duration (non-fatal): {e}") + final_size = out_path.stat().st_size if out_path.exists() else 0 logger.info(f"Qobuz download complete: {out_path} ({final_size / (1024*1024):.1f} MB)") return str(out_path) diff --git a/webui/static/helper.js b/webui/static/helper.js index 4e0ba0cf..c19c68ef 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3610,6 +3610,7 @@ const WHATS_NEW = { { title: 'Fix "Replace Lower Quality" Setting Not Persisting', desc: 'The import section appeared twice in the settings save payload — the second instance (with only staging_path) overwrote the first (with replace_lower_quality). Merged into a single block' }, { title: 'Inbound Music Request API', desc: 'New POST /api/v1/request endpoint — trigger downloads from Discord bots, Home Assistant, curl, or any external tool. Async with status polling and optional notify_url callback. New "Webhook Received" automation trigger and "Search & Download" action in the Automation Hub' }, { title: 'Fix Spotify Enrichment Worker Infinite Loop', desc: 'Artists with an existing Spotify ID but no match status got stuck in the enrichment queue — the worker processed them every 3 seconds forever without marking them as done. Now correctly marks them as matched' }, + { title: 'Reject Qobuz 30-Second Samples', desc: 'Qobuz previews (30s samples for tracks requiring a subscription or region-restricted) are now detected and rejected. Checks the API sample flag before downloading, and validates file duration after download as a safety net' }, // --- April 14, 2026 --- { date: 'April 14, 2026' },