goon/app/extractors/tubes/galaxyporn.py
goon-foss 365c97a1de 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.
2026-08-27 08:59:28 +02:00

168 lines
8.4 KiB
Python

"""galaxyporn.net, iframe → seekplayer engine. Dodany 2026-07-26.
Scene page ma jeden `<iframe src="https://<host>/#<hash>">` rodziny seekplayer
(galaxy.upns.online / galaxy.4meplayer.pro / sport.seekplays.com / news.upns.pro,
100% próbki 50 scen z całej głębokości archiwum). Silnik mamy już w repo
(`hosters/seekplayer_engine.py`, ten sam AES key/IV + `/api/v1/video?id=`), więc
oddajemy iframe do generycznego `extract_stream_from_hoster` i nic nie dublujemy.
HLS jest hotlink-guarded na Referer (bez niego 403 na master i na segmentach),
a token podpisany pod IP fetchera → manifest i segmenty idą przez `/proxy/hls`.
"""
from __future__ import annotations
import logging
import re
from urllib.parse import urlparse
from app.extractors._fetch import browser_get, fetch_tube_html
from app.extractors._models import HosterDead, StreamSource
from app.extractors.hosters import seekplayer_engine
log = logging.getLogger(__name__)
_BASE = "https://galaxyporn.net"
_IFRAME_RE = re.compile(r'<iframe[^>]+src="([^"]+)"', re.IGNORECASE)
#: Hostery, ktorych NIE resolwujemy serwerowo, tylko oddajemy telefonowi do WebView.
_NEEDS_WEBVIEW_RE = re.compile(r"vikingfile\.", re.IGNORECASE)
#: Link do pliku na vikingfile, gdziekolwiek w HTML (iframe ALBO zwykly <a href>).
#: Serwis rotuje domena (2026-08-22: vikingfile.com oddaje 302 na vik1ngfile.site),
#: wiec lapiemy obie pisownie i dowolna koncowke, a nie sam .com.
_VIKINGFILE_RE = re.compile(r"https?://vik[i1]ngfile\.[a-z]{2,6}/f/[A-Za-z0-9]+", re.IGNORECASE)
def _follow_redirect(url: str, *, timeout: float) -> str:
"""Adres po przekierowaniach; przy bledzie oddaje wejsciowy (lepszy niz nic)."""
try:
res = browser_get(url, timeout=min(timeout, 20.0), follow_redirects=True)
final = str(getattr(res, "url", "") or "")
return final or url
except Exception as e:
log.info("galaxyporn: nie rozwiazalem przekierowania %s: %s", url, e)
return url
def _vikingfile_source(
html_text: str, page_url: str, *, timeout: float = 30.0
) -> list[StreamSource] | None:
"""Link do vikingfile z DOWOLNEGO miejsca strony, nie tylko z iframe'a.
Na starszych stronach vikingfile siedzi w zwyklym `<a href>`, a iframe wskazuje
stary, martwy player. Dlatego szukamy po calym HTML, nie po samym iframie.
"""
mv = _VIKINGFILE_RE.search(html_text)
if not mv:
return None
# WAZNE: vikingfile.com oddaje 302 na INNA domene (2026-08-22: vik1ngfile.site).
# WebView w aplikacji blokuje przejscie top-frame na inny eTLD+1, zeby popundery nie
# przejmowaly ekranu, wiec to przekierowanie bylo odrzucane i user dostawal CZARNY
# EKRAN (zgloszenie 21d6f5af). Widac to bylo dopiero na emulatorze, bo z serwera
# httpx sledzi przekierowanie po cichu. Oddajemy wiec adres DOCELOWY, zeby aplikacja
# od razu otwierala wlasciwa domene i zadna zmiana domeny w ogole nie zachodzila.
# Domena zapasowa potrafi rotowac, dlatego rozwiazujemy ja za kazdym razem, a nie
# wpisujemy na sztywno.
link = _follow_redirect(mv.group(0), timeout=timeout)
return [
StreamSource(
link=link,
quality="hoster",
type="hoster",
referer=page_url,
raw={"extractor": "galaxyporn", "hoster": "vikingfile"},
)
]
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html_text = fetch_tube_html(page_url, timeout=timeout)
m = _IFRAME_RE.search(html_text)
if not m:
log.info("galaxyporn: no iframe on %s", page_url)
return _vikingfile_source(html_text, page_url, timeout=timeout)
iframe_src = m.group(1).strip()
if iframe_src.startswith("//"):
iframe_src = "https:" + iframe_src
# Wołamy silnik BEZPOŚREDNIO, nie przez `extract_stream_from_hoster`: wrapper
# weryfikuje wynikowy URL, a HLS galaxyporn jest hotlink-guarded (403 bez
# Referera) → wrapper kasował poprawnie zdekodowany stream (sprawdzone: engine
# zwracał m3u8, wrapper None dla tych samych scen).
# Od sierpnia 2026 serwis przenosi NOWE wgrania na vikingfile.com: probka
# 2026-08-21 dala 12/12 najnowszych scen na vikingfile i 12/12 najstarszych dalej
# na rodzinie seekplayera. Vikingfile chowa wideo za RENDEROWANYM Cloudflare
# Turnstile (`turnstile.render('#captcha', ...)`, bez trybu invisible), a element
# <video> jest pusty i ukryty, dopoki wyzwanie nie zostanie rozwiazane. Serwerowo
# nie da sie tego obejsc, wiec oddajemy embed telefonowi: w WebView z domowego IP
# user zobaczy widget i przejdzie dalej. To NIE dotyczy seekplayera, ktory jako SPA
# w WebView pokazuje pusty player, dlatego rozroznienie jest po hoscie.
if _NEEDS_WEBVIEW_RE.search(iframe_src):
# Tak samo jak w galezi zapasowej: rozwiazujemy 302 na domene docelowa. Bez tego
# ta sciezka (nowsze sceny, gdzie vikingfile JEST iframem) oddawala stary adres,
# aplikacja dostawala przekierowanie na inny eTLD+1, WebView je blokowal i user
# widzial czarny ekran. Poprawka z 2026-08-22 objela tylko druga galaz, wiec
# polowa scen dalej byla zepsuta.
return [
StreamSource(
link=_follow_redirect(iframe_src, timeout=timeout),
quality="hoster",
type="hoster",
referer=page_url,
raw={"extractor": "galaxyporn", "hoster": "vikingfile"},
)
]
try:
sources = seekplayer_engine.extract(iframe_src, timeout=timeout)
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
# wpisach zostawil stary iframe (news.upns.pro, sport.seekplays.com,
# galaxy.4meplayer.pro), ktory dzis wypisuje "Video is not ready yet", a jego API
# oddaje payload BEZ jakiegokolwiek adresu wideo (75% probki 16 scen). Wygladalo to
# na skasowany katalog, ale na TYCH SAMYCH stronach jest link do vikingfile, a pliki
# tam zyja: sprawdzone trzy, 890 MB / 742 MB / 2,5 GB, nazwy zgodne ze scenami.
# Dlatego zamiast poddawac sie, siegamy po vikingfile. Kolejnosc jest celowa:
# seekplayer PIERWSZY, bo resolwuje sie po naszej stronie i gra bez CAPTCHA,
# a vikingfile to ratunek, ktory kosztuje usera przejscie Turnstile w WebView.
log.info("galaxyporn: seekplayer padl dla %s, probuje vikingfile", iframe_src)
return _vikingfile_source(html_text, page_url, timeout=timeout)
# Referer MUSI być hostem PLAYERA (np. https://galaxy.4meplayer.pro/), NIE
# galaxyporn.net, zweryfikowane: ten sam manifest z Refererem galaxyporn = 403,
# z Refererem playera = 200 + lista wariantów. Silnik ustawia go poprawnie, więc
# go zachowujemy; fallback liczymy z hosta iframe'a.
player_origin = f"https://{urlparse(iframe_src).hostname}/"
out: list[StreamSource] = []
for s in sources:
raw = dict(s.raw or {})
# Token HLS jest Referer+IP-bound → manifest+segmenty przez /proxy/hls
# (bez mobile_direct_ok, inaczej telefon dostanie 403).
raw["proxy_no_verify"] = True
out.append(
StreamSource(
link=s.link,
type=s.type or ("m3u8" if ".m3u8" in s.link.lower() else "mp4"),
quality=s.quality,
referer=s.referer or player_origin,
raw=raw,
)
)
return out