goon/app/extractors/hosters/mixdrop.py
jtrzupek 81090ca8d2 fix(extractors): mixdrop hardening, yespornvip extractor, freshporno revert
Mixdrop (bug #3/#10 czarny ekran): wymagane UA+Accept headers (bez nich shell
bez P.A.C.K.E.R.). Detect dead-video page -> raise HosterDead zamiast None
(mobile dostaje skip-to-next sygnal). Dispatch regex obejmuje nowy canonical
domain `miixdrop` (double-i).

Yespornvip (bug #1): nowy KVS engine extractor. Origin `tube:yespornvip`
istnial w playback_sources ale brak handlera w _REGISTRY -> try_extract None.
Flashvars `video_url: 'function/0/<get_file_url>'`, function/0 to passthrough.
480p mp4 z mobile_direct_ok=True.

Freshporno (bug #9 revert): wrocony na _vps_blocked_fallback (WebView path).
Krotko-zywy switch na native extract z force_proxy=True cofniety bo app idzie
publicznie - VPS bandwidth/anonimowosc priorytet nad UX flicker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:23:37 +02:00

100 lines
4 KiB
Python

"""Mixdrop embed hoster — P.A.C.K.E.R. eval → MDCore.wurl direct mp4.
Pattern (verified 2026-05-15 via curl_cffi impersonate=chrome120):
1. Fetch `https://mixdrop.my/e/<id>` → 200 z 95KB body, redirect 301 do
`https://m1xdrop.bz/e/<id>` (current TLD).
2. Body zawiera P.A.C.K.E.R. obfuscated JS block:
`eval(function(p,a,c,k,e,d){...}('...packed...',N,N,'...|...'.split('|'),0,{}))`
3. yt-dlp's `decode_packed_codes()` rozkrywa do ~390 chars JavaScript:
`MDCore.wurl="//a-delivery22.mxcontent.net/v2/<id>.mp4?s=<sig>&e=<exp>&_t=<ts>"`
4. URL na `mxcontent.net` zwraca **direct mp4** (Content-Type: video/mp4,
Content-Length: ~485MB) — działa z Hetzner VPS IP, brak token IP-bind.
`s` to signed token (HMAC?), `e` to expiry timestamp (unix sec), `_t` to
issued timestamp. Token jest valid ~24h od `_t`. Refetching embed page po
expiry zwraca nowy URL.
Active mango movies: 203 playbacks origin='mangoporn:mixdrop' w DB.
"""
from __future__ import annotations
import logging
import re
from app.extractors._fetch import _DEFAULT_UA, browser_get
from app.extractors._models import HosterDead, StreamSource
log = logging.getLogger(__name__)
_PACKER_RE = re.compile(
r"eval\(function\(p,a,c,k,e,d\)\{.+?\}\(.+?\)\)",
re.DOTALL,
)
_MP4_URL_RE = re.compile(r'MDCore\.wurl\s*=\s*"([^"]+\.mp4[^"]*)"')
# Dead-video page (200 OK but no packer, only the "sorry" shell). Wcześniej nasz
# extractor zwracał None bez sygnału "dead" → playback.py nie ustawiał dead_at,
# mobile dostawał pusty wynik → czarny ekran zamiast skip-to-next-source.
_DEAD_RE = re.compile(
r"can't find the video|WE ARE SORRY",
re.IGNORECASE,
)
def extract(page_url: str, *, timeout: float = 30.0) -> list[StreamSource] | None:
# UA + Accept są wymagane — bez nich mixdrop dla VALID video zwraca minimalny
# body bez P.A.C.K.E.R. (sam stream_proxy._refetch_mixdrop_url też tak robi).
# Brak headerów powodował że extract() na żywym mixdrop ID dostawał shell bez
# packera → no match → None → mobile dostawał czarny ekran.
headers = {
"User-Agent": _DEFAULT_UA,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
res = browser_get(page_url, headers=headers, timeout=timeout)
if res.status_code != 200 or not res.text:
log.info("mixdrop: fetch fail status=%s url=%s", res.status_code, page_url)
return None
m = _PACKER_RE.search(res.text)
if not m:
if _DEAD_RE.search(res.text):
raise HosterDead(f"mixdrop {page_url}: video not found")
log.info("mixdrop: no P.A.C.K.E.R. block in %s (page changed?)", page_url)
return None
try:
from yt_dlp.utils import decode_packed_codes
decoded = decode_packed_codes(m.group(0))
except Exception as e:
log.warning("mixdrop: decode_packed_codes failed: %s", e)
return None
url_match = _MP4_URL_RE.search(decoded)
if not url_match:
log.info("mixdrop: no MDCore.wurl in decoded payload (len=%d)", len(decoded))
return None
raw_url = url_match.group(1)
# URL z mixdrop często jest protocol-relative (`//a-delivery22...`).
if raw_url.startswith("//"):
raw_url = "https:" + raw_url
return [
StreamSource(
link=raw_url,
quality=None, # mixdrop nie listuje quality variants w MDCore
type="mp4",
referer="https://mixdrop.my/",
# mxcontent CDN wymaga **same-session cookies** z embed page +
# Chrome JA3. Backend `extract` zamyka sesję po fetch → mobile
# próbuje mp4 bez cookies → 403. Proxy MUSI re-fetchować embed
# w fresh curl_cffi session, extract nowy mp4 URL, stream.
# `refetch_url` w raw → token field `rf` → proxy refresh logic.
raw={
"proxy_impersonate": True,
"refetch_url": page_url, # embed page do re-extract
"refetch_hoster": "mixdrop",
},
)
]