"""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}, ) ]