mirror of
https://github.com/p-stream/providers.git
synced 2026-03-11 17:55:36 +00:00
fix dood
This commit is contained in:
parent
09872c43d8
commit
13b29ae942
1 changed files with 81 additions and 135 deletions
|
|
@ -1,34 +1,25 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
|
|
||||||
|
import { flags } from '@/entrypoint/utils/targets';
|
||||||
import { makeEmbed } from '@/providers/base';
|
import { makeEmbed } from '@/providers/base';
|
||||||
|
|
||||||
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 10);
|
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 10);
|
||||||
|
|
||||||
const PASS_MD5_PATTERNS: RegExp[] = [
|
const PASS_MD5_PATTERNS: RegExp[] = [
|
||||||
/\$\.get\('\/pass_md5[^']*'\)/,
|
/\$\.get\(['"](\/pass_md5\/[^'"]+)['"]/,
|
||||||
/\$\.get\("\/pass_md5[^"]*"\)/,
|
/\$\.get\(["`](\/pass_md5\/[^"']+)["`]/,
|
||||||
/\$\.get\s*\('\/pass_md5([^']+)'\)/,
|
/\$\.get\s*\(['"](\/pass_md5\/[^'"]+)['"]/,
|
||||||
/\$\.get\s*\("\/pass_md5([^"]+)"\)/,
|
/\$\.get\s*\(["`](\/pass_md5\/[^"']+)["`]/,
|
||||||
/fetch\(\s*["'](\/pass_md5[^"']+)["']\s*\)/,
|
|
||||||
/axios\.get\(\s*["'](\/pass_md5[^"']+)["']\s*\)/,
|
|
||||||
/open\(\s*["']GET["']\s*,\s*["'](\/pass_md5[^"']+)["']\s*\)/,
|
|
||||||
/url\s*:\s*["'](\/pass_md5[^"']+)["']/,
|
|
||||||
/location\.href\s*=\s*["'](\/pass_md5[^"']+)["']/,
|
|
||||||
/(\/pass_md5\.php[^"']*)/,
|
|
||||||
/["'](\/pass_md5\/[^"']+)["']/,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const TOKEN_PATTERNS: RegExp[] = [/token["']?\s*[:=]\s*["']([^"']+)["']/, /makePlay\([^)]*token=([^"&']+)/];
|
const TOKEN_PATTERNS: RegExp[] = [/token["']?\s*[:=]\s*["']([^"']+)["']/, /makePlay.*?token=([^"&']+)/];
|
||||||
|
|
||||||
function extractFirst(html: string, patterns: RegExp[]): string | null {
|
function extractFirst(html: string, patterns: RegExp[]): string | null {
|
||||||
for (const pat of patterns) {
|
for (const pat of patterns) {
|
||||||
const m = pat.exec(html);
|
const m = pat.exec(html);
|
||||||
if (m) {
|
if (m && m[1]) {
|
||||||
// capture group if available else try to parse from full match
|
return m[1];
|
||||||
if (m.length > 1 && m[1]) return m[1];
|
|
||||||
const match = m[0];
|
|
||||||
const inner = /\/pass_md5[^'"')]+/.exec(match)?.[0] ?? null;
|
|
||||||
if (inner) return inner;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -42,150 +33,105 @@ function resolveAbsoluteUrl(base: string, maybeRelative: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function extractVideoUrl(ctx: any, streamingLink: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1',
|
||||||
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Sec-Fetch-Site': 'none',
|
||||||
|
'Sec-Fetch-Mode': 'navigate',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
'Sec-Fetch-Dest': 'document',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await ctx.proxiedFetcher.full(streamingLink, {
|
||||||
|
headers,
|
||||||
|
allowRedirects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const passMd5Match = extractFirst(response.body, PASS_MD5_PATTERNS);
|
||||||
|
if (!passMd5Match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = `${response.finalUrl.split('://')[0]}://${response.finalUrl.split('://')[1].split('/')[0]}`;
|
||||||
|
const passMd5Url = resolveAbsoluteUrl(baseUrl, passMd5Match);
|
||||||
|
|
||||||
|
const passMd5Response = await ctx.proxiedFetcher(passMd5Url, {
|
||||||
|
headers,
|
||||||
|
cookies: response.cookies,
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoUrl = passMd5Response.trim();
|
||||||
|
|
||||||
|
const tokenMatch = extractFirst(response.body, TOKEN_PATTERNS);
|
||||||
|
if (tokenMatch) {
|
||||||
|
const randomString = nanoid();
|
||||||
|
const expiry = Date.now();
|
||||||
|
return `${videoUrl}${randomString}?token=${tokenMatch}&expiry=${expiry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoUrl;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const doodScraper = makeEmbed({
|
export const doodScraper = makeEmbed({
|
||||||
id: 'dood',
|
id: 'dood',
|
||||||
name: 'dood',
|
name: 'dood',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
rank: 173,
|
rank: 173,
|
||||||
flags: [],
|
flags: [flags.CORS_ALLOWED],
|
||||||
async scrape(ctx) {
|
async scrape(ctx) {
|
||||||
// Resolve any interstitial/redirect links (e.g., primewire wrappers)
|
|
||||||
let pageUrl = ctx.url;
|
let pageUrl = ctx.url;
|
||||||
if (pageUrl.includes('primewire')) {
|
|
||||||
const req = await ctx.proxiedFetcher.full(pageUrl);
|
// Replace dood.watch with myvidplay.com to avoid Cloudflare protection
|
||||||
pageUrl = req.finalUrl;
|
try {
|
||||||
|
const url = new URL(pageUrl);
|
||||||
|
if (url.hostname === 'dood.watch') {
|
||||||
|
pageUrl = `https://myvidplay.com${url.pathname}${url.search}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If URL parsing fails, keep original URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize to embed page /e/{id} when a /d/{id} download page is provided
|
const redirectReq = await ctx.proxiedFetcher.full(pageUrl);
|
||||||
const initial = new URL(pageUrl);
|
pageUrl = redirectReq.finalUrl;
|
||||||
const idMatch = initial.pathname.match(/\/(?:d|e)\/([A-Za-z0-9]+)/);
|
|
||||||
const origin = (() => {
|
|
||||||
try {
|
|
||||||
return `${initial.protocol}//${initial.host}`;
|
|
||||||
} catch {
|
|
||||||
return 'https://d000d.com';
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
const embedUrl = idMatch ? `${origin}/e/${idMatch[1]}` : pageUrl;
|
|
||||||
|
|
||||||
// Fetch the dood embed page (consistent location of scripts)
|
const videoUrl = await extractVideoUrl(ctx, pageUrl);
|
||||||
const pageResp = await ctx.proxiedFetcher.full<string>(embedUrl);
|
if (!videoUrl) {
|
||||||
const html = pageResp.body;
|
throw new Error('dood: could not extract video URL');
|
||||||
const finalPageUrl = pageResp.finalUrl || embedUrl;
|
|
||||||
const pageOrigin = (() => {
|
|
||||||
try {
|
|
||||||
const u = new URL(finalPageUrl);
|
|
||||||
return `${u.protocol}//${u.host}`;
|
|
||||||
} catch {
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Try to read thumbnail track (both quote styles)
|
|
||||||
const thumbnailTrack = html.match(/thumbnails:\s*\{\s*vtt:\s*['"]([^'"]+)['"]/);
|
|
||||||
|
|
||||||
// Find pass_md5 path in the main page, or fallback to iframes
|
|
||||||
let passPath = extractFirst(html, PASS_MD5_PATTERNS);
|
|
||||||
|
|
||||||
if (!passPath) {
|
|
||||||
const iframeSrcs = Array.from(html.matchAll(/<iframe[^>]+src=["']([^"']+)["']/gi))
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((m) => m[1]);
|
|
||||||
for (const src of iframeSrcs) {
|
|
||||||
try {
|
|
||||||
const abs = resolveAbsoluteUrl(finalPageUrl, src);
|
|
||||||
const sub = await ctx.proxiedFetcher.full<string>(abs, {
|
|
||||||
headers: {
|
|
||||||
Referer: finalPageUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
passPath = extractFirst(sub.body, PASS_MD5_PATTERNS);
|
|
||||||
if (passPath) break;
|
|
||||||
} catch {
|
|
||||||
// ignore iframe failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: scan external scripts referenced by the page for pass_md5 usage
|
// Extract thumbnail if available
|
||||||
if (!passPath) {
|
const pageResp = await ctx.proxiedFetcher.full(pageUrl, {
|
||||||
const scriptSrcs = Array.from(html.matchAll(/<script[^>]+src=["']([^"']+)["']/gi))
|
|
||||||
.slice(0, 8)
|
|
||||||
.map((m) => m[1]);
|
|
||||||
for (const src of scriptSrcs) {
|
|
||||||
try {
|
|
||||||
const abs = resolveAbsoluteUrl(finalPageUrl, src);
|
|
||||||
const sub = await ctx.proxiedFetcher.full<string>(abs, {
|
|
||||||
headers: {
|
|
||||||
Referer: finalPageUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
passPath = extractFirst(sub.body, PASS_MD5_PATTERNS);
|
|
||||||
if (passPath) break;
|
|
||||||
} catch {
|
|
||||||
// ignore script failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: if a /d/{id} page exists, try scanning it as some variants only expose pass_md5 there
|
|
||||||
if (!passPath && idMatch) {
|
|
||||||
try {
|
|
||||||
const downloadUrl = `${pageOrigin}/d/${idMatch[1]}`;
|
|
||||||
const sub = await ctx.proxiedFetcher.full<string>(downloadUrl, {
|
|
||||||
headers: { Referer: finalPageUrl },
|
|
||||||
});
|
|
||||||
passPath = extractFirst(sub.body, PASS_MD5_PATTERNS);
|
|
||||||
} catch {
|
|
||||||
// ignore download page failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!passPath) throw new Error('dood: pass_md5 path not found');
|
|
||||||
|
|
||||||
const passUrl = resolveAbsoluteUrl(pageOrigin, passPath.startsWith('/') ? passPath : `/${passPath}`);
|
|
||||||
const doodPage = await ctx.proxiedFetcher<string>(passUrl, {
|
|
||||||
headers: {
|
headers: {
|
||||||
Referer: finalPageUrl,
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
},
|
},
|
||||||
method: 'GET',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = extractFirst(html, TOKEN_PATTERNS);
|
const thumbnailMatch = pageResp.body.match(/thumbnails:\s*\{\s*vtt:\s*['"]([^'"]+)['"]/);
|
||||||
const rawUrl = (doodPage ?? '')
|
const thumbUrl = thumbnailMatch ? resolveAbsoluteUrl(pageUrl, thumbnailMatch[1]) : null;
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.replace(/^['"]|['"]$/g, '');
|
|
||||||
const normalizedUrl = (() => {
|
|
||||||
if (!rawUrl) return '';
|
|
||||||
if (rawUrl.startsWith('//')) return `https:${rawUrl}`;
|
|
||||||
if (rawUrl.startsWith('/')) return resolveAbsoluteUrl(pageOrigin, rawUrl);
|
|
||||||
if (rawUrl.startsWith('http')) return rawUrl;
|
|
||||||
return resolveAbsoluteUrl(pageOrigin, rawUrl);
|
|
||||||
})();
|
|
||||||
const finalDownloadUrl = token ? `${normalizedUrl}${nanoid()}?token=${token}&expiry=${Date.now()}` : normalizedUrl;
|
|
||||||
|
|
||||||
if (!finalDownloadUrl.startsWith('http')) throw new Error('Invalid URL');
|
const pageOrigin = new URL(pageUrl).origin;
|
||||||
|
|
||||||
const thumbUrl = (() => {
|
|
||||||
if (!thumbnailTrack) return null;
|
|
||||||
const t = thumbnailTrack[1];
|
|
||||||
if (t.startsWith('//')) return `https:${t}`;
|
|
||||||
if (t.startsWith('http')) return t;
|
|
||||||
return resolveAbsoluteUrl(origin, t);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stream: [
|
stream: [
|
||||||
{
|
{
|
||||||
id: 'primary',
|
id: 'primary',
|
||||||
type: 'file',
|
type: 'file',
|
||||||
flags: [], // I dont think it will work without headers
|
flags: [flags.CORS_ALLOWED],
|
||||||
captions: [],
|
captions: [],
|
||||||
qualities: {
|
qualities: {
|
||||||
unknown: {
|
unknown: {
|
||||||
type: 'mp4',
|
type: 'mp4',
|
||||||
url: finalDownloadUrl,
|
url: videoUrl,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
preferredHeaders: {
|
preferredHeaders: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue