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.
140 lines
5.8 KiB
Python
140 lines
5.8 KiB
Python
"""porndupe.com: skórka KVS, pliki na OK.ru, klucz sesji zaszyfrowany w HTML. Dodany 2026-09-05.
|
|
|
|
Strona wygląda jak zwykły KVS (kt_player, flashvars z czterema jakościami), ale to atrapa:
|
|
każdy `get_stream/<id>-<q>.mp4?md5=…×tamp=…` odpowiada dosłownie `error`, także
|
|
z ciasteczkiem sesji, z Refererem strony i embedu, i po permutacji md5 kodem licencyjnym
|
|
(sprawdzone cztery warianty). Prawdziwy tor widać dopiero w DevTools:
|
|
|
|
generate_mp4('<base64 JSON>', '<hasło 16 zn.>', '<id ok.ru>', '<id porndupe>')
|
|
→ JSON: {ciphertext, iv, salt, iterations: 999}
|
|
→ PBKDF2-SHA512(hasło, salt, 999, 32 B) + AES-256-CBC → klucz sesji OK.ru
|
|
→ GET api.ok.ru/fb.do?method=video.get&session_key=…&vids=<id ok.ru>
|
|
→ {url_low, url_medium, url_high, url_fullhd, duration (ms), created_ms, thumbnail_url}
|
|
|
|
Hasło i ładunek zmieniają się co wczytanie, ale odszyfrowany klucz sesji jest stały,
|
|
czyli to zaciemnienie, nie sesja per użytkownik. Klucz aplikacji jest wpisany na sztywno
|
|
w kt_player_new.js. Wariant PBKDF2 ustalony empirycznie: SHA-1 i SHA-256 dają bad decrypt,
|
|
SHA-512 oddaje dokładnie klucz z trace'u przeglądarki.
|
|
|
|
**Token okcdn NIE jest związany z IP.** URL ma wbite `srcIp=<IP pobierającego>`, ale
|
|
pobranie z VPS URL-a wygenerowanego z łącza domowego dało 206 i ten sam plik (514 MB,
|
|
`ftypisom`), tak samo jak przy 4k69 i 8kporner. Stąd `mobile_direct_ok`. Ważność ~24 h.
|
|
|
|
**Cap 720p, świadomie.** OK.ru dławi pojedyncze połączenie do ~2,1 Mbps i 1080p kończy się
|
|
„loading forever" (decyzja podjęta dla 4k69 i 8kporner, patrz `kporner8.py`). Oddajemy
|
|
`url_high` (720p) jako najlepszą, niżej 480p i 360p; `url_fullhd` pomijamy.
|
|
|
|
Ten moduł jest też używany przez scraper porndupe do POBRANIA DŁUGOŚCI i daty przy
|
|
ingeście, bo HTML strony nie ma ani `duration`, ani `uploadDate`, a scena bez długości
|
|
znika w apce pod filtrem 60 s.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import re
|
|
from datetime import UTC, date, datetime
|
|
|
|
import httpx
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
from app.extractors._fetch import fetch_tube_html
|
|
from app.extractors._models import StreamSource
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_BASE = "https://www.porndupe.com"
|
|
_APP_KEY = "CBAFJIICABABABABA" # stały, wpisany w kt_player_new.js
|
|
_FIELDS = ",".join(
|
|
f"video.{f}" for f in (
|
|
"url_low", "url_medium", "url_high", "url_fullhd",
|
|
"duration", "created_ms", "thumbnail_url",
|
|
)
|
|
)
|
|
_GEN_RE = re.compile(
|
|
r"generate_mp4\s*\(\s*'([A-Za-z0-9+/=]+)'\s*,\s*'([^']+)'\s*,\s*'(\d+)'\s*,\s*'(\d+)'"
|
|
)
|
|
#: OK.ru → etykieta. `url_fullhd` celowo pominięte (cap 720p, patrz docstring).
|
|
_QUALITY = (("url_high", "720p"), ("url_medium", "480p"), ("url_low", "360p"))
|
|
_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/152.0.0.0 Safari/537.36"
|
|
|
|
|
|
def decrypt_session_key(blob_b64: str, passphrase: str) -> str:
|
|
"""Odszyfruj klucz sesji OK.ru z ładunku `generate_mp4`."""
|
|
p = json.loads(base64.b64decode(blob_b64))
|
|
salt = bytes.fromhex(p["salt"])
|
|
iv = bytes.fromhex(p["iv"])
|
|
ct = base64.b64decode(p["ciphertext"])
|
|
key = hashlib.pbkdf2_hmac("sha512", passphrase.encode(), salt, int(p.get("iterations", 999)), 32)
|
|
dec = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor()
|
|
pt = dec.update(ct) + dec.finalize()
|
|
return pt[: -pt[-1]].decode() # PKCS7
|
|
|
|
|
|
def okru_video(page_html: str, *, timeout: float = 25.0) -> dict | None:
|
|
"""Metadane i URL-e OK.ru dla strony porndupe. Zwraca `videos[0]` uzupełnione o
|
|
`okid` i `vid` (id porndupe) albo None, gdy strona nie ma `generate_mp4` lub API padło."""
|
|
m = _GEN_RE.search(page_html)
|
|
if not m:
|
|
return None
|
|
blob, passphrase, okid, vid = m.groups()
|
|
try:
|
|
session_key = decrypt_session_key(blob, passphrase)
|
|
except Exception as e:
|
|
log.info("porndupe: odszyfrowanie klucza padło: %s", e)
|
|
return None
|
|
url = (
|
|
f"https://api.ok.ru/fb.do?application_key={_APP_KEY}&method=video.get"
|
|
f"&fields={_FIELDS}&session_key={session_key}&vids={okid}"
|
|
)
|
|
try:
|
|
r = httpx.get(
|
|
url,
|
|
headers={"User-Agent": _UA, "Origin": _BASE, "Referer": _BASE + "/"},
|
|
timeout=timeout,
|
|
)
|
|
videos = r.json().get("videos") or []
|
|
except Exception as e:
|
|
log.info("porndupe: api.ok.ru padło dla okid=%s: %s", okid, e)
|
|
return None
|
|
if not videos:
|
|
return None
|
|
v = dict(videos[0])
|
|
v["okid"], v["vid"] = okid, vid
|
|
return v
|
|
|
|
|
|
def okru_duration_and_date(v: dict) -> tuple[int | None, date | None]:
|
|
"""`duration` jest w milisekundach, `created_ms` to epoch ms wgrania na OK.ru."""
|
|
dur = v.get("duration")
|
|
dur_s = int(dur) // 1000 if isinstance(dur, (int, float)) and dur > 0 else None
|
|
cm = v.get("created_ms")
|
|
rel = datetime.fromtimestamp(int(cm) / 1000, tz=UTC).date() if cm else None
|
|
return dur_s, rel
|
|
|
|
|
|
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
|
|
html_text = fetch_tube_html(page_url, timeout=timeout)
|
|
v = okru_video(html_text)
|
|
if not v:
|
|
log.info("porndupe: brak generate_mp4/OK.ru na %s", page_url)
|
|
return None
|
|
out: list[StreamSource] = []
|
|
for field, label in _QUALITY:
|
|
link = v.get(field)
|
|
if link:
|
|
out.append(
|
|
StreamSource(
|
|
link=link,
|
|
type="mp4",
|
|
quality=label,
|
|
referer=None, # okcdn oddaje 206 bez Referera, sprawdzone z VPS
|
|
raw={"mobile_direct_ok": True, "engine": "okru"},
|
|
)
|
|
)
|
|
if not out:
|
|
log.info("porndupe: OK.ru bez url_low/medium/high dla %s", page_url)
|
|
return None
|
|
return out
|