goon/app/extractors/tubes/_source_getfile.py
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

76 lines
3 KiB
Python

"""Współdzielony resolver dla tubów z `<source src=.../get_file/...mp4>` + IP-bound CDN
(hdporn.gg, fullmovies.xxx, ta sama platforma, `/get_file/8512/`).
2026-06-04 (DevTools + cross-IP investigation, naprawia „hdporngg loading forever"):
get_file 302-redirectuje do `fpvcdn.com` z **IP fetchera wbitym w URL** (`ip=<kto-fetchnął>`),
więc finalny CDN jest IP-bound do tego kto resolvuje. Dlatego oddajemy get_file URL
**NIEZRESOLWOWANY** (mobile_direct), ExoPlayer na telefonie sam follow-uje 302, fpvcdn
bindje się do IP telefonu, gra. (Resolve na VPS → bind do IP VPS → mobile 403.)
Zweryfikowane: get_file jest STATELESS (świeża sesja działa) + ważny ≥90s, więc telefon
ma czas (resolve→picker→tap = sekundy). Źródło **2160p/4K konsekwentnie time-outuje na
fpvcdn (~30s)** → POMIJAMY je (to była przyczyna „loading forever", player ładował 4K
pierwsze); 720p/480p/1080p resolvują w ~1s.
"""
from __future__ import annotations
import logging
import re
from app.extractors._fetch import _DEFAULT_IMPERSONATE, _DEFAULT_UA, _HAS_CURL_CFFI
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_SOURCE_RE = re.compile(
r"<source\s+src=['\"]([^'\"]+/get_file/[^'\"]+\.mp4[^'\"]*)['\"]"
r"[^>]*?(?:title|label)=['\"]?([^'\">]*)",
re.IGNORECASE,
)
# fpvcdn nie serwuje 4K (30s timeout), skip żeby player nie wisiał na nim.
_SKIP_QUALITY_RE = re.compile(r"2160|1440|4k", re.IGNORECASE)
def resolve(page_url: str, base_url: str, *, timeout: float = 30.0) -> list[StreamSource] | None:
if not _HAS_CURL_CFFI:
log.info("source_getfile: curl_cffi unavailable — %s", page_url)
return None
from curl_cffi import requests as cf
try:
html = cf.get(
page_url, impersonate=_DEFAULT_IMPERSONATE,
headers={"User-Agent": _DEFAULT_UA, "Accept": "text/html,application/xhtml+xml"},
timeout=timeout,
).text
except Exception as e:
log.info("source_getfile: page fetch failed %s: %s", page_url, e)
return None
seen: set[str] = set()
out: list[StreamSource] = []
for m in _SOURCE_RE.finditer(html):
url = m.group(1).strip()
quality = (m.group(2) or "").strip()
if url.startswith("//"):
url = "https:" + url
if url in seen:
continue
seen.add(url)
if _SKIP_QUALITY_RE.search(quality):
log.info("source_getfile: skip broken-CDN quality %r on %s", quality, page_url)
continue
out.append(StreamSource(
link=url, type="mp4", quality=quality or None,
referer=base_url + "/", raw={"mobile_direct_ok": True},
))
if not out:
log.info("source_getfile: no playable <source>/get_file on %s", page_url)
return None
def _rank(s: StreamSource) -> int:
mm = re.search(r"(\d{3,4})", s.quality or "")
return int(mm.group(1)) if mm else -1
out.sort(key=_rank, reverse=True)
return out