galaxyporn.net carries paysite rips with a dedicated <div id="video-actors"> cast block (no sidebar pollution), studio + date in the title prefix, healthy /page/N/ pagination. Measured orphan risk is low: 92% of a 75-performer sample already have a tpdb/stashdb ref in our DB and 79% of titles match scenes we already hold. seekplayer_engine host regex widened to cover seekplays|4meplayer|ezplayer| seeks|upn and the pro|cloud|one TLDs. This is the same engine (identical AES key/IV and /api/v1/video endpoint), just newer domains; the whitelist is additive so existing hosts keep matching (verified, including that upns.evil.com is still rejected). Unlocks all four galaxyporn player hosts and the same family on other sites. Two things the extractor had to get right: - Call seekplayer_engine directly rather than via extract_stream_from_hoster: the wrapper verifies the resulting URL and the hotlink-guarded HLS 403s, so it discarded correctly decoded streams. - Referer must be the PLAYER origin, not galaxyporn.net. With the site referer the manifest 403s; with the player origin it returns 200. Verified end to end: manifest 200 -> variant 200 (570 segments) -> segment 206. Duration is absent from the HTML, JSON-LD and player payload, and a NULL duration would silently hide scenes behind the min_duration_sec filter (the porntrex incident), so it is computed from the player's thumbnail.vtt last cue. The player API throttles bursts, hence the retry/backoff: that took missing durations from 9/21 down to 3/21. The value runs ~2% short of the true length (one thumbnail interval), which beats having none. Pilot: 21 scenes/page with studio 21/21 and clean cast (max 3), ingest of 2 pages = 42 seen, 34 new, 8 merged, 0 errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
69 lines
3 KiB
Python
69 lines
3 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 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)
|
|
|
|
|
|
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 None
|
|
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).
|
|
sources = seekplayer_engine.extract(iframe_src, timeout=timeout)
|
|
if not sources:
|
|
# Nie oddajemy iframe'a jako type='hoster' — seekplayer to SPA, w WebView
|
|
# user zobaczy pusty player. Lepiej None (źródło jako nierozwiązane).
|
|
log.info("galaxyporn: seekplayer resolve failed for %s", iframe_src)
|
|
return None
|
|
|
|
# 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
|