"""freshporno.org — KVS engine, BEZ `` tagów. Page używa kt_player (KVS Flash + JS legacy player) — URLs są wewnątrz JavaScript flashvars JSON (`video_url: 'function/0/'`) i w `` linkach z labelem "MP4 720p" / "MP4 480p". Bierzemy anchor pattern bo ma WSZYSTKIE quality z explicit labelem (vs flashvars ma tylko main+alt, max 2 jakości). `MP4 p, ...` Sidebar suggested videos używają `data-preview="...get_file/.../_preview.mp4"` — inny pattern (nie ``), więc anchor regex je naturalnie pomija. CDN token IP-bound do VPS — mobile dostanie 403 na direct, fallback proxy działa. get_file 302 → `cdn4.freshporno.org/remote_control.php?...&file=` direct mp4 (nie HLS). Type='mp4'. """ 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__) # `MP4 p, ` — main + alt streams. _ANCHOR_QUALITY_RE = re.compile( r']*href="(?Phttps?://[^"]+/get_file/[^"]+\.mp4/)\?download=true[^"]*"' r'[^>]*>\s*MP4\s+(?P\d{3,4}p)', re.IGNORECASE, ) def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: html = fetch_tube_html(page_url, timeout=timeout) seen_keys: set[str] = set() result: list[StreamSource] = [] for m in _ANCHOR_QUALITY_RE.finditer(html): url = m.group("url") quality = m.group("q") # Dedupe po basename (path bez query string). basename = url.rstrip("/").split("/")[-1] if basename in seen_keys: continue seen_keys.add(basename) # `force_proxy=True` (2026-05-20): freshporno get_file 302 → cdn4.freshporno.org # IP-bound (cv= HMAC token). Mobile direct = 403/SSL fail → fallback proxy # generuje "mrugnięcie" (user bug 743eefbf "najpierw strona potem video"). # Force_proxy wymusza mobile użycie proxied URL od razu — bez flickera + # natywny ExoPlayer + quality picker zachowane. result.append(StreamSource( link=url, type="mp4", quality=quality, raw={"force_proxy": True}, )) if not result: log.info("freshporno: no MP4 anchor matches on %s", page_url) return None def _quality_key(s: StreamSource) -> int: if not s.quality: return -1 try: return int(s.quality.rstrip("p")) except ValueError: return -1 result.sort(key=_quality_key, reverse=True) return result