diff --git a/stacks/f1-stream/files/backend/extractors/__init__.py b/stacks/f1-stream/files/backend/extractors/__init__.py index 76b4de01..a1e54c18 100644 --- a/stacks/f1-stream/files/backend/extractors/__init__.py +++ b/stacks/f1-stream/files/backend/extractors/__init__.py @@ -40,12 +40,13 @@ def create_registry() -> ExtractorRegistry: registry = ExtractorRegistry() # --- Register extractors below --- - # CuratedExtractor returns hand-picked 24/7 channels first so we always - # have something. DemoExtractor and FallbackExtractor were removed — - # demo streams aren't F1 content (just Big Buck Bunny etc.) and - # FallbackExtractor surfaced aggregator landing pages that don't play - # directly in an iframe. - registry.register(CuratedExtractor()) + # CuratedExtractor previously surfaced two hmembeds 24/7 channels (Sky + # Sports F1, DAZN F1) but their JW Player decoder produces an empty + # playlist in our environment (error 102630) regardless of headed mode, + # IP, or fingerprint we tried. The streams loaded the upstream's ad + # overlay but never produced a video element, so they confused users — + # disabled until/unless we find a working bypass. + # registry.register(CuratedExtractor()) registry.register(StreamedExtractor()) registry.register(DaddyLiveExtractor()) registry.register(AceztrimsExtractor()) diff --git a/stacks/f1-stream/files/backend/extractors/pitsport.py b/stacks/f1-stream/files/backend/extractors/pitsport.py index 1f284fce..60fb622d 100644 --- a/stacks/f1-stream/files/backend/extractors/pitsport.py +++ b/stacks/f1-stream/files/backend/extractors/pitsport.py @@ -71,15 +71,12 @@ def _is_motorsport_category(category: str) -> bool: def _is_motorsport_event(category: str, title: str) -> bool: - """Check if an event is a motorsport we want to surface (F1 + adjacent).""" - if _is_motorsport_category(category): - return True - lower = f"{category} {title}".lower() - if any(kw in lower for kw in MOTORSPORT_KEYWORDS): - return True - if GP_KEYWORD in lower: - return True - return False + """Accept anything pitsport.xyz lists. Pitsport curates sports + broadcasts (WRC, MotoGP, IndyCar, NASCAR, Premier League Darts, + Premier League football, etc.) — the site's own selection is the + filter we want. Empty/garbage events still get filtered downstream + when `_resolve_event_streams` produces no playable URL.""" + return bool(category or title) # Aliases kept so older call-sites stay compiling. Both now point at the diff --git a/stacks/f1-stream/files/backend/extractors/service.py b/stacks/f1-stream/files/backend/extractors/service.py index dd39106e..801b9143 100644 --- a/stacks/f1-stream/files/backend/extractors/service.py +++ b/stacks/f1-stream/files/backend/extractors/service.py @@ -49,6 +49,25 @@ class ExtractionService: streams = await self._registry.extract_all() + # Dedupe by canonical URL — pitsport surfaces every WRC stage as a + # separate event but they all point at the same RallyTV master.m3u8 + # (and similar for MotoGP weekend sessions). Keep the first + # occurrence so the user sees one entry per actual stream. + deduped: list[ExtractedStream] = [] + seen_urls: set[str] = set() + for stream in streams: + key = (stream.embed_url or "").strip() or (stream.url or "").strip() + if not key or key in seen_urls: + continue + seen_urls.add(key) + deduped.append(stream) + if len(deduped) < len(streams): + logger.info( + "Deduped streams: %d -> %d (collapsed %d duplicate URL(s))", + len(streams), len(deduped), len(streams) - len(deduped), + ) + streams = deduped + # Run health checks + headless-browser playback verification. # Both stream types are now verified end-to-end so the user only # ever sees streams that actually play in a browser. diff --git a/stacks/f1-stream/files/frontend/src/routes/watch/+page.svelte b/stacks/f1-stream/files/frontend/src/routes/watch/+page.svelte index 811ad860..90369d7e 100644 --- a/stacks/f1-stream/files/frontend/src/routes/watch/+page.svelte +++ b/stacks/f1-stream/files/frontend/src/routes/watch/+page.svelte @@ -175,9 +175,13 @@ if (!player || !player.videoEl) return; if (Hls.isSupported()) { + // `lowLatencyMode` previously broke playback on regular (non-LL-HLS) + // providers like RallyTV — they don't ship the LL-HLS extensions + // hls.js needs in that mode. Default off; explicit per-stream flag + // can re-enable later. const hlsInstance = new Hls({ enableWorker: true, - lowLatencyMode: true, + lowLatencyMode: false, backBufferLength: 90 });