goon/app/extractors/tubes/fullporner.py
goon-foss b485511f3f feat(fullporner): browse scraper pelnych scen + extractor bez tokenu
Pelne sceny, nie klipy: mediana ~33 min, zero ponizej 10 min, ~140-240 dziennie,
listing chronologiczny mimo naglowka Featured.

Extractor sklada URL sam, bez tokenu i bez wygasania: id w iframe jest zapisane
wspak, a koncowka to maska bitowa jakosci (1/2/4/8 = 360/480/720/1080). Id bywa
numeryczne albo szesnastkowe, a to drugie to ~25% katalogu. Blokada CDN jest na
naglowku (bez UA 403, bez Referera 404), NIE na fingerprincie TLS, wiec telefon
przechodzi -> mobile_direct_ok. Zweryfikowane 206 + faststart do 1080p.

Metadane cienkie: brak studia, mediana 3 tagi, tytuly przepisane pod SEO. Za to
data jako unix timestamp i dlugosc co do sekundy. Bramka na obsade, bo sceny bez
performera scalily sie 0/23 w dry-runie.

Phash miniatury liczony celowo i to on robi robote: dry-run resolvera bez niego
dawal 31% scalen, pilot z nim dal 65% (seen 62, new 22, updated 40, errors 0).
85 z 89 performerow ma ref tpdb/stashdb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 12:04:34 +02:00

77 lines
3.1 KiB
Python

"""fullporner.com — bezpośredni mp4 z xiaoshenke.net. Dodany 2026-07-27.
Najprostszy i najtrwalszy stream w całym portfolio: URL buduje się deterministycznie,
**bez tokenu, bez podpisu i bez wygasania**. Nie ma tu nic do resolvowania po stronie
hostera, wystarczy przeczytać dwie liczby z iframe'a.
Strona sceny osadza `<iframe src="//xiaoshenke.net/video/4458723/14">`, a player robi
z tego:
var id = "4458723".split('').reverse().join(''); // id mediów zapisane WSPAK
function btq(f) { 1→360, 2→480, 4→720, 8→1080 } // /14 = 8+4+2 = 1080,720,480
v.media = `//${location.hostname}/vid/${id}/${quality}`
czyli `https://xiaoshenke.net/vid/3278544/1080`. Że id jest odwrócone, potwierdza
miniaturka z listingu: `imgs.xiaoshenke.net/thumb/3278544.jpg`.
**Warunek dostępu to Referer + jakikolwiek realistyczny User-Agent.** Sprawdzone
osobno, bo to przesądza o `mobile_direct_ok`: zwykły httpx (a więc NIE fingerprint
TLS Chrome'a) z Refererem i UA ExoPlayera dostaje 206, a ten sam request bez UA
dostaje 403. Blokada jest więc na nagłówku, nie na fingerprincie, i telefon ją
przechodzi — `playback.py` wysyła oba nagłówki.
Zweryfikowane: 1080p = 1,51 GB, 720p = 754 MB, 206 na Range, `ftypisom` w pierwszym
KB (faststart, seek działa od razu). Przy błędzie player dokleja `/b` (backup CDN) —
nie używamy tego, bo podstawowy URL działa, ale gdyby zaczęło sypać 404, to jest
pierwsza rzecz do sprawdzenia.
"""
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__)
_BASE = "https://fullporner.com"
# Id bywa numeryczne (`4458723`) ALBO szesnastkowe (`a6487a97766a6`) — to drugie to
# ~25% katalogu i przy `\d+` wypadało z ingestu. Odwracanie działa dla obu, bo player
# robi zwykły reverse stringa.
_IFRAME_RE = re.compile(r'src="(?://)?[^"]*xiaoshenke\.net/video/([0-9a-f]+)/(\d+)"')
# Maska bitowa z URL-a iframe'a, 1:1 z `btq()` w playerze.
_QUALITY_BITS = ((1, 360), (2, 480), (4, 720), (8, 1080))
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html_text = fetch_tube_html(page_url, timeout=timeout)
m = _IFRAME_RE.search(html_text)
if not m:
log.info("fullporner: brak iframe xiaoshenke na %s", page_url)
return None
media_id = m.group(1)[::-1] # id w iframe jest wspak
mask = int(m.group(2))
out: list[StreamSource] = []
for bit, height in _QUALITY_BITS:
if not mask & bit:
continue
out.append(
StreamSource(
link=f"https://xiaoshenke.net/vid/{media_id}/{height}",
type="mp4",
quality=f"{height}p",
# Bez Referera CDN oddaje 404, bez UA 403 — oba dokłada playback.py.
referer=_BASE + "/",
raw={"mobile_direct_ok": True},
)
)
if not out:
log.info("fullporner: maska jakości %s nie dała żadnej rendycji na %s", mask, page_url)
return None
out.sort(key=lambda s: int((s.quality or "0p")[:-1]), reverse=True)
return out