goon/app/extractors/tubes/fourk69.py
jtrzupek 80fd83cb4e feat(tubes): add 4k69 + neporn browse scrapers, shared PlayTube base
4k69.com (~65k scenes): same PlayTube CMS as hqfap - common logic moved
to _playtube.py (sitemap catalog, JSON-LD, pills). Studio classified by
matching category pills against the studios index page. Streams are
get_file (fullmovies family) returned unresolved with mobile_direct,
2160p skipped.

neporn.com: KVS engine, latest-updates listing, JSON-LD + video:duration
meta, performers from models links with flashvars video_tags fallback
for fresh uploads. Resolve via _kvs; final URL portable cross-IP.

superporn.com rejected: Cloudflare 403 from VPS on all TLS impersonations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:15:13 +02:00

56 lines
2 KiB
Python

"""4k69.com — get_file stream extractor (platforma jak fullmovies/hdporngg).
Scene page (SSR za Cloudflare → curl_cffi) ma 3 get_file URL-e na www.4kporno.xxx
(`..._2160m.mp4` / `_720m` / `_480m`) — w JSON-LD contentUrl i w JS playera, NIE
w `<source>` tagach (dlatego nie _source_getfile, tylko skan całej strony).
Jak fpvcdn (fullmovies, ta sama rodzina `/get_file/8512/`): get_file binduje CDN
do IP fetchera, jest stateless i ważny ≥90s → oddajemy NIEZRESOLWOWANE z
mobile_direct_ok — telefon follow-uje 302 z własnym IP (cross-IP test 2026-06-10:
lokalny ISP 206 video/mp4). 2160p pomijamy (CDN time-out ~30s, jak fpvcdn).
"""
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__)
_GET_FILE_RE = re.compile(r"https://[a-z0-9.\-]+/get_file/[^\s\"'\\]+\.mp4/?", re.IGNORECASE)
_QUALITY_RE = re.compile(r"_(\d{3,4})[mp]?\.mp4", re.IGNORECASE)
_SKIP_QUALITY_RE = re.compile(r"^(2160|1440)$")
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 _GET_FILE_RE.finditer(html):
url = m.group(0)
if url in seen:
continue
seen.add(url)
qm = _QUALITY_RE.search(url)
quality_num = qm.group(1) if qm else None
if quality_num and _SKIP_QUALITY_RE.match(quality_num):
continue
# `_preview.mp4` itp. bez liczby jakości — pomiń (trailer, nie scena).
if not quality_num:
continue
out.append(StreamSource(
link=url,
quality=f"{quality_num}p",
type="mp4",
referer="https://4k69.com/",
raw={"mobile_direct_ok": True},
))
if not out:
log.info("4k69: no get_file URLs on %s", page_url)
return None
out.sort(key=lambda s: int((s.quality or "0p")[:-1]), reverse=True)
return out