goon/app/extractors/tubes/paradisehill.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

96 lines
3.5 KiB
Python

"""paradisehill.cc, direct mp4 extractor.
Paradisehill embed strony renderują video.js z `og:video` meta tagiem wskazującym
na `/player/<id>/` iframe. Ten iframe zawiera inline JS:
var videoList = [
{"sources":[{"src":"https://v1.paradisehill.cc/video/<hash>_part1.mp4","type":"video/mp4"}]},
{"sources":[{"src":"...part2.mp4",...}]},
...
];
Wieloczęściowe filmy są dzielone na part1..partN (~20-30 min każda). v1.paradisehill.cc
serwuje direct mp4 z Referer = paradisehill scene page; nie ma session auth ani token
bind (zweryfikowane 2026-05-15 z VPS Hetzner, status 200, ISO Media MP4).
Zwracamy listę StreamSource, jeden per part. Mobile player uznaje pierwszy element
(`best`) za główny; jeśli kiedyś potrzebowalibyśmy chapter switching, parts są w
`raw["parts"]` jako URL-e.
"""
from __future__ import annotations
import json
import logging
import re
from urllib.parse import urljoin
from app.extractors._fetch import browser_get, _DEFAULT_UA
from app.extractors._models import HosterDead, StreamSource
log = logging.getLogger(__name__)
_OG_VIDEO_RE = re.compile(r'<meta\s+property=["\']og:video["\']\s+content=["\']([^"\']+)["\']', re.IGNORECASE)
_VIDEOLIST_RE = re.compile(r'var\s+videoList\s*=\s*(\[[^;]+\]);', re.DOTALL)
_MP4_RE = re.compile(r'https?://[^\s"\'<>]+\.mp4(?:\?[^\s"\'<>]*)?', re.IGNORECASE)
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
headers = {
"User-Agent": _DEFAULT_UA,
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "en-US,en;q=0.9",
}
r = browser_get(page_url, headers=headers, timeout=timeout)
if r.status_code == 404 or r.status_code == 410:
raise HosterDead(f"paradisehill {page_url}: HTTP {r.status_code}")
if r.status_code != 200 or not r.text:
log.info("paradisehill: page fetch fail %s status=%s", page_url, r.status_code)
return None
m = _OG_VIDEO_RE.search(r.text)
if not m:
log.info("paradisehill: no og:video meta in %s", page_url)
return None
player_url = urljoin(page_url, m.group(1))
r2 = browser_get(player_url, headers={**headers, "Referer": page_url}, timeout=timeout)
if r2.status_code != 200 or not r2.text:
log.info("paradisehill: player iframe fail %s status=%s", player_url, r2.status_code)
return None
vl = _VIDEOLIST_RE.search(r2.text)
parts: list[str] = []
if vl:
try:
data = json.loads(vl.group(1))
for item in data:
for src in (item.get("sources") or []):
u = src.get("src")
if u and u not in parts:
parts.append(u)
except json.JSONDecodeError as e:
log.info("paradisehill: videoList JSON decode fail in %s: %s", player_url, e)
if not parts:
for m in _MP4_RE.finditer(r2.text):
u = m.group(0)
if u not in parts:
parts.append(u)
if not parts:
log.info("paradisehill: no mp4 in player iframe %s", player_url)
return None
referer = page_url
sources: list[StreamSource] = []
for i, url in enumerate(parts):
sources.append(
StreamSource(
link=url,
quality=None,
type="mp4",
referer=referer,
raw={"part_index": i, "total_parts": len(parts), "parts": parts} if len(parts) > 1 else None,
)
)
return sources