MovixOpenSource/extension/Firefox/extractors.js
Movix 39bbba6611 Correction de l'extraction fsvid + vidzy sur extension, userscript, app android et server.py
ils changent à chaque foit j'en ai marre la j'ai fait un truc ca devrais s'adapter à tous les situations, on verra ils font quoi
2026-08-05 00:48:52 +02:00

1580 lines
54 KiB
JavaScript

/**
* Movix Extension - Direct M3U8 Extractors
* Replaces server.py extraction logic - runs entirely in the extension service worker.
* No VIP check needed since it runs locally.
*/
// ===== Configuration =====
const PROXY_BASE = 'https://proxiesembed.movix.fun';
// AES constants for SeekStreaming (embed4me)
const SEEKSTREAMING_AES_KEY_HEX = '6b69656d7469656e6d7561393131636131323334353637383930';
const SEEKSTREAMING_AES_KEY_RAW = 'kiemtienmua911ca';
const SEEKSTREAMING_AES_IV_RAW = '1234567890oiuytr';
// Cache: simple in-memory TTL cache
class TTLCache {
constructor(maxSize = 500, ttlMs = 7200000) {
this._cache = new Map();
this._maxSize = maxSize;
this._ttlMs = ttlMs;
}
get(key) {
const entry = this._cache.get(key);
if (!entry) return null;
if (Date.now() - entry.ts > this._ttlMs) {
this._cache.delete(key);
return null;
}
return entry.value;
}
set(key, value) {
if (this._cache.size >= this._maxSize) {
// Evict oldest
const firstKey = this._cache.keys().next().value;
this._cache.delete(firstKey);
}
this._cache.set(key, { value, ts: Date.now() });
}
}
// Caches per service
const caches = {
voe: new TTLCache(500, 7200000),
fsvid: new TTLCache(500, 60000),
vidzy: new TTLCache(500, 7200000),
vidmoly: new TTLCache(500, 7200000),
sibnet: new TTLCache(500, 7200000),
uqload: new TTLCache(500, 7200000),
doodstream: new TTLCache(500, 3600000),
seekstreaming: new TTLCache(500, 300000),
};
// ===== Utility Functions =====
const PACKER_SIGNATURE_PATTERN = new RegExp(
'ev' + 'al\\s*\\(\\s*function\\s*\\(\\s*p\\s*,\\s*a\\s*,\\s*c\\s*,\\s*k\\s*,\\s*e\\s*,\\s*d\\s*\\)'
);
const UQLOAD_ROOT_DOMAINS = Object.freeze([
'uqload.is',
'uqload.bz',
'uqload.cx',
'uqload.com',
'uqload.net',
'uqload.org',
'uqload.to',
'uqload.io',
'uqload.co',
]);
function getUqloadRootDomain(hostname) {
const host = String(hostname || '').toLowerCase().replace(/\.$/, '');
return UQLOAD_ROOT_DOMAINS.find(
root => host === root || host.endsWith(`.${root}`)
) || null;
}
function parseAllowedUqloadUrl(rawUrl) {
let parsed;
try {
parsed = new URL(String(rawUrl || '').trim());
} catch {
throw new Error('Invalid Uqload URL');
}
if (
parsed.protocol !== 'https:' ||
parsed.username ||
parsed.password ||
(parsed.port && parsed.port !== '443') ||
!getUqloadRootDomain(parsed.hostname)
) {
throw new Error('Invalid Uqload URL');
}
return parsed;
}
function normalizeUqloadEmbedUrl(rawUrl) {
const parsed = parseAllowedUqloadUrl(rawUrl);
const lastPart = parsed.pathname.split('/').filter(Boolean).pop() || '';
const videoId = lastPart.replace(/^embed-/i, '').replace(/\.html$/i, '');
if (!/^[a-z0-9_-]+$/i.test(videoId)) {
throw new Error('Invalid Uqload URL');
}
return `${parsed.origin}/embed-${videoId}.html`;
}
function getUqloadSiteOrigin(rawUrl) {
const parsed = parseAllowedUqloadUrl(rawUrl);
return `https://${getUqloadRootDomain(parsed.hostname)}`;
}
function md5Hash(str) {
// Simple hash for cache keys (not cryptographic, just for dedup)
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return 'h_' + Math.abs(hash).toString(36);
}
/**
* Follow redirects and extract final HTML
*/
async function fetchWithRedirects(url, headers, maxRedirects = 3, timeoutMs = 3000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
let currentUrl = url;
let html = '';
const resp = await fetch(currentUrl, { headers, signal: controller.signal, redirect: 'follow' });
html = await resp.text();
currentUrl = resp.url || currentUrl;
for (let i = 0; i < maxRedirects; i++) {
// Check if we have the content we need
if (/type=["']\s*application\/json\s*["']/.test(html) && html.includes('<script')) {
break;
}
let target = null;
const patterns = [
/window\.location\.href\s*=\s*['"]([^'"]+)['"]/,
/http-equiv=["']refresh["'][^>]*content=["'][^;]+;\s*url=([^"']+)/i,
/https?:\/\/[a-z0-9.-]+\/e\/[a-z0-9]+/i
];
for (const pat of patterns) {
const m = html.match(pat);
if (m) {
target = m[1] || m[0];
break;
}
}
if (!target) break;
try {
const absUrl = target.startsWith('http') ? target : new URL(target, currentUrl).href;
const r = await fetch(absUrl, {
headers: { ...headers, 'Referer': currentUrl },
signal: controller.signal,
redirect: 'follow'
});
html = await r.text();
currentUrl = r.url || absUrl;
} catch {
break;
}
}
return { html, finalUrl: currentUrl };
} finally {
clearTimeout(timer);
}
}
/**
* Dean Edwards Packer unpacker (same as background.js)
*/
function unpackPacker(p, a, c, k, e, d) {
e = function (c) {
return (c < a ? '' : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36));
};
if (!''.replace(/^/, String)) {
while (c--) {
d[e(c)] = k[c] || e(c);
}
k = [function (e) { return d[e] }];
e = function () { return '\\w+' };
c = 1;
}
while (c--) {
if (k[c]) {
p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
}
}
return p;
}
/**
* Deobfuscate packed eval(function(p,a,c,k,e,d) JavaScript
* Handles both standard Dean Edwards packer and simple packer (Fsvid)
* Properly handles escaped quotes inside the payload string
*/
function deobfuscatePackedScript(html) {
// Find the packer signature while tolerating formatter whitespace.
const markerMatch = PACKER_SIGNATURE_PATTERN.exec(html);
if (!markerMatch) return null;
const evalIdx = markerMatch.index;
// Find .split('|') after eval to locate end of packer call
let splitPos = html.indexOf(".split('|')", evalIdx);
if (splitPos === -1) splitPos = html.indexOf('.split("|")', evalIdx);
if (splitPos === -1) return null;
// Extract the relevant section
const section = html.substring(evalIdx, splitPos + 15);
// Robust regex: handles escaped quotes in payload using ((?:[^'\\]|\\.)*)
// Format: }('PAYLOAD',RADIX,COUNT,'KEYWORDS'.split('|')
const match = section.match(/\}\s*\(\s*'((?:[^'\\]|\\.)*)'\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*'((?:[^'\\]|\\.)*)'\s*\.split/s);
if (match) {
// Unescape the payload (JS string literal: \' → ' , \" → " , \\ → \)
const payload = match[1].replace(/\\'/g, "'").replace(/\\"/g, '"');
const radix = parseInt(match[2]);
const count = parseInt(match[3]);
const keywords = match[4].split('|');
if (radix < 2 || radix > 62 || count < 0 || count > 10000 || count > keywords.length) {
return null;
}
return unpackPacker(payload, radix, count, keywords, null, {});
}
// Try with double quotes
const match2 = section.match(/\}\s*\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*"((?:[^"\\]|\\.)*)"\s*\.split/s);
if (match2) {
// Unescape the payload (JS string literal: \' → ' , \" → " , \\ → \)
const payload = match2[1].replace(/\\"/g, '"').replace(/\\'/g, "'");
const radix = parseInt(match2[2]);
const count = parseInt(match2[3]);
const keywords = match2[4].split('|');
if (radix < 2 || radix > 62 || count < 0 || count > 10000 || count > keywords.length) {
return null;
}
return unpackPacker(payload, radix, count, keywords, null, {});
}
return null;
}
function extractM3u8UrlFromDecodedScript(script, embedUrl) {
const MAX_MEDIA_URL_LENGTH = 16384;
const MAX_XOR_PAYLOAD_LENGTH = 32768;
const normalizeCandidate = rawCandidate => {
const candidate = String(rawCandidate || '')
.replace(/\\\//g, '/')
.replace(/&amp;/gi, '&')
.trim()
.replace(/\\+$/, '');
if (
!candidate ||
candidate.length > MAX_MEDIA_URL_LENGTH ||
!candidate.toLowerCase().includes('.m3u8') ||
candidate.toLowerCase().includes('troll')
) {
return null;
}
let parsed;
try {
if (/^https:\/\//i.test(candidate)) {
parsed = new URL(candidate);
} else if (
(candidate.startsWith('/') && !candidate.startsWith('//')) ||
candidate.startsWith('./') ||
candidate.startsWith('../')
) {
parsed = new URL(candidate, embedUrl);
} else {
return null;
}
} catch {
return null;
}
if (
parsed.protocol !== 'https:' ||
!parsed.hostname ||
parsed.username ||
parsed.password ||
(parsed.port && parsed.port !== '443') ||
!parsed.pathname.toLowerCase().includes('.m3u8') ||
parsed.href.length > MAX_MEDIA_URL_LENGTH
) {
return null;
}
return parsed.href;
};
// Replay the decoder described by the player without evaluating remote JS.
// Seed, step, mask, variable names, payload, and reverse order all come
// from the current page, so provider-side parameter rotations keep working.
const rollingXorPattern =
/(?:var\s+)?([A-Za-z_$][\w$]*)\s*=\s*atob\(\s*[A-Za-z_$][\w$]*\s*\)[\s\S]{0,512}?for\s*\(\s*var\s+([A-Za-z_$][\w$]*)\s*=\s*0\s*;\s*\2\s*<\s*\1\.length\s*;\s*\2\+\+\s*\)\s*\{[\s\S]{0,512}?(?:var\s+)?([A-Za-z_$][\w$]*)\s*=\s*\(\s*([\s\S]{1,128}?)\s*\)\s*&\s*(0[xX][0-9a-fA-F]+|\d+)\s*;[\s\S]{0,512}?([A-Za-z_$][\w$]*)\s*\+=\s*String\.fromCharCode\(\s*\1\.charCodeAt\(\s*\2\s*\)\s*\^\s*\3\s*\)[\s\S]{0,256}?\}\s*return\s+\6(\s*\.split\(\s*["']["']\s*\)\s*\.reverse\(\s*\)\s*\.join\(\s*["']["']\s*\))?\s*\}\)\s*\(\s*["']([A-Za-z0-9+/_=-]{1,32768})["']\s*\)/g;
const numericLiteralPattern = '(?:0[xX][0-9a-fA-F]+|\\d+)';
const parseRollingParameters = (expression, indexName) => {
const indexToken = indexName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const normalized = expression.replace(/[\s()]/g, '');
const layouts = [
{
pattern: new RegExp(
`^(${numericLiteralPattern})\\+${indexToken}\\*(${numericLiteralPattern})$`,
),
seed: 1,
step: 2,
},
{
pattern: new RegExp(
`^(${numericLiteralPattern})\\+(${numericLiteralPattern})\\*${indexToken}$`,
),
seed: 1,
step: 2,
},
{
pattern: new RegExp(
`^${indexToken}\\*(${numericLiteralPattern})\\+(${numericLiteralPattern})$`,
),
seed: 2,
step: 1,
},
{
pattern: new RegExp(
`^(${numericLiteralPattern})\\*${indexToken}\\+(${numericLiteralPattern})$`,
),
seed: 2,
step: 1,
},
{
pattern: new RegExp(
`^(${numericLiteralPattern})\\+${indexToken}$`,
),
seed: 1,
fixedStep: 1,
},
{
pattern: new RegExp(
`^${indexToken}\\+(${numericLiteralPattern})$`,
),
seed: 1,
fixedStep: 1,
},
];
for (const layout of layouts) {
const match = normalized.match(layout.pattern);
if (!match) continue;
return {
seed: Number(match[layout.seed]),
step:
layout.fixedStep === undefined
? Number(match[layout.step])
: layout.fixedStep,
};
}
return null;
};
const reverseBeforeXorPattern =
/(?:var\s+)?(?<encoded>[A-Za-z_$][\w$]*)\s*=\s*atob\(\s*[A-Za-z_$][\w$]*\s*\)\s*,\s*(?<bytes>[A-Za-z_$][\w$]*)\s*=\s*\k<encoded>\.split\(\s*["']["']\s*\)\.reverse\(\s*\)\.join\(\s*["']["']\s*\)\s*,\s*(?<output>[A-Za-z_$][\w$]*)\s*=\s*["']["']\s*;[\s\S]{0,256}?for\s*\(\s*var\s+(?<index>[A-Za-z_$][\w$]*)\s*=\s*0\s*;\s*\k<index>\s*<\s*\k<bytes>\.length\s*;\s*\k<index>\+\+\s*\)\s*\{[\s\S]{0,256}?(?:var\s+)?(?<key>[A-Za-z_$][\w$]*)\s*=\s*\(\s*(?<keyExpression>[\s\S]{1,128}?)\s*\)\s*&\s*(?<mask>0[xX][0-9a-fA-F]+|\d+)\s*;[\s\S]{0,256}?\k<output>\s*\+=\s*String\.fromCharCode\(\s*\k<bytes>\.charCodeAt\(\s*\k<index>\s*\)\s*\^\s*\k<key>\s*\)[\s\S]{0,128}?\}\s*return\s+\k<output>\s*\}\)\s*\(\s*["'](?<payload>[A-Za-z0-9+/_=-]{1,32768})["']\s*\)/g;
for (const match of String(script || '').matchAll(reverseBeforeXorPattern)) {
const groups = match.groups || {};
const parameters = parseRollingParameters(groups.keyExpression || '', groups.index || '');
const mask = Number(groups.mask);
if (
!parameters ||
!Number.isSafeInteger(parameters.seed) ||
parameters.seed < 0 ||
parameters.seed > 0xffffffff ||
!Number.isSafeInteger(parameters.step) ||
parameters.step < 0 ||
parameters.step > 0xffffffff ||
!Number.isSafeInteger(mask) ||
mask < 0 ||
mask > 255
) continue;
const payload = groups.payload || '';
const normalizedPayload = payload.replace(/-/g, '+').replace(/_/g, '/');
if (!payload || normalizedPayload.length % 4 === 1) continue;
const paddedPayload = normalizedPayload.padEnd(
normalizedPayload.length + ((4 - (normalizedPayload.length % 4)) % 4),
'=',
);
try {
const reversed = Array.from(atob(paddedPayload)).reverse();
const decodedBytes = Uint8Array.from(
reversed,
(character, index) =>
character.charCodeAt(0) ^
((parameters.seed + index * parameters.step) & mask),
);
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(decodedBytes);
const candidate = normalizeCandidate(decoded);
if (candidate) return candidate;
} catch {}
}
for (const match of String(script || '').matchAll(rollingXorPattern)) {
const parameters = parseRollingParameters(match[4], match[2]);
const mask = Number(match[5]);
if (
!parameters ||
!Number.isSafeInteger(parameters.seed) ||
parameters.seed < 0 ||
parameters.seed > 0xffffffff ||
!Number.isSafeInteger(parameters.step) ||
parameters.step < 0 ||
parameters.step > 0xffffffff ||
!Number.isSafeInteger(mask) ||
mask < 0 ||
mask > 255
) {
continue;
}
const payload = match[8];
if (payload.length > MAX_XOR_PAYLOAD_LENGTH) continue;
const normalizedPayload = payload.replace(/-/g, '+').replace(/_/g, '/');
if (normalizedPayload.length % 4 === 1) continue;
const paddedPayload = normalizedPayload.padEnd(
normalizedPayload.length + ((4 - (normalizedPayload.length % 4)) % 4),
'=',
);
try {
const encrypted = atob(paddedPayload);
const decodedBytes = Uint8Array.from(
encrypted,
(character, index) =>
character.charCodeAt(0) ^
((parameters.seed + index * parameters.step) & mask),
);
if (match[7]) decodedBytes.reverse();
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(decodedBytes);
const candidate = normalizeCandidate(decoded);
if (candidate) return candidate;
} catch {
// Try the next decoder or one of the legacy formats below.
}
}
const xorPattern =
/var\s+[A-Za-z_$][\w$]*\s*=\s*\[([0-9,\s]+)\]\s*,\s*[A-Za-z_$][\w$]*\s*=\s*atob\(\s*[A-Za-z_$][\w$]*\s*\)[\s\S]{0,2000}?\}\)\s*\(\s*["']([A-Za-z0-9+/_=-]{1,32768})["']\s*\)/g;
for (const match of String(script || '').matchAll(xorPattern)) {
const key = match[1].split(',').map(value => Number(value.trim()));
if (
key.length < 1 ||
key.length > 64 ||
key.some(value => !Number.isInteger(value) || value < 0 || value > 255)
) {
continue;
}
const payload = match[2];
if (payload.length > MAX_XOR_PAYLOAD_LENGTH) continue;
const normalizedPayload = payload.replace(/-/g, '+').replace(/_/g, '/');
if (normalizedPayload.length % 4 === 1) continue;
const paddedPayload = normalizedPayload.padEnd(
normalizedPayload.length + ((4 - (normalizedPayload.length % 4)) % 4),
'=',
);
try {
const encrypted = atob(paddedPayload);
const decodedBytes = Uint8Array.from(
encrypted,
(character, index) => character.charCodeAt(0) ^ key[index % key.length],
);
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(decodedBytes);
const candidate = normalizeCandidate(decoded);
if (candidate) return candidate;
} catch {
// Try the legacy formats below.
}
}
const legacyPatterns = [
/sources:\s*\[\s*\{[^}]*?src:\s*["']([^"']+\.m3u8[^"']*)["']/,
/src:\s*["']([^"']+\.m3u8[^"']*)["']/,
/file:\s*["']([^"']+\.m3u8[^"']*)["']/,
/sources:\s*\[\s*\{[^}]*?["']([^"']+\.m3u8[^"']*)["']/,
/["']([^"']*\.m3u8[^"']*)["']/,
];
for (const pattern of legacyPatterns) {
const match = String(script || '').match(pattern);
const candidate = match ? normalizeCandidate(match[1]) : null;
if (candidate) return candidate;
}
return null;
}
function extractFsvidVidzyM3u8FromHtml(html, embedUrl) {
const direct = extractM3u8UrlFromDecodedScript(html, embedUrl);
if (direct) return direct;
const decoded = deobfuscatePackedScript(html);
return decoded ? extractM3u8UrlFromDecodedScript(decoded, embedUrl) : null;
}
function normalizeFsvidVidzyEmbedUrl(rawUrl, provider) {
if (provider !== 'fsvid' && provider !== 'vidzy') return null;
try {
const parsed = new URL(String(rawUrl || '').trim());
if (
parsed.protocol !== 'https:' ||
parsed.username ||
parsed.password ||
(parsed.port && parsed.port !== '443') ||
!parsed.hostname ||
!/\/embed(?:[-/])/i.test(parsed.pathname)
) return null;
return parsed.href;
} catch {
return null;
}
}
function normalizeFsvidVidzyMediaUrl(rawCandidate, embedUrl, provider) {
if (!normalizeFsvidVidzyEmbedUrl(embedUrl, provider)) return null;
const candidate = String(rawCandidate || '')
.replace(/\\\//g, '/')
.replace(/&amp;/gi, '&')
.trim()
.replace(/\\+$/, '');
if (
!candidate ||
candidate.length > 16384 ||
!candidate.toLowerCase().includes('.m3u8') ||
candidate.toLowerCase().includes('troll')
) return null;
try {
const parsed = /^https:\/\//i.test(candidate)
? new URL(candidate)
: new URL(candidate, embedUrl);
if (
parsed.protocol !== 'https:' ||
parsed.username ||
parsed.password ||
(parsed.port && parsed.port !== '443') ||
!parsed.hostname ||
!parsed.pathname.toLowerCase().includes('.m3u8') ||
parsed.href.length > 16384
) return null;
return parsed.href;
} catch {
return null;
}
}
async function extractFsvidVidzyM3u8(html, embedUrl, provider) {
const staticCandidate = normalizeFsvidVidzyMediaUrl(
extractFsvidVidzyM3u8FromHtml(html, embedUrl),
embedUrl,
provider,
);
if (staticCandidate) return staticCandidate;
if (!globalThis.MovixQuickJS?.extractPlayerM3u8) return null;
try {
const dynamicCandidate = await globalThis.MovixQuickJS.extractPlayerM3u8(
html,
embedUrl,
provider,
);
return normalizeFsvidVidzyMediaUrl(dynamicCandidate, embedUrl, provider);
} catch (error) {
console.warn(`[EXT-${provider.toUpperCase()}] QuickJS fallback failed:`, error);
return null;
}
}
function extractUqloadMediaUrl(html) {
const candidates = [];
const collect = value => {
const normalized = String(value || '').replace(/\\\//g, '/');
for (const match of normalized.matchAll(/https:\/\/[^\s"'\\<>]+/gi)) {
const candidate = match[0].replace(/[),;]+$/, '');
try {
parseAllowedUqloadUrl(candidate);
candidates.push(candidate);
} catch {
// Ignore URLs outside the Uqload domain allowlist.
}
}
};
collect(html);
const decoded = deobfuscatePackedScript(html);
if (decoded) collect(decoded);
return (
candidates.find(url => /\/master\.m3u8(?:[?#]|$)/i.test(url)) ||
candidates.find(url => /\.m3u8(?:[?#]|$)/i.test(url)) ||
candidates.find(url => /\/v\.mp4(?:[?#]|$)/i.test(url)) ||
null
);
}
/**
* Extract JSON from VOE HTML
*/
function extractJsonFromHtml(html) {
// Pattern 1: script type=application/json
let match = html.match(/<script[^>]*type=["']?\s*application\/json\s*["']?[^>]*>\s*([\s\S]*?)\s*<\/script>/i);
if (match) {
try {
const parsed = JSON.parse(match[1].trim());
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'string') {
return parsed;
}
} catch { }
}
// Pattern 2: Large string array
match = html.match(/\[\s*"(?:[^"\\]|\\.){100,}"\s*\]/);
if (match) {
try {
return JSON.parse(match[0]);
} catch { }
}
return null;
}
/**
* ROT13 implementation
*/
function rot13(str) {
return str.replace(/[a-zA-Z]/g, function (c) {
const base = c <= 'Z' ? 65 : 97;
return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base);
});
}
/**
* Decrypt VOE data
*/
function decryptVoeData(encrypted) {
try {
let step1 = rot13(encrypted);
const symbols = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
for (const sym of symbols) {
step1 = step1.split(sym).join('');
}
// Base64 decode
let step2;
try {
step2 = atob(step1);
} catch (e) {
console.error('[EXT-VOE] atob step1 failed:', e.name, e.message);
console.log('[EXT-VOE] step1 (first 100 chars):', step1.substring(0, 100));
return null;
}
// Shift chars by -3 and reverse
const step3 = [...step2].map(c => String.fromCharCode(c.charCodeAt(0) - 3)).reverse().join('');
// Base64 decode again
let step4;
try {
step4 = atob(step3);
} catch (e) {
console.error('[EXT-VOE] atob step3 failed:', e.name, e.message);
return null;
}
return JSON.parse(step4);
} catch (e) {
console.error('[EXT-VOE] Decryption error:', e.name || 'Unknown', e.message || String(e));
return null;
}
}
/**
* Convert hex to Uint8Array
*/
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}
/**
* AES-CBC decryption for SeekStreaming using Web Crypto API
*/
async function decryptAesCbc(hexData, keyStr, ivStr) {
try {
const cleanHex = hexData.trim().replace(/"/g, '');
const data = hexToBytes(cleanHex);
const keyBytes = new TextEncoder().encode(keyStr);
const ivBytes = new TextEncoder().encode(ivStr);
const cryptoKey = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'AES-CBC' }, false, ['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv: ivBytes },
cryptoKey,
data
);
return new TextDecoder().decode(decrypted);
} catch (e) {
console.error('[EXT-SEEKSTREAMING] AES decryption error:', e);
return null;
}
}
// ===== Extraction Functions =====
/**
* Extract M3U8 from VOE.SX embed
*/
async function extractVoe(voeUrl) {
console.log(`[EXT-VOE] Extracting from: ${voeUrl}`);
const cacheKey = md5Hash(voeUrl);
const cached = caches.voe.get(cacheKey);
if (cached) return { ...cached, fromCache: true };
try {
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
'Referer': 'https://voe.sx/',
};
const { html, finalUrl } = await fetchWithRedirects(voeUrl, headers, 3, 3000);
console.log(`[EXT-VOE] Fetched ${html.length} chars, final URL: ${finalUrl}`);
const jsonContent = extractJsonFromHtml(html);
if (!jsonContent || !Array.isArray(jsonContent) || jsonContent.length === 0) {
console.error('[EXT-VOE] JSON content not found in HTML');
console.log('[EXT-VOE] HTML snippet:', html.substring(0, 500));
return { success: false, error: 'VOE: JSON content not found' };
}
console.log(`[EXT-VOE] Found JSON array with ${jsonContent.length} element(s), first element length: ${jsonContent[0].length}`);
const decrypted = decryptVoeData(jsonContent[0]);
if (!decrypted) {
return { success: false, error: 'VOE: Decryption failed' };
}
console.log('[EXT-VOE] Decrypted keys:', Object.keys(decrypted));
const sourceUrl = decrypted.source || '';
if (!sourceUrl.includes('.m3u8')) {
console.error('[EXT-VOE] No M3U8 in source:', sourceUrl.substring(0, 100));
return { success: false, error: 'VOE: No M3U8 source found' };
}
console.log(`[EXT-VOE] M3U8 found: ${sourceUrl.substring(0, 80)}...`);
// Return the direct URL - extension handles CORS via DNR
const result = { hlsUrl: sourceUrl, success: true, source: 'voe' };
caches.voe.set(cacheKey, result);
return result;
} catch (e) {
const errName = e.name || 'Unknown';
const errMsg = e.message || String(e);
if (errName === 'AbortError') {
console.error('[EXT-VOE] Fetch timeout (12s)');
return { success: false, error: 'VOE: Fetch timeout' };
}
console.error(`[EXT-VOE] Error [${errName}]: ${errMsg}`);
return { success: false, error: `VOE: ${errName} - ${errMsg}` };
}
}
/**
* Extract M3U8 from Fsvid embed
* Fsvid uses a simple Dean Edwards packer with video.js sources
*/
async function extractFsvid(fsvidUrl) {
console.log(`[EXT-FSVID] Extracting from: ${fsvidUrl}`);
const embedUrl = normalizeFsvidVidzyEmbedUrl(fsvidUrl, 'fsvid');
if (!embedUrl) {
console.warn('[EXT-FSVID] Invalid URL, skipping');
return { success: false, error: 'Fsvid: Invalid URL' };
}
const cacheKey = md5Hash(embedUrl);
const cached = caches.fsvid.get(cacheKey);
const cachedMediaUrl = normalizeFsvidVidzyMediaUrl(cached?.m3u8Url, embedUrl, 'fsvid');
if (cached && cachedMediaUrl) {
console.log('[EXT-FSVID] Cache hit');
return { ...cached, m3u8Url: cachedMediaUrl, fromCache: true };
}
try {
// Fsvid requires referer from one of the allowed streaming sites
// (not from fsvid.lol itself - it returns "Veuillez utiliser une URL valide" otherwise)
const FSVID_REFERERS = ['https://fsmirror46.lol/', 'https://fs12.lol/', 'https://french-stream.one/'];
const headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
'referer': FSVID_REFERERS[0],
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
};
// Fetch with timeout using AbortController
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
let resp;
try {
resp = await fetch(embedUrl, { headers, signal: controller.signal });
} finally {
clearTimeout(timer);
}
console.log(`[EXT-FSVID] Fetch status: ${resp.status}, ok: ${resp.ok}`);
if (!resp.ok) {
console.error(`[EXT-FSVID] HTTP error ${resp.status}`);
return { success: false, error: `Fsvid: HTTP ${resp.status}` };
}
const html = await resp.text();
console.log(`[EXT-FSVID] HTML length: ${html.length}`);
const m3u8Url = await extractFsvidVidzyM3u8(html, embedUrl, 'fsvid');
if (!m3u8Url) {
console.error('[EXT-FSVID] No safe M3U8 URL found in page');
return { success: false, error: 'Fsvid: M3U8 not found in page' };
}
console.log(`[EXT-FSVID] Final M3U8 URL: ${m3u8Url}`);
const result = { m3u8Url, success: true, source: 'fsvid' };
caches.fsvid.set(cacheKey, result);
return result;
} catch (e) {
if (e.name === 'AbortError') {
console.error('[EXT-FSVID] Fetch timeout (10s)');
return { success: false, error: 'Fsvid: Fetch timeout' };
}
console.error('[EXT-FSVID] Error:', e);
return { success: false, error: e.message || 'Fsvid extraction failed' };
}
}
/**
* Extract M3U8 from Vidzy embed
*/
async function extractVidzy(vidzyUrl) {
console.log(`[EXT-VIDZY] Extracting from: ${vidzyUrl}`);
const embedUrl = normalizeFsvidVidzyEmbedUrl(vidzyUrl, 'vidzy');
if (!embedUrl) return { success: false, error: 'Vidzy: Invalid URL' };
const cacheKey = md5Hash(embedUrl);
const cached = caches.vidzy.get(cacheKey);
const cachedMediaUrl = normalizeFsvidVidzyMediaUrl(cached?.m3u8Url, embedUrl, 'vidzy');
if (cached && cachedMediaUrl) {
return { ...cached, m3u8Url: cachedMediaUrl, fromCache: true };
}
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
const headers = {
'accept': 'text/html,*/*',
'referer': 'https://vidzy.org/',
'user-agent': 'Mozilla/5.0 Chrome/140.0.0.0'
};
let resp;
try {
resp = await fetch(embedUrl, { headers, signal: controller.signal });
} finally {
clearTimeout(timer);
}
if (!resp.ok) return { success: false, error: `Vidzy: HTTP ${resp.status}` };
const html = await resp.text();
const m3u8Url = await extractFsvidVidzyM3u8(html, embedUrl, 'vidzy');
if (!m3u8Url) return { success: false, error: 'Vidzy: M3U8 not found in page' };
const result = { m3u8Url, success: true, source: 'vidzy' };
caches.vidzy.set(cacheKey, result);
return result;
} catch (e) {
console.error('[EXT-VIDZY] Error:', e);
return { success: false, error: e.message || 'Vidzy extraction failed' };
}
}
/**
* Extract M3U8 from Vidmoly embed
*/
async function extractVidmoly(vidmolyUrl) {
console.log(`[EXT-VIDMOLY] Extracting from: ${vidmolyUrl}`);
const cacheKey = md5Hash(vidmolyUrl);
const cached = caches.vidmoly.get(cacheKey);
if (cached) return { ...cached, fromCache: true };
try {
const headers = {
'accept': 'text/html,*/*',
'referer': 'https://voirdrama.to/',
'user-agent': 'Mozilla/5.0 Chrome/143.0.0.0'
};
const { html } = await fetchWithRedirects(vidmolyUrl, headers, 3, 3000);
// Try multiple patterns
const patterns = [
/sources:\s*\[\s*\{\s*file:\s*["']([^"']+)["']/i,
/file:\s*["']([^"']+\.m3u8[^"']*)["']/i,
/https?:\/\/[^\s"'<>]+\.m3u8[^\s"'<>]*/i
];
let sourceUrl = null;
for (const pat of patterns) {
const m = html.match(pat);
if (m) {
sourceUrl = m[1] || m[0];
break;
}
}
if (!sourceUrl) return { success: false, error: 'Vidmoly: M3U8 not found' };
const result = { m3u8Url: sourceUrl, success: true, source: 'vidmoly' };
caches.vidmoly.set(cacheKey, result);
return result;
} catch (e) {
console.error('[EXT-VIDMOLY] Error:', e);
return { success: false, error: e.message || 'Vidmoly extraction failed' };
}
}
/**
* Extract MP4 from Sibnet embed
*/
async function extractSibnet(sibnetUrl) {
console.log(`[EXT-SIBNET] Extracting from: ${sibnetUrl}`);
const cacheKey = md5Hash(sibnetUrl);
const cached = caches.sibnet.get(cacheKey);
if (cached) return { ...cached, fromCache: true };
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
const headers = {
'accept': 'text/html,*/*',
'referer': 'https://video.sibnet.ru/',
'user-agent': 'Mozilla/5.0 Chrome/140.0.0.0'
};
const resp = await fetch(sibnetUrl, { headers, signal: controller.signal });
if (!resp.ok) { clearTimeout(timer); return { success: false, error: `Sibnet: HTTP ${resp.status}` }; }
const html = await resp.text();
// Find mp4 URL in player.src pattern
const mp4Match = html.match(/player\.src\(\[\{\s*src:\s*["']([^"']+\.mp4[^"']*)["']/);
if (!mp4Match) { clearTimeout(timer); return { success: false, error: 'Sibnet: MP4 not found' }; }
let mp4Url = mp4Match[1];
if (!mp4Url.startsWith('http')) {
mp4Url = `https://video.sibnet.ru${mp4Url}`;
}
clearTimeout(timer);
// Follow the 302 redirect to get the final CDN URL (e.g. dv97.sibnet.ru)
// so the page player can fetch it directly without cross-origin redirect issues.
try {
const mp4Resp = await fetch(mp4Url, {
headers: {
'accept': '*/*',
// Only resolve the redirect chain — don't download the video
'range': 'bytes=0-0',
'referer': 'https://video.sibnet.ru/',
'user-agent': 'Mozilla/5.0 Chrome/145.0.0.0'
},
redirect: 'follow'
});
// response.url contains the final URL after all redirects
if (mp4Resp.url && mp4Resp.url !== mp4Url) {
mp4Url = mp4Resp.url;
console.log(`[EXT-SIBNET] Followed redirect to: ${mp4Url}`);
}
// Stop any body download (in case the server ignored the Range header)
try { await mp4Resp.body?.cancel(); } catch { /* already closed */ }
} catch (e) {
console.warn('[EXT-SIBNET] Could not follow redirect, using original URL:', e);
}
const result = { m3u8Url: mp4Url, success: true, source: 'sibnet' };
caches.sibnet.set(cacheKey, result);
return result;
} catch (e) {
console.error('[EXT-SIBNET] Error:', e);
return { success: false, error: e.message || 'Sibnet extraction failed' };
}
}
/**
* Extract HLS or MP4 from Uqload embed
*/
async function extractUqload(uqloadUrl) {
console.log(`[EXT-UQLOAD] Extracting from: ${uqloadUrl}`);
const cacheKey = md5Hash(uqloadUrl);
const cached = caches.uqload.get(cacheKey);
if (cached) return { ...cached, fromCache: true };
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const fullUrl = normalizeUqloadEmbedUrl(uqloadUrl);
const siteOrigin = getUqloadSiteOrigin(fullUrl);
const headers = {
'User-Agent': 'Mozilla/5.0 Chrome/91.0.0.0',
'Accept': 'text/html,*/*',
'Referer': `${siteOrigin}/`,
'Origin': siteOrigin,
};
// Try embed and non-embed versions without leaving the validated host.
const urls = [fullUrl, fullUrl.replace('/embed-', '/')];
let html = null;
for (const url of urls) {
try {
const resp = await fetch(url, { headers, signal: controller.signal });
if (resp.ok) {
html = await resp.text();
break;
}
} catch {
continue;
}
}
if (!html) return { success: false, error: 'Uqload: Could not fetch page' };
if (html.includes('File was deleted')) return { success: false, error: 'Uqload: File was deleted' };
const videoUrl = extractUqloadMediaUrl(html);
if (!videoUrl) return { success: false, error: 'Uqload: video URL not found' };
const result = { m3u8Url: videoUrl, success: true, source: 'uqload' };
caches.uqload.set(cacheKey, result);
return result;
} catch (e) {
console.error('[EXT-UQLOAD] Error:', e);
return { success: false, error: e.message || 'Uqload extraction failed' };
} finally {
clearTimeout(timer);
}
}
/**
* Extract video URL from DoodStream embed
*/
async function extractDoodStream(doodUrl) {
console.log(`[EXT-DOODSTREAM] Extracting from: ${doodUrl}`);
const cacheKey = md5Hash(doodUrl);
const cached = caches.doodstream.get(cacheKey);
if (cached) return { ...cached, fromCache: true };
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://d0000d.com/',
};
// Step 1: Fetch the embed page
const resp = await fetch(doodUrl, { headers, redirect: 'follow', signal: controller.signal });
if (!resp.ok) { clearTimeout(timer); return { success: false, error: `DoodStream: HTTP ${resp.status}` }; }
const html = await resp.text();
// Step 2: Extract pass_md5 URL and token
const passMatch = html.match(/\/pass_md5\/[\w-]+\/(?<token>[\w-]+)/);
if (!passMatch) {
clearTimeout(timer);
return {
success: false,
error: 'DoodStream: File was deleted',
reason: 'deleted',
};
}
const parsedUrl = new URL(doodUrl);
const domain = `${parsedUrl.protocol}//${parsedUrl.host}`;
const passMd5Url = passMatch[0];
const token = passMatch.groups?.token || passMatch[0].split('/').pop();
// Step 3: Call pass_md5 endpoint
const passHeaders = {
'Referer': domain,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
};
const passResp = await fetch(`${domain}${passMd5Url}`, { headers: passHeaders, signal: controller.signal });
const baseUrl = await passResp.text();
clearTimeout(timer);
// Step 4: Build final video URL
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomStr = '';
for (let i = 0; i < 10; i++) {
randomStr += chars.charAt(Math.floor(Math.random() * chars.length));
}
const expiry = Date.now();
const videoUrl = `${baseUrl}${randomStr}?token=${token}&expiry=${expiry}`;
const result = { m3u8Url: videoUrl, success: true, source: 'doodstream' };
caches.doodstream.set(cacheKey, result);
return result;
} catch (e) {
console.error('[EXT-DOODSTREAM] Error:', e);
return { success: false, error: e.message || 'DoodStream extraction failed' };
}
}
/**
* Extract HLS URL from a SeekStreaming root-fragment embed
*/
const SEEKSTREAMING_USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36';
function safelyDecodeSeekStreamingUrl(value) {
let decoded = String(value || '').trim();
for (let index = 0; index < 2; index += 1) {
try {
const next = decodeURIComponent(decoded);
if (next === decoded) break;
decoded = next;
} catch {
return null;
}
}
return decoded;
}
function parseSeekStreamingEmbedUrl(input) {
const decoded = safelyDecodeSeekStreamingUrl(input);
if (!decoded || /[\u0000-\u001f\u007f]/.test(decoded)) return null;
try {
const url = new URL(decoded);
if (
!['http:', 'https:'].includes(url.protocol) ||
url.username ||
url.password ||
url.pathname !== '/' ||
url.search
) return null;
const videoId = url.hash.slice(1);
if (!/^[A-Za-z0-9_-]{1,128}$/.test(videoId)) return null;
const origin = url.origin;
return {
embedUrl: `${origin}/#${videoId}`,
hostname: url.hostname.toLowerCase(),
videoId,
origin,
referer: `${origin}/`,
cacheKey: `${url.hostname.toLowerCase()}:${videoId}`,
};
} catch {
return null;
}
}
function selectSeekStreamingPlaybackSource(data) {
if (!data || typeof data !== 'object') return null;
for (const kind of ['source', 'master', 'masterUrl']) {
const rawUrl = data[kind];
if (typeof rawUrl !== 'string') continue;
try {
const url = new URL(rawUrl);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) continue;
return { kind, url: url.href };
} catch {
continue;
}
}
return null;
}
function selectSeekStreamingPlaybackSources(data) {
if (!data || typeof data !== 'object') return [];
const candidates = [];
const seen = new Set();
const add = (kind, rawUrl) => {
if (typeof rawUrl !== 'string') return;
try {
const url = new URL(rawUrl);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return;
if (seen.has(url.href)) return;
seen.add(url.href);
candidates.push({ kind, url: url.href });
} catch {
// Ignore invalid playback candidates.
}
};
add('cfNative', data.cfNative);
const source = selectSeekStreamingPlaybackSource(data);
if (source) add('source', source.url);
return candidates;
}
function getSeekStreamingRequestHeaders(embedUrl) {
const parsed = typeof embedUrl === 'object' && embedUrl?.origin
? embedUrl
: parseSeekStreamingEmbedUrl(embedUrl);
if (!parsed) return null;
return { Origin: parsed.origin, Referer: parsed.referer };
}
function getSeekStreamingPlaybackRulePattern(rawUrl) {
try {
const parsed = new URL(rawUrl);
if (!['http:', 'https:'].includes(parsed.protocol)) return null;
const pathSegments = parsed.pathname.split('/').filter(Boolean);
const directorySegments = parsed.pathname.endsWith('/')
? pathSegments
: pathSegments.slice(0, -1);
const mediaTypeIndex = directorySegments.length - 2;
const mediaType = directorySegments[mediaTypeIndex];
const videoId = directorySegments[mediaTypeIndex + 1];
const v4Index = directorySegments.indexOf('v4');
if (
v4Index !== -1 &&
mediaTypeIndex > v4Index &&
/^[a-z0-9_-]+$/i.test(mediaType || '') &&
/^[a-z0-9_-]+$/i.test(videoId || '')
) {
return `*://*/${mediaType}/${videoId}/*`;
}
const slash = parsed.pathname.lastIndexOf('/');
const directory = slash >= 0 ? parsed.pathname.slice(0, slash + 1) : '/';
return `*://${parsed.host}${directory}*`;
} catch {
return null;
}
}
async function extractSeekStreaming(seekUrl) {
const parsed = parseSeekStreamingEmbedUrl(seekUrl);
if (!parsed) {
return { success: false, error: 'SeekStreaming: invalid embed URL' };
}
const cacheKey = md5Hash(parsed.cacheKey);
const cached = caches.seekstreaming.get(cacheKey);
if (cached) return { ...cached, fromCache: true };
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const apiUrl = new URL('/api/v1/video', parsed.origin);
apiUrl.search = new URLSearchParams({
id: parsed.videoId,
w: '1920',
h: '1080',
r: '',
}).toString();
const headers = {
'User-Agent': SEEKSTREAMING_USER_AGENT,
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
...getSeekStreamingRequestHeaders(parsed),
};
const response = await fetch(apiUrl.href, { headers, signal: controller.signal });
if (!response.ok) {
return { success: false, error: `SeekStreaming: API HTTP ${response.status}` };
}
const decryptedRaw = await decryptAesCbc(
await response.text(),
SEEKSTREAMING_AES_KEY_RAW,
SEEKSTREAMING_AES_IV_RAW,
);
const selected = selectSeekStreamingPlaybackSources(JSON.parse(decryptedRaw));
if (selected.length === 0) {
return { success: false, error: 'SeekStreaming: no direct source found' };
}
const result = {
hlsUrl: selected[0].url,
hlsCandidates: selected,
success: true,
source: 'seekstreaming',
origin: parsed.origin,
referer: parsed.referer,
};
caches.seekstreaming.set(cacheKey, result);
return result;
} catch (error) {
return {
success: false,
error: error?.name === 'AbortError'
? 'SeekStreaming: upstream timeout'
: 'SeekStreaming extraction failed',
};
} finally {
clearTimeout(timer);
}
}
// ===== Detection =====
const EMBED_PATTERNS = {
voe: url => {
const voeDomains = ['voe.sx', 'voe.st', 'voe.gx', 'ralphysuccessfull.org', 'claudiosepulchral.org',
'anthonysaline.org', 'auraleanline.org', 'letsupload.io'];
return voeDomains.some(d => url.toLowerCase().includes(d));
},
fsvid: url => url.toLowerCase().includes('fsvid'),
vidzy: url => url.toLowerCase().includes('vidzy'),
vidmoly: url => url.toLowerCase().includes('vidmoly'),
sibnet: url => url.toLowerCase().includes('sibnet.ru'),
uqload: url => /uqload\.(is|cx|com|bz|net|org|to|io|co)/i.test(url),
doodstream: url => {
const lower = url.toLowerCase();
return lower.includes('d0000d.com') || lower.includes('doodstream.com') || lower.includes('dood.')
|| lower.includes('myvidplay.com') || lower.includes('dsvplay.com') || lower.includes('doply.net');
},
seekstreaming: url => Boolean(parseSeekStreamingEmbedUrl(url)),
};
const EXTRACT_FN = {
voe: extractVoe,
fsvid: extractFsvid,
vidzy: extractVidzy,
vidmoly: extractVidmoly,
sibnet: extractSibnet,
uqload: extractUqload,
doodstream: extractDoodStream,
seekstreaming: extractSeekStreaming,
};
const PRIORITIES = {
voe: 1, fsvid: 1, vidzy: 1, vidmoly: 1, sibnet: 1, seekstreaming: 1,
uqload: 2, doodstream: 2,
};
/**
* Detect which embed type a URL belongs to
*/
function detectEmbedType(url) {
for (const [type, detector] of Object.entries(EMBED_PATTERNS)) {
if (detector(url)) return type;
}
return null;
}
/**
* Detect all supported embeds from a list of sources
*/
function detectSupportedEmbeds(sources) {
const detected = [];
for (const source of sources) {
const url = typeof source === 'string' ? source : (source.link || source.url || '');
if (!url) continue;
const type = detectEmbedType(url);
if (type) {
detected.push({
type,
url,
priority: PRIORITIES[type] || 3,
});
}
}
return detected.sort((a, b) => a.priority - b.priority);
}
/**
* Extract a single embed URL - main dispatcher
*/
async function extractSingle(type, url) {
const fn = EXTRACT_FN[type];
if (!fn) return { success: false, error: `Unknown embed type: ${type}` };
return await fn(url);
}
/**
* Extract all embeds in parallel from a list of sources
* Returns all results as they complete
*/
async function extractAll(sources) {
const detected = detectSupportedEmbeds(sources);
if (detected.length === 0) return [];
console.log(`[EXT-EXTRACT] Launching ${detected.length} extractions in parallel:`, detected.map(e => e.type));
const promises = detected.map(async (embed) => {
const startTime = Date.now();
try {
const result = await extractSingle(embed.type, embed.url);
return {
type: embed.type,
url: embed.url,
...result,
duration: Date.now() - startTime,
};
} catch (e) {
return {
type: embed.type,
url: embed.url,
success: false,
error: e.message || 'Unknown error',
duration: Date.now() - startTime,
};
}
});
const results = await Promise.allSettled(promises);
const finalResults = results.map(r => r.status === 'fulfilled' ? r.value : { success: false, error: 'Promise rejected' });
const successCount = finalResults.filter(r => r.success).length;
console.log(`[EXT-EXTRACT] Done: ${successCount}/${finalResults.length} successful`);
return finalResults;
}
// ===== DNR header helpers for extracted URLs =====
/**
* Set up DNR headers for a service's extracted URL so the browser player can use it
*/
async function setupHeadersForService(type, url, referer) {
// Fsvid needs different referers:
// - Embed page (fsvid.lol/embed-xxx) → fsmirror46.lol (required by fsvid to serve content)
// - CDN/M3U8 (s1.fsvid.lol, s2.fsvid.lol, etc.) → fsvid.lol (required by CDN)
let fsvidHeaders;
let uqloadHeaders;
if (type === 'fsvid' && url) {
try {
const hostname = new URL(url).hostname;
// CDN subdomains (s1.fsvid.lol, s2.fsvid.lol, etc.) need fsvid.lol referer
// Embed pages (fsvid.lol) need the current mirror referer
if (hostname === 'fsvid.lol') {
fsvidHeaders = { 'Referer': 'https://fsmirror46.lol/', 'Origin': 'https://fsmirror46.lol' };
} else {
fsvidHeaders = { 'Referer': 'https://fsvid.lol/', 'Origin': 'https://fsvid.lol' };
}
} catch {
fsvidHeaders = { 'Referer': 'https://fsvid.lol/', 'Origin': 'https://fsvid.lol' };
}
}
if (type === 'uqload' && url) {
try {
const origin = getUqloadSiteOrigin(url);
uqloadHeaders = { 'Referer': `${origin}/`, 'Origin': origin };
} catch {
return null;
}
}
const seekHeaders = type === 'seekstreaming'
? getSeekStreamingRequestHeaders(referer || url)
: null;
if (type === 'seekstreaming' && !seekHeaders) return null;
const headerMap = {
voe: { 'Referer': 'https://voe.sx/', 'Origin': 'https://voe.sx' },
fsvid: fsvidHeaders || { 'Referer': 'https://fsvid.lol/', 'Origin': 'https://fsvid.lol' },
vidzy: { 'Referer': 'https://vidzy.org/', 'Origin': 'https://vidzy.org' },
vidmoly: { 'Referer': 'https://voirdrama.to/', 'Origin': 'https://voirdrama.to' },
sibnet: { 'Referer': 'https://video.sibnet.ru/', 'Origin': 'https://video.sibnet.ru' },
uqload: uqloadHeaders,
doodstream: { 'Referer': referer || 'https://d0000d.com/', 'Origin': referer ? new URL(referer).origin : 'https://d0000d.com' },
seekstreaming: seekHeaders,
cinep: { 'Referer': 'https://purstream.mx/', 'Origin': 'https://purstream.mx' },
kisskh: { 'Referer': 'https://kisskh.nl/', 'Origin': 'https://kisskh.nl' },
};
const hdrs = headerMap[type];
if (!hdrs || !url) return;
try {
const parsedUrl = new URL(url);
// Sibnet redirects to CDN subdomains (e.g. dv97.sibnet.ru),
// so we use a wildcard pattern to cover all subdomains.
const domainPattern = type === 'seekstreaming'
? getSeekStreamingPlaybackRulePattern(url)
: (type === 'sibnet'
? '*://*.sibnet.ru/*'
: `*://${parsedUrl.hostname}/*`);
if (!domainPattern) return null;
return { domainPattern, headers: hdrs };
} catch (e) {
console.error(`[EXT-EXTRACT] Failed to setup headers for ${type}:`, e);
return null;
}
}
/**
* Returns a { voe: size, fsvid: size, ... } object with the number of
* entries currently cached for each extractor.
*/
function getCacheSizes() {
const out = {};
for (const [key, cache] of Object.entries(caches)) {
out[key] = cache._cache.size;
}
return out;
}
/**
* Clears one extractor's cache (by type) or all caches.
*/
function clearCaches(type) {
if (type && caches[type]) {
caches[type]._cache.clear();
return;
}
for (const cache of Object.values(caches)) {
cache._cache.clear();
}
}
// Export everything for use in background.js
// (In service worker, we'll import via importScripts or just include in order)
if (typeof globalThis !== 'undefined') {
globalThis.MovixExtractors = {
extractVoe,
extractFsvid,
extractVidzy,
extractVidmoly,
extractSibnet,
extractUqload,
extractDoodStream,
extractSeekStreaming,
extractSingle,
extractAll,
detectEmbedType,
detectSupportedEmbeds,
setupHeadersForService,
getCacheSizes,
clearCaches,
EXTRACT_FN,
EMBED_PATTERNS,
};
}