Regex w backendzie dopasowywal wylacznie vikingfile.com, mimo ze serwis rotuje domena. Poszerzony na vik[i1]ngfile z dowolna koncowka. W aplikacji ten sam wzorzec byl juz poszerzony, backend zostal w tyle. Na 20 losowych scenach: 16 oddaje vik1ngfile.site, 4 nie maja zadnego zrodla (stary player padl, a linku do vikingfile na stronie nie ma wcale). Te 4 to uczciwe 503, nie awaria po naszej stronie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
7.7 KiB
Python
157 lines
7.7 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 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 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.
|
|
log.info("galaxyporn: seekplayer rzucil 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
|