Metadane ma najlepsze z tej serii: kanal = realne studio na 97% scen, 96% performerow z refem tpdb/stashdb, dokladna dlugosc w sekundach, swiezosc w godzinach, 120 scen na stronie listingu. Nie wlaczamy go jednak do ALL_BROWSE_SCRAPERS, bo jednostka tresci jest zla: to klipy 5-10 minut, nie sceny. Mediana 571 s na stronie 30 i 459 s na 200, zero filmow >=20 min w calym katalogu. Pilot na 240 scenach dal 1 canonical match, bo dlugosc i tytul, czyli sygnaly resolvera, sa przepisane pod tube. Przy ~340 uploadach dziennie to ~10k nierozwiazywalnych wierszy miesiecznie. Kod zostaje kompletny i zweryfikowany: POST /api/video-info przez Bright Data (CF blokuje sam POST z VPS), HLS playerData.src zamiast mp4 sources (te maja ip= requestera w tokenie), master konczy sie .mp4 wiec lapie go istniejacy hls_needs_passthrough. Gdyby serwis zaczal wrzucac pelne sceny, wystarczy dopisac klase z powrotem do rejestru. 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},
|
|
)
|
|
]
|