fix(pornditt): server-side KVS resolve; extract shared _kvs helper
pornditt is the same kt_player KVS engine as yespornvip: flashvars carry function/0/-obfuscated get_file urls + license_code, and the VPS reaches it (HTTP 200). It was on _vps_blocked_fallback (WebView), where the scrape grabbed the VAST preroll ad (trafostatic) instead of content (user bug "pornditt łapie reklamę zamiast video"). Extracted the verified yespornvip logic into app/extractors/tubes/_kvs.py (resolve_kvs: fetch page → decode function/0 get_file via kt_player algo → follow 302 in-session → portable CDN, multi-quality). yespornvip.py and new pornditt.py are now thin wrappers. Registry: porndittcom _vps_blocked_fallback → pornditt.extract. Verified on prod: pornditt → 720p/480p on twa.tgprn.com (portable, fresh-session 206 video/mp4); yespornvip still → 1080/720/480p on cdn5 (refactor intact). Backend-only, no OTA — mobile plays mp4+mobile_direct_ok natively with quality picker, zero WebView/ads. Note: a real-browser residential load shows MEDIA_ERR on the content (the page's own player flow / ad gating); server-side decode+follow sidesteps the player entirely, which is why it resolves cleanly. The original bug scene (40f118e1) has its video deleted on pornditt — verified on a live scene (156091). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31d9076f27
commit
920740b76f
4 changed files with 198 additions and 162 deletions
|
|
@ -33,6 +33,7 @@ from app.extractors.tubes import (
|
||||||
latestpornvideo,
|
latestpornvideo,
|
||||||
paradisehill,
|
paradisehill,
|
||||||
porn00,
|
porn00,
|
||||||
|
pornditt,
|
||||||
pornhat,
|
pornhat,
|
||||||
porntrex,
|
porntrex,
|
||||||
pornxp,
|
pornxp,
|
||||||
|
|
@ -82,7 +83,10 @@ _REGISTRY: dict[str, Callable[[str], list[StreamSource] | None]] = {
|
||||||
# IP (potwierdzone Chrome DevTools MCP 2026-05-15). Mobile WebView + INJECTED_JS
|
# IP (potwierdzone Chrome DevTools MCP 2026-05-15). Mobile WebView + INJECTED_JS
|
||||||
# (PlayerScreen.tsx:805) skanuje <video>.src + XHR — łapie URL po decode-ie player JS.
|
# (PlayerScreen.tsx:805) skanuje <video>.src + XHR — łapie URL po decode-ie player JS.
|
||||||
"xhamstercom": _vps_blocked_fallback.extract,
|
"xhamstercom": _vps_blocked_fallback.extract,
|
||||||
"porndittcom": _vps_blocked_fallback.extract,
|
# pornditt — KVS jak yespornvip (function/0 + license). VPS dociera → resolve
|
||||||
|
# server-side (decode + follow 302 → portable twa.tgprn.com CDN). Wcześniej WebView
|
||||||
|
# fallback łapał VAST preroll (trafostatic) zamiast contentu. Patrz pornditt.py/_kvs.py.
|
||||||
|
"porndittcom": pornditt.extract,
|
||||||
"fpoxxx": _vps_blocked_fallback.extract,
|
"fpoxxx": _vps_blocked_fallback.extract,
|
||||||
"sxylandcom": _vps_blocked_fallback.extract,
|
"sxylandcom": _vps_blocked_fallback.extract,
|
||||||
# Aggregator tubes — generic embed-iframe → hoster unpacker
|
# Aggregator tubes — generic embed-iframe → hoster unpacker
|
||||||
|
|
|
||||||
163
app/extractors/tubes/_kvs.py
Normal file
163
app/extractors/tubes/_kvs.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
"""Współdzielony resolver dla KVS (kt_player) tube'ów z `function/0/` + `license_code`.
|
||||||
|
|
||||||
|
Wzorzec (yespornvip, pornditt, ... — wszystkie KVS na tym samym silniku):
|
||||||
|
flashvars: `video_url` / `video_alt_url` / `video_alt_url2` = `function/0/https://<host>/
|
||||||
|
get_file/<srv>/<HASH>/.../<id>.mp4/` (480/720/1080p) + `license_code: '$...'`.
|
||||||
|
kt_player dekoduje HASH (permutacja pierwszych 32 znaków algorytmem license_code —
|
||||||
|
algo zgodny z yt-dlp KVS `_kvs_get_real_url`, zweryfikowany 2026-05-31 że odtwarza
|
||||||
|
output kt_player).
|
||||||
|
|
||||||
|
Zdekodowany `get_file` 302-redirectuje do CDN (cdn5.yesporn.vip / twa.tgprn.com / ...)
|
||||||
|
który serwuje wideo (206). Finalny URL jest **time-bound signed, NIE IP/cookie-bound** —
|
||||||
|
portable cross-IP. get_file token jest session-bound, więc 302 MUSI być rozwiązany w tej
|
||||||
|
samej sesji curl_cffi co fetch strony. Oddajemy finalny portable CDN url per jakość →
|
||||||
|
mobile gra direct natywnie, multi-quality, zero WebView/reklam/proxy.
|
||||||
|
|
||||||
|
Dlaczego server-side (nie WebView): WebView ładuje ad-heavy stronę + VAST preroll, a
|
||||||
|
scrape łapie reklamę zamiast contentu. Direct get_file→CDN omija player/reklamę.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import urllib.parse as _up
|
||||||
|
|
||||||
|
from app.extractors._fetch import _DEFAULT_IMPERSONATE, _DEFAULT_UA, _HAS_CURL_CFFI
|
||||||
|
from app.extractors._models import StreamSource
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_URL_RE = re.compile(
|
||||||
|
r"(video(?:_alt)?_url\d*)\s*:\s*[\"'](function/0/[^\"']+)[\"']",
|
||||||
|
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 _license_token(license_code: str) -> list[int]:
|
||||||
|
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 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 = _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, base_url: str, get_file_url: str, timeout: float) -> str | None:
|
||||||
|
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_url + "/", "Range": "bytes=0-1"},
|
||||||
|
)
|
||||||
|
final = str(r.url)
|
||||||
|
status = r.status_code
|
||||||
|
r.close()
|
||||||
|
except Exception as e:
|
||||||
|
log.info("kvs: get_file resolve failed (%s): %s", get_file_url[:60], e)
|
||||||
|
return None
|
||||||
|
if status >= 400 or "/get_file/" in final:
|
||||||
|
log.info("kvs: get_file resolve bad status=%s final=%s", status, final[:70])
|
||||||
|
return None
|
||||||
|
return final
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_kvs(page_url: str, *, base_url: str, timeout: float = 60.0) -> list[StreamSource] | None:
|
||||||
|
"""Fetch KVS page → decode function/0 get_file (per jakość) → follow 302 → portable CDN.
|
||||||
|
|
||||||
|
Zwraca StreamSource'y posortowane malejąco po jakości, lub None gdy nic nie wyszło.
|
||||||
|
base_url: scheme+host hosta (np. 'https://yesporn.vip') — do Referera i logów.
|
||||||
|
"""
|
||||||
|
if not _HAS_CURL_CFFI:
|
||||||
|
log.info("kvs: curl_cffi unavailable — cannot resolve %s", page_url)
|
||||||
|
return None
|
||||||
|
from curl_cffi import requests as _cf_requests
|
||||||
|
session = _cf_requests.Session(impersonate=_DEFAULT_IMPERSONATE)
|
||||||
|
try:
|
||||||
|
resp = session.get(
|
||||||
|
page_url,
|
||||||
|
headers={"User-Agent": _DEFAULT_UA, "Accept": "text/html,application/xhtml+xml"},
|
||||||
|
timeout=timeout, allow_redirects=True,
|
||||||
|
)
|
||||||
|
html = resp.text if resp.status_code < 400 else ""
|
||||||
|
except Exception as e:
|
||||||
|
log.info("kvs: page fetch failed %s: %s", page_url, e)
|
||||||
|
return None
|
||||||
|
if not html:
|
||||||
|
log.info("kvs: empty page %s", page_url)
|
||||||
|
return None
|
||||||
|
|
||||||
|
licm = _LICENSE_RE.search(html)
|
||||||
|
if not licm:
|
||||||
|
log.info("kvs: 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 = real_url(m.group(2), license_code)
|
||||||
|
if decoded in seen_dec:
|
||||||
|
continue
|
||||||
|
seen_dec.add(decoded)
|
||||||
|
final = _resolve_get_file(session, base_url, decoded, timeout)
|
||||||
|
if not final:
|
||||||
|
continue
|
||||||
|
result.append(
|
||||||
|
StreamSource(
|
||||||
|
link=final,
|
||||||
|
type="mp4",
|
||||||
|
quality=quality_by_var.get(var_name) or None,
|
||||||
|
referer=base_url + "/",
|
||||||
|
raw={"mobile_direct_ok": True},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
log.info("kvs: no resolvable get_file on %s", page_url)
|
||||||
|
return None
|
||||||
|
|
||||||
|
result.sort(key=lambda s: _quality_rank(s.quality), reverse=True)
|
||||||
|
return result
|
||||||
23
app/extractors/tubes/pornditt.py
Normal file
23
app/extractors/tubes/pornditt.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""pornditt.com — KVS (kt_player) direct stream extractor. Patrz app/extractors/tubes/_kvs.py.
|
||||||
|
|
||||||
|
User bug 2026-05-31 (scene 40f118e1): "Pornditt łapie reklamę zamiast video". pornditt
|
||||||
|
był na _vps_blocked_fallback (WebView), gdzie scrape łapał VAST preroll (trafostatic) zamiast
|
||||||
|
contentu. Identyczny silnik jak yespornvip: flashvars `video_url`/`video_alt_url` =
|
||||||
|
`function/0/...get_file/...` + `license_code`; VPS dociera (HTTP 200). Resolve server-side:
|
||||||
|
decode + follow 302 → portable CDN (twa.tgprn.com, time-bound, NIE IP/cookie-bound —
|
||||||
|
zweryfikowane cross-IP 2026-06-01 fresh session → 206 video/mp4). Native, multi-quality,
|
||||||
|
zero WebView/reklam.
|
||||||
|
|
||||||
|
NB: runtime `window.flashvars.video_url` pokazuje już ZDEKODOWANY plain get_file, ale raw
|
||||||
|
HTML (server-fetch) ma formę `function/0/...` + license — dekodujemy sami (_kvs.real_url).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.extractors._models import StreamSource
|
||||||
|
from app.extractors.tubes import _kvs
|
||||||
|
|
||||||
|
_BASE = "https://v.pornditt.com"
|
||||||
|
|
||||||
|
|
||||||
|
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
|
||||||
|
return _kvs.resolve_kvs(page_url, base_url=_BASE, timeout=timeout)
|
||||||
|
|
@ -1,171 +1,17 @@
|
||||||
"""yesporn.vip — KVS (kt_player) engine direct stream extractor.
|
"""yesporn.vip — KVS (kt_player) direct stream extractor. Patrz app/extractors/tubes/_kvs.py.
|
||||||
|
|
||||||
REWRITE 2026-05-31 (user "yespornvip otwiera reklamy / 410 / fallback do strony"):
|
VPS dociera do yesporn.vip (HTTP 200, odblokowane jak porntrex 2026-05-22) → resolvujemy
|
||||||
VPS znów dociera do yesporn.vip (HTTP 200 — odblokowane jak porntrex 2026-05-22),
|
SERVER-SIDE (decode function/0 get_file + follow 302 → portable cdn5 url) zamiast WebView
|
||||||
więc resolvujemy SERVER-SIDE zamiast WebView fallback (który pokazywał stronę +
|
fallback który pokazywał stronę + preroll-reklamę. Native, multi-quality, zero reklam.
|
||||||
preroll-reklamę i scrape łapał reklamę zamiast wideo).
|
Zweryfikowane 2026-05-31: prod 3×cdn5, emulator 1080p picker → native decode.
|
||||||
|
|
||||||
KVS flashvars: `video_url` / `video_alt_url` / `video_alt_url2` (480/720/1080p), każdy w
|
|
||||||
formie `function/0/https://yesporn.vip/get_file/<srv>/<HASH>/...mp4/` + `license_code`.
|
|
||||||
kt_player dekoduje HASH algorytmem license_code (permutacja pierwszych 32 znaków —
|
|
||||||
identyczny algo jak yt-dlp KVS; zweryfikowany 2026-05-31 że odtwarza output kt_player).
|
|
||||||
|
|
||||||
Zdekodowany `get_file` 302 → `cdn5.yesporn.vip/remote_control.php?time=&cv=` który
|
|
||||||
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 re
|
|
||||||
import time
|
|
||||||
import urllib.parse as _up
|
|
||||||
|
|
||||||
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
|
||||||
|
from app.extractors.tubes import _kvs
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_BASE = "https://yesporn.vip"
|
_BASE = "https://yesporn.vip"
|
||||||
|
|
||||||
# flashvars: `video_url: 'function/0/https://.../get_file/...'` (+ alt/alt2 dla wyższych q).
|
|
||||||
_URL_RE = re.compile(
|
|
||||||
r"(video(?:_alt)?_url\d*)\s*:\s*'(function/0/[^']+)'",
|
|
||||||
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:
|
||||||
# Wspólna sesja: get_file token jest session-bound, więc 302 MUSI lecieć w tej samej
|
return _kvs.resolve_kvs(page_url, base_url=_BASE, timeout=timeout)
|
||||||
# sesji curl_cffi co fetch strony.
|
|
||||||
if not _HAS_CURL_CFFI:
|
|
||||||
# Bez curl_cffi nie zrobimy same-session resolve — brak fallbacku (WebView fallback
|
|
||||||
# pokazywał reklamy; lepiej None niż reklama).
|
|
||||||
log.info("yespornvip: curl_cffi unavailable — cannot resolve")
|
|
||||||
return None
|
|
||||||
|
|
||||||
from curl_cffi import requests as _cf_requests
|
|
||||||
session = _cf_requests.Session(impersonate=_DEFAULT_IMPERSONATE)
|
|
||||||
try:
|
|
||||||
resp = session.get(
|
|
||||||
page_url,
|
|
||||||
headers={"User-Agent": _DEFAULT_UA, "Accept": "text/html,application/xhtml+xml"},
|
|
||||||
timeout=timeout, allow_redirects=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
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue