diff --git a/app/connectors/direct_scrapers/__init__.py b/app/connectors/direct_scrapers/__init__.py index 7a84ca3..5841c91 100644 --- a/app/connectors/direct_scrapers/__init__.py +++ b/app/connectors/direct_scrapers/__init__.py @@ -134,6 +134,7 @@ 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.hqfap import HQFapScraper # 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.eporner_api import EpornerApiScraper # noqa: E402 @@ -212,12 +213,11 @@ 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 — 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. + # HQFapScraper — PlayTube CMS (re-uploader pornhd.pet). Usunięty 2026-06-25 gdy CAŁA + # biblioteka CDN serwowała `/upload/videos/video_down.mp4` stub; PRZYWRÓCONY 2026-07-07 + # bo strona wróciła na CDN vstor.top z realnymi plikami (portable cross-IP), user request. + HQFapScraper, + # FourK69Scraper — USUNIĘTY 2026-06-25 (ten sam stub), NIE sprawdzany ponownie. # 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/hqfap.py b/app/connectors/direct_scrapers/hqfap.py new file mode 100644 index 0000000..cb1a099 --- /dev/null +++ b/app/connectors/direct_scrapers/hqfap.py @@ -0,0 +1,28 @@ +"""hqfap.com — latest-vids browse scraper (PlayTube CMS, patrz _playtube.py). + +Dołączony 2026-06-10 (user request), usunięty 2026-06-25 (cała biblioteka CDN padła +na stub), przywrócony 2026-07-07 (strona wróciła na CDN vstor.top z realnymi plikami, +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 (vstor.top obecnie, cdnde.com/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 9a720f2..69691ab 100644 --- a/app/extractors/__init__.py +++ b/app/extractors/__init__.py @@ -31,6 +31,7 @@ from app.extractors.tubes import ( freshporno, fullmovies, hdporngg, + hqfap, hqporner, neporn, latestpornvideo, @@ -164,10 +165,11 @@ _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 + 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. + # hqfap (PlayTube CMS) — JSON-LD contentUrl = direct mp4. USUNIĘTY 2026-06-25 (cała + # biblioteka serwowała `/upload/videos/video_down.mp4` stub), PRZYWRÓCONY 2026-07-07: + # strona wróciła na CDN vstor.top z realnymi plikami (portable cross-IP, zweryfikowane), + # user request. 4k69 zostaje usunięty (nie sprawdzany ponownie). + "hqfapcom": hqfap.extract, # 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/hqfap.py b/app/extractors/tubes/hqfap.py new file mode 100644 index 0000000..900dc00 --- /dev/null +++ b/app/extractors/tubes/hqfap.py @@ -0,0 +1,84 @@ +"""hqfap.com — direct stream extractor. + +Scene page (SSR, za Cloudflare → curl_cffi w fetch_tube_html) ma JSON-LD +VideoObject z `contentUrl` = direct mp4. Generacje hostingu w katalogu: + + - obecne sceny (2026-07-07): `d*.vstor.top/whlvid///.../_p.mp4` + — token time-bound, PORTABLE cross-IP (206 z residential ISP i VPS Hetzner, + weryfikacja 2026-07-07 po powrocie strony), + - starsze: `v4.cdnde.com/...?video=&time=&ip=` (`ip` nieegzekwowany) + oraz `vd*.okcdn.ru/?expires=...&srcIp=...` (ok.ru) — również portable cross-IP. + +Wszystkie generacje grają direct z telefonu → StreamSource ma `mobile_direct_ok` +(vstor.top nie łapie się w `_TIME_BOUND_CDN_RE` w playback.py, więc flagujemy jawnie), +zero proxy/WebView. + +Historia: wyłączony 2026-06-22 i USUNIĘTY 2026-06-25 gdy CAŁA biblioteka CDN +serwowała stały `/upload/videos/video_down.mp4` stub. Strona wróciła na nowy CDN +(vstor.top) z realnymi plikami → przywrócony 2026-07-07 (user request). Guard na +stub zostaje defensywnie. +""" +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 + + # Guard: `/upload/videos/video_down.mp4` (+ mirror *.workers.dev) to stały ~3MB + # "server down" placeholder (powód usunięcia 2026-06-25). Jeśli wróci — traktuj + # jak brak źródła (lepiej żadne niż stub). + 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/", + raw={"mobile_direct_ok": True}, + ) + ]