goon/app/extractors/__init__.py
goon-foss 9906c665ec feat(porndupe): konektor, skorka KVS z plikami na OK.ru i zaszyfrowanym kluczem sesji
Z szesciu stron z listy piec juz jest lub bylo w bazie: fullmovies.xxx,
hdporn.gg, neporn.com sa zywe (ingest z dzisiaj, odtwarzanie 84-99%),
a hqfap.com i 4k69.com nadal oddaja Cloudflare JS-challenge na podstronach
(403 "Just a moment" takze z lacza domowego, wiec to nie blokada po IP).
Jedyna nowa to porndupe.

Porndupe wyglada jak KVS, ale to atrapa: kazdy get_stream/<id>-<q>.mp4?md5=
odpowiada doslownie "error" (sprawdzone z sesja, Refererem strony i embedu,
z permutacja md5 kodem licencyjnym). Prawdziwy tor widac w DevTools:

  generate_mp4('<b64 JSON>', '<haslo>', '<okid>', '<id>')
  -> {ciphertext, iv, salt, iterations: 999}
  -> PBKDF2-SHA512 + AES-256-CBC -> klucz sesji OK.ru (staly; haslo i
     ladunek zmieniaja sie co wczytanie, to zaciemnienie)
  -> api.ok.ru/fb.do method=video.get -> url_low/medium/high/fullhd,
     duration (ms), created_ms, thumbnail_url

Wariant PBKDF2 ustalony empirycznie: SHA-1/SHA-256 daja bad decrypt,
SHA-512 oddaje dokladnie klucz z trace'u.

okcdn: srcIp wbite w URL, NIE egzekwowane (URL z lacza domowego pobrany
z VPS -> 206, 514 MB, ftypisom) -> mobile_direct_ok. Cap 720p, bo OK.ru
dlawi polaczenie do ~2,1 Mbps (decyzja z 4k69/8kporner).

Browse po id (vjav pattern): kazdy listing to catch-all z tymi samymi 24
linkami, sitemapy pokrywaja 3500 z 12 887 id. /video/<id>/ -> 301 slug
(istnieje) albo 302 / (brak). Dlugosc z api.ok.ru przy ingescie (HTML nie
ma duration ani uploadDate). Daty CELOWO nie: created_ms to dwie fale
hurtowego wgrania (07.2024, 04.2025), nie premiery.

Katalog ZAMROZONY od kwietnia 2025, wiec to jednorazowy import, nie zrodlo
swiezosci. Wchodzi, bo 87% scen (52/60) nie mamy, a to tresc studyjna
(Nubile Films, NF Busty, Family Strokes) trafiajaca w kanon, w odroznieniu
od hentaicloud (17%). Pilot crawl_page(1): 22/22 z dlugoscia, 21/22 z
performerem, studia i tagi per-video.

Blad diagnostyczny po drodze: strona blankuje sie do about:blank po wykryciu
CDP (disable-devtool z jsdelivr); trace przezyl, DOM nie.
2026-09-05 22:34:39 +02:00

331 lines
19 KiB
Python

"""Stream URL extractors per-tube.
Public API:
- `try_extract(sitetag, page_url) -> list[StreamSource] | None`
- `StreamSource` (dataclass)
- `HosterDead` (exception)
- `extract_stream_from_hoster(iframe_url, *, referer)`, generic packer-based hoster extract
- `fetch_tube_html(url)`, Chrome TLS fingerprint fetch (curl_cffi)
- `browser_get(url)`, low-level
Architektura: każdy tube ma osobny moduł `app.extractors.tubes.<tube>` który eksportuje
`extract(page_url) -> list[StreamSource] | None`. Registry niżej mapuje sitetag →
modułowy extractor. `try_extract()` to thin wrapper z exception handlingiem.
Po removalu porn-app dependency, ten moduł jest jedynym mechanizmem rozwiązywania
streamów, playback.py nie wpada już do porn-app /stream API.
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from app.extractors._fetch import browser_get, fetch_tube_html
from app.extractors._models import HosterDead, StreamSource, TubePageError
from app.extractors.hoster import extract_stream_from_hoster, unpack_packer
from app.extractors.tubes import (
_embed_iframe,
_vps_blocked_fallback,
_ytdlp,
eporner,
freshporno,
fullmovies,
fullvideosporn,
galaxyporn,
hdporngg,
hqfap,
perverzija,
hqporner,
fullporner,
hentaigasm,
hentaiplay,
hentaimama,
naughtymachinima,
rule34video,
xanimeporn,
kporner8,
sexu,
javflix,
neporn,
porndupe,
latestpornvideo,
paradisehill,
porn00,
pornbusy,
pornmike,
porntrex,
supjav,
sxyprn,
vjav,
watchporn,
xhamster,
yespornvip,
youperv,
)
log = logging.getLogger(__name__)
# Sitetag → extractor function. Sitetag pasuje do format'u z origin: `pornapp:<sitetag>`
# (lub po Fazie 2 migracji: `tube:<sitetag>`).
#
# Mainstream tubes (pornhub/xvideos/xnxx/xhamster/redtube/youporn/porntrex) używają
# yt-dlp jako extractor, battle-tested, aktualizowane przez upstream przy zmianach
# HTML. Aggregator tubes (xmoviesforyou/watchporn/siska/...) używają generic
# embed-iframe extractor (page → /e/<id> iframe → P.A.C.K.E.R. unpack). Custom kod
# tylko tam gdzie tube ma niestandardowy schemat (eporner XHR, sxyprn URL transform).
_REGISTRY: dict[str, Callable[[str], list[StreamSource] | None]] = {
# hqporner, dedicated extractor zwraca multi-quality `<source>` mp4 URLs
# (bigcdn.cc / hqwo.cc / flyflv) z `force_proxy=True`. CDN URLs IP-bound do
# VPS, więc playback.py routuje przez proxy, mobile dostaje quality picker
# + natywny ExoPlayer, bez WebView.
# Bug-report e8ddd8d4: WebView fallback (`_vps_blocked_fallback`) ładował
# hqporner.com scene page w WebView, ale ta strona ma ad-iframes (adtng,
# goaserv, mavrtracktor) + pop-under-triggery → user klikał i widział
# reklamę zamiast video. INJECTED_JS w PlayerScreen.tsx nie chwytał
# popupów dośc szybko. Powrót do natywnego = `<source>` mp4 picker omija
# tę ścieżkę całkowicie.
"hqpornercom": hqporner.extract,
"epornercom": eporner.extract,
"sxyprncom": sxyprn.extract,
# Mainstream tubes, yt-dlp
# NB: 2026-05-18 cross-IP test potwierdził że xvideos/xnxx/pornhub/youporn/redtube
# CDN URLs są **time-bound** (nie IP-bound), mobile_direct_ok auto-detect w
# playback.py daje mobile direct fetch, zero VPS bandwidth.
# pornhub + redtube, USUNIĘTE CAŁKOWICIE 2026-06-22 (user request). Scrapery były
# disabled od 2026-05-12 (0.4% canonical match, głównie skrócone amatorskie clipy),
# zamrożone sceny/źródła skasowane z DB. Brak ekstraktorów → zero resolve.
"xvideoscom": _ytdlp.extract,
"xnxxcom": _ytdlp.extract,
"youporncom": _ytdlp.extract,
# porntrex KVS, 2026-05-22 VPS znów dociera (HTTP 200). Dedykowany extractor:
# flashvars `video_url` → `get_file` 302 → CDN time-bound signed URL
# (`expires`+`md5`, NIE IP-bound) → mobile gra direct, zero VPS bandwidth.
"porntrexcom": porntrex.extract,
# fpoxxx, KVS, plain get_file + license. 2026-06-01 (task #20): get_file 302 →
# `videos3.fpo.xxx/remote_control.php?acctoken=<base64>`, zdekodowany acctoken
# zawiera WBITY IP serwera-resolvera → definitywnie IP-bound. WebView only.
"fpoxxx": _vps_blocked_fallback.extract,
# sxyland, embeduje playmogo.com/e/<id> (= klon DoodStream: doodcdn.io + pass_md5
# + niewidzialny CF Turnstile; Chrome-DevTools verify 2026-06-08, bug-report 827a50a1).
# Strona sxyland NIE jest Turnstile-gated (VPS curl wyciąga iframe URL z HTML), więc
# _embed_iframe wyłuskuje embed playmogo i oddaje jako type='hoster' → mobile
# doodstream.ts resolvuje phone-side (phone IP przechodzi invisible Turnstile) → direct
# mp4 → autoplay. Wcześniej _vps_blocked_fallback ładował CAŁĄ stronę sxyland w WebView
# (ads + klik-to-play + brak autoplay = dokładnie objaw z reportu 827a50a1).
"sxylandcom": _embed_iframe.extract,
# Aggregator tubes, generic embed-iframe → hoster unpacker
"latestpornvideocom": latestpornvideo.extract,
"xmoviesforyoucom": _embed_iframe.extract,
# watchporn, 2026-07-02 przebudowany na KVS (DoodStream-CAPTCHA zniknął).
# flashvars get_file direct mp4, same-session 302 resolve, token nie IP-bound.
"watchporn": watchporn.extract,
# youperv, fluidplayer, zwykły <source> mp4 na files.klubnichka-hd.com. Bez tokena
# i bez IP-bindingu; CDN pilnuje tylko Referera (cross-IP 206 z VPS) → mobile direct.
"youpervcom": youperv.extract,
# galaxyporn, iframe rodziny seekplayer (upns/4meplayer/seekplays); silnik już mamy,
# extractor tylko wyłuskuje iframe. HLS Referer+IP-bound → /proxy/hls.
"galaxyporncom": galaxyporn.extract,
# pornbusy, dispatch po embedURL: loadvid (POST-manifest przez /proxy/hls),
# rodzina seekplayer (istniejący silnik), zpi.cx (embedURL to gotowy plik).
# upload18 + ogon → None (token IP-bound, resolve wymusiłby proxy całego wideo).
"pornbusycom": pornbusy.extract,
# pornmike, gołe <source> mp4 na twincdn: bez tokenu, bez Referera, bez expiry.
# Cross-IP 206 z VPS i z innego kraju → mobile gra direct, zero proxy/WebView.
"pornmike": pornmike.extract,
# 8kporner, JWPlayer `sources` (NIE JSON-LD contentUrl, ten jest zawsze 144p!).
# okcdn/vkuser jak 4k69: srcIp nieegzekwowane → mobile direct. Cap 720p, bo CDN
# dławi do ~2,1 Mbps, a 1080p+ potrzebuje więcej → "loading forever".
"8kpornercom": kporner8.extract,
# hentaimama — player w 3 krokach: id posta ze strony → POST admin-ajax (ZA CF,
# wymaga Bright Data) → strona playera → bezpośredni mp4. Preferujemy gdvid,
# bo oddaje plik bez tokenu, więc URL nie wygasa. Portable, mobile_direct_ok.
"hentaimama": hentaimama.extract,
# hentaigasm — najprostszy w wertykale: konfiguracja JWPlayera WPROST w HTML,
# bez AJAX-a i bez CF. Sciezka ma SPACJE, trzeba zakodowac. Bez tokenu, bez
# Referera, 206 z VPS → mobile_direct_ok.
"hentaigasm": hentaigasm.extract,
# hentaiplay — mp4 wprost w HTML (video.js). Wymagamy URL-a KONCZACEGO sie na .mp4
# i odrzucamy wp-content/uploads, bo serwis nazywa miniaturki od pliku wideo
# (…-RAW.mp4_snapshot_07.59.jpg) i naiwne .mp4 lapaloby oba.
"hentaiplay": hentaiplay.extract,
# xanimeporn — Fluid Player z PUSTYM <source>, wideo wstrzykiwane w runtime.
# Wyjście: zakładka Downloads, czyli videos.xanimeporn.com/download.php?id=<postId>.
# Oddajemy download.php, nie docelowy plik, żeby token generował się przy odtwarzaniu.
# naughtymachinima — jawne <source> bez tokenu, najprostszy playback wertykalu.
# Odsiewamy pusty src="" (placeholder playera) i /images/preroll/NMintro.mp4.
"naughtymachinima": naughtymachinima.extract,
# rule34video — KVS z JAWNYMI flashvars (4 jakosci), ale `v-acctoken` jest
# ZWIAZANY Z IP tego, kto pobral strone (test krzyzowy: proxy→VPS = 403).
# Dlatego BEZ mobile_direct_ok: telefon resolwuje sam, upfront, przez
# isGetFileUrl()/getfileResolver, tak jak hdporn.gg i fullmovies.xxx.
"rule34video": rule34video.extract,
"xanimeporn": xanimeporn.extract,
# fullporner, iframe xiaoshenke: id mediów zapisane WSPAK + maska bitowa jakości.
# Zero tokenu i zero wygasania, URL składamy sami. Blokada jest na nagłówku (bez
# UA → 403, bez Referera → 404), NIE na fingerprincie TLS, więc telefon przechodzi.
"fullporner": fullporner.extract,
# sexu, POST /api/video-info przez Bright Data (CF blokuje sam POST z VPS).
# Bierzemy HLS `playerData.src`, NIE mp4 `sources`, te mają w tokenie `ip=`
# requestera, więc na telefonie dałyby 403. Master kończy się `.mp4` → wchodzi
# hls_needs_passthrough i manifest idzie przez /proxy/hls, segmenty direct.
"sexu": sexu.extract,
# fullvideosporn (= sextu), TXXX: videofile.php → dekod → get_file → 302 → znvcdn.
# Token CDN portable cross-IP → mobile direct. UWAGA: VPS dostaje od CDN 429
# (reputacja IP datacenter), więc health-check playbacku z VPS jest ślepy.
"fullvideosporn": fullvideosporn.extract,
"siskavideo": _embed_iframe.extract,
"porn4dayspw": _embed_iframe.extract,
"porndishcom": _embed_iframe.extract,
# xxxfreewatch, DELISTED 2026-05-18. 790 solo-orphan scen, 0% match, CF-walled z VPS.
"latestleaksco": _embed_iframe.extract,
"mypornerleakcom": _embed_iframe.extract,
# xhamster, 2026-06-08 PRZEPIĘTE z _vps_blocked_fallback na natywny server-side HLS.
# Re-test (DevTools + cross-IP): VPS pobiera scene page bez CF challenge, master m3u8
# w SSR HTML, manifest+segmenty time-bound (portable, nie IP-bound). Mobile gra HLS
# direct, multi-quality, zero VPS proxy/WebView/reklam. Patrz tubes/xhamster.py.
# ~155k solo-scen upgrade z WebView-z-reklamami na natywne. Wcześniej WebView fallback
# ładował ad-heavy stronę z phone IP (działało, ale gorszy UX + preroll VAST).
"xhamstercom": xhamster.extract,
# Freshporno KVS (function/0 + license). 2026-06-04 DevTools + cross-IP re-test
# NAPRAWIA błąd z #20: finalny cdn4.freshporno.org/remote_control.php jest PORTABLE
# (token time-bound nie IP-bound, VPS odtworzył token z residential → 206) ale
# wymaga browser-TLS (curl_cffi chrome/ExoPlayer → 206; plain curl → 000). W #20
# testowałem plain-curl-em poza sesją → 000 → błędnie „nieosiągalny" → WebView.
# Teraz backend-resolve jak yespornvip/pornditt (_kvs używa curl_cffi chrome).
# Native, multi-quality, zero proxy/WebView. (zweryfikowane na emulatorze przed deploy)
"freshpornoorg": freshporno.extract,
# porn00, KVS (plain get_file + license). 2026-06-04 DevTools + cross-IP re-test
# NAPRAWIA błąd z #20: finalny fe.porn00.org/...?token=&expires= jest PORTABLE
# (token time-bound nie IP-bound, Bright Data residential proxy z innego IP → 206)
# ale wymaga browser-TLS (curl_cffi chrome → 206; plain curl → 403). W #20
# testowałem finalny URL plain-curl-em → 403 → błędnie „IP-bound" → WebView.
# Teraz backend-resolve przez _kvs (curl_cffi chrome), native multi-quality,
# ZERO proxy (wcześniej force_proxy łamał no-proxy). Same mechanizm co freshporno.
"porn00org": porn00.extract,
# pornxp, `<source> //sr.porn-xp.com/<token>/.../720.mp4` (redirect → xpxp.eu).
# 2026-06-01 (task #20): 403 cross-IP → token w path IP-bound. WebView only.
"pornxpph": _vps_blocked_fallback.extract,
# yesporn.vip, KVS engine. VPS znów dociera (HTTP 200, odblokowane jak porntrex),
# więc resolvujemy SERVER-SIDE: dekoduj flashvars `video_url`/alt/alt2 (function/0/ +
# license_code, algo kt_player) → follow get_file 302 → portable cdn5 url (time-bound,
# NIE IP/cookie-bound, zweryfikowane cross-IP 2026-05-31). Mobile gra direct natywnie,
# multi-quality, ZERO WebView/reklam/preroll. Wcześniej WebView fallback pokazywał
# ad-heavy stronę a scrape łapał preroll-reklamę (bkcdn) zamiast wideo.
"yespornvip": yespornvip.extract,
# Direct-scraping tubes (mają też search scraper w connectors/direct_scrapers/)
# używają identycznego embed-iframe pattern dla streamingu.
# hdporn92com, DELISTED 2026-05-18. Scene pages to SEO shell bez player iframe,
# JS hijackuje kliki na popunder. Wszystkie playback_sources mass-marked dead.
# 0dayxx + pornditt + pornhat, USUNIĘTE CAŁKOWICIE 2026-06-22 (user request): orphan
# factories (0-0.2% canonical match), zastępujemy lepszymi źródłami. Dane skasowane.
# CF-protected tube, curl_cffi w fetch_tube_html bypassa JA3, embed-iframe pattern.
# perverzija: natywny HLS przez xtremestream/xs1.php. Uwaga na regresję z 2026-08-18:
# przestawiliśmy to wtedy na embed-iframe/mixdrop na podstawie JEDNEJ strony, która
# akurat miała mixdrop. Sprawdzenie 8 losowych stron (2026-08-20) daje 8/8 działających
# manifestów xs1.php, na SZEŚCIU różnych subdomenach (perv, j2, moviekh, pervl1,
# pervl3, pervm1) — host rotuje, regex w `perverzija.py` to obejmuje. Warunkiem 200 jest
# Referer równy URL-owi playera; zły Referer daje 403 z HTML-em i to on wyglądał jak
# "Input does not start with #EXTM3U". Zanim to znowu przełączysz: sprawdź PRÓBKĘ stron.
"perverzijacom": perverzija.extract,
# Special: WebView-only (Yii2 session-bound player).
"paradisehillcc": paradisehill.extract,
# PornDoe, dołączony 2026-05-21 (theporndude audit). Stream URL nie inline w
# SSR HTML (player JS init po Play click), więc WebView fallback: mobile pobiera
# /watch/<id> z phone IP, player JS dekoduje video.src, INJECTED_JS scrape.
# 0 VPS bandwidth, zgodne z pre-public bandwidth/anonimowość priorytet.
"porndoecom": _vps_blocked_fallback.extract,
# fullmovies.xxx + hdporn.gg, BRAKOWAŁO extractora (try_extract→None→"no stream";
# fullmovies.xxx + hdporn.gg, ta sama platforma (`<source>/get_file/8512/`).
# 2026-06-04 (DevTools + cross-IP, naprawia „loading forever" + bug 19866e9e):
# get_file binduje fpvcdn do IP FETCHERA + jest stateless + ważny ≥90s, więc
# oddajemy get_file NIEZRESOLWOWANY (mobile_direct), telefon follow-uje 302 →
# fpvcdn z IP telefonu → gra. POMIJAMY 4K (time-out 30s na fpvcdn = przyczyna
# „loading forever"; 720/480p gra ~1s). Native, multi-quality, ZERO proxy/WebView.
# (#19866e9e wcześniej źle: założyłem „get_file 403 IP-bound" testem plain-curl.)
"fullmoviesxxx": fullmovies.extract,
"hdporngg": hdporngg.extract,
# hqfap (PlayTube CMS), JSON-LD contentUrl = direct mp4. USUNIĘTY 2026-06-25 (cała
# biblioteka serwowała `/upload/videos/video_down.mp4` stub), PRZYWRÓCONY 2026-07-07:
# strona wróciła na CDN vstor.top z realnymi plikami (portable cross-IP, zweryfikowane),
# user request. 4k69 zostaje usunięty (nie sprawdzany ponownie).
# 2026-07-28: hqfap wdrożył Cloudflare z JS-challenge na strony /watch/ 403 zarówno
# z VPS, jak i z residential IP, także przy pełnym komplecie nagłówków przeglądarki
# (strona główna dalej 200, więc blokada jest celowana w strony wideo). Phone-side
# `resolveHqfapPage` (zwykły fetch) nie ma jak przejść challenge, więc padał i user
# lądował na stronie zamiast na wideo (bug-report 92e9133e "Hqfap przekierowuje na
# stronę"; telemetria to PRZEMILCZAŁA, zero eventów od 27.07, bo błąd jest przed
# playerem). WebView (Chrome engine + JS + cookie) challenge przechodzi, więc
# przełączamy na tryb WebView; INJECTED_JS i tak wyłuska mp4 i wróci na native player.
# "hqfapcom", ZDJĘTY 2026-08-02, patrz komentarz w direct_scrapers/__init__.py.
# javflix (JAV, WordPress), przyciski serwerów to `<a class="myLink" href="<embed>">`
# (streamtape/voe/doodstream/emturbovid). Generyczny _embed_iframe łapie je anchor-hoster
# patternem → type='hoster', telefon resolwuje (voe backend, dood/filemoon phone-side).
# Wrapper javflix.extract odsiewa placeholder players.mp4.
"javflix": javflix.extract,
# vjav (JAV, TXXX network), videofile.php -> get_file HLS (patrz tubes/vjav.py).
"vjav": vjav.extract,
# supjav (JAV, embed-aggregator), data-link reverse-hex -> lk1 -> hoster (type=hoster).
"supjav": supjav.extract,
# neporn, KVS function/0 + license (jak freshporno). Server-side _kvs resolve →
# data001.neporn.com/remote_control.php portable (cross-IP 206, 2026-06-10).
"neporncom": neporn.extract,
# porndupe, skorka KVS z atrapa get_stream (odpowiada "error"); prawdziwy plik na
# OK.ru, klucz sesji zaszyfrowany w HTML (PBKDF2-SHA512 + AES-256-CBC). okcdn
# srcIp NIEegzekwowane (cross-IP 206) -> mobile direct. Cap 720p jak 4k69/8kporner.
"porndupe": porndupe.extract,
# superporn, `<source>` mp4 (cdnst*.superporn.com) token IP-bound do fetchera
# (403 cross-IP, test 2026-06-10), a sama strona CF-blocked z VPS. Resolve MUSI
# być phone-side: WebView ładuje stronę z residential IP telefonu, INJECTED_JS
# bierze video.src. Ingest HTML idzie osobno przez Bright Data proxy (scraper).
"superporncom": _vps_blocked_fallback.extract,
}
def try_extract(sitetag: str, page_url: str) -> list[StreamSource] | None:
"""Próbuje rozwiązać stream URL dla danego tube'a + page_url.
Zwraca listę StreamSource (różne quality/kontener) lub None gdy:
- brak extractora dla tego sitetag
- extractor zwrócił None / nie znalazł URL'a
Raises HosterDead gdy embed page wprost mówi że video deleted/not found,
caller (playback.py) łapie i oznacza playback_source.dead_at.
"""
extractor = _REGISTRY.get(sitetag)
if extractor is None:
return None
try:
return extractor(page_url)
except (HosterDead, TubePageError):
raise
except Exception as e:
log.warning("extractor for %s failed on %s: %s", sitetag, page_url, e)
return None
def supported_sitetags() -> tuple[str, ...]:
"""Zwraca listę sitetag-ów które mają zarejestrowany extractor."""
return tuple(_REGISTRY.keys())
def is_vps_blocked_fallback(sitetag: str) -> bool:
"""True gdy sitetag resolvuje się TYLKO przez WebView fallback (IP-bound CDN /
ad-heavy / CAPTCHA, np. fpoxxx, pornxpph). Takie źródła dają gorszy
UX (reklamy, czarny ekran) niż natywny KVS/direct resolve, więc UI powinien je
rankować NIŻEJ gdy scena ma też natywne źródło (bug-report 2026-06-07: scena
pokazywała fpoxxx-WebView przed działającym freshporno bo sort był alfabetyczny)."""
return _REGISTRY.get(sitetag) is _vps_blocked_fallback.extract
__all__ = [
"try_extract",
"supported_sitetags",
"StreamSource",
"HosterDead",
"TubePageError",
"extract_stream_from_hoster",
"unpack_packer",
"fetch_tube_html",
"browser_get",
]