fix(seekplayer): sprawdzaj czy adres CDN naprawde odpowiada, plus obsluga sciezki Tiktok
Zgloszenie a405b9d6 "znowu galaxyporn nie dziala". Przyczyna byla inna niz sie wydawalo:
nasz resolver dzialal, tylko oddawal adres do pliku, ktorego juz nie ma.
Co ustalone przez reverse engineering ich bundla i pomiary:
1. Adres strumienia siedzi w KILKU polach, a serwis sam podaje w `streamingConfig`,
ktorej sciezki uzyc i w jakiej kolejnosci:
{Cloudflare: cf, Tiktok: hlsVideoTiktok, Google: hlsVideoGoogle, "In-House": source}.
Czytalismy WYLACZNIE `source`, wiec sceny majace tylko `hlsVideoTiktok` traktowalismy
jak martwe. A one graja: zweryfikowany manifest 1080p, 200 i z serwerowni, i z lacza
domowego. Doszlo skladanie adresu wg ich regul (`/hls/` -> `/hlsmod/<domain>/` + params).
2. WAZNIEJSZE: adres do usunietego pliku wyglada normalnie, a CDN oddaje na niego 502.
Sprawdzone: wezly 94.131.217.172/175/177 rotuja i WSZYSTKIE daja 502 dla jednego pliku,
podczas gdy TEN SAM wezel .172 serwuje inny plik poprawnie. Czyli CDN zyje, brakuje
konkretnych plikow. Oddawalismy taki adres jako dobry, aplikacja pokazywala czarny
ekran, a ratunek z vikingfile nigdy sie nie uruchamial, bo lista zrodel nie byla pusta.
Teraz kazdy kandydat jest sprawdzany (`_responds`) i zwracamy tylko ten, ktory
naprawde odpowiada manifestem.
3. galaxyporn: gdy player mowi wprost, ze pliku nie ma, a na stronie NIE MA tez linku do
vikingfile, przepuszczamy `HosterDead` dalej zamiast zwracac None. Wczesniej takie
sceny dawaly 503 "sprobuj ponownie" i wracaly w kolko.
Weryfikacja, ze `_responds` nie odrzuca dzialajacych: scena `#puwcko` -> http=200,
responds=True, prawdziwy #EXTM3U (przyjeta); `#bgitoa` -> 502 na wszystkich wezlach
(odrzucona). Na probce 20 scen galaxyporn 85% trafia teraz do vikingfile zamiast dostawac
martwy adres. Scena ze zgloszenia zostala oznaczona martwa przez prawdziwy endpoint
resolve (410 zamiast czarnego ekranu).
Czego NIE zrobilem: vikingfile wymaga tokenu z WIDOCZNEGO checkboxa Cloudflare
(`POST <adres strony>` z `cf-turnstile-response=<token>`). Obchodzenia tego nie buduje.
This commit is contained in:
parent
62c1aaf250
commit
365c97a1de
2 changed files with 130 additions and 57 deletions
|
|
@ -41,7 +41,7 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
from cryptography.hazmat.primitives import padding
|
from cryptography.hazmat.primitives import padding
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
@ -92,6 +92,69 @@ def _decrypt(hex_str: str) -> str:
|
||||||
return (unpadder.update(pt) + unpadder.finalize()).decode("utf-8", errors="replace")
|
return (unpadder.update(pt) + unpadder.finalize()).decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Mapowanie z ICH bundla (`index-*.js`, funkcja skladajaca liste zrodel):
|
||||||
|
# {Cloudflare: cf, Tiktok: hlsVideoTiktok, Google: hlsVideoGoogle, "In-House": source}
|
||||||
|
# Kolejnosc prob i przekształcenia adresu przychodza w `streamingConfig`, ktore serwis
|
||||||
|
# ustawia per plik. Do 2026-08-26 czytalismy WYLACZNIE `source`, wiec sceny majace
|
||||||
|
# wylacznie `hlsVideoTiktok` (a takie sa) traktowalismy jak martwe, mimo ze graja.
|
||||||
|
_STREAM_FIELDS = {
|
||||||
|
"Cloudflare": "cf",
|
||||||
|
"Tiktok": "hlsVideoTiktok",
|
||||||
|
"Google": "hlsVideoGoogle",
|
||||||
|
"In-House": "source",
|
||||||
|
}
|
||||||
|
# Ile kandydatow realnie sprawdzamy. Sprawdzenie to osobne pobranie, a to sciezka
|
||||||
|
# resolve, na ktora czeka uzytkownik; przy tylu proba i tak trafiamy w to, co dziala.
|
||||||
|
_MAX_CANDIDATES = 3
|
||||||
|
_VERIFY_TIMEOUT = 8.0
|
||||||
|
|
||||||
|
|
||||||
|
def _build_candidate(host: str, raw_value: str | None, cfg: dict, key: str) -> str | None:
|
||||||
|
"""Sklada adres dla jednej sciezki CDN wg regul z `streamingConfig.adjust`.
|
||||||
|
|
||||||
|
Odwzorowanie ich funkcji: pomijamy wylaczone, zamieniamy `/hls/` na
|
||||||
|
`/hlsmod/<domain>/` (tak kieruja ruch na CDN TikToka) i doklejamy `params`.
|
||||||
|
"""
|
||||||
|
adjust = (cfg.get("adjust") or {}).get(key) or {}
|
||||||
|
if adjust.get("disabled") or not raw_value:
|
||||||
|
return None
|
||||||
|
url = raw_value if raw_value.startswith("http") else host + raw_value
|
||||||
|
domain = adjust.get("domain")
|
||||||
|
if domain and "/hls/" in url:
|
||||||
|
url = url.replace("/hls/", "/hlsmod/" + domain + "/")
|
||||||
|
params = adjust.get("params")
|
||||||
|
if isinstance(params, dict) and params:
|
||||||
|
url += ("&" if "?" in url else "?") + urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def _responds(url: str, referer: str) -> bool:
|
||||||
|
"""Czy pod tym adresem NAPRAWDE cos jest.
|
||||||
|
|
||||||
|
Konieczne, bo serwis podaje adresy do plikow, ktorych juz nie ma: ten sam wezel CDN
|
||||||
|
serwuje jeden plik poprawnie, a na inny oddaje 502 (sprawdzone 2026-08-26, wezly
|
||||||
|
94.131.217.172/175/177 rotuja i wszystkie zwracaja 502 dla usunietego pliku). Bez tej
|
||||||
|
weryfikacji oddawalismy martwy adres, aplikacja pokazywala czarny ekran, a ratunek
|
||||||
|
z vikingfile nigdy sie nie uruchamial, bo lista zrodel nie byla pusta.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
r = browser_get(
|
||||||
|
url,
|
||||||
|
headers={"User-Agent": _DEFAULT_UA, "Referer": referer},
|
||||||
|
timeout=_VERIFY_TIMEOUT,
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug("seekplayer-engine: weryfikacja %s padla (%s)", url, e)
|
||||||
|
return False
|
||||||
|
if r.status_code != 200:
|
||||||
|
return False
|
||||||
|
head = (r.text or "")[:200]
|
||||||
|
# Manifest HLS albo lista adresow (wariant `cf` oddaje plik tekstowy z linkami).
|
||||||
|
return "#EXTM3U" in head or head.strip().startswith("http")
|
||||||
|
|
||||||
|
|
||||||
def extract(page_url: str, *, timeout: float = 30.0) -> list[StreamSource] | None:
|
def extract(page_url: str, *, timeout: float = 30.0) -> list[StreamSource] | None:
|
||||||
parsed = urlparse(page_url)
|
parsed = urlparse(page_url)
|
||||||
if not parsed.hostname or not _HOST_RE.match(parsed.hostname):
|
if not parsed.hostname or not _HOST_RE.match(parsed.hostname):
|
||||||
|
|
@ -138,60 +201,59 @@ def extract(page_url: str, *, timeout: float = 30.0) -> list[StreamSource] | Non
|
||||||
if isinstance(data, dict) and data.get("error"):
|
if isinstance(data, dict) and data.get("error"):
|
||||||
raise HosterDead(f"seekplayer-engine {page_url}: {data['error']}")
|
raise HosterDead(f"seekplayer-engine {page_url}: {data['error']}")
|
||||||
|
|
||||||
source = (data.get("source") or "").strip()
|
# Adresy strumienia siedza w kilku polach naraz, a serwis sam mowi w
|
||||||
|
# `streamingConfig`, ktorej sciezki uzyc i w jakiej kolejnosci. Do 2026-08-26
|
||||||
|
# czytalismy tylko `source`, przez co sceny majace wylacznie `hlsVideoTiktok`
|
||||||
|
# (zweryfikowane: oddaje manifest 1080p, dziala i z serwerowni, i z lacza domowego)
|
||||||
|
# traktowalismy jak martwe.
|
||||||
|
#
|
||||||
|
# `source` to m3u8 na wezle CDN pod GOLYM IP. Token ma ~2,8 h TTL i NIE jest zwiazany
|
||||||
|
# z IP (sprawdzone 2026-08-14), a wezly rotuja, wiec padniecie odtwarzania leczy
|
||||||
|
# ponowny resolve (`BACKEND_RERESOLVE_ORIGINS` w PlayerScreen), nie WebView.
|
||||||
|
#
|
||||||
|
# `proxy_no_verify` NIE JEST przez nic czytane; flaga jest informacyjna i zapisuje
|
||||||
|
# realne ryzyko: gdyby ktorys wezel zaczal serwowac cert niepasujacy do IP, trzeba
|
||||||
|
# bedzie dopisac galaz `verify=False` w `stream_proxy.py`.
|
||||||
|
|
||||||
# Source: m3u8 na CDN edge pod GOŁYM IP (np. `185.237.107.146/v4/<sig>/<exp>/…/master.m3u8`).
|
cfg = {}
|
||||||
# Token ma ~2,8 h TTL i NIE jest związany z IP (sprawdzone 2026-08-14), a węzły
|
raw_cfg = data.get("streamingConfig")
|
||||||
# rotują: dwa kolejne resolve tej samej sceny wracają z różnymi adresami. Dlatego
|
if isinstance(raw_cfg, str) and raw_cfg.strip():
|
||||||
# padnięcie odtwarzania na tym źródle leczy się ponownym resolve, a nie WebView-em
|
try:
|
||||||
# (`BACKEND_RERESOLVE_ORIGINS` w PlayerScreen).
|
cfg = json.loads(raw_cfg)
|
||||||
#
|
except Exception:
|
||||||
# `proxy_no_verify` NIE JEST przez nic czytane. Wcześniejszy komentarz twierdził, że
|
cfg = {}
|
||||||
# `stream_proxy.py` ma gałąź `verify=False` dla hostów-IP — nie ma jej, w tym pliku nie
|
order = cfg.get("order") or ["In-House"]
|
||||||
# występuje słowo `verify` ani razu. W praktyce certyfikaty tych węzłów przechodzą
|
|
||||||
# weryfikację (fetch przez proxy oddaje 200), więc flaga jest informacyjna. Zostaje,
|
|
||||||
# bo zapisuje realne ryzyko: gdyby któryś nod zaczął serwować cert niepasujący do IP,
|
|
||||||
# trzeba będzie tę gałąź dopisać.
|
|
||||||
#
|
|
||||||
# `cf` (fallback za Cloudflare) CELOWO NIE JEST PRZEKAZYWANY DALEJ. Sprawdzone
|
|
||||||
# 2026-08-14, sześć wariantów, wszystkie 403:
|
|
||||||
#
|
|
||||||
# httpx + Referer hostera · httpx + Referer galaxyporn.net
|
|
||||||
# curl_cffi chrome120 z Refererem · curl_cffi bez Referera (oddaje stronę CF)
|
|
||||||
# znacznik czasu bieżący · znacznik z linku głównego
|
|
||||||
#
|
|
||||||
# I to samo 403 z łącza RESIDENTIAL, więc to nie jest blokada datacenter. Adres jest
|
|
||||||
# najpewniej związany z sesją tego samego wywołania `/api/v1/video`, a my sięgamy po
|
|
||||||
# niego osobno i po czasie (znacznik w nazwie pliku bywa 5 dni stary). Podpięcie go
|
|
||||||
# jako zapasowego źródła dokładałoby request, który ZAWSZE pada, na ścieżce błędu.
|
|
||||||
#
|
|
||||||
# Padnięcie pojedynczego noda leczy ponowny resolve: kolejne wywołania wracają z
|
|
||||||
# różnymi adresami CDN (`BACKEND_RERESOLVE_ORIGINS` w PlayerScreen, commit 47547fe).
|
|
||||||
if not source:
|
|
||||||
# Brak `source` to NIE jest usterka parsowania ani chwilowy problem sieci.
|
|
||||||
# W ich własnym bundlu (`index-*.js`, funkcja startowa playera) warunek brzmi
|
|
||||||
# dosłownie: gdy w odpowiedzi nie ma `source`, player pokazuje „Video is not
|
|
||||||
# ready yet" i kończy. Potwierdzone 2026-08-24 w przeglądarce z łącza domowego
|
|
||||||
# na `vip.easyvidplayer.com/#96kzl`: dokładnie ten komunikat, a plakat obok
|
|
||||||
# oddał 502. To samo z VPS i przez brytyjskie proxy, więc nie chodzi o IP.
|
|
||||||
#
|
|
||||||
# Rzucamy `HosterDead`, bo tylko wtedy `resolve_movie_playback` oznaczy źródło
|
|
||||||
# martwe i przestanie je podsuwać. Zwracanie None dawało WebView z tym samym
|
|
||||||
# komunikatem, w kółko, i tak wyglądały zgłoszenia z 2026-08-24.
|
|
||||||
raise HosterDead(f"seekplayer-engine {page_url}: brak pola source")
|
|
||||||
|
|
||||||
sources: list[StreamSource] = []
|
# Budujemy kandydatow w KOLEJNOSCI, ktora serwis sam podaje, i zwracamy pierwszego,
|
||||||
if source:
|
# ktory naprawde odpowiada. Weryfikacja jest tu kluczowa: adres do usunietego pliku
|
||||||
sources.append(
|
# wyglada normalnie, a oddaje 502, i bez sprawdzenia oddawalismy go dalej jako dobry.
|
||||||
|
referer = f"{host}/"
|
||||||
|
sprawdzone = 0
|
||||||
|
for key in order:
|
||||||
|
if sprawdzone >= _MAX_CANDIDATES:
|
||||||
|
break
|
||||||
|
candidate = _build_candidate(host, data.get(_STREAM_FIELDS.get(key, "")), cfg, key)
|
||||||
|
if not candidate:
|
||||||
|
continue
|
||||||
|
sprawdzone += 1
|
||||||
|
if not _responds(candidate, referer):
|
||||||
|
log.info("seekplayer-engine: sciezka %s nie odpowiada dla %s", key, page_url)
|
||||||
|
continue
|
||||||
|
return [
|
||||||
StreamSource(
|
StreamSource(
|
||||||
link=source,
|
link=candidate,
|
||||||
quality=None,
|
quality=None,
|
||||||
type="m3u8",
|
type="m3u8",
|
||||||
referer=f"{host}/",
|
referer=referer,
|
||||||
raw={
|
raw={"proxy_no_verify": True, "engine": "seekplayer", "sciezka": key},
|
||||||
"proxy_no_verify": True,
|
|
||||||
"engine": "seekplayer",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
)
|
]
|
||||||
return sources or None
|
|
||||||
|
# Zadna sciezka nie odpowiada. To NIE jest usterka parsowania ani chwilowy problem
|
||||||
|
# sieci: w ich wlasnym bundlu brak zrodla konczy sie komunikatem "Video is not ready
|
||||||
|
# yet", a przy adresie do usunietego pliku CDN oddaje 502 ze WSZYSTKICH wezlow
|
||||||
|
# (sprawdzone 2026-08-26; ten sam wezel serwuje inny plik poprawnie).
|
||||||
|
#
|
||||||
|
# Rzucamy `HosterDead`, bo tylko wtedy `resolve_movie_playback` oznaczy zrodlo martwe.
|
||||||
|
# Dla scen galaxyporn ten wyjatek jest lapany i uruchamia ratunek z vikingfile.
|
||||||
|
raise HosterDead(f"seekplayer-engine {page_url}: zadna sciezka CDN nie odpowiada")
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from app.extractors._fetch import browser_get, fetch_tube_html
|
from app.extractors._fetch import browser_get, fetch_tube_html
|
||||||
from app.extractors._models import StreamSource
|
from app.extractors._models import HosterDead, StreamSource
|
||||||
from app.extractors.hosters import seekplayer_engine
|
from app.extractors.hosters import seekplayer_engine
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -115,11 +115,22 @@ def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | Non
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sources = seekplayer_engine.extract(iframe_src, timeout=timeout)
|
sources = seekplayer_engine.extract(iframe_src, timeout=timeout)
|
||||||
except Exception as e:
|
except HosterDead as e:
|
||||||
# Martwy player potrafi RZUCIC (HosterDead przy 404 z /api/v1/video), a nie tylko
|
# Martwy player RZUCA (404 z /api/v1/video albo zadna sciezka CDN nie odpowiada),
|
||||||
# oddac pustke. Bez tego wyjatek wychodzil na zewnatrz i ratunek z vikingfile
|
# a nie tylko oddaje pustke. Bez tego wyjatek wychodzil na zewnatrz i ratunek
|
||||||
# nigdy sie nie wykonywal.
|
# z vikingfile nigdy sie nie wykonywal.
|
||||||
log.info("galaxyporn: seekplayer rzucil dla %s (%s), probuje vikingfile", iframe_src, e)
|
log.info("galaxyporn: seekplayer rzucil dla %s (%s), probuje vikingfile", iframe_src, e)
|
||||||
|
ratunek = _vikingfile_source(html_text, page_url, timeout=timeout)
|
||||||
|
if ratunek:
|
||||||
|
return ratunek
|
||||||
|
# Player mowi wprost, ze pliku nie ma, a na stronie nie ma tez vikingfile. To jest
|
||||||
|
# PEWNA smierc, wiec przepuszczamy wyjatek dalej: `resolve_playback` oznaczy zrodlo
|
||||||
|
# martwym i scena przestanie sie pokazywac. Zwrocenie None dawalo 503
|
||||||
|
# "sprobuj ponownie", wiec ta sama martwa scena wracala w kolko (zgloszenie
|
||||||
|
# a405b9d6, scena bez vikingfile i z wszystkimi wezlami CDN na 502).
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log.info("galaxyporn: seekplayer padl dla %s (%s), probuje vikingfile", iframe_src, e)
|
||||||
return _vikingfile_source(html_text, page_url, timeout=timeout)
|
return _vikingfile_source(html_text, page_url, timeout=timeout)
|
||||||
if not sources:
|
if not sources:
|
||||||
# Iframe bywa ZWIETRZALY. Serwis przeniosl pliki na vikingfile, ale w starszych
|
# Iframe bywa ZWIETRZALY. Serwis przeniosl pliki na vikingfile, ale w starszych
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue