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:
parent
9a789a8551
commit
585e5d59f5
8 changed files with 17 additions and 264 deletions
|
|
@ -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.fullmovies import FullmoviesScraper # 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.superporn import SuperpornScraper # 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
|
||||
# full-crawl. (youporn pominięty — JSON-LD bez actor/keywords, scene-perf/tagi = nav A-Z.)
|
||||
XVideosBrowseScraper,
|
||||
# HQFapScraper / FourK69Scraper — WYŁĄCZONE 2026-06-22 (user request, na razie).
|
||||
# Oba na PlayTube CMS, ingestowały świeżo i wyglądały żywo, ALE playback w obu padł:
|
||||
# - hqfap: hosting migrował na `/upload/videos/video_down.mp4` = STAŁY ~3MB stub
|
||||
# "server down" dla KAŻDEJ sceny (extractor go odrzuca → None),
|
||||
# - 4k69: get_file nie zwraca już grywalnego URL (extractor resolves nothing → None).
|
||||
# Scena bez grywalnego źródła = śmieciowy wpis, więc nie ingestujemy nowych. Istniejące
|
||||
# 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,
|
||||
# HQFapScraper / FourK69Scraper — USUNIĘTE CAŁKOWICIE 2026-06-25. Oba PlayTube CMS;
|
||||
# disabled 2026-06-22 gdy playback padł, re-check 2026-06-25 potwierdził że CAŁA
|
||||
# biblioteka CDN znikła: wide-sample przez pełny zakres id (hqfap 0/80 real, 4k69
|
||||
# 0/40 real) — każda scena serwuje stały `/upload/videos/video_down.mp4` "server
|
||||
# down" stub, nie realny plik. Dane (28k solo-orphan scen + 46k sources) skasowane
|
||||
# z DB, pliki scraperów/extractorów i wpisy w _REGISTRY usunięte.
|
||||
# NepornScraper — dołączony 2026-06-10 (user request). KVS engine (jak freshporno/
|
||||
# porn00), /latest-updates/N/. JSON-LD (title+desc+uploadDate+thumb) + video:duration
|
||||
# meta + /models/ performerzy + /categories/ tagi. Brak studio (tytuł bywa
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -29,10 +29,8 @@ from app.extractors.tubes import (
|
|||
_ytdlp,
|
||||
eporner,
|
||||
freshporno,
|
||||
fourk69,
|
||||
fullmovies,
|
||||
hdporngg,
|
||||
hqfap,
|
||||
hqporner,
|
||||
neporn,
|
||||
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.)
|
||||
"fullmoviesxxx": fullmovies.extract,
|
||||
"hdporngg": hdporngg.extract,
|
||||
# hqfap — JSON-LD contentUrl = direct mp4 (cdnde.com nowsze / okcdn.ru starsze).
|
||||
# Cross-IP test 2026-06-10: oba CDN-y portable (`ip=`/`srcIp=` nie egzekwowane),
|
||||
# tokeny time-bound → on-demand fetch daje świeży URL. Mobile direct, zero proxy.
|
||||
"hqfapcom": hqfap.extract,
|
||||
# 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,
|
||||
# hqfap + 4k69 (PlayTube CMS) — USUNIĘTE CAŁKOWICIE 2026-06-25. Cała biblioteka CDN
|
||||
# znikła: każda scena serwuje stały `/upload/videos/video_down.mp4` "server down" stub
|
||||
# zamiast realnego pliku (wide-sample przez pełny zakres id: hqfap 0/80 real, 4k69
|
||||
# 0/40 real). Dane skasowane, scrapery/extractory usunięte.
|
||||
# neporn — KVS function/0 + license (jak freshporno). Server-side _kvs resolve →
|
||||
# data001.neporn.com/remote_control.php portable (cross-IP 206, 2026-06-10).
|
||||
"neporncom": neporn.extract,
|
||||
|
|
|
|||
|
|
@ -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("&", "&")
|
||||
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
|
||||
|
|
@ -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/",
|
||||
)
|
||||
]
|
||||
|
|
@ -58,6 +58,8 @@ export type RootStackParamList = {
|
|||
// 'tube:<sitetag>' źródła — telemetria odtwarzania zasilająca ranking źródeł.
|
||||
// Opcjonalne; brak → telemetria pomijana (canonical/non-tube).
|
||||
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
|
||||
// ma /movies/{id}/progress oddzielnie od /scenes/{id}/progress (2026-05-28).
|
||||
// Default 'scene' dla back-compat z istniejącymi nav callami.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ interface RouteParams {
|
|||
// 'tube:<sitetag>' źródła — do telemetrii odtwarzania (ranking źródeł). Opcjonalne;
|
||||
// brak → telemetria pomijana (np. canonical/paradisehill bez tube-origin).
|
||||
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'.
|
||||
// Player dispatcheruje upsertProgress vs upsertMovieProgress. Wcześniej
|
||||
// MovieDetail przekazywał movieId jako sceneId — backend /scenes/<movieId>/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue