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>
77 lines
3.1 KiB
Python
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
|