goon/app/extractors/tubes/rule34video.py
goon-foss 2939e03d59 feat(3d): konektor rule34video - piate zrodlo wertykalu, pierwsze 3D
Animacje SFM/Blender. Kanon nie istnieje tak samo jak przy hentai, wiec
kazda pozycja jest orphanem i to poprawne.

Playback: KVS z jawnymi flashvars (4 jakosci, bez license_code), ale
`v-acctoken` jest ZWIAZANY Z IP tego, kto pobral strone. Test krzyzowy:
URL wygenerowany przez proxy i pobrany z VPS = 403, ten sam URL z IP
generujacego = 206 ftypisom 70 MB. Po zdekodowaniu tokenu adresu nie
widac wprost (inaczej niz fpoxxx), wiec samo ogladanie nie wystarcza.

Dlatego bez mobile_direct_ok: telefon resolwuje sam, UPFRONT, istniejaca
sciezka isGetFileUrl()/resolveGetFilePage z SceneDetailScreen. Rozszerzam
getfileResolver o flashvars, bo dotad umial tylko <source> (hdporn.gg,
fullmovies.xxx). Zweryfikowane end-to-end z residential IP: strona ->
flashvars 720p -> 302 na boomio-cdn -> 206 ftypisom 39,8 MB.

Bramka dlugosci zestrojona z `min_ingest_duration_sec` (prod 180 s)
zamiast wlasnej liczby: pierwsza wersja miala 60 s i liczniki klamaly
(seen 57, new 31, skipped 26). Po zestrojeniu seen 20, new 20, skipped 0.
Krotka tresc wycina ~70% katalogu (mediana 68 s).

Tagi TYLKO z class=tag_item. `class=button` wyglada na wlasciwe, a jest
chmura "Top Tags": przy filmie o Lux siedzialy tam "cell (dbz)" i
"queen of pain (dota)". Autor animacji zapisany jako studio.
2026-08-13 08:47:04 +02:00

69 lines
3 KiB
Python

"""rule34video.com — KVS `get_file` ZWIĄZANY Z IP. Dodany 2026-08-13.
Silnik to KVS, ale bez zaciemniania: flashvars trzymają cztery gotowe URL-e, bez
`function/0/` i bez potrzeby dekodowania `license_code`:
video_url: …/get_file/51/<hash>/4543000/4543893/4543893_360.mp4/?v-acctoken=…
video_alt_url: … _480p.mp4 (video_alt_url_text: '480p')
video_alt_url2: … _720p.mp4 (video_alt_url2_text: '720p')
video_alt_url3: … _1080p.mp4 (video_alt_url3_text: '1080p')
**`v-acctoken` jest związany z IP tego, kto pobrał STRONĘ.** Test krzyżowy 2026-08-13:
URL wygenerowany przez proxy i pobrany z VPS-a → **403**; ten sam URL pobrany z IP, które
go wygenerowało → 206, `ftypisom`, 70 MB. Po zdekodowaniu (`4533|163838|0|<md5>`) adresu
nie widać wprost, w odróżnieniu od fpoxxx, gdzie IP siedziało w treści tokenu — czyli
samo oglądanie tokenu NIE wystarcza, żeby to wykryć, trzeba zrobić test krzyżowy.
Dlatego **NIE ma tu `mobile_direct_ok`**: URL zresolwowany na VPS-ie jest dla telefonu
martwy. Zamiast tego oddajemy go jako zwykły link `get_file`, a resztę robi istniejąca
maszyneria po stronie telefonu: `SceneDetailScreen` wykrywa `isGetFileUrl()` i JESZCZE
PRZED wejściem do playera pobiera stronę z telefonu (token dla IP telefonu), a
`getfileResolver` wyciąga z niej flashvars. Ten sam mechanizm obsługuje hdporn.gg
i fullmovies.xxx, tam różnicą jest tylko `<source>` zamiast flashvars.
Backend zwraca więc URL-e po to, żeby apka znała listę jakości i etykiety, nie po to,
żeby je odtwarzać z VPS-a.
"""
from __future__ import annotations
import logging
import re
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_BASE = "https://rule34video.com"
_URL_RE = re.compile(r"(video_(?:alt_)?url\d*)\s*:\s*'(https?://[^']+\.mp4[^']*)'")
_LABEL_RE = re.compile(r"(video_(?:alt_)?url\d*)_text\s*:\s*'([^']{1,12})'")
#: Kolejność od najlepszej jakości. 4K nie występuje, serwis kończy na 1080p.
_QUALITY_RANK = {"1080p": 0, "720p": 1, "480p": 2, "360p": 3}
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html_text = fetch_tube_html(page_url, timeout=timeout)
labels = dict(_LABEL_RE.findall(html_text))
out: list[StreamSource] = []
seen: set[str] = set()
for var, link in _URL_RE.findall(html_text):
if link in seen:
continue
seen.add(link)
out.append(
StreamSource(
link=link,
type="mp4",
quality=labels.get(var),
referer=_BASE + "/",
# Bez `mobile_direct_ok`: token jest IP-bound, telefon resolwuje sam.
raw={},
)
)
if not out:
log.info("rule34video: brak video_url we flashvars na %s", page_url)
return None
out.sort(key=lambda s: _QUALITY_RANK.get((s.quality or "").lower(), 99))
return out