"""Extracteur Vidzy (vidzy.org/.cc/.live) — page embed videojs → m3u8. La page embed ne contient pas l'URL vidéo en clair : le script videojs appelle une fonction de décodage inline `atob(s)` + reverse + XOR, avec une graine dérivée du hostname (`somme des codes des caractères & 0xFF`). Si le décodage échoue, la page sert un leurre `https://s1.fsvid.lol/troll/master.m3u8` (même valeur pour tous les épisodes) — on le rejette explicitement. Algorithme (reproduit fidèlement depuis le JS de la page) : b = base64decode(s) ; a = b[::-1] r[i] = chr(a[i] ^ ((0x3D + i*89 + H) & 0xFF)) avec H = sum(ord(hostname)) & 0xFF """ import base64 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.http import fetch logger = logging.getLogger(__name__) _B64_RE = re.compile( r"atob\(s\)(?s:.){0,800}?\}\)\(\"(?P[A-Za-z0-9+/=]{50,})\"", ) _CLEAR_PATTERNS = ( re.compile(r'sources\s*:\s*\[\s*\{\s*src\s*:\s*["\'](?Phttps?://[^"\']+?\.m3u8[^"\']*)["\']'), re.compile(r'_fsvHls\s*=\s*"(?Phttps?://[^"]+?\.m3u8[^"]*)"'), ) _TROLL = "/troll/" def _decode(b64: str, hostname: str) -> str | None: try: data = base64.b64decode(b64) except (ValueError, TypeError): return None seed = sum(ord(c) for c in hostname) & 0xFF decoded = "".join(chr(b ^ ((0x3D + i * 89 + seed) & 0xFF)) for i, b in enumerate(data[::-1])) return decoded if decoded.startswith(("http://", "https://")) else None @register_hoster class VidzyExtractor(HosterExtractor): name = "vidzy" domains = ("vidzy.org", "vidzy.cc", "vidzy.live") async def extract(self, embed_url: str) -> VideoLink: html = await fetch(embed_url) hostname = urlparse(embed_url).hostname or "" for match in _B64_RE.finditer(html): url = _decode(match.group("b64"), hostname) if url and _TROLL not in url: return self._video_link(url, embed_url) logger.warning("vidzy : décodage atob+XOR sans résultat pour %s", embed_url) for pattern in _CLEAR_PATTERNS: for match in pattern.finditer(html): url = match.group("url") if _TROLL not in url: return self._video_link(url, embed_url) raise ScrapeError( f"vidzy : URL vidéo introuvable dans {embed_url} (leurre anti-bot ou page modifiée)" ) def _video_link(self, url: str, embed_url: str) -> VideoLink: return VideoLink( url=url, hoster=self.name, headers={ "Referer": f"https://{urlparse(embed_url).hostname}/", "User-Agent": get_settings().user_agent, }, is_hls=".m3u8" in url, )