chore(ingest): hard-remove hqfap + 4k69 (entire CDN library gone)

Re-check 2026-06-25 across the full id range confirmed both PlayTube tubes
serve only the fixed `/upload/videos/video_down.mp4` "server down" stub, never
a real file: hqfap 0/80 real (79 stub, 1 none), 4k69 0/40 real (38 stub, 2
none). Both were disabled 2026-06-22; CDN never came back, so removing entirely
(mirrors the pornhub/redtube/0dayxx/pornditt/pornhat removals).

Removed the extractor registry entries (hqfapcom, 4k69com) + module files and
the browse scrapers + imports. Prod DB data deleted separately (28,398
solo-orphan scenes + 46,196 playback_sources). `_playtube.py` kept: superporn
and neporn still use its JSON-LD helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-25 11:07:47 +02:00
parent 9a789a8551
commit 585e5d59f5
8 changed files with 17 additions and 264 deletions

View file

@ -136,8 +136,6 @@ from app.connectors.direct_scrapers.shyfap import ShyfapScraper # noqa: E402, F
from app.connectors.direct_scrapers.yesporn import YesPornVipScraper # noqa: E402 from app.connectors.direct_scrapers.yesporn import YesPornVipScraper # noqa: E402
from app.connectors.direct_scrapers.fullmovies import FullmoviesScraper # noqa: E402 from app.connectors.direct_scrapers.fullmovies import FullmoviesScraper # noqa: E402
from app.connectors.direct_scrapers.hdporngg import HDPornGGScraper # noqa: E402 from app.connectors.direct_scrapers.hdporngg import HDPornGGScraper # noqa: E402
from app.connectors.direct_scrapers.fourk69 import FourK69Scraper # noqa: E402,F401 — disabled 2026-06-22 (broken playback), kept for backref/re-enable
from app.connectors.direct_scrapers.hqfap import HQFapScraper # noqa: E402,F401 — disabled 2026-06-22 (broken playback), kept for backref/re-enable
from app.connectors.direct_scrapers.neporn import NepornScraper # noqa: E402 from app.connectors.direct_scrapers.neporn import NepornScraper # noqa: E402
from app.connectors.direct_scrapers.superporn import SuperpornScraper # noqa: E402 from app.connectors.direct_scrapers.superporn import SuperpornScraper # noqa: E402
from app.connectors.direct_scrapers.eporner_api import EpornerApiScraper # noqa: E402 from app.connectors.direct_scrapers.eporner_api import EpornerApiScraper # noqa: E402
@ -211,17 +209,12 @@ ALL_BROWSE_SCRAPERS: list[type[BaseBrowseScraper]] = [
# Mega-katalog ~13M → deep_crawl._PAGE_CAP["xvideoscom"]=1800 (~50k najnowszych), nie # Mega-katalog ~13M → deep_crawl._PAGE_CAP["xvideoscom"]=1800 (~50k najnowszych), nie
# full-crawl. (youporn pominięty — JSON-LD bez actor/keywords, scene-perf/tagi = nav A-Z.) # full-crawl. (youporn pominięty — JSON-LD bez actor/keywords, scene-perf/tagi = nav A-Z.)
XVideosBrowseScraper, XVideosBrowseScraper,
# HQFapScraper / FourK69Scraper — WYŁĄCZONE 2026-06-22 (user request, na razie). # HQFapScraper / FourK69Scraper — USUNIĘTE CAŁKOWICIE 2026-06-25. Oba PlayTube CMS;
# Oba na PlayTube CMS, ingestowały świeżo i wyglądały żywo, ALE playback w obu padł: # disabled 2026-06-22 gdy playback padł, re-check 2026-06-25 potwierdził że CAŁA
# - hqfap: hosting migrował na `/upload/videos/video_down.mp4` = STAŁY ~3MB stub # biblioteka CDN znikła: wide-sample przez pełny zakres id (hqfap 0/80 real, 4k69
# "server down" dla KAŻDEJ sceny (extractor go odrzuca → None), # 0/40 real) — każda scena serwuje stały `/upload/videos/video_down.mp4` "server
# - 4k69: get_file nie zwraca już grywalnego URL (extractor resolves nothing → None). # down" stub, nie realny plik. Dane (28k solo-orphan scen + 46k sources) skasowane
# Scena bez grywalnego źródła = śmieciowy wpis, więc nie ingestujemy nowych. Istniejące # z DB, pliki scraperów/extractorów i wpisy w _REGISTRY usunięte.
# live playback_sources oznaczone dead na prodzie (znikają z /sources + has_playback).
# Reversible: odkomentuj + odżyw sources gdy hosting wróci. Extractory zostają w
# _REGISTRY (hqfapcom/4k69com) — gotowe gdyby content wrócił.
# HQFapScraper,
# FourK69Scraper,
# NepornScraper — dołączony 2026-06-10 (user request). KVS engine (jak freshporno/ # NepornScraper — dołączony 2026-06-10 (user request). KVS engine (jak freshporno/
# porn00), /latest-updates/N/. JSON-LD (title+desc+uploadDate+thumb) + video:duration # porn00), /latest-updates/N/. JSON-LD (title+desc+uploadDate+thumb) + video:duration
# meta + /models/ performerzy + /categories/ tagi. Brak studio (tytuł bywa # meta + /models/ performerzy + /categories/ tagi. Brak studio (tytuł bywa

View file

@ -1,66 +0,0 @@
"""4k69.com — latest-vids browse scraper (PlayTube CMS, patrz _playtube.py).
Dołączony 2026-06-10 (user request; probe 2026-06-01 odrzucił po stronie głównej
"JS-rendered" błędnie, scene pages mają pełny SSR + JSON-LD). 7 video sitemapów
~65k scen, content w dużej mierze studyjny (paysite re-upload, 4K).
Specyfika vs baza: studio NIE ma własnego pola na scenie nazwy studiów występują
jako kategorie ("21 Sextury", "Adult Time") obok zwykłych ("Anal", "4K").
Klasyfikacja: lista wszystkich studiów z `/studios` (fetch raz per instancję,
match po znormalizowanej nazwie alfanumerycznej pill "Adult Time" vs slug
"AdultTime"). Studio bywa też w prefiksie tytułu, ale kategoria jest pewniejsza.
Playback: JSON-LD contentUrl + dwa dodatkowe get_file w HTML (2160m/720m/480m,
www.4kporno.xxx) ta sama platforma co fullmovies/hdporngg: get_file binduje CDN
do IP fetchera, więc oddajemy NIEZRESOLWOWANE (mobile_direct), telefon follow-uje
302 z własnym IP. Extractor `4k69com` pomija 2160p (CDN time-out, jak fpvcdn).
"""
from __future__ import annotations
import logging
import re
from app.connectors.direct_scrapers._playtube import BasePlayTubeScraper
from app.extractors import browser_get
log = logging.getLogger(__name__)
_STUDIO_LINK_RE = re.compile(r"href=['\"][^'\"]*/videos/studio/([^'\"]+)['\"]", re.IGNORECASE)
def _norm(name: str) -> str:
"""`Adult Time` / `AdultTime` → `adulttime` (porównanie pill vs studio slug)."""
return re.sub(r"[^a-z0-9]", "", name.lower())
class FourK69Scraper(BasePlayTubeScraper):
sitetag = "4k69com"
base_url = "https://4k69.com"
def __init__(self) -> None:
super().__init__()
self._studio_set: set[str] | None = None
def _load_studio_set(self) -> set[str]:
"""Znormalizowane nazwy wszystkich studiów z /studios. Pusty set = fetch
fail (graceful: sceny pójdą bez studio, composite ma performer+title+dur)."""
if self._studio_set is not None:
return self._studio_set
try:
r = browser_get(f"{self.base_url}/studios", timeout=self._timeout)
r.raise_for_status()
self._studio_set = {_norm(m) for m in _STUDIO_LINK_RE.findall(r.text) if _norm(m)}
log.info("4k69: studio list loaded — %d studios", len(self._studio_set))
except Exception as e:
log.warning("4k69: studios page fetch failed: %s", e)
self._studio_set = set()
return self._studio_set
def _pick_studio(self, category_names: list[str]) -> str | None:
studios = self._load_studio_set()
if not studios:
return None
for name in category_names:
if _norm(name) in studios:
return name
return None

View file

@ -1,26 +0,0 @@
"""hqfap.com — latest-vids browse scraper (PlayTube CMS, patrz _playtube.py).
Dołączony 2026-06-10 (user request). Re-uploader katalogu pornhd.pet (~120k scen,
thumbnaile to base64-encoded oryginalne URL-e w `/uploads/images/`).
Specyfika vs baza: studio siedzi w kategoriach z suffixem " Clips"
("Filthy Kings Clips" studio "Filthy Kings"); reszta kategorii tagi.
Playback: direct mp4 z JSON-LD contentUrl (cdnde.com nowsze / okcdn.ru starsze),
tokeny time-bound i portable cross-IP natywny extractor `hqfapcom`.
"""
from __future__ import annotations
from app.connectors.direct_scrapers._playtube import BasePlayTubeScraper
class HQFapScraper(BasePlayTubeScraper):
sitetag = "hqfapcom"
base_url = "https://hqfap.com"
def _pick_studio(self, category_names: list[str]) -> str | None:
for name in category_names:
if name.lower().endswith(" clips"):
studio_name = name[: -len(" clips")].strip()
if studio_name:
return studio_name
return None

View file

@ -29,10 +29,8 @@ from app.extractors.tubes import (
_ytdlp, _ytdlp,
eporner, eporner,
freshporno, freshporno,
fourk69,
fullmovies, fullmovies,
hdporngg, hdporngg,
hqfap,
hqporner, hqporner,
neporn, neporn,
latestpornvideo, latestpornvideo,
@ -163,16 +161,10 @@ _REGISTRY: dict[str, Callable[[str], list[StreamSource] | None]] = {
# (#19866e9e wcześniej źle: założyłem „get_file 403 IP-bound" testem plain-curl.) # (#19866e9e wcześniej źle: założyłem „get_file 403 IP-bound" testem plain-curl.)
"fullmoviesxxx": fullmovies.extract, "fullmoviesxxx": fullmovies.extract,
"hdporngg": hdporngg.extract, "hdporngg": hdporngg.extract,
# hqfap — JSON-LD contentUrl = direct mp4 (cdnde.com nowsze / okcdn.ru starsze). # hqfap + 4k69 (PlayTube CMS) — USUNIĘTE CAŁKOWICIE 2026-06-25. Cała biblioteka CDN
# Cross-IP test 2026-06-10: oba CDN-y portable (`ip=`/`srcIp=` nie egzekwowane), # znikła: każda scena serwuje stały `/upload/videos/video_down.mp4` "server down" stub
# tokeny time-bound → on-demand fetch daje świeży URL. Mobile direct, zero proxy. # zamiast realnego pliku (wide-sample przez pełny zakres id: hqfap 0/80 real, 4k69
"hqfapcom": hqfap.extract, # 0/40 real). Dane skasowane, scrapery/extractory usunięte.
# 4k69 — 2026-06-14 player zmigrowany na jwplayer + okcdn.ru (OK.ru CDN). Natywny
# fourk69.extract parsuje okcdn `file`+`label` ze strony (SSR za CF → proxy). okcdn
# srcIp NIE egzekwowane (cross-IP test) → mobile_direct_ok, telefon gra direct.
# Pełny reverse-engineer w fourk69.py (zgłoszenie 5de3fbc5). [Krótko był na
# _vps_blocked_fallback/WebView, ale to łapało VAST preroll zamiast contentu.]
"4k69com": fourk69.extract,
# neporn — KVS function/0 + license (jak freshporno). Server-side _kvs resolve → # neporn — KVS function/0 + license (jak freshporno). Server-side _kvs resolve →
# data001.neporn.com/remote_control.php portable (cross-IP 206, 2026-06-10). # data001.neporn.com/remote_control.php portable (cross-IP 206, 2026-06-10).
"neporncom": neporn.extract, "neporncom": neporn.extract,

View file

@ -1,68 +0,0 @@
"""4k69.com — okcdn.ru (OK.ru CDN) direct stream extractor.
2026-06-14: 4k69 zmigrowało player z get_file (4kporno.xxx) na jwplayer + okcdn.ru
(OK.ru video CDN). Strona (SSR za Cloudflare curl_cffi/proxy) ma w inline jwplayer
setupie pary `"file": "<okcdn url>", "label": "<jakość>"` na WSZYSTKIE jakości
(4K/2K/1080p/720p/480p/360p/240p). To samo w LD-JSON `contentUrl` (jeden, niższy).
okcdn URL ma `expires=` (time-bound), `srcIp=` (IP edge Cloudflare który frontował
fetch) i `sig=` per jakość. KLUCZOWE (reverse-engineer + cross-IP test 2026-06-14):
`srcIp` NIE jest egzekwowane URL gra z dowolnego IP (206 video/mp4 z residential IP
srcIp). Więc resolwujemy server-side i oddajemy `mobile_direct_ok` telefon gra
DIRECT, zero VPS proxy, zero WebView/reklam (VAST preroll jest runtime-only, nie ma go
w statycznym HTML, więc parsując HTML omijamy go całkiem).
Pomijamy 4K/2K (jak wcześniej 2160/1440 za duże na mobile).
"""
from __future__ import annotations
import logging
import re
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
# Pary file+label z jwplayer setupu: "file":"<okcdn>","label":"1080p". Label bierzemy
# wprost ze strony (pewniejsze niż mapowanie OK.ru type=N).
_OKCDN_FILE_RE = re.compile(
r'"file"\s*:\s*"(https?://[^"]*okcdn[^"]+)"\s*,\s*"label"\s*:\s*"([^"]+)"',
re.IGNORECASE,
)
# Za duże na mobile (jak stary skip 2160/1440).
_SKIP_LABEL_RE = re.compile(r"^(4k|2k|2160|1440)", re.IGNORECASE)
def _quality_num(label: str) -> int:
m = re.match(r"(\d+)", label or "")
return int(m.group(1)) if m else 0
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html = fetch_tube_html(page_url, timeout=timeout)
seen: set[str] = set()
out: list[StreamSource] = []
for m in _OKCDN_FILE_RE.finditer(html):
url = m.group(1).replace("&amp;", "&")
label = m.group(2).strip()
if url in seen:
continue
seen.add(url)
if _SKIP_LABEL_RE.match(label):
continue
out.append(StreamSource(
link=url,
quality=label,
type="mp4",
referer="https://4k69.com/",
# srcIp nieegzekwowane (cross-IP test 2026-06-14) → telefon gra direct.
raw={"mobile_direct_ok": True},
))
if not out:
log.info("4k69: no okcdn sources on %s", page_url)
return None
out.sort(key=lambda s: _quality_num(s.quality or ""), reverse=True)
return out

View file

@ -1,79 +0,0 @@
"""hqfap.com — direct stream extractor.
Scene page (SSR, za Cloudflare curl_cffi w fetch_tube_html) ma JSON-LD
VideoObject z `contentUrl` = direct mp4. Dwie generacje hostingu w katalogu:
- nowsze sceny: `v4.cdnde.com/...?video=<b64>&time=<epoch>&ip=<addr>` param
`ip` NIE jest egzekwowany (cross-IP test 2026-06-10: lokalny ISP i VPS Hetzner
oba 206), token time-bound resolve on-demand daje świeży URL,
- starsze sceny: `vd*.okcdn.ru/?expires=...&srcIp=...&sig=...` (ok.ru) również
portable cross-IP (206 z innego IP niż fetcher).
Mobile gra direct (mobile_direct auto-detect w playback.py), zero proxy/WebView.
"""
from __future__ import annotations
import json
import logging
import re
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_JSONLD_RE = re.compile(
r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
re.IGNORECASE | re.DOTALL,
)
# Fallback gdy JSON-LD nie parsuje się jako JSON (trailing comma itp.).
_CONTENT_URL_RE = re.compile(r'"contentUrl"\s*:\s*"([^"]+)"')
_QUALITY_RE = re.compile(r"_(\d{3,4})p\.mp4", re.IGNORECASE)
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html = fetch_tube_html(page_url, timeout=timeout)
content_url: str | None = None
for m in _JSONLD_RE.finditer(html):
raw = m.group(1).strip()
if not raw:
continue
try:
data = json.loads(raw)
except (json.JSONDecodeError, ValueError):
continue
items = data if isinstance(data, list) else [data]
for obj in items:
if isinstance(obj, dict) and obj.get("@type") == "VideoObject":
content_url = (obj.get("contentUrl") or "").strip() or None
break
if content_url:
break
if not content_url:
rm = _CONTENT_URL_RE.search(html)
content_url = rm.group(1).strip() if rm else None
if not content_url or not content_url.startswith("http"):
log.warning("hqfap: no contentUrl in JSON-LD for %s", page_url)
return None
# hqfap migrował: `/upload/videos/video_down.mp4` (+ mirror *.workers.dev) serwuje
# STAŁY ~3MB placeholder dla KAŻDEJ sceny, niezależnie od deklarowanej długości
# (5/5 scen = 3.04MB przy 14-47min, weryfikacja 2026-06-21, browser MediaSource grał
# ten sam stub; user-reports „server down" c382d441/ef10b946). To NIE jest realne
# wideo → traktujemy jak brak źródła (lepiej żadne niż 3MB „server down" clip).
# Realne starsze sceny (cdnde.com / okcdn.ru direct mp4) przechodzą normalnie.
if "/upload/videos/video_down.mp4" in content_url:
log.info("hqfap: stub video_down.mp4 (placeholder, no real video) on %s", page_url)
return None
qm = _QUALITY_RE.search(content_url)
quality = f"{qm.group(1)}p" if qm else None
return [
StreamSource(
link=content_url,
quality=quality,
type="mp4",
referer="https://hqfap.com/",
)
]

View file

@ -58,6 +58,8 @@ export type RootStackParamList = {
// 'tube:<sitetag>' źródła — telemetria odtwarzania zasilająca ranking źródeł. // 'tube:<sitetag>' źródła — telemetria odtwarzania zasilająca ranking źródeł.
// Opcjonalne; brak → telemetria pomijana (canonical/non-tube). // Opcjonalne; brak → telemetria pomijana (canonical/non-tube).
origin?: string; origin?: string;
// Post page URL dla IP-bound tubów (sxyprn/eporner/fpoxxx) — re-resolve on error.
resolvePageUrl?: string;
// 'movie' = MovieDetail wywołał Player z movieId zamiast sceneId. Backend // 'movie' = MovieDetail wywołał Player z movieId zamiast sceneId. Backend
// ma /movies/{id}/progress oddzielnie od /scenes/{id}/progress (2026-05-28). // ma /movies/{id}/progress oddzielnie od /scenes/{id}/progress (2026-05-28).
// Default 'scene' dla back-compat z istniejącymi nav callami. // Default 'scene' dla back-compat z istniejącymi nav callami.

View file

@ -30,6 +30,11 @@ interface RouteParams {
// 'tube:<sitetag>' źródła — do telemetrii odtwarzania (ranking źródeł). Opcjonalne; // 'tube:<sitetag>' źródła — do telemetrii odtwarzania (ranking źródeł). Opcjonalne;
// brak → telemetria pomijana (np. canonical/paradisehill bez tube-origin). // brak → telemetria pomijana (np. canonical/paradisehill bez tube-origin).
origin?: string; origin?: string;
// Post/scene page URL dla tubów IP-bound resolwowanych phone-side (sxyprn/eporner/
// fpoxxx). Gdy native player padnie na initial-load (token bound do innego IP /
// wygasł), Player RE-RESOLVUJE świeżo z tej strony (nowy token, bieżący IP) zamiast
// retry martwego URL-a. Zero VPS bandwidth. Ustawiane przez SceneDetail.openAsVideo.
resolvePageUrl?: string;
// 'scene' (default — back-compat z istniejącymi nav callami) lub 'movie'. // 'scene' (default — back-compat z istniejącymi nav callami) lub 'movie'.
// Player dispatcheruje upsertProgress vs upsertMovieProgress. Wcześniej // Player dispatcheruje upsertProgress vs upsertMovieProgress. Wcześniej
// MovieDetail przekazywał movieId jako sceneId — backend /scenes/<movieId>/ // MovieDetail przekazywał movieId jako sceneId — backend /scenes/<movieId>/