goon/app/extractors/tubes/sexu.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

95 lines
3.3 KiB
Python

"""sexu.com, HLS z `POST /api/video-info`. Dodany 2026-07-27.
Player jest hydratowany po stronie klienta (`/js/vjs/hydrate-source.js`): strona sceny
nie zawiera żadnego URL-a wideo, dopiero `POST /api/video-info` z `videoId=<id>` oddaje
JSON-a z `playerData`.
**Ten POST wymaga Bright Data.** Z gołego IP VPS-a Cloudflare oddaje na niego challenge
(GET-y na listing i detale przechodzą bez problemu, blokowany jest sam POST). Idzie tu
JSON, nie wideo, więc mieści się w tym, do czego proxy służy.
**Bierzemy `playerData.src` (HLS), a NIE `playerData.sources` (mp4).** Wygląda to
odwrotnie do intuicji, bo mp4 są wygodniejsze, ale:
sources[].src → /key=…,end=…,ip=158.46.155.106/sec=protect/…-720p-x.mp4
playerData.src → /key=…,end=…/multi=854x480:480p,1280x720:720p/media=hls4A/…
mp4 mają w tokenie `ip=` requestera, czyli IP wyjściowe proxy, na telefonie dałyby 403.
Token HLS `ip=` nie ma i jest przenośny: sprawdzone z VPS-a (inne IP niż proxy) master
200, wariant 200, segment 206. Telefon gra go z własnego IP.
Ścieżka mastera kończy się na `.mp4`, nie `.m3u8`, więc ExoPlayer wziąłby go za
progresywny plik i padł. Łapie to istniejąca logika `hls_needs_passthrough` w
`playback.py` (typ `m3u8` + `mobile_direct_ok` + brak `.m3u8` w ścieżce) → manifest
(~430 B) leci przez `/proxy/hls`, a segmenty prosto z CDN.
"""
from __future__ import annotations
import json
import logging
import re
from app.config import get_settings
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_BASE = "https://sexu.com"
_SCENE_ID_RE = re.compile(r"/(\d+)/?$")
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
m = _SCENE_ID_RE.search(page_url.split("?")[0])
if not m:
log.info("sexu: nie wyłuskałem id z %s", page_url)
return None
video_id = m.group(1)
proxy = get_settings().brightdata_proxy_url
if not proxy:
log.info("sexu: brak BRIGHTDATA_PROXY_URL — CF zablokuje POST /api/video-info")
return None
from curl_cffi import requests as cr
try:
session = cr.Session(
impersonate="chrome120",
proxies={"http": proxy, "https": proxy},
verify=False,
)
# Strona sceny najpierw, bez jej ciasteczek CF odrzuca POST-a.
session.get(page_url, timeout=timeout)
res = session.post(
f"{_BASE}/api/video-info",
data={"videoId": video_id},
headers={
"X-Requested-With": "XMLHttpRequest",
"Referer": page_url,
},
timeout=timeout,
)
data = json.loads(res.text)
except Exception as e:
log.info("sexu: video-info failed %s: %s", page_url, e)
return None
if not data.get("success"):
log.info("sexu: video-info success=false na %s", page_url)
return None
src = (data.get("playerData") or {}).get("src")
if not src:
log.info("sexu: brak playerData.src na %s", page_url)
return None
if src.startswith("//"):
src = "https:" + src
return [
StreamSource(
link=src,
type="m3u8",
quality="720p", # master multi=854x480,1280x720, ExoPlayer wybierze sam
referer=_BASE + "/",
raw={"mobile_direct_ok": True},
)
]