Files
ohm_streaming/app/scrapers/hosters/vidzy.py
T
Roman 79a44297fd Ajout de la source French-Stream (french-stream.lat) : séries et films
- Scraper DataLife Engine : recherche AJAX, fiches par saison, épisodes via
  ep-data.php (VF/VOSTFR), films via film_api.php, nouveautés /series/
- Config YAML externalisée (sélecteurs réparables sans coder)
- Domaines étendus : uqload.vc, vidzy.cc/.live
- 11 tests (126 au total)
2026-09-25 14:08:10 +00:00

79 lines
2.8 KiB
Python

"""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<b64>[A-Za-z0-9+/=]{50,})\"",
)
_CLEAR_PATTERNS = (
re.compile(r'sources\s*:\s*\[\s*\{\s*src\s*:\s*["\'](?P<url>https?://[^"\']+?\.m3u8[^"\']*)["\']'),
re.compile(r'_fsvHls\s*=\s*"(?P<url>https?://[^"]+?\.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,
)