From 365c97a1de03a0748bfbc976e10b26890c8285e4 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Thu, 27 Aug 2026 08:59:28 +0200 Subject: [PATCH] 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//` + 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 ` z `cf-turnstile-response=`). Obchodzenia tego nie buduje. --- app/extractors/hosters/seekplayer_engine.py | 166 ++++++++++++++------ app/extractors/tubes/galaxyporn.py | 21 ++- 2 files changed, 130 insertions(+), 57 deletions(-) diff --git a/app/extractors/hosters/seekplayer_engine.py b/app/extractors/hosters/seekplayer_engine.py index d5def61..eeeed58 100644 --- a/app/extractors/hosters/seekplayer_engine.py +++ b/app/extractors/hosters/seekplayer_engine.py @@ -41,7 +41,7 @@ from __future__ import annotations import json import logging import re -from urllib.parse import urlparse +from urllib.parse import urlencode, urlparse from cryptography.hazmat.primitives import padding 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") + +# 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//` (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: parsed = urlparse(page_url) 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"): 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///…/master.m3u8`). - # Token ma ~2,8 h TTL i NIE jest związany z IP (sprawdzone 2026-08-14), a węzły - # rotują: dwa kolejne resolve tej samej sceny wracają z różnymi adresami. Dlatego - # padnięcie odtwarzania na tym źródle leczy się ponownym resolve, a nie WebView-em - # (`BACKEND_RERESOLVE_ORIGINS` w PlayerScreen). - # - # `proxy_no_verify` NIE JEST przez nic czytane. Wcześniejszy komentarz twierdził, że - # `stream_proxy.py` ma gałąź `verify=False` dla hostów-IP — nie ma jej, w tym pliku nie - # 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") + cfg = {} + raw_cfg = data.get("streamingConfig") + if isinstance(raw_cfg, str) and raw_cfg.strip(): + try: + cfg = json.loads(raw_cfg) + except Exception: + cfg = {} + order = cfg.get("order") or ["In-House"] - sources: list[StreamSource] = [] - if source: - sources.append( + # Budujemy kandydatow w KOLEJNOSCI, ktora serwis sam podaje, i zwracamy pierwszego, + # ktory naprawde odpowiada. Weryfikacja jest tu kluczowa: adres do usunietego pliku + # 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( - link=source, + link=candidate, quality=None, type="m3u8", - referer=f"{host}/", - raw={ - "proxy_no_verify": True, - "engine": "seekplayer", - }, + referer=referer, + raw={"proxy_no_verify": True, "engine": "seekplayer", "sciezka": key}, ) - ) - 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") diff --git a/app/extractors/tubes/galaxyporn.py b/app/extractors/tubes/galaxyporn.py index e9b7ba5..72e364a 100644 --- a/app/extractors/tubes/galaxyporn.py +++ b/app/extractors/tubes/galaxyporn.py @@ -16,7 +16,7 @@ import re from urllib.parse import urlparse 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 log = logging.getLogger(__name__) @@ -115,11 +115,22 @@ def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | Non try: sources = seekplayer_engine.extract(iframe_src, timeout=timeout) - except Exception as e: - # Martwy player potrafi RZUCIC (HosterDead przy 404 z /api/v1/video), a nie tylko - # oddac pustke. Bez tego wyjatek wychodzil na zewnatrz i ratunek z vikingfile - # nigdy sie nie wykonywal. + except HosterDead as e: + # Martwy player RZUCA (404 z /api/v1/video albo zadna sciezka CDN nie odpowiada), + # a nie tylko oddaje pustke. Bez tego wyjatek wychodzil na zewnatrz i ratunek + # z vikingfile nigdy sie nie wykonywal. 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) if not sources: # Iframe bywa ZWIETRZALY. Serwis przeniosl pliki na vikingfile, ale w starszych