fix: porndish gral cudze filmy, zwiastuny i zastepcze filmy jako scena, reklama fpoxxx

porndish: cache serwisu oddawal dla kazdego adresu ten sam wpis, wiec ~40 tys.
scen gralo jeden z dwoch filmow. Strona z ?nc=<godzina> omija cache, kanoniczny
adres musi miec ten sam slug, inaczej 502.
sxyland: zwiastuny z kafelkow "podobne" (data-trailer) byly brane za film;
skasowany plik nowplay podstawial wspolny zastepczy film, teraz HosterDead.
fpoxxx: strona skasowanego filmu (no-player) to HosterDead, nie strona z
reklama w WebView.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-10-15 12:21:22 +02:00
parent 671ba97b52
commit d521e6379d
5 changed files with 134 additions and 4 deletions

View file

@ -35,6 +35,8 @@ from app.extractors.tubes import (
hdporngg,
hqfap,
perverzija,
porndish,
fpoxxx,
hqporner,
fullporner,
hentaigasm,
@ -106,7 +108,8 @@ _REGISTRY: dict[str, Callable[[str], list[StreamSource] | None]] = {
# fpoxxx, KVS, plain get_file + license. 2026-06-01 (task #20): get_file 302 →
# `videos3.fpo.xxx/remote_control.php?acctoken=<base64>`, zdekodowany acctoken
# zawiera WBITY IP serwera-resolvera → definitywnie IP-bound. WebView only.
"fpoxxx": _vps_blocked_fallback.extract,
# Skasowany film = HosterDead zamiast strony z reklama w WebView (2026-10-15).
"fpoxxx": fpoxxx.extract,
# sxyland, embeduje playmogo.com/e/<id> (= klon DoodStream: doodcdn.io + pass_md5
# + niewidzialny CF Turnstile; Chrome-DevTools verify 2026-06-08, bug-report 827a50a1).
# Strona sxyland NIE jest Turnstile-gated (VPS curl wyciąga iframe URL z HTML), więc
@ -177,7 +180,8 @@ _REGISTRY: dict[str, Callable[[str], list[StreamSource] | None]] = {
"fullvideosporn": fullvideosporn.extract,
"siskavideo": _embed_iframe.extract,
"porn4dayspw": _embed_iframe.extract,
"porndishcom": _embed_iframe.extract,
# Wlasny ekstraktor: obejscie cache serwisu, ktory oddawal cudze wpisy (2026-10-15).
"porndishcom": porndish.extract,
# xxxfreewatch, DELISTED 2026-05-18. 790 solo-orphan scen, 0% match, CF-walled z VPS.
"latestleaksco": _embed_iframe.extract,
"mypornerleakcom": _embed_iframe.extract,

View file

@ -113,6 +113,10 @@ _HOSTER_FALLBACK_URL_RE = re.compile(
# Sygnatury "video not found" / "deleted" które hostery wstawiają w HTML embed page.
# Gdy widzimy te markery, to wiemy że link jest martwy, raise HosterDead, caller w
# playback.py oznaczy playback_source.dead_at.
_NOWPLAY_INNER_RE = re.compile(
r"""<iframe[^>]+src=["']((?:https?:)?//[^"']*nowplay\.[a-z]+/emb[^"']*)["']""", re.IGNORECASE
)
_HOSTER_DEAD_PATTERNS = (
"Video not found",
"video not found",
@ -266,6 +270,15 @@ def extract_stream_from_hoster(
return None
html = r.text
# nowplay.to (sxyland od 10.2026): gdy plik skasowano, strona emb nie ma packera, tylko
# ramke do INNEGO wideo nowplay (57-minutowy zastepczy film, wspolny dla wszystkich
# skasowanych). Bez tego telefon dostawal hoster i gral cudzy film w WebView.
if "nowplay." in iframe_url.lower() and "eval(function(p,a,c,k,e" not in html:
inner = _NOWPLAY_INNER_RE.findall(html)
own = iframe_url.split("//", 1)[-1].rstrip("/")
if inner and all(i.split("//", 1)[-1].rstrip("/") != own for i in inner):
raise HosterDead(f"hoster {iframe_url}: plik usuniety, nowplay podstawia zastepczy film")
if any(p in html for p in _HOSTER_DEAD_PATTERNS):
raise HosterDead(f"hoster {iframe_url} reports video deleted/not found")

View file

@ -26,6 +26,7 @@ from urllib.parse import urlparse
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource
from app.extractors.hoster import extract_stream_from_hoster
from app.extractors._models import HosterDead
log = logging.getLogger(__name__)
@ -253,6 +254,11 @@ def _is_player_iframe(url: str) -> bool:
return False
_PREVIEW_ATTR_RE = re.compile(
r"""\sdata-(?:trailer|preview)[\w-]*\s*=\s*(?:"[^"]*"|'[^']*')""", re.IGNORECASE
)
def _extract_direct_stream_urls(page_html: str, page_url: str) -> list[StreamSource]:
"""Stage 0.5: scan page for direct mp4/m3u8 URLs (porn4days→iceyfile/gounlimited
download links, xmoviesforyou native player). Returns deduplicated sources
@ -272,6 +278,11 @@ def _extract_direct_stream_urls(page_html: str, page_url: str) -> list[StreamSou
page_host = (urlparse(page_url).hostname or "").lstrip("www.")
page_referer = f"https://{page_host}/" if page_host else page_url
# Kafelki "podobne" maja zwiastuny w data-trailer/data-preview; sxyland oddawal
# 25-sekundowy cudzy trailer (gammacdn .../tr_..._720p.mp4) jako film sceny, losowo,
# bo kafelki zmieniaja sie przy kazdym pobraniu (zgloszenie 23269128, 2026-10-12).
page_html = _PREVIEW_ATTR_RE.sub("", page_html)
for m in _DIRECT_MP4_RE.finditer(page_html):
url = m.group("url")
if url in seen:
@ -290,6 +301,8 @@ def _extract_direct_stream_urls(page_html: str, page_url: str) -> list[StreamSou
low = url.lower()
if "videos_screenshots/" in low or "/preview_" in low:
continue
if "/trailers/" in low or "//trailers-" in low or "/trailer/" in low:
continue
if "/get_file/" in low and "?" not in url:
continue
# Quality wykrycie z nazwy pliku
@ -322,7 +335,11 @@ def extract(
page_url: str,
*,
timeout: float = 60.0,
page_html: str | None = None,
) -> list[StreamSource] | None:
# `page_html` podaje ekstraktor, ktory musial pobrac strone po swojemu (porndish:
# obejscie zepsutego cache serwisu i kontrola, czy to wlasciwy wpis).
if page_html is None:
page_html = fetch_tube_html(page_url, timeout=timeout)
# Stage 0.5: direct .mp4/.m3u8 URLs on page (porn4days→iceyfile, niektóre xmoviesforyou
@ -482,11 +499,20 @@ def extract(
# URL z embed page, ale ani VPS proxy ani mobile go nie pobierze (No route to host
# albo 403). Bez iframe fallbacku mobile dostaje 503 i utyka. Z fallbackiem chain:
# direct → proxy → iframe1 (WebView) → iframe2 (WebView) → page (WebView).
# Ramki, o ktorych hoster mowi wprost "plik usuniety" w sposob pewny (na razie tylko
# zastepczy film nowplay). Ogolnego HosterDead tu nie liczymy: strony DoodStream maja
# ukryty blok "not found" w kazdym HTML-u.
dead_iframes: set[str] = set()
for idx, iframe_url in enumerate(iframe_urls):
try:
stream_url = extract_stream_from_hoster(
iframe_url, referer=referer, timeout=timeout,
)
except HosterDead as e:
if "nowplay." in iframe_url.lower():
dead_iframes.add(iframe_url)
log.info("embed_iframe: %s", e)
continue
except Exception as e:
log.warning("embed_iframe: extract failed for %s: %s", iframe_url, e)
continue
@ -508,7 +534,7 @@ def extract(
# jako hoster fallback. WebView załaduje embed page z własną sesją/IP/cookies,
# może pobrać video gdy VPS proxy blocked.
for u in iframe_urls:
if u == iframe_url:
if u == iframe_url or u in dead_iframes:
continue
host_label = (urlparse(u).hostname or "").lstrip("www.").split(".")[0] or None
sources.append(StreamSource(link=u, type="hoster", quality=host_label))
@ -525,8 +551,12 @@ def extract(
# vs "luluvid"). Na końcu page_url jako safety-net, gdy wszystkie iframe'y są
# martwe (np. sxyland → xtapes.porn 301→camcaps.to 403), mobile WebView otworzy
# główną stronę aggregator tube'a, gdzie user może wybrać alternatywny player.
if iframe_urls and all(u in dead_iframes for u in iframe_urls):
raise HosterDead(f"wszystkie odtwarzacze na {page_url} maja usuniety plik")
sources = []
for u in iframe_urls:
if u in dead_iframes:
continue
host_label = (urlparse(u).hostname or "").lstrip("www.").split(".")[0] or None
sources.append(StreamSource(link=u, type="hoster", quality=host_label))
page_host_label = (urlparse(page_url).hostname or "").lstrip("www.").split(".")[0] or "page"

View file

@ -0,0 +1,31 @@
"""fpo.xxx: kontrola usunietego filmu przed oddaniem strony do WebView.
Odtwarzanie idzie na telefonie (`fpoxxxResolver.ts`, KVS license_code z IP telefonu). Gdy
telefon nic nie znajdzie, backend oddawal sama strone jako hoster. Na stronie skasowanego
filmu (`<div class="no-player">`, "Deleted by the owner request", bez license_code) jedynym
wideo jest reklama tsyndicate, ktora WebView wylapywal i puszczal jako scene (zgloszenie
1ee12943, 2026-10-12: "fpoxxx daje reklamy zamiast scen"; ok. 10% zrodel fpoxxx to takie
strony). Tu taka strona to HosterDead, wiec zrodlo znika zamiast grac reklame.
"""
from __future__ import annotations
import logging
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import HosterDead, StreamSource, TubePageError
from app.extractors.tubes import _vps_blocked_fallback
log = logging.getLogger(__name__)
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource]:
try:
html = fetch_tube_html(page_url, timeout=min(timeout, 20.0))
except TubePageError:
raise
except Exception as e:
log.info("fpoxxx: strona %s niepobrana (%s), oddaje do WebView", page_url, e)
return _vps_blocked_fallback.extract(page_url, timeout=timeout)
if 'class="no-player"' in html and "license_code" not in html:
raise HosterDead(f"fpoxxx {page_url}: film usuniety (no-player)")
return _vps_blocked_fallback.extract(page_url, timeout=timeout)

View file

@ -0,0 +1,52 @@
"""porndish.com: strona sceny z obejsciem zepsutego cache serwisu, potem embed-iframe.
Zgloszenie 88085cda (2026-10-12, "nic z porndish nie dziala"): cache strony porndish
(`x-cache-handler: autoptimize-pro-cache`, `cf-cache-status: HIT`) oddaje dla kazdego
`/porn/<slug>/` CUDZY wpis. Nowsze adresy dostawaly "[DadCrush] Carly Kiss", starsze
"[MyLifeInMiami] ...", wiec kazda z ~40 tys. scen porndish grala jeden z dwoch tych samych
filmow. Telemetria liczyla to jako sukces (wideo ruszalo, tylko nie to).
Dowolny parametr w zapytaniu omija cache (`BYPASS`), wiec dokladamy `?nc=<godzina>`
(jeden wariant na godzine, zeby nie mnozyc wpisow w ich cache). Do tego kontrola: kanoniczny
adres strony musi miec ten sam slug co zadany, inaczej nie oddajemy niczego (502), bo
cudzy film jest gorszy niz brak filmu.
"""
from __future__ import annotations
import logging
import re
import time
from urllib.parse import urlsplit
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource, TubePageError
from app.extractors.tubes import _embed_iframe
log = logging.getLogger(__name__)
_CANONICAL_RE = re.compile(
r"""<link[^>]+rel=["']canonical["'][^>]+href=["']([^"']+)["']"""
r"""|<meta[^>]+property=["']og:url["'][^>]+content=["']([^"']+)["']""",
re.IGNORECASE,
)
def _slug(url: str) -> str:
return urlsplit(url).path.rstrip("/").rsplit("/", 1)[-1].lower()
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
sep = "&" if "?" in page_url else "?"
busted = f"{page_url}{sep}nc={int(time.time() // 3600)}"
html = fetch_tube_html(busted, timeout=timeout)
want = _slug(page_url)
m = _CANONICAL_RE.search(html)
got = _slug(m.group(1) or m.group(2)) if m else ""
if want and got and got != want:
log.warning("porndish: %s oddal cudzy wpis (%s)", page_url, got)
raise TubePageError(502, page_url)
# Adres z `nc` takze dla zapasu "cala strona w WebView": bez niego telefon tez dostalby
# cudzy wpis z cache.
return _embed_iframe.extract(busted, timeout=timeout, page_html=html)