goon/app/extractors/tubes/fullvideosporn.py
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

144 lines
5.7 KiB
Python

"""fullvideosporn.com (= sextu.com pod nową marką), TXXX network, direct mp4.
To NIE jest klon fullmovies.xxx (inny silnik, inny katalog, 0/140 pokrycia tytułów).
**Bramka cookie**: pierwszy request z nowej sesji dostaje HTTP 429 + 342-bajtowy JS
challenge ustawiający ciasteczko (`document.cookie = 'PxeA3f=<num>; max-age=600'`).
Wystarczy wyregexować parę nazwa=wartość i powtórzyć request, bez proxy, bez
przeglądarki. Ciasteczko żyje 600 s, więc trzymamy je w sesji modułu.
Stream (identyczny jak vjav, rodzina TXXX):
GET /api/videofile.php?video_id=<id>&lifetime=8640000
→ [{"format":"_lq.mp4","video_url":"<zaciemniony>",...}]
dekod (`_txxx.decode_video_url`) → `/get_file/...` (relatywny, prepend host)
GET get_file bez follow → 302 → `https://sextuN.znvcdn.com/t=.../....mp4`
`_lq.mp4` w nazwie myli, to realnie 1280x720 (~1,29 Mbps), jedyny dostępny format.
**Cross-IP**: finalny URL CDN wybity z IP VPS gra z residential (206, `video/mp4`,
bez Referera) → token NIE jest IP-bound, `mobile_direct_ok`. ALE sam VPS dostaje od
CDN 429 (reputacja IP datacenter), więc **health-check playbacku z VPS będzie
fałszywie negatywny** nie traktuj tego jako regresji.
"""
from __future__ import annotations
import json
import logging
import re
from urllib.parse import urlparse
from app.extractors._fetch import _DEFAULT_IMPERSONATE, _DEFAULT_UA, _HAS_CURL_CFFI
from app.extractors._models import StreamSource
from app.extractors.tubes import _txxx
log = logging.getLogger(__name__)
_BASE = "https://fullvideosporn.com"
_VIDEO_ID_RE = re.compile(r"/video/(\d+)/")
# `<script>window.addEventListener('click',()=>{...document.cookie = 'PxeA3f=980886937; max-age=600...`
_COOKIE_CHALLENGE_RE = re.compile(
"document.cookie[^']*'([A-Za-z0-9_]+)=([^;']+)", re.IGNORECASE
)
def _new_session():
from curl_cffi import requests as cf
return cf.Session(impersonate=_DEFAULT_IMPERSONATE)
def gated_get(session, url: str, *, timeout: float = 30.0, headers: dict | None = None,
allow_redirects: bool = True):
"""GET przechodzący bramkę 429 (patrz docstring modułu). Ciasteczko ląduje w sesji,
więc kolejne requesty tej samej sesji idą od razu. Zwraca response (albo None)."""
h = {"User-Agent": _DEFAULT_UA, "Accept": "text/html,application/xhtml+xml"}
if headers:
h.update(headers)
try:
r = session.get(url, headers=h, timeout=timeout, allow_redirects=allow_redirects)
except Exception as e:
log.info("fullvideosporn: fetch failed %s: %s", url, e)
return None
if r.status_code == 429:
m = _COOKIE_CHALLENGE_RE.search(r.text or "")
if not m:
log.info("fullvideosporn: 429 bez challenge-cookie na %s", url)
return r
session.cookies.set(m.group(1), m.group(2))
try:
r = session.get(url, headers=h, timeout=timeout, allow_redirects=allow_redirects)
except Exception as e:
log.info("fullvideosporn: retry po cookie failed %s: %s", url, e)
return None
return r
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
if not _HAS_CURL_CFFI:
log.info("fullvideosporn: curl_cffi unavailable")
return None
m = _VIDEO_ID_RE.search(page_url)
if not m:
log.info("fullvideosporn: brak video id w %s", page_url)
return None
vid = m.group(1)
session = _new_session()
api = f"{_BASE}/api/videofile.php?video_id={vid}&lifetime=8640000"
r = gated_get(
session, api, timeout=timeout,
headers={"Referer": page_url, "X-Requested-With": "XMLHttpRequest",
"Accept": "application/json,text/plain,*/*"},
)
if r is None or r.status_code != 200 or not r.text:
log.info("fullvideosporn: videofile.php status=%s", getattr(r, "status_code", None))
return None
try:
data = json.loads(r.text)
except Exception as e:
log.info("fullvideosporn: videofile.php parse fail: %s", e)
return None
if not isinstance(data, list):
return None
out: list[StreamSource] = []
seen: set[str] = set()
for entry in data:
if not isinstance(entry, dict):
continue
fmt = (entry.get("format") or "").strip()
if fmt.strip("_").replace(".mp4", "").lower() == "tr":
continue # trailer, nie pełne wideo
get_file = _txxx.decode_video_url(entry.get("video_url") or "")
if not get_file or "/get_file/" not in get_file:
continue
if get_file.startswith("/"):
get_file = _BASE + get_file
# get_file 302 → finalny CDN. Rozwiązujemy TU (w sesji z ciasteczkiem), bo
# telefon nie ma tej sesji; finalny URL jest portable cross-IP.
rr = gated_get(session, get_file, timeout=timeout,
headers={"Referer": page_url}, allow_redirects=False)
final = None
if rr is not None and rr.status_code in (301, 302, 303, 307, 308):
final = rr.headers.get("location")
elif rr is not None and rr.status_code == 200:
final = get_file # brak redirectu, get_file sam serwuje plik
if not final or final in seen:
continue
seen.add(final)
out.append(
StreamSource(
link=final,
type="mp4",
quality="720p", # `_lq` w nazwie myli: realnie 1280x720
referer=_BASE + "/",
# Token CDN time-bound, NIE IP-bound (zweryfikowane cross-IP) →
# telefon gra direct, zero proxy.
raw={"mobile_direct_ok": True},
)
)
if not out:
log.info("fullvideosporn: brak dekodowalnego video_url dla %s", page_url)
return None
return out