Mobile: P.A.C.K.E.R. hoster resolver (luluvid/streamwish)
Backend (VPS IP) dostaje CAPTCHA od luluvid/streamwish → try_extract zwraca type='hoster' → WebView fallback ze stroną+reklamami. Mobile IP usera renderuje pełny embed z packed JWPlayer config. - packerHoster.ts: port unpack_packer (hoster.py) do TS — eval-unpack P.A.C.K.E.R. → JWPlayer sources file URL, ad-roll filter - PlayerScreen: resolve useEffect probuje DoodStream LUB P.A.C.K.E.R. → sukces = NativeVideoPlayer bez reklam, fail = WebView fallback Naprawia latestpornvideo (luluvid) — bug 02444895 "Luluvid czarny ekran". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
642f1ab8b8
commit
feef312e8d
2 changed files with 157 additions and 11 deletions
136
mobile/src/lib/packerHoster.ts
Normal file
136
mobile/src/lib/packerHoster.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Mobile-side P.A.C.K.E.R. hoster resolver.
|
||||
*
|
||||
* Powód: hostery typu luluvid / streamwish-forki pakują JWPlayer config
|
||||
* (`sources:[{file:"https://...m3u8"}]`) w `eval(function(p,a,c,k,e,d){...})`
|
||||
* P.A.C.K.E.R. obfuskację. Backend (VPS Hetzner IP) dostaje od nich CAPTCHA /
|
||||
* pustą stronę, więc `try_extract` zwraca type='hoster' → WebView fallback,
|
||||
* który pokazuje stronę hostera z reklamami.
|
||||
*
|
||||
* Mobile IP usera (residential / komórka) zwykle NIE triggeruje CAPTCHA —
|
||||
* embed page renderuje pełny HTML z packed JWPlayer config. Rozpakowujemy
|
||||
* go tutaj i zwracamy direct m3u8/mp4 do natywnego ExoPlayera.
|
||||
*
|
||||
* Port `unpack_packer` z app/extractors/hoster.py (ten sam regex + base-N).
|
||||
* Komplementarny do doodstream.ts (pass_md5 protokół) — tamten dla DoodStream,
|
||||
* ten dla P.A.C.K.E.R.-JWPlayer hosterów.
|
||||
*/
|
||||
|
||||
const PACKER_HOSTS = new Set([
|
||||
'luluvid.com', 'luluvdo.com', 'lulustream.com', 'lulu.st',
|
||||
// StreamWish family — ten sam P.A.C.K.E.R.-JWPlayer wzorzec
|
||||
'streamwish.com', 'streamwish.to', 'embedwish.com', 'wishembed.pro',
|
||||
'awish.pro', 'dwish.pro', 'mwish.pro', 'swishsrv.com',
|
||||
'streamwish.site', 'hlswish.com', 'kswplayer.info',
|
||||
]);
|
||||
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
|
||||
|
||||
export interface ResolveResult {
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function isPackerHoster(url: string): boolean {
|
||||
if (!url) return false;
|
||||
try {
|
||||
const host = new URL(url).hostname.toLowerCase().replace(/^www\./, '');
|
||||
if (PACKER_HOSTS.has(host)) return true;
|
||||
for (const h of PACKER_HOSTS) {
|
||||
if (host.endsWith('.' + h)) return true;
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Port `_base_n` z hoster.py — token w bazie `base` (max 62, a-zA-Z0-9).
|
||||
function baseN(token: string, base: number): number | null {
|
||||
let result = 0;
|
||||
for (const ch of token) {
|
||||
let d: number;
|
||||
if (ch >= '0' && ch <= '9') d = ch.charCodeAt(0) - 48;
|
||||
else if (ch >= 'a' && ch <= 'z') d = ch.charCodeAt(0) - 97 + 10;
|
||||
else if (ch >= 'A' && ch <= 'Z') d = ch.charCodeAt(0) - 65 + 36;
|
||||
else return null;
|
||||
if (d >= base) return null;
|
||||
result = result * base + d;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Port `unpack_packer` — odwraca eval(function(p,a,c,k,e,d){...}('PAYLOAD',BASE,COUNT,'kw'.split('|'))).
|
||||
export function unpackPacker(js: string): string | null {
|
||||
const m = js.match(
|
||||
/\}\s*\(\s*'((?:\\'|[^'])+)'\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*'((?:\\'|[^'])*)'\s*\.split\('\|'\)/s,
|
||||
);
|
||||
if (!m) return null;
|
||||
let payload = m[1];
|
||||
const base = parseInt(m[2], 10);
|
||||
const count = parseInt(m[3], 10);
|
||||
const keywords = m[4].split('|');
|
||||
payload = payload.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||
return payload.replace(/\b\w+\b/g, (token) => {
|
||||
const idx = baseN(token, base);
|
||||
if (idx === null || idx >= count || idx >= keywords.length) return token;
|
||||
return keywords[idx] || token;
|
||||
});
|
||||
}
|
||||
|
||||
// JWPlayer `sources:[{file:"URL"}]` lub `file:"URL"` — m3u8/mp4/mpd.
|
||||
const FILE_RE = /(?:["']?file["']?|sources?)\s*[:=]\s*["'](https?:\/\/[^"']+\.(?:m3u8|mp4|mpd)[^"']*)["']/i;
|
||||
const AD_RE = /\/(?:preroll|midroll|postroll|preplay|ads?|advert|promo)\d*\.(?:mp4|m3u8|webm)/i;
|
||||
|
||||
export async function resolvePackerHoster(
|
||||
embedUrl: string,
|
||||
sourceUrl?: string,
|
||||
): Promise<ResolveResult> {
|
||||
if (!isPackerHoster(embedUrl)) return { error: 'not_packer_hoster' };
|
||||
|
||||
// /d/ (download) → /e/ (embed player) — embed ma packed JWPlayer config.
|
||||
const url = embedUrl.replace('/d/', '/e/');
|
||||
const host = (() => { try { return new URL(url).hostname; } catch { return ''; } })();
|
||||
const referer = sourceUrl || `https://${host}/`;
|
||||
|
||||
let html: string;
|
||||
try {
|
||||
const r = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': UA, 'Referer': referer },
|
||||
});
|
||||
if (!r.ok) return { error: `embed http ${r.status}` };
|
||||
html = await r.text();
|
||||
} catch (e: any) {
|
||||
return { error: `embed fetch fail: ${e?.message || e}` };
|
||||
}
|
||||
|
||||
if (/video not found|file (?:was )?deleted|not found/i.test(html.slice(0, 4000))) {
|
||||
return { error: 'video_deleted' };
|
||||
}
|
||||
|
||||
// Najpierw raw HTML (czasem file: jest poza packerem), potem unpacked.
|
||||
let videoUrl: string | null = null;
|
||||
const rawMatch = html.match(FILE_RE);
|
||||
if (rawMatch && !AD_RE.test(rawMatch[1])) {
|
||||
videoUrl = rawMatch[1];
|
||||
}
|
||||
if (!videoUrl) {
|
||||
const unpacked = unpackPacker(html);
|
||||
if (!unpacked) {
|
||||
// Brak packera = CAPTCHA gate (mobile IP też zablokowane?) lub pusta strona.
|
||||
if (html.length < 2000 || /challenge-platform|turnstile/i.test(html)) {
|
||||
return { error: 'captcha_gate' };
|
||||
}
|
||||
return { error: 'no_packer_in_html' };
|
||||
}
|
||||
const um = unpacked.match(FILE_RE);
|
||||
if (um && !AD_RE.test(um[1])) videoUrl = um[1];
|
||||
}
|
||||
|
||||
if (!videoUrl) return { error: 'no_video_url' };
|
||||
|
||||
return {
|
||||
url: videoUrl,
|
||||
headers: { 'User-Agent': UA, 'Referer': referer },
|
||||
};
|
||||
}
|
||||
|
|
@ -883,22 +883,32 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
|
|||
const [skipResolve, setSkipResolve] = React.useState(false);
|
||||
const [resolveAttempted, setResolveAttempted] = React.useState(false);
|
||||
|
||||
// Stage 0.8: DoodStream-variant resolver (porn-app rs0.java port). Mobile IP
|
||||
// może uniknąć Cloudflare Turnstile gate który blokuje Hetzner VPS. Fetch
|
||||
// embed page, parse pass_md5 + splash_error, construct direct mp4 URL.
|
||||
// Sukces → NativeVideoPlayer. Fail → fallback do WebView.
|
||||
// Stage 0.8: mobile-side hoster resolvery. Mobile IP usera unika Cloudflare
|
||||
// Turnstile / CAPTCHA gate który blokuje Hetzner VPS — embed page renderuje
|
||||
// pełny HTML z player config, z którego liczymy direct m3u8/mp4 URL.
|
||||
// - DoodStream-variant (playmogo/dood*) — pass_md5 protokół (doodstream.ts)
|
||||
// - P.A.C.K.E.R.-JWPlayer (luluvid/streamwish) — eval-unpack (packerHoster.ts)
|
||||
// Sukces → NativeVideoPlayer (bez reklam/cookie). Fail → fallback do WebView.
|
||||
React.useEffect(() => {
|
||||
if (resolveAttempted || skipResolve) return;
|
||||
(async () => {
|
||||
const { isDoodStream, resolveDoodStream } = await import('../lib/doodstream');
|
||||
if (!isDoodStream(url)) {
|
||||
setResolveAttempted(true);
|
||||
return;
|
||||
}
|
||||
setResolveStatus('pending');
|
||||
const source = refererHost ? `https://${refererHost.replace(/^https?:\/\//, '')}/` : undefined;
|
||||
const result = await resolveDoodStream(url, source);
|
||||
const { isDoodStream, resolveDoodStream } = await import('../lib/doodstream');
|
||||
const { isPackerHoster, resolvePackerHoster } = await import('../lib/packerHoster');
|
||||
|
||||
let result: { url?: string; headers?: Record<string, string>; error?: string } | null = null;
|
||||
if (isDoodStream(url)) {
|
||||
setResolveStatus('pending');
|
||||
result = await resolveDoodStream(url, source);
|
||||
} else if (isPackerHoster(url)) {
|
||||
setResolveStatus('pending');
|
||||
result = await resolvePackerHoster(url, source);
|
||||
}
|
||||
|
||||
setResolveAttempted(true);
|
||||
if (!result) {
|
||||
return; // nie nasz hoster — WebView fallback (INJECTED_JS) zajmie się resztą
|
||||
}
|
||||
if (result.url) {
|
||||
setResolveStatus('idle');
|
||||
nav.replace('Player', {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue