From 975cf4cf3dc72f7b439569e3898c8a2bf50277b3 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 14 Jun 2026 16:42:25 +0200 Subject: [PATCH] fix(hifi): decline 30s preview manifests, fall through to a real source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some Monochrome instances only have 30-second Tidal DOWNLOAD access: the HLS variant playlist for a 220s track comes back as ~30s of segments + ENDLIST (verified live on us-west.monochrome.tf — lossless=30s, hires=403). The client downloaded that 30s file, which then got quarantined by the new audio guard. Detect it at manifest time: sum the playlist's EXTINF runtime and compare to the track's real duration (get_track_info). When the playlist is < 85% of the track, decline the manifest and rotate the instance, so the download falls through to a real source (Soulseek/Qobuz/Tidal/Deezer) instead of fetching a preview. Best-effort — unknown duration disables the check (the post-download audio guard remains the safety net). Co-Authored-By: Claude Opus 4.8 --- core/hifi_client.py | 54 ++++++++++++++++++++- tests/test_hifi_preview_detection.py | 70 ++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 tests/test_hifi_preview_detection.py diff --git a/core/hifi_client.py b/core/hifi_client.py index 0497426d..7d085154 100644 --- a/core/hifi_client.py +++ b/core/hifi_client.py @@ -40,6 +40,30 @@ from core.quality.source_map import quality_from_tidal_tier, quality_tier_for_so logger = get_logger("hifi_client") +# A media playlist whose total runtime is below this fraction of the track's +# real duration is a preview (some Monochrome instances only have 30s Tidal +# DOWNLOAD access — a 220s track comes back as ~30s of segments + ENDLIST). +_PREVIEW_DURATION_RATIO = 0.85 +_EXTINF_RE = re.compile(r'#EXTINF:\s*([0-9.]+)') + + +def hls_total_seconds(playlist_text: str) -> float: + """Sum the ``#EXTINF`` segment durations in an HLS media playlist.""" + return sum(float(x) for x in _EXTINF_RE.findall(playlist_text or '')) + + +def is_preview_playlist(playlist_s: float, track_s: float, + ratio: float = _PREVIEW_DURATION_RATIO) -> bool: + """True when the playlist runtime is far shorter than the track's real + duration (a preview). Returns False when either duration is unknown, so a + missing reference never false-positives — the post-download audio guard is + the safety net. + """ + if not playlist_s or not track_s or track_s <= 0: + return False + return playlist_s < track_s * ratio + + # HLS quality presets mapping to /trackManifests/ format parameters HLS_QUALITY_MAP = { 'hires': { @@ -623,7 +647,8 @@ class HiFiClient(DownloadSourcePlugin): return init_uri, segment_uris - def _get_hls_manifest(self, track_id: int, quality: str = 'lossless') -> Optional[Dict]: + def _get_hls_manifest(self, track_id: int, quality: str = 'lossless', + expected_duration_s: float = 0) -> Optional[Dict]: q_info = HLS_QUALITY_MAP.get(quality, HLS_QUALITY_MAP['lossless']) formats = q_info['formats'] @@ -663,6 +688,7 @@ class HiFiClient(DownloadSourcePlugin): logger.warning(f"Failed to parse HLS playlist for track {track_id}: {e}") return None + media_text = playlist_text if '#EXT-X-STREAM-INF' in playlist_text and segment_uris: playlist_uri = segment_uris[0] try: @@ -670,11 +696,26 @@ class HiFiClient(DownloadSourcePlugin): variant_resp = self.session.get(playlist_uri, allow_redirects=True, timeout=30) variant_resp.raise_for_status() variant_text = variant_resp.text + media_text = variant_text init_uri, segment_uris = self._parse_hls_playlist(variant_text, playlist_uri) except Exception as e: logger.warning(f"Failed to fetch variant playlist for track {track_id}: {e}") return None + # Preview detection — some instances only have 30s Tidal DOWNLOAD + # access, returning a playlist far shorter than the real track. Decline + # it (and rotate the instance) so the orchestrator falls through to a + # real source instead of fetching a 30s file that gets quarantined. + playlist_s = hls_total_seconds(media_text) + if is_preview_playlist(playlist_s, expected_duration_s): + logger.warning( + f"HiFi manifest for track {track_id} ({quality}) is a " + f"{playlist_s:.0f}s preview of a {expected_duration_s:.0f}s track — " + f"declining this instance" + ) + self._rotate_instance(self._current_instance) + return None + if init_uri: logger.info(f"HiFi HLS manifest for track {track_id}: " f"init segment + {len(segment_uris)} segments ({quality})") @@ -842,12 +883,21 @@ class HiFiClient(DownloadSourcePlugin): MIN_AUDIO_SIZE = 100 * 1024 + # Real track duration — lets _get_hls_manifest reject 30s preview + # manifests before we download them. Best-effort: 0 disables the check. + try: + _info = self.get_track_info(track_id) or {} + expected_duration_s = float(_info.get('duration_s') or 0) + except Exception: + expected_duration_s = 0 + for q_key in chain: if self.shutdown_check and self.shutdown_check(): logger.info("Shutdown detected, aborting HiFi download") return None - manifest_info = self._get_hls_manifest(track_id, quality=q_key) + manifest_info = self._get_hls_manifest(track_id, quality=q_key, + expected_duration_s=expected_duration_s) if ( not manifest_info or ( diff --git a/tests/test_hifi_preview_detection.py b/tests/test_hifi_preview_detection.py new file mode 100644 index 00000000..ae0bed7e --- /dev/null +++ b/tests/test_hifi_preview_detection.py @@ -0,0 +1,70 @@ +"""HiFi/Monochrome preview detection. + +Some Monochrome instances only have 30-second Tidal preview access for +usage=DOWNLOAD: the HLS variant playlist for a 220s track contains only ~30s +of segments (with #EXT-X-ENDLIST). Detect that at manifest time so HiFi +declines and the orchestrator falls through to a real source — instead of +downloading the 30s file and quarantining it after the fact. +""" + +import sys +import types + +import pytest + +# hifi_client imports config/db at module load; stub the heavy bits if needed. +from core.hifi_client import hls_total_seconds, is_preview_playlist + + +_PREVIEW_PLAYLIST = """#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:4 +#EXTINF:3.994, +seg1.mp4 +#EXTINF:3.994, +seg2.mp4 +#EXTINF:3.994, +seg3.mp4 +#EXTINF:3.994, +seg4.mp4 +#EXTINF:3.994, +seg5.mp4 +#EXTINF:3.994, +seg6.mp4 +#EXTINF:3.994, +seg7.mp4 +#EXTINF:1.9, +seg8.mp4 +#EXT-X-ENDLIST +""" + + +def test_total_seconds_sums_extinf(): + assert hls_total_seconds(_PREVIEW_PLAYLIST) == pytest.approx(29.86, abs=0.1) + + +def test_total_seconds_empty(): + assert hls_total_seconds("") == 0.0 + assert hls_total_seconds("#EXTM3U\nno segments\n") == 0.0 + + +def test_preview_when_playlist_far_shorter_than_track(): + # 30s playlist for a 220s track → preview. + assert is_preview_playlist(playlist_s=29.9, track_s=220) is True + + +def test_not_preview_when_playlist_matches_track(): + assert is_preview_playlist(playlist_s=218.0, track_s=220) is False + + +def test_not_preview_when_track_duration_unknown(): + # No reference → can't decide; don't false-positive (post-download guard + # is the safety net). + assert is_preview_playlist(playlist_s=29.9, track_s=0) is False + assert is_preview_playlist(playlist_s=0, track_s=220) is False + + +def test_short_real_track_not_flagged(): + # A genuinely ~30s track whose playlist is ~30s is not a preview. + assert is_preview_playlist(playlist_s=29.0, track_s=31) is False