goon/app/extractors/tubes/perverzija.py
goon-foss 671ba97b52 fix(perverzija): strona i listing przez proxy przy 403 Cloudflare
Od 2026-10-15 ok. 04:31 UTC Cloudflare oddaje 403 na IPv4 VPS-a (IPv6 200).
Ekstraktor nie patrzyl na status, API dawalo 503 i perverzija nie grala
nikomu; listing zwracal [] jak koniec katalogu, a uzupelnianie dlugosci
palilo proby. HTML przez Bright Data tylko przy 403, player bez zmian.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:16:24 +02:00

118 lines
5.1 KiB
Python

"""perverzija.com (tube.perverzija.com), natywny HLS zamiast WebView.
Ustalone trace'em CDP (2026-07-28, bug-reports 40ec88cc / 92e9133e "zacina się na
pierwszych sekundach i robi loop"):
Strona osadza iframe `pervl4.xtremestream.xyz/player/index.php?data=<md5>`. Samo źródło
tego iframe'a NIE zawiera streamu (jest tam wyłącznie preroll/midroll z opencdn.b-cdn.net
to one leciały w pętli). Player dociąga wideo dopiero w runtime:
GET /player/xs1.php?data=<md5>&q=<480|720> → GOTOWY manifest HLS (#EXTM3U, VOD,
segmenty po ~4,17 s z PEŁNYMI URL-ami do pervl4.xspcdn{01..10}.sa.com/cdn/down/...)
Segmenty mają rozszerzenie `.html` (kamuflaż), ale to zwykły HLS, nic nie trzeba
deszyfrować ani składać. Wystarczy oddać URL `xs1.php` jako `type='m3u8'`.
WAŻNE (mobile): ścieżka manifestu NIE kończy się na `.m3u8`, a expo-video bez contentType
potraktowałby ją jako progressive i padł, dokładnie casus pornhat. Dlatego manifest musi
iść przez `/proxy/hls` (passthrough); segmenty i tak lecą direct z CDN, więc przez VPS
przechodzi ~41 KB, nie wideo.
`referer` celowo wskazuje na `index.php?data=<md5>`, taki Referer wysyła przeglądarka
przy wywołaniu xs1.php i bez niego jest ryzyko 403.
"""
from __future__ import annotations
import logging
import re
from app.config import get_settings
from app.extractors._models import StreamSource
from app.extractors import browser_get
log = logging.getLogger(__name__)
def fetch_perverzija(url: str, *, timeout: float):
"""GET strony perverzija, przy blokadzie Cloudflare przez Bright Data.
2026-10-15 ok. 04:31 UTC Cloudflare zaczal odpowiadac 403 z challenge'em na IPv4 VPS-a
(IPv6 dalej 200), dla stron scen i dla REST-a listingu. Ekstraktor nie patrzyl na status,
szedl w zapas embed-iframe, ten tez dostawal 403, API oddawalo 503 i perverzija
przestala grac wszystkim (zgloszenie 1c662356). Proxy (ryczalt, ISP) tylko dla HTML-a,
player xtremestream i segmenty ida jak dotad.
"""
res = browser_get(url, timeout=timeout)
if res.status_code != 403:
return res
proxy = get_settings().brightdata_proxy_url
if not proxy:
log.warning("perverzija: 403 (Cloudflare) na %s, brak proxy", url)
return res
log.info("perverzija: 403 na %s, ponawiam przez proxy", url)
return browser_get(url, timeout=timeout, proxy=proxy)
# iframe playera: https://pervl4.xtremestream.xyz/player/index.php?data=<32 hex>
_PLAYER_IFRAME_RE = re.compile(
r"https?://(?P<host>[a-z0-9\-]+\.xtremestream\.[a-z]+)/player/index\.php\?data=(?P<id>[0-9a-f]{16,64})",
re.IGNORECASE,
)
# Player wystawia te dwie jakości (trace: xs1.php?...&q=480 i &q=720). Kolejność =
# preferencja (najlepsza pierwsza), tak jak w pozostałych ekstraktorach.
_QUALITIES = ("720", "480")
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
try:
res = fetch_perverzija(page_url, timeout=timeout)
html = res.text if hasattr(res, "text") else str(res)
except Exception as e:
log.info("perverzija page fetch failed %s: %s", page_url, e)
return None
if getattr(res, "status_code", 200) == 403:
# Zapas embed-iframe pobiera strone jeszcze raz bez proxy i tez dostaje 403.
log.warning("perverzija: strona %s nadal 403", page_url)
return None
m = _PLAYER_IFRAME_RE.search(html)
if not m:
# ~10% katalogu siedzi na innym playerze niż xtremestream (próbka 30 stron
# 2026-08-20: 27 xtremestream, 1 mixdrop, 1 playhydrax, 1 nie do pobrania).
# Oddajemy je generycznemu embed-iframe: wychodzi type='hoster', czyli telefon
# resolwuje je po swojej stronie, zamiast żeby scena wyglądała na martwą.
log.info("perverzija: brak xtremestream na %s, probuje embed-iframe", page_url)
from app.extractors.tubes import _embed_iframe
try:
return _embed_iframe.extract(page_url, timeout=timeout)
except Exception as e:
log.info("perverzija: embed-iframe fallback tez padl %s: %s", page_url, e)
return None
host, vid = m.group("host"), m.group("id")
player_url = f"https://{host}/player/index.php?data={vid}"
# Od 2026-10-12 segmenty sa szyfrowane (AES-128, klucz spod tokenu zwiazanego z IP),
# wiec manifest sklada backend w `/proxy/hls` (producent `xtremestream`, patrz
# extractors/hosters/xtremestream.py). Link to adres playera z jakoscia w zapytaniu;
# producent bierze z niego id i `q`. `mobile_direct_ok` zostaje: segmenty NIE sa
# IP-bound (test z dwoch IP 2026-10-14), przez VPS leci manifest i 16 B klucza.
out: list[StreamSource] = []
for q in _QUALITIES:
out.append(
StreamSource(
link=f"{player_url}&q={q}",
quality=f"{q}p",
type="m3u8",
referer=player_url,
raw={
"extractor": "perverzija",
"player": player_url,
"q": q,
"manifest_producer": "xtremestream",
"mobile_direct_ok": True,
},
)
)
return out or None