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

94 lines
3.1 KiB
Python

"""eporner.com, direct stream extractor.
Page → wyciągnij `EP.video.player.vid` + `EP.video.player.hash` → XHR
`/xhr/video/<vid>?hash=<base36>` → JSON z multi-quality mp4 URL'ami (240p-1080p).
Bez hasha gvideo.eporner.com URL'e zwracają 403 (są podpisane krótkoterminowymi
tokenami które XHR zwraca świeże). Algorytm hex→base36 ekwiwalentny temu z
aplikacji AIO Streamer (`rf1.java`):
BigInteger(hex[0:8],16).toString(36) + BigInteger(hex[8:16],16).toString(36)
+ BigInteger(hex[16:24],16).toString(36) + BigInteger(hex[24:32],16).toString(36)
URL-e są bound do IP requestera (więc resolver musi pobiegać z VPS-a, nie z
klienta) i ważne ~kilka godzin.
"""
from __future__ import annotations
import json
import logging
import re
from app.extractors._fetch import _DEFAULT_UA, browser_get, fetch_tube_html
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_VID_RE = re.compile(r"EP\.video\.player\.vid\s*=\s*'([^']+)'")
_HASH_RE = re.compile(r"EP\.video\.player\.hash\s*=\s*'([0-9a-fA-F]{32})'")
def _hash_to_b36(hex_hash: str) -> str:
"""Konwertuje 32-znakowy hex hash na base36 w 4 chunkach po 8 znaków."""
parts = []
for i in (0, 8, 16, 24):
n = int(hex_hash[i : i + 8], 16)
if n == 0:
parts.append("0")
continue
s = ""
while n > 0:
d = n % 36
ch = chr(ord("0") + d) if d < 10 else chr(ord("a") + d - 10)
s = ch + s
n //= 36
parts.append(s)
return "".join(parts)
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html = fetch_tube_html(page_url, timeout=timeout)
m_vid = _VID_RE.search(html)
m_hash = _HASH_RE.search(html)
if not m_vid or not m_hash:
log.warning("eporner: no vid/hash in %s", page_url)
return None
vid = m_vid.group(1)
hash_b36 = _hash_to_b36(m_hash.group(1))
xhr_url = (
f"https://www.eporner.com/xhr/video/{vid}?hash={hash_b36}"
"&domain=www.eporner.com&pixelRatio=1&playerWidth=0&playerHeight=0"
"&fallback=false&embed=false&supportedFormats=mp4"
)
headers = {
"User-Agent": _DEFAULT_UA,
"Referer": page_url,
"Accept": "*/*",
"X-Requested-With": "XMLHttpRequest",
}
try:
r = browser_get(xhr_url, headers=headers, timeout=timeout, follow_redirects=True)
r.raise_for_status()
data = json.loads(r.text)
except Exception as e:
log.warning("eporner xhr fetch %s failed: %s", xhr_url, e)
return None
if not data.get("available", True):
log.info("eporner xhr %s: available=false", vid)
return None
mp4_dict = (data.get("sources") or {}).get("mp4") or {}
sources: list[StreamSource] = []
for label, info in mp4_dict.items():
if not isinstance(info, dict):
continue
src = info.get("src")
if not src:
continue
sources.append(StreamSource(link=src, quality=label, type="mp4"))
if not sources:
log.warning("eporner xhr %s: no mp4 sources in JSON", vid)
return None
return sources