fix(yespornvip): server-side KVS resolve to portable CDN, drop WebView fallback

yespornvip was on the WebView fallback, which loaded the ad-heavy host page; the
INJECTED_JS scrape grabbed the preroll ad video (bkcdn.net, ~30s) instead of the
content, so the native player showed a 30s ad. The get_file content url is also
session/cookie-bound (410 for a cookieless ExoPlayer request).

Key finding: the VPS now reaches yesporn.vip (HTTP 200 — unblocked, same as
porntrex got 2026-05-22), so we can resolve server-side like porntrex instead of
relying on the browser. KVS flashvars carry function/0/-obfuscated get_file urls +
license_code; decode the hash with the kt_player algorithm (yt-dlp KVS algo,
verified to reproduce kt_player's output), then follow each quality's get_file 302
in the same curl_cffi session → final cdn5 url. That url is time-bound signed but
NOT IP/cookie-bound — verified portable cross-IP (VPS-resolved url fetched from a
different IP → 206 video/mp4).

New app/extractors/tubes/yespornvip.py returns 480p/720p/1080p portable CDN urls;
registry switched from _vps_blocked_fallback → yespornvip.extract. Mobile plays
direct natively with a working quality picker — zero WebView, zero ads, zero proxy.
Verified on prod (3 cdn5 sources) and emulator (quality picker → 1080p native
decode at 1920px, no WebView, no ad). Backend-only; no OTA needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-05-31 22:33:10 +02:00
parent 00ea8c3fd4
commit fbfe2b2bf4
2 changed files with 155 additions and 65 deletions

View file

@ -116,18 +116,13 @@ _REGISTRY: dict[str, Callable[[str], list[StreamSource] | None]] = {
# mobile dostaje proxy URL od razu, ExoPlayer gra bez WebView. # mobile dostaje proxy URL od razu, ExoPlayer gra bez WebView.
"porn00org": porn00.extract, "porn00org": porn00.extract,
"pornxpph": _vps_blocked_fallback.extract, "pornxpph": _vps_blocked_fallback.extract,
# yesporn.vip — KVS engine. Initial extractor probował dwóch ścieżek z # yesporn.vip — KVS engine. VPS znów dociera (HTTP 200, odblokowane jak porntrex),
# flashvars, oba okazały się ślepe (2026-05-30 verify): # więc resolvujemy SERVER-SIDE: dekoduj flashvars `video_url`/alt/alt2 (function/0/ +
# - `video_url` (`/get_file/7/`) wymaga PHPSESSID z embed page session; # license_code, algo kt_player) → follow get_file 302 → portable cdn5 url (time-bound,
# mobile fetch bez cookies → 404. # NIE IP/cookie-bound, zweryfikowane cross-IP 2026-05-31). Mobile gra direct natywnie,
# - `event_reporting2` (`/get_file/1/`) zwraca 200 OK ale `Content-Type: # multi-quality, ZERO WebView/reklam/preroll. Wcześniej WebView fallback pokazywał
# image/gif` — to 1px analytics tracker, NIE video. # ad-heavy stronę a scrape łapał preroll-reklamę (bkcdn) zamiast wideo.
# Switch na WebView fallback: mobile loaduje embed w WebView z phone IP, "yespornvip": yespornvip.extract,
# kt_player JS decoduje URL inside browser context (cookies + session set
# properly), INJECTED_JS scrapuje `<video>.src` po decode. UX flicker
# (page renderuje się przed video) vs bandwidth/anonimowość — public-app
# priorytet wygrywa (zob. [[feedback-no-video-proxy]]).
"yespornvip": _vps_blocked_fallback.extract,
# Direct-scraping tubes (mają też search scraper w connectors/direct_scrapers/) # Direct-scraping tubes (mają też search scraper w connectors/direct_scrapers/)
# — używają identycznego embed-iframe pattern dla streamingu. # — używają identycznego embed-iframe pattern dla streamingu.
# hdporn92com — DELISTED 2026-05-18. Scene pages to SEO shell bez player iframe, # hdporn92com — DELISTED 2026-05-18. Scene pages to SEO shell bez player iframe,

View file

@ -1,76 +1,171 @@
"""yesporn.vip — KVS engine direct stream extractor. """yesporn.vip — KVS (kt_player) engine direct stream extractor.
User bug-report 2026-05-27 (#1, scene 0d8ec317): "Yespornvip dalej nie działa". REWRITE 2026-05-31 (user "yespornvip otwiera reklamy / 410 / fallback do strony"):
Origin `tube:yespornvip` istniał w playback_sources ale brak wpisu w `_REGISTRY` VPS znów dociera do yesporn.vip (HTTP 200 odblokowane jak porntrex 2026-05-22),
`try_extract()` zwracał None mobile player no-source. więc resolvujemy SERVER-SIDE zamiast WebView fallback (który pokazywał stronę +
preroll-reklamę i scrape łapał reklamę zamiast wideo).
Detail page sceny linkuje do `/embed/<id>` w iframe. Embed page renderuje KVS KVS flashvars: `video_url` / `video_alt_url` / `video_alt_url2` (480/720/1080p), każdy w
player z `flashvars`: formie `function/0/https://yesporn.vip/get_file/<srv>/<HASH>/...mp4/` + `license_code`.
- `video_url: 'function/0/https://yesporn.vip/get_file/7/<token>/.../<id>.mp4/?embed=true'` kt_player dekoduje HASH algorytmem license_code (permutacja pierwszych 32 znaków
**NIE używać.** Server `/get_file/7/` wymaga PHPSESSID + cookies z embed identyczny algo jak yt-dlp KVS; zweryfikowany 2026-05-31 że odtwarza output kt_player).
page (testowane 2026-05-29: 404 nawet z fresh tokenem, mobile-side cookies
nie matchują VPS-side session). KVS kt_player.js loaduje to wewnątrz
embed page gdzie cookies dostępne.
- `event_reporting2: 'https://yesporn.vip/get_file/1/<token>/.../<id>.mp4/'`
server `/get_file/1/` jest standalone, time-bound signed URL, **200 OK**
direct fetch z headerami (UA + Referer). Bierzemy to.
- `video_url_text: '480p'` quality label.
Single quality (480p) z embed. CDN time-bound signed, mobile gra direct z phone Zdekodowany `get_file` 302 `cdn5.yesporn.vip/remote_control.php?time=&cv=` który
IP (zero VPS bandwidth). serwuje wideo bezpośrednio (206 video/mp4). Ten finalny URL jest **time-bound signed,
NIE IP/cookie-bound** zweryfikowane cross-IP (VPS resolve fetch z innego IP = 206).
Resolvujemy 302 w tej samej sesji co fetch strony (get_file token bywa session-bound),
oddajemy finalny portable CDN url per jakość. Mobile gra direct natywnie, zero WebView,
zero reklam, zero VPS proxy bandwidth.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
import re import re
import time
import urllib.parse as _up
from app.extractors._fetch import fetch_tube_html from app.extractors._fetch import _DEFAULT_IMPERSONATE, _DEFAULT_UA, _HAS_CURL_CFFI, fetch_tube_html
from app.extractors._models import StreamSource from app.extractors._models import StreamSource
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_BASE = "https://yesporn.vip" _BASE = "https://yesporn.vip"
# `event_reporting2: 'https://.../get_file/1/.../<id>.mp4/'` — analytics ping URL # flashvars: `video_url: 'function/0/https://.../get_file/...'` (+ alt/alt2 dla wyższych q).
# który (per testom 2026-05-29) jest sessionless i 200 OK direct. _URL_RE = re.compile(
_EVENT_REPORTING2_RE = re.compile( r"(video(?:_alt)?_url\d*)\s*:\s*'(function/0/[^']+)'",
r"event_reporting2\s*:\s*'(https?://[^']+/get_file/[^']+\.mp4/?[^']*)'",
re.IGNORECASE, re.IGNORECASE,
) )
_QUALITY_RE = re.compile(r"video_url_text\s*:\s*'([^']*)'", re.IGNORECASE) _TEXT_RE = re.compile(
r"(video(?:_alt)?_url\d*)_text\s*:\s*'([^']*)'",
re.IGNORECASE,
)
_LICENSE_RE = re.compile(r"license_code\s*:\s*'(\$[^']+)'", re.IGNORECASE)
_HASH_LENGTH = 32
def _kvs_license_token(license_code: str) -> list[int]:
"""kt_player license token (algo zgodny z yt-dlp KVS)."""
license_code = license_code.replace("$", "")
license_values = [int(c) for c in license_code]
modlicense = license_code.replace("0", "1")
center = len(modlicense) // 2
modlicense = str(4 * abs(int(modlicense[:center + 1]) - int(modlicense[center:])))[:center + 1]
return [
(license_values[index + offset] + current) % 10
for index, current in enumerate(int(c) for c in modlicense)
for offset in range(4)
]
def _kvs_real_url(video_url: str, license_code: str) -> str:
"""Dekoduje `function/0/...get_file/N/<HASH>/...` permutując pierwsze 32 znaki HASH."""
if not video_url.startswith("function/0/"):
return video_url
parsed = _up.urlparse(video_url[len("function/0/"):])
lt = _kvs_license_token(license_code)
parts = parsed.path.split("/")
h = parts[3][:_HASH_LENGTH]
idx = list(range(_HASH_LENGTH))
acc = 0
for src in reversed(range(_HASH_LENGTH)):
acc += lt[src]
dest = (src + acc) % _HASH_LENGTH
idx[src], idx[dest] = idx[dest], idx[src]
parts[3] = "".join(h[i] for i in idx) + parts[3][_HASH_LENGTH:]
return _up.urlunparse(parsed._replace(path="/".join(parts)))
def _quality_rank(label: str | None) -> int:
if not label:
return -1
m = re.search(r"(\d{3,4})\s*p", label, re.IGNORECASE)
return int(m.group(1)) if m else -1
def _resolve_get_file(session, get_file_url: str, timeout: float) -> str | None:
"""Follow get_file 302 → finalny portable CDN url (w sesji z cookies strony)."""
sep = "&" if "?" in get_file_url else "?"
url = f"{get_file_url}{sep}rnd={int(time.time() * 1000)}"
try:
r = session.get(
url, timeout=timeout, allow_redirects=True, stream=True,
headers={"Referer": _BASE + "/", "Range": "bytes=0-1"},
)
final = str(r.url)
status = r.status_code
r.close()
except Exception as e:
log.info("yespornvip: get_file resolve failed (%s): %s", get_file_url[:60], e)
return None
if status >= 400 or "/get_file/" in final:
log.info("yespornvip: get_file resolve bad status=%s final=%s", status, final[:70])
return None
return final
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
if "/embed/" not in page_url: # Wspólna sesja: get_file token jest session-bound, więc 302 MUSI lecieć w tej samej
# Detail page → derive embed URL via /video/<id>/<slug>/ → /embed/<id>. # sesji curl_cffi co fetch strony.
m = re.search(r"/video/(\d+)/", page_url) if not _HAS_CURL_CFFI:
if m: # Bez curl_cffi nie zrobimy same-session resolve — brak fallbacku (WebView fallback
embed_url = f"{_BASE}/embed/{m.group(1)}" # pokazywał reklamy; lepiej None niż reklama).
else: log.info("yespornvip: curl_cffi unavailable — cannot resolve")
log.info("yespornvip: cannot derive embed from %s", page_url)
return None
else:
embed_url = page_url
html = fetch_tube_html(embed_url, timeout=timeout)
m = _EVENT_REPORTING2_RE.search(html)
if not m:
log.info("yespornvip: no event_reporting2 in flashvars on %s", embed_url)
return None return None
url = m.group(1) from curl_cffi import requests as _cf_requests
quality = None session = _cf_requests.Session(impersonate=_DEFAULT_IMPERSONATE)
q_match = _QUALITY_RE.search(html) try:
if q_match: resp = session.get(
quality = q_match.group(1).strip() or None page_url,
headers={"User-Agent": _DEFAULT_UA, "Accept": "text/html,application/xhtml+xml"},
return [ timeout=timeout, allow_redirects=True,
StreamSource(
link=url,
type="mp4",
quality=quality,
referer=_BASE + "/",
raw={"mobile_direct_ok": True},
) )
] html = resp.text if resp.status_code < 400 else ""
except Exception as e:
log.info("yespornvip: page fetch failed %s: %s", page_url, e)
return None
if not html:
log.info("yespornvip: empty page %s", page_url)
return None
licm = _LICENSE_RE.search(html)
if not licm:
log.info("yespornvip: no license_code on %s", page_url)
return None
license_code = licm.group(1)
quality_by_var: dict[str, str] = {}
for m in _TEXT_RE.finditer(html):
quality_by_var[m.group(1).lower()] = m.group(2).strip()
seen_dec: set[str] = set()
result: list[StreamSource] = []
for m in _URL_RE.finditer(html):
var_name = m.group(1).lower()
decoded = _kvs_real_url(m.group(2), license_code)
if decoded in seen_dec:
continue
seen_dec.add(decoded)
final = _resolve_get_file(session, decoded, timeout)
if not final:
continue
result.append(
StreamSource(
link=final,
type="mp4",
quality=quality_by_var.get(var_name) or None,
referer=_BASE + "/",
# cdn5 remote_control.php url: time-bound signed, NIE IP/cookie-bound
# (zweryfikowane cross-IP) → mobile gra direct, zero proxy.
raw={"mobile_direct_ok": True},
)
)
if not result:
log.info("yespornvip: no resolvable get_file on %s", page_url)
return None
result.sort(key=lambda s: _quality_rank(s.quality), reverse=True)
return result