From 585e5d59f5132b2dd53dded7c8e96f558fd73e04 Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Thu, 25 Jun 2026 11:07:47 +0200 Subject: [PATCH] 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) --- app/connectors/direct_scrapers/__init__.py | 19 ++---- app/connectors/direct_scrapers/fourk69.py | 66 ------------------ app/connectors/direct_scrapers/hqfap.py | 26 ------- app/extractors/__init__.py | 16 ++--- app/extractors/tubes/fourk69.py | 68 ------------------- app/extractors/tubes/hqfap.py | 79 ---------------------- mobile/src/navigation.tsx | 2 + mobile/src/screens/PlayerScreen.tsx | 5 ++ 8 files changed, 17 insertions(+), 264 deletions(-) delete mode 100644 app/connectors/direct_scrapers/fourk69.py delete mode 100644 app/connectors/direct_scrapers/hqfap.py delete mode 100644 app/extractors/tubes/fourk69.py delete mode 100644 app/extractors/tubes/hqfap.py diff --git a/app/connectors/direct_scrapers/__init__.py b/app/connectors/direct_scrapers/__init__.py index 1292fc5..ff08368 100644 --- a/app/connectors/direct_scrapers/__init__.py +++ b/app/connectors/direct_scrapers/__init__.py @@ -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 diff --git a/app/connectors/direct_scrapers/fourk69.py b/app/connectors/direct_scrapers/fourk69.py deleted file mode 100644 index d30facc..0000000 --- a/app/connectors/direct_scrapers/fourk69.py +++ /dev/null @@ -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 diff --git a/app/connectors/direct_scrapers/hqfap.py b/app/connectors/direct_scrapers/hqfap.py deleted file mode 100644 index 32caf8f..0000000 --- a/app/connectors/direct_scrapers/hqfap.py +++ /dev/null @@ -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 diff --git a/app/extractors/__init__.py b/app/extractors/__init__.py index f13807a..870c96a 100644 --- a/app/extractors/__init__.py +++ b/app/extractors/__init__.py @@ -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, diff --git a/app/extractors/tubes/fourk69.py b/app/extractors/tubes/fourk69.py deleted file mode 100644 index c49809b..0000000 --- a/app/extractors/tubes/fourk69.py +++ /dev/null @@ -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": "", "label": ""` 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":"","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 diff --git a/app/extractors/tubes/hqfap.py b/app/extractors/tubes/hqfap.py deleted file mode 100644 index 67e233c..0000000 --- a/app/extractors/tubes/hqfap.py +++ /dev/null @@ -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=&time=&ip=` — 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']+type=["\']application/ld\+json["\'][^>]*>(.*?)', - 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/", - ) - ] diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx index 6e16415..5685a66 100644 --- a/mobile/src/navigation.tsx +++ b/mobile/src/navigation.tsx @@ -58,6 +58,8 @@ export type RootStackParamList = { // 'tube:' ź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. diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index c0003dd..31b6e9a 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -30,6 +30,11 @@ interface RouteParams { // 'tube:' ź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//