"""Extracteur Luluvid/Luluvdo (luluvdo.com, luluvid.com, lulustream.com) — embed → m3u8. Famille StreamSB : la page embed configure jwplayer dans du JS packé (p,a,c,k,e,d) avec `sources:[{file:"https://.../master.m3u8?t="}]`. Le token CDN (tnmr.org…) est lié à l'IP **et à l'User-Agent** de la requête d'embed : la VideoLink doit donc renvoyer le User-Agent du socle pour que le téléchargement passe — sans lui, le CDN répond 403. """ import logging import re from urllib.parse import urlparse from app.config import get_settings from app.scrapers.base import HosterExtractor, ScrapeError, VideoLink, register_hoster from app.scrapers.hosters._packer import unpack_packed_js from app.scrapers.http import fetch logger = logging.getLogger(__name__) _PATTERNS = ( re.compile(r'sources\s*:\s*\[\s*\{[^}]*?file\s*:\s*["\'](?P[^"\']+\.m3u8[^"\']*)["\']'), re.compile(r'file\s*:\s*["\'](?Phttps?://[^"\']+?\.m3u8[^"\']*)["\']'), re.compile(r'["\'](?Phttps?://[^"\']*?\.m3u8(?:\?[^"\']*)?)["\']'), ) @register_hoster class LuluvdoExtractor(HosterExtractor): name = "luluvdo" domains = ("luluvdo.com", "luluvid.com", "lulustream.com") async def extract(self, embed_url: str) -> VideoLink: html = await fetch(embed_url) for content in (html, unpack_packed_js(html) or ""): for pattern in _PATTERNS: for match in pattern.finditer(content): url = match.group("url") if url.startswith("http"): return VideoLink( url=url, hoster=self.name, headers={ "Referer": f"https://{urlparse(embed_url).hostname}/", "User-Agent": get_settings().user_agent, "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8", }, is_hls=".m3u8" in url, ) raise ScrapeError(f"luluvdo : URL vidéo introuvable dans {embed_url}")