From 4518aff71c3d843b8fc33d37f78d34ce638fb692 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 7 May 2026 23:16:39 +0000 Subject: [PATCH] =?UTF-8?q?f1-stream:=20Stremio=20addon=20extractor=20?= =?UTF-8?q?=E2=80=94=20TvVoo=20+=20StremVerse=20Sky=20F1=20/=20DAZN=20F1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 parallel research agents surveyed Stremio addons, F1 TV / Sky / DAZN official APIs, IPTV M3U lists, and free-to-air broadcasters. The clean finding: two community Stremio addons already index Sky Sports F1 + DAZN F1 via their public HTTP APIs — no Stremio client required, just GET /stream//.json on the addon's hosted instance. New `stremio.py` extractor pulls from: - **TvVoo** (`https://tvvoo.hayd.uk/manifest.json`) — wraps Vavoo IPTV. Lists Sky Sports F1 UK + Sky Sports F1 HD + Sky Sport F1 IT + Sky Sport F1 HD DE + DAZN F1 ES. Returns 2 IP-bound m3u8 URLs per channel. Source: github.com/qwertyuiop8899/tvvoo. Vavoo's CDN SSL certs are currently expired so most clients fail verification today — addon framework is right but delivery is degraded. - **StremVerse** (`https://stremverse.onrender.com/manifest.json`) — Returns 11+ streams per id (`stremevent_591` = F1, `stremevent_866` = MotoGP). Mix of DRM-walled DASH, JW-broken-chain JWT URLs, and HuggingFace-Space proxies that 404 without a per-instance api_password. The extractor surfaces 15 candidate URLs per run; verifier filters to the playable subset. Today that subset is 0 (Vavoo cert expiry + JW chain + proxy auth), but the wiring is correct: as the addons fix delivery or rotate to fresh URLs, candidates will start passing. Other agent findings worth noting (not coded but documented): - F1 TV Pro live = Widevine DASH; impossible without a CDM. VOD is clean HLS but only post-session. - Sky Go / DAZN / Viaplay / Canal+ = all Widevine + geo-fenced + active DMCA enforcement. Pursuing not feasible. - ServusTV AT (free F1 race weekends) = clean public HLS at rbmn-live.akamaized.net/hls/live/2002825/geoSTVATweb/master.m3u8 but geo-fenced; needs an Austrian-IP egress proxy/VPN. - iptv-org/iptv has an F1 Channel (Pluto TV IE) at jmp2.uk/plu-6661739641af6400080cd8f1.m3u8 — 24/7 free, BG works, but only historic races + shoulder programming. Worth adding as a curated entry later. - boxboxbox.* (community-favourite F1 race-weekend domain) is dead across all known TLDs as of today. Co-Authored-By: Claude Opus 4.7 --- .../files/backend/extractors/__init__.py | 5 + .../files/backend/extractors/stremio.py | 161 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 stacks/f1-stream/files/backend/extractors/stremio.py diff --git a/stacks/f1-stream/files/backend/extractors/__init__.py b/stacks/f1-stream/files/backend/extractors/__init__.py index 3f6194b8..72e4d667 100644 --- a/stacks/f1-stream/files/backend/extractors/__init__.py +++ b/stacks/f1-stream/files/backend/extractors/__init__.py @@ -15,6 +15,7 @@ from backend.extractors.aceztrims import AceztrimsExtractor from backend.extractors.chrome_browser import ChromeBrowserExtractor from backend.extractors.curated import CuratedExtractor from backend.extractors.dd12 import DD12Extractor +from backend.extractors.stremio import StremioAddonExtractor from backend.extractors.subreddit import SubredditExtractor from backend.extractors.daddylive import DaddyLiveExtractor from backend.extractors.discord_source import DiscordExtractor @@ -63,6 +64,10 @@ def create_registry() -> ExtractorRegistry: # JW Player file URL. The site embeds the m3u8 in HTML so curl-based # parsing is enough — no browser needed. registry.register(DD12Extractor()) + # StremioAddonExtractor calls Stremio addon HTTP APIs (TvVoo, StremVerse) + # which already index Sky F1 / DAZN F1 / Vavoo IPTV channels. No + # Stremio client needed — just /stream//.json calls. + registry.register(StremioAddonExtractor()) registry.register(DaddyLiveExtractor()) registry.register(AceztrimsExtractor()) registry.register(PitsportExtractor()) diff --git a/stacks/f1-stream/files/backend/extractors/stremio.py b/stacks/f1-stream/files/backend/extractors/stremio.py new file mode 100644 index 00000000..3d046677 --- /dev/null +++ b/stacks/f1-stream/files/backend/extractors/stremio.py @@ -0,0 +1,161 @@ +"""Stremio-addon-driven extractor. + +Stremio addons expose a public HTTP API: each addon has a manifest at +`/manifest.json` and per-resource endpoints like +`/stream//.json` returning `{streams:[{url,name,...}]}`. + +This extractor calls a curated set of live-TV addons that surface F1 +and Sky-Sports-class motorsport channels. We treat each returned URL as +an ExtractedStream and let the playback verifier confirm playability. +We don't need a Stremio client — we just call the documented HTTP API. + +Findings from initial research (2026-05-07): +- **TvVoo** (`tvvoo.hayd.uk`) — wraps the Vavoo IPTV network, lists + Sky Sports F1 (UK + IT + DE), DAZN F1, Movistar F1, Canal+ F1, + Viaplay F1. The returned m3u8 URLs are IP-bound at the Vavoo CDN + (`*.ngolpdkyoctjcddxshli469r.org/sunshine/...`); they're tokenised + to whichever IP fetched the manifest. Currently their SSL certs have + expired which fails most clients — the addon framework is right but + delivery is degraded today. +- **StremVerse** (`stremverse.onrender.com`) — returns 11+ streams per + catalog id (`stremevent_591`=F1, `stremevent_866`=MotoGP). Mix of + DRM-walled DASH, JW-Player-broken-chain JWT, and apar151 HuggingFace + proxy URLs. Master playlists parse; variant URLs sometimes return 404 + if they're meant to be resolved by the addon's player rather than + directly. + +Adding a new addon = one entry in `_ADDONS`. Each addon's resolver only +needs the manifest + stream endpoints; the addon does the heavy lifting. +""" + +import asyncio +import logging +from dataclasses import dataclass +from typing import Iterable + +import httpx + +from backend.extractors.base import BaseExtractor +from backend.extractors.models import ExtractedStream + +logger = logging.getLogger(__name__) + +USER_AGENT = ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/17.4 Safari/605.1.15" +) + + +@dataclass(frozen=True) +class _Addon: + name: str + base: str # e.g. "https://tvvoo.hayd.uk" + stream_ids: tuple[tuple[str, str, str], ...] + """(stream_type, stream_id, label) per F1/motorsport entry.""" + + +# Curated addon list — see module docstring. These IDs are documented in +# the addons' manifests / channel lists. Update when channel names/IDs +# rotate. +_ADDONS: tuple[_Addon, ...] = ( + _Addon( + name="TvVoo", + base="https://tvvoo.hayd.uk", + stream_ids=( + ("tv", "vavoo_SKY%20SPORTS%20F1|group:uk", "Sky Sports F1 UK (Vavoo)"), + ("tv", "vavoo_SKY%20SPORTS%20F1%20HD|group:uk", "Sky Sports F1 HD UK (Vavoo)"), + ("tv", "vavoo_SKY%20SPORT%20F1|group:it", "Sky Sport F1 IT (Vavoo)"), + ("tv", "vavoo_SKY%20SPORT%20F1%20HD|group:de", "Sky Sport F1 DE (Vavoo)"), + ("tv", "vavoo_DAZN%20F1|group:es", "DAZN F1 ES (Vavoo)"), + ), + ), + _Addon( + name="StremVerse", + base="https://stremverse.onrender.com", + stream_ids=( + ("tv", "stremevent_591", "Formula 1 (StremVerse)"), + ("tv", "stremevent_866", "MotoGP (StremVerse)"), + ), + ), +) + + +class StremioAddonExtractor(BaseExtractor): + """Pull F1 + Sky-class motorsport URLs from public Stremio addons.""" + + @property + def site_key(self) -> str: + return "stremio" + + @property + def site_name(self) -> str: + return "Stremio Addon" + + async def extract(self) -> list[ExtractedStream]: + async with httpx.AsyncClient( + timeout=15.0, + follow_redirects=True, + headers={"User-Agent": USER_AGENT}, + # Some addons (TvVoo→Vavoo) hand back URLs whose origin certs + # are expired; honest-default verify=True is preserved here so + # the verifier sees the same TLS errors a browser would. + ) as client: + tasks = [] + for addon in _ADDONS: + for stype, sid, label in addon.stream_ids: + tasks.append(self._resolve(client, addon, stype, sid, label)) + results = await asyncio.gather(*tasks, return_exceptions=True) + + streams: list[ExtractedStream] = [] + for r in results: + if isinstance(r, Exception): + logger.debug("[stremio] resolve failed: %s", r) + continue + streams.extend(r) + + logger.info("[stremio] surfaced %d candidate stream URL(s) across %d addon(s)", + len(streams), len(_ADDONS)) + return streams + + async def _resolve( + self, client: httpx.AsyncClient, addon: _Addon, + stype: str, sid: str, label: str, + ) -> list[ExtractedStream]: + url = f"{addon.base}/stream/{stype}/{sid}.json" + try: + resp = await client.get(url) + except Exception as e: + logger.debug("[stremio] %s fetch failed: %s", url, e) + return [] + if resp.status_code != 200: + logger.debug("[stremio] %s -> HTTP %d", url, resp.status_code) + return [] + try: + data = resp.json() + except Exception: + return [] + + out: list[ExtractedStream] = [] + for idx, s in enumerate(data.get("streams") or []): + stream_url = (s.get("url") or "").strip() + if not stream_url: + continue + # Skip DRM-tagged entries — they need Widevine which neither + # our verifier nor a clean hls.js path can play. + if "DRM" in (s.get("name") or "").upper(): + continue + title = label + if idx > 0: + title = f"{label} #{idx + 1}" + out.append( + ExtractedStream( + url=stream_url, + site_key=self.site_key, + site_name=f"{addon.name}", + quality="", + title=title, + stream_type="m3u8", + ) + ) + return out