Znalezione w nocnym smoke tescie (9 ostrzezen, w tym trzy ekstraktory po 0/3). vjav: /api/videofile.php oddaje z serwerowni 429 i 343-bajtowa strone, ktora w JS ustawia JEDNO ciasteczko i sie przeladowuje. Wartosc jest wprost w tej odpowiedzi, wiec czytamy ja i powtarzamy zapytanie, bez wykonywania JS. Sprawdzone: druga proba oddaje 200 i poprawny JSON. Bylo 0/3, jest 5/5. galaxyporn: serwis przenosi NOWE wgrania na vikingfile.com. Probka: 12/12 najnowszych scen na vikingfile, 12/12 najstarszych dalej na rodzinie seekplayera. Vikingfile chowa wideo za RENDEROWANYM Cloudflare Turnstile (turnstile.render, bez trybu invisible), a <video> jest puste i ukryte, dopoki wyzwanie nie padnie. Serwerowo nie do obejscia, wiec oddajemy embed jako type=hoster i telefon otwiera go w WebView z domowego IP. Rozroznienie po hoscie, bo seekplayer jako SPA w WebView pokazuje pusty player. Najnowsze sceny: bylo 0/3, jest 3/3. ZOSTAJE OTWARTE: rodzina seekplayera zmienila format odpowiedzi /api/v1/video. Stary klucz AES dalej odszyfrowuje poprawnie, ale w miejsce sources sa teraz pola pk, streamingConfig i hlsVideoTiktok ze SCIEZKA wzgledna. Zlozenie jej z hostem playera daje 404, a z domenami z streamingConfig 403, wiec URL jest podpisywany po stronie klienta z pola pk. Dotyczy starego katalogu galaxyporn ORAZ pornbusy (0/5). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
5.3 KiB
Python
125 lines
5.3 KiB
Python
"""vjav.com, TXXX-network JAV tube. HLS stream extractor.
|
||
|
||
vjav to SPA na silniku TXXX (ktk_player + MSE/blob src). Stream NIE jest w HTML
|
||
strony (player renderuje się JS-owo). Zamiast tego AJAX endpoint zwraca zaciemniony
|
||
URL pliku:
|
||
|
||
GET /api/videofile.php?video_id=<id>&lifetime=8640000
|
||
-> [{"format":"_hq.mp4","video_url":"<base64+cyrylica>","is_default":1, ...}]
|
||
|
||
`video_url` to base64 w DWÓCH warstwach zaciemnienia: (1) wielkie łacińskie litery
|
||
podmienione na cyrylicę-homoglif (М->M, С->C, А->A, Е->E), (2) custom alfabet base64
|
||
gdzie `,`->`/`, `~`->`=`, `-`->`+`. Po odkręceniu obu + b64decode dostajemy get_file:
|
||
|
||
# stare wideo (shared txxx pool), URL absolutny:
|
||
https://videotxxx.com/ext/get_file/9/<hash>/<floor>/<mid>/<mid>_hq.mp4/?d=..&br=..&ti=..
|
||
# nowe wideo (własna infra vjav, server N), URL RELATYWNY (prepend https://vjav.com):
|
||
/get_file/3/<hash>/<floor>/<mid>/<mid>_hq.mp4/?d=..&br=..&ti=..
|
||
|
||
Dopisanie `&f=video.m3u8` -> 302 chain -> finalny HLS na *.ahcdn.com
|
||
(`key=..,end=..,limit=3`, time-bound, NIE IP-bound, `referer=none` na whiteliscie
|
||
-> portable, gra z residential IP telefonu bez sesji vjav).
|
||
|
||
Zwracamy get_file (`?f=video.m3u8`) jako type='m3u8' + `mobile_direct_ok` -> playback.py
|
||
owija w `/proxy/hls/<token>/play.m3u8` (passthrough manifestu ~1KB przez VPS, segmenty
|
||
direct z telefonu; patrz stream_proxy.proxy_hls_manifest). Media id w get_file (318577)
|
||
!= page id (5197), dlatego MUSIMY przejść przez videofile.php. RE 2026-07-10.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
import re
|
||
|
||
from app.extractors import browser_get
|
||
from app.extractors._models import StreamSource
|
||
from app.extractors.tubes import _txxx
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
_BASE = "https://vjav.com"
|
||
_VIDEO_ID_RE = re.compile(r"/videos/(\d+)/")
|
||
|
||
# Dekoder video_url wyniesiony do `_txxx.py`, ten sam silnik TXXX obsługuje też
|
||
# fullvideosporn/sextu, więc trzymamy jedną implementację. Alias zachowany dla
|
||
# czytelności modułu i ewentualnych importów.
|
||
_decode_video_url = _txxx.decode_video_url
|
||
|
||
|
||
#: Wyzwanie anty-botowe: `document.cookie = 'NAZWA=WARTOSC; max-age=...'` w ciele 429.
|
||
_CHALLENGE_COOKIE_RE = re.compile(r"document\.cookie\s*=\s*'([^=]+)=([^;']+)")
|
||
|
||
|
||
def _quality_label(fmt: str | None) -> str | None:
|
||
"""`_hq.mp4` -> 'HD', `_sd.mp4` -> 'SD'. Trailer (`_tr`) filtrowany osobno."""
|
||
if not fmt:
|
||
return None
|
||
tok = fmt.strip("_").replace(".mp4", "").lower()
|
||
return {"hq": "HD", "sd": "SD", "lq": "LOW", "hd": "HD"}.get(tok, tok.upper() or None)
|
||
|
||
|
||
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
|
||
m = _VIDEO_ID_RE.search(page_url)
|
||
if not m:
|
||
log.info("vjav: brak video id w %s", page_url)
|
||
return None
|
||
vid = m.group(1)
|
||
api = f"{_BASE}/api/videofile.php?video_id={vid}&lifetime=8640000"
|
||
hdr = {"Referer": page_url, "X-Requested-With": "XMLHttpRequest"}
|
||
try:
|
||
res = browser_get(api, timeout=timeout, headers=hdr)
|
||
body = res.text if hasattr(res, "text") else res
|
||
# Serwis dlawi ruch z serwerowni: zamiast JSON-a oddaje 429 i mikroskopijna
|
||
# strone, ktora w JS ustawia jedno ciasteczko i przeladowuje sie. Wartosc jest
|
||
# WPROST w tej odpowiedzi, wiec wystarczy ja odczytac i powtorzyc zapytanie,
|
||
# bez zadnego wykonywania JS. Sprawdzone 2026-08-21: pierwsza proba 429 i 343
|
||
# bajty, druga z ciasteczkiem 200 i poprawny JSON z video_url. Bez tego caly
|
||
# vjav resolwowal sie do zera (smoke test: 0/3 OK, "extractor None").
|
||
if (ck := _CHALLENGE_COOKIE_RE.search(body)) is not None:
|
||
hdr = dict(hdr, Cookie=f"{ck.group(1)}={ck.group(2)}")
|
||
res = browser_get(api, timeout=timeout, headers=hdr)
|
||
body = res.text if hasattr(res, "text") else res
|
||
data = json.loads(body)
|
||
except Exception as e:
|
||
log.info("vjav: videofile.php fetch/parse fail %s: %s", api, e)
|
||
return None
|
||
if not isinstance(data, list):
|
||
return None
|
||
|
||
seen: set[str] = set()
|
||
out: list[StreamSource] = []
|
||
for entry in data:
|
||
if not isinstance(entry, dict):
|
||
continue
|
||
fmt = (entry.get("format") or "").strip()
|
||
# `_tr.mp4` = trailer preview, NIE pełne wideo, pomijamy.
|
||
if fmt.strip("_").replace(".mp4", "").lower() == "tr":
|
||
continue
|
||
vu = entry.get("video_url")
|
||
if not vu:
|
||
continue
|
||
get_file = _decode_video_url(vu)
|
||
if not get_file or "/get_file/" not in get_file:
|
||
continue
|
||
# Nowe wideo daje URL relatywny (/get_file/...), prepend host vjav.
|
||
if get_file.startswith("/"):
|
||
get_file = _BASE + get_file
|
||
m3u8 = get_file + ("&" if "?" in get_file else "?") + "f=video.m3u8"
|
||
if m3u8 in seen:
|
||
continue
|
||
seen.add(m3u8)
|
||
out.append(
|
||
StreamSource(
|
||
link=m3u8,
|
||
type="m3u8",
|
||
quality=_quality_label(fmt),
|
||
referer=page_url,
|
||
# Finalny HLS (txxx.ahcdn.com) time-bound + referer=none whitelisted ->
|
||
# portable. mobile_direct_ok -> playback.py owija w /proxy/hls passthrough.
|
||
raw={"mobile_direct_ok": True},
|
||
)
|
||
)
|
||
if not out:
|
||
log.info("vjav: brak dekodowalnego video_url dla %s", page_url)
|
||
return None
|
||
return out
|