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>
95 lines
3.3 KiB
Python
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},
|
|
)
|
|
]
|