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:
parent
671ba97b52
commit
d521e6379d
5 changed files with 134 additions and 4 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
31
app/extractors/tubes/fpoxxx.py
Normal file
31
app/extractors/tubes/fpoxxx.py
Normal 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)
|
||||
52
app/extractors/tubes/porndish.py
Normal file
52
app/extractors/tubes/porndish.py
Normal 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)
|
||||
Loading…
Add table
Reference in a new issue