"""Generic embed-iframe stream extractor. Wzorzec stosowany przez większość aggregator tubes (latestpornvideo, xmoviesforyou, watchporn, siska, porn4days, porndish, xxxfreewatch, latestleaks, mypornerleak, porndittcom, fpoxxx, hdporn92, sxyland, 0dayxx, ...): 1. Fetch page HTML 2. Znajdź `";` # Server-side widzimy tylko pusty `
` (iframe wstrzykuje się JS-em # po kliknięciu "Video Player 1" button). Match na `src=\"\"` w raw HTML source — # `\"` to backslash-quote z JSON-style escape'owania, `\/` to backslash-slash. # Po match'u czyścimy `\/` → `/` w captured URL. _JS_ESCAPED_IFRAME_SRC_RE = re.compile( r'src=\\"(?Phttps?:[^"]+?)\\"', re.IGNORECASE, ) # Data attribute pattern (mypornerleak's `data-embed="https://cdnstream.top/e/..."`). # Tube renderuje `
` + JS które po user click # wstawia iframe ze stored URL'em. Bez tego nasz ANY_IFRAME_RE widzi tylko pusty # holder + ad iframes → falls back na page-as-hoster (mobile WebView na aggregator # stronę = full-screen ad redirect, bug-report f1a01585 2026-05-17). _DATA_EMBED_RE = re.compile( r'data-embed=["\'](?P(?:https?:)?(?://)?[a-z0-9.-]+\.[a-z]{2,8}/[^"\']+)', re.IGNORECASE, ) # Blacklista hosterów — wyłączamy je z fallbacku iframe→hoster (WebView). Dwie kategorie: # # 1) Dead — w 100% przypadków zwracają 403/404/CAPTCHA, mobile pokazuje czarny ekran. # xtapes.porn → 301 → camcaps.to → 403 (zmigrowane 2026-04, infrastruktura zniknęła). # # 2) Malware — działają, ale serwują drive-by downloads (.reg/.exe/.msi) i pop-unders # przez chain reklamowy. yt-dlp odmawia ich ekstrakcji (oznaczone jako piracy), więc # nie ma mode'u "direct mp4 do ExoPlayera bez WebView" — jedyna opcja to WebView, # a WebView pozwala JS-owi hostera ściągnąć user execu plik. Bezpieczniej wyciąć je # całkowicie i pokazać user'owi page_url aggregator tube'a (rzadziej malicious niż # sam hoster). User nadal może tam wybrać alt player jeśli istnieje. # streamtape.com — notorious dla drive-by .reg downloads (zgłoszone 2026-05-07 # porn4days.pw → streamtape.com/e/ → popup + ściąganie .reg). # Hostname boundary: `(?:^|//|\.)` PRZED domain + `/` PO żeby `/filemoon.html` # w path nie matchował, tylko prawdziwy hostname (`https://filemoon.to/...` lub # `cdn.filemoon.to/...`). Bez tego DEAD_HOSTER_RE potencjalnie false-positive # blacklistował legit URL-e z fragmentami w ścieżce (code-review #17). DEAD_HOSTER_RE = re.compile( r'(?:^|//|\.)' r'(?:' # camcaps.to dead. xtapes.porn ZNIESIONE 2026-05-15 (Chrome DevTools verify: # → reelshdd.com/.mp4 z residential IP = działa, tylko VPS blocked). r'camcaps\.to' # dead r'|streamtape\.[a-z]+|streamta\.pe|streamtap\.com|streamcrypt\.net' # malware r'|scloud\.ninja|stape\.fun|tapecontent\.net|streamtapeadblock\.[a-z]+' # streamtape mirrors r'|openload\.co|openload\.io|oload\.[a-z]+' # openload (offline od 2019) # filemoon.* — wszystkie mirrory (filemoon.to/sx/nl/in/ru/co + aliasy # kerapoxy.cc, lvturbo.com) serwują od ~2026-05 ten sam SPA "Byse Frontend" # placeholder bez player JS. Globalny shutdown. Siska/perverzija/xmoviesforyou # mają filemoon jako default embed → wszystkie sceny przez ten path = dead # iframe (bug-report 16966e77 2026-05-16 "Niby 404 ale graficzne"). Blacklist # eliminuje próby + wymusza fallback na alt hostera / TubePageError None. r'|filemoon\.[a-z]{2,4}|kerapoxy\.cc|lvturbo\.com|emturbovid\.com' # dead 2026-05 r')' r'(?:[:/]|$)', # port, path, lub end-of-string re.IGNORECASE, ) # CAPTCHA-walled hosterzy — DoodStream variants serwują 5KB Cloudflare Turnstile # challenge response na server-side requests z VPS IP. Na phone (T-Mobile/PLAY) # czasem przechodzi, mobile-side resolver (mobile/src/lib/doodstream.ts) ma szansę. # ALE: gdy scene ma alt-hostera (np. luluvid, filemoon), tamten zazwyczaj nie ma # CAPTCHA gate → ExoPlayer odpala bezpośrednio. Sortujemy więc DoodStream NA KONIEC # listy — Stage 1 (server-side extract) i Stage 2 (mobile hoster picker) tryują # najpierw clean hosty, fallback na Dood. Wzorzec sync z mobile/src/lib/doodstream.ts. CAPTCHA_HOSTER_RE = re.compile( r'(?:' r'(?:playmogo|doodstream|doodporn|ds2play|ds2video|d000d|d0o0d|do0od|do7go|dooood|d0000d)\.[a-z]{2,8}' r'|dood\.(?:la|li|ws|so|to|watch|work|yt|re)' r')', re.IGNORECASE, ) # IP-BOUND CDN URLs — stream URLs które bindują się do requester IP (VPS resolve # → mobile 403). Stage 1 server-side ekstrakta `extract_stream_from_hoster` zwraca # tę URL, ale mobile direct nie pobierze. Lepiej dropować mp4/m3u8 wynik i upaść # na hoster fallback (mobile WebView wyciągnie URL z phone IP/session). # # Bandwidth cost (public release): te tubes by szli całością przez VPS proxy. # Skip ich z Stage 1 → mobile WebView → 0 VPS bandwidth. _IP_BOUND_CDN_RE = re.compile( r"\b(?:" r"premilkyway\.com" # latestpornvideo r"|tnmr\.org" # mypornerleak (legacy CDN) r"|acek-cdn\.com" # mypornerleak (current CDN, shared KVS infra) # URL signature shared across these CDNs: `/hls2///.../master.m3u8?t=&s=&e=&srv=&asn=` # — `asn` query param = Autonomous System Number bind. Generic match jako safety net. r")\b", re.IGNORECASE, ) def _hoster_priority(url: str) -> int: """Niższa wartość = wcześniej w liście. CAPTCHA-walled hosty (DoodStream variants) na końcu.""" if CAPTCHA_HOSTER_RE.search(url): return 1 return 0 def _is_player_iframe(url: str) -> bool: """Heurystyka: czy iframe wygląda na player a nie reklamę.""" if AD_DOMAIN_RE.search(url): return False if AD_QUERY_RE.search(url): return False if DEAD_HOSTER_RE.search(url): return False # Player path — `/e/`, `/embed/`, `/video/embed/` if PLAYER_PATH_RE.search(url): return True # Bare slug — `/` (no folder, no query) — sdefx.cloud-style. parsed = urlparse(url if url.startswith("http") else "https:" + url.lstrip("/")) if BARE_SLUG_PATH_RE.match(parsed.path) and not parsed.query: return True return False def _extract_direct_stream_urls(page_html: str, page_url: str) -> list[StreamSource]: """Stage 0.5: scan page for direct mp4/m3u8 URLs (porn4days→iceyfile/gounlimited download links, xmoviesforyou native player). Returns deduplicated sources ordered by quality desc. Returns empty list jeśli żadnych nie znaleziono lub wszystkie są na blacklisted hosterach (streamtape itp.). """ seen: set[str] = set() sources: list[tuple[int, StreamSource]] = [] # (quality_int, source) for sorting quality_map = { "2160p": 2160, "1080p": 1080, "720p": 720, "480p": 480, "360p": 360, "240p": 240, "144p": 144, } page_host = (urlparse(page_url).hostname or "").lstrip("www.") page_referer = f"https://{page_host}/" if page_host else page_url for m in _DIRECT_MP4_RE.finditer(page_html): url = m.group("url") if url in seen: continue seen.add(url) if DEAD_HOSTER_RE.search(url) or AD_DOMAIN_RE.search(url): continue # Filter pseudo-direct URLs: file hosters (rapidgator/k2s — premium auth gate) # i embed pages z .mp4 suffix (playmogo/dood /d/ — to HTML, nie video). if _NOT_DIRECT_STREAM_RE.search(url): continue # Quality wykrycie z nazwy pliku q_int = 0 q_label = "mp4" for q_str, q_val in quality_map.items(): if q_str in url.lower(): q_int = q_val q_label = q_str break sources.append((q_int, StreamSource(link=url, type="mp4", quality=q_label, referer=page_referer))) for m in _DIRECT_M3U8_RE.finditer(page_html): url = m.group("url") if url in seen: continue seen.add(url) if DEAD_HOSTER_RE.search(url) or AD_DOMAIN_RE.search(url): continue if _NOT_DIRECT_STREAM_RE.search(url): continue # m3u8 traktujemy jako wyższą jakość (adaptive) sources.append((10000, StreamSource(link=url, type="m3u8", quality="auto", referer=page_referer))) sources.sort(key=lambda x: -x[0]) return [s for _, s in sources] def extract( page_url: str, *, timeout: float = 60.0, ) -> list[StreamSource] | None: page_html = fetch_tube_html(page_url, timeout=timeout) # Stage 0.5: direct .mp4/.m3u8 URLs on page (porn4days→iceyfile, niektóre xmoviesforyou # native players). Wymagamy quality marker w path (`p.mp4`) żeby uniknąć # thumbnail-preview false-positives. Jeśli znaleziono → zwracamy, skipping iframe # processing — direct stream URL > WebView fallback. direct_sources = _extract_direct_stream_urls(page_html, page_url) if direct_sources: log.info("embed_iframe: found %d direct stream URLs on %s", len(direct_sources), page_url) return direct_sources # Znajdź WSZYSTKIE iframe-y które wyglądają na player. Wcześniej braliśmy tylko # pierwszy, ale niektóre tubes (siskavideo) mają kilku hosterów na stronie — # gdy pierwszy ma CF challenge / ad-heavy player (playmogo), drugi (luluvid) # może być cleaner. Dedupe po URL żeby nie dublować tego samego playera. # Trzymamy też raw_iframes (pre-filter) żeby odróżnić "page nie ma iframe-a" od # "page miał iframe ale został zablacklistowany jako malware/dead". raw_iframes_count = 0 iframe_urls: list[str] = [] seen: set[str] = set() for m in ANY_IFRAME_RE.finditer(page_html): candidate = m.group("url").strip() # Reklamowe iframe-y nie liczą się jako "raw iframe" (są zawsze obecne). if AD_DOMAIN_RE.search(candidate) or AD_QUERY_RE.search(candidate): continue raw_iframes_count += 1 if not _is_player_iframe(candidate): continue if candidate.startswith("//"): candidate = "https:" + candidate elif not candidate.startswith("http"): candidate = "https://" + candidate if candidate in seen: continue seen.add(candidate) iframe_urls.append(candidate) # JS-hidden backup servers (porn4days SERVER_URL pattern). Niektóre tube'y # renderują tylko 1 iframe a backup hosterów trzymają w `const SERVER2_URL = "..."` # JS variable + clickable "Server 2" button. Bez tego ekstraktor widzi tylko # SERVER1 (najczęściej iceyfile/streamtape) — gdy ten 404 / malware-blocked, # cały scrape ginie. Backupy mogą być na czystych hosterach (turbovidhls/veev.to). for m in _JS_SERVER_URL_RE.finditer(page_html): candidate = m.group("url").strip() if AD_DOMAIN_RE.search(candidate) or AD_QUERY_RE.search(candidate): continue if not _is_player_iframe(candidate): continue if candidate.startswith("//"): candidate = "https:" + candidate if candidate in seen: continue seen.add(candidate) iframe_urls.append(candidate) # Anchor-href hoster links (xmoviesforyou pattern). Tube page nie ma iframe playera, # tylko `` download buttons. Bez tego # extractor zwraca tylko page-as-hoster → WebView na catalog page, user musi # manualnie klikać hoster button. Po wyciągnięciu user dostaje hoster bezpośrednio # w mobile sources list. for m in _ANCHOR_HOSTER_RE.finditer(page_html): candidate = m.group("url").strip() if AD_DOMAIN_RE.search(candidate) or AD_QUERY_RE.search(candidate): continue if not _is_player_iframe(candidate): continue if candidate.startswith("//"): candidate = "https:" + candidate if candidate in seen: continue seen.add(candidate) iframe_urls.append(candidate) # Data attribute embed (mypornerleak: `data-embed="https://cdnstream.top/e/..."`). # Iframe sam wstawia się przez muliframe.js po user click; HTML server-side # ma tylko placeholder div + data-embed URL. for m in _DATA_EMBED_RE.finditer(page_html): candidate = m.group("url").strip() if AD_DOMAIN_RE.search(candidate) or AD_QUERY_RE.search(candidate): continue if not _is_player_iframe(candidate): continue if candidate.startswith("//"): candidate = "https:" + candidate elif not candidate.startswith("http"): candidate = "https://" + candidate if candidate in seen: continue seen.add(candidate) iframe_urls.append(candidate) # Escape'owane iframe src w JS string literals (porndish pattern). Iframe wstrzykuje # się do DOM po kliknięciu user'a — server-side HTML widzimy tylko `
`, # a iframe URL jest w `const doodstreamContent = "