goon/app/extractors/tubes/supjav.py
goon-foss 7e2807b42e
Some checks failed
Backend tests / test (push) Has been cancelled
feat(supjav): resolve voe hoster to direct stream instead of WebView
supjav is an embed-aggregator (voe/streamtape/turbovid/fc2stream). It was playing
through the WebView fallback because none of its hosters had a native phone-side
resolver wired into the scene player. voe.sx does have one (voeResolver.ts, already
used for movies) — it just was not wired for scenes.

- PlayerScreen: wire isVoeUrl/resolveVoePage into the hoster routing (like
  dood/filemoon), so a voe embed resolves on the phone to a direct m3u8
  (token bound to the phone /16, plays direct, no WebView, no proxy).
- supjav extractor: return voe first (sorted by URL), and stop putting the raw
  hostname in `quality` (the digit in "fc2stream" was parsed as quality 2 and won
  _pick_best) — use a clean digit-free server label instead.
- SceneDetailScreen: for hoster-only scenes, open the first embed (the extractor
  puts the natively-resolvable one first) rather than _pick_best's choice.

Verified: supjav's voe embed resolves to a real HLS manifest (200,
application/vnd.apple.mpegurl, #EXTM3U). WebView stays only as last-resort fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:52:26 +02:00

120 lines
5.1 KiB
Python

"""supjav.com — JAV embed-aggregator. Resolve data-link → hoster (phone-side).
supjav chowa realny hoster za `data-link` (hex) na przyciskach serwerów (TV/FST/ST/VOE).
base.js: klik serwera → iframe `src = lk1.supremejav.com/supjav.php?l=<data-link>&bg=`.
supjav.php robi `OLID = data-link.reverse()` (odwrócenie stringa hex) i ładuje
`supjav.php?c=<OLID>`, które 302-uje na realny hoster (RE 2026-07-10):
TV → turbovidhls.com, FST → fc2stream.tv, ST → streamtape.com, VOE → voe.sx
Flow ekstraktora (on-demand, play time):
1. fetch detail `/<id>.html` PRZEZ proxy (Bright Data — supjav CF-blokuje VPS IP),
2. parse `data-link` z `.btn-server`,
3. reverse hex → GET `lk1.supremejav.com/supjav.php?c=<rev>` (direct z VPS, follow 302)
→ finalny hoster URL,
4. zwróć type='hoster' → telefon resolwuje (dood/filemoon natywnie, reszta WebView
fallback z residential IP). lk1 osiągalny z VPS bez proxy; tylko detail wymaga proxy.
"""
from __future__ import annotations
import logging
import re
import curl_cffi.requests as _rq
from app.config import get_settings
from app.extractors import browser_get
from app.extractors._fetch import _DEFAULT_IMPERSONATE
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_LK = "https://lk1.supremejav.com/supjav.php"
_DATA_LINK_RE = re.compile(r'class="btn-server[^"]*"\s+data-link="([0-9a-f]{16,})"', re.IGNORECASE)
# reklama/tracker domeny które lk1 czasem zwraca zamiast hostera — odrzucamy.
_AD_RE = re.compile(r"(snaptrckr|trackwilltrk|mayzaent|eix304|doppiocdn|/ad\?)", re.IGNORECASE)
def fetch_supjav_html(url: str, *, proxy: str | None, timeout: float, tries: int = 6) -> str:
"""Fetch supjav przez proxy z retry. Bright Data rotuje IP per-request, a CF
przepuszcza tylko część IP (reszta dostaje ~6KB challenge). Retry aż trafimy IP
który przechodzi (realna strona ma `data-link=`/`<h1>` i jest duża). Zwraca '' gdy
wszystkie próby padły."""
last = ""
for _ in range(max(1, tries)):
try:
res = browser_get(url, timeout=timeout, proxy=proxy)
html = res.text if hasattr(res, "text") else res
except Exception:
html = ""
last = html or last
if html and len(html) > 15000 and ("data-link=" in html or "<h1>" in html):
return html
return last
def _resolve_hoster(data_link: str, timeout: float) -> str | None:
"""reverse hex → lk1 supjav.php?c= → follow 302 → finalny hoster URL."""
olid = data_link[::-1]
try:
s = _rq.Session(impersonate=_DEFAULT_IMPERSONATE)
r = s.get(
f"{_LK}?c={olid}",
headers={"Referer": "https://lk1.supremejav.com/"},
allow_redirects=True,
timeout=timeout,
)
final = str(r.url)
r.close()
except Exception as e:
log.info("supjav: lk1 resolve failed (%s): %s", data_link[:12], e)
return None
# supjav.php bez realnego hostera zostaje na lk1 (albo leci na ad) → odrzuć.
if "supremejav.com" in final or _AD_RE.search(final):
return None
# lk1 dokleja fragment `#supjav.com@<code>` (metadata) — hostery go ignorują, tniemy.
return final.split("#", 1)[0]
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
proxy = get_settings().brightdata_proxy_url
if not proxy:
log.info("supjav: brak proxy — nie mogę pobrać CF-blokowanego detalu %s", page_url)
return None
html = fetch_supjav_html(page_url, proxy=proxy, timeout=timeout)
if not html:
log.info("supjav: detail fetch failed (CF) %s", page_url)
return None
data_links = list(dict.fromkeys(_DATA_LINK_RE.findall(html)))
if not data_links:
log.info("supjav: brak data-link na %s", page_url)
return None
seen: set[str] = set()
out: list[StreamSource] = []
for dl in data_links:
hoster = _resolve_hoster(dl, timeout=min(timeout, 40.0))
if not hoster or hoster in seen:
continue
seen.add(hoster)
host = hoster.split("/")[2] if "://" in hoster else hoster
# Label serwera bez cyfr (host "fc2stream.tv" psułby _quality_to_int → "2" jako
# jakość → zły `best`). Zostawiamy skróconą, bezcyfrową nazwę do pickera.
label = re.sub(r"\.[a-z]+$", "", host.split(".")[0]) if host else None
label = re.sub(r"\d+", "", label).upper() if label else None
out.append(
StreamSource(
link=hoster,
type="hoster",
quality=label or None,
referer="https://supjav.com/",
)
)
if not out:
log.info("supjav: żaden data-link nie rozwiązał się na hoster (%s)", page_url)
return None
# voe.sx PIERWSZY (sort po URL, niezależnie od label) — telefon resolwuje go do
# bezpośredniego m3u8 (voeResolver.ts, phone-IP-bound → gra direct, zero WebView).
# Reszta (turbovid/fc2stream/streamtape) bez phone-side resolvera → WebView, więc niżej.
out.sort(key=lambda s: 0 if "//voe." in s.link or ".voe." in s.link else 1)
return out