MovixOpenSource/userscript/movix.user.js

5114 lines
150 KiB
JavaScript

// ==UserScript==
// @name Movix Proxy Extension (Tampermonkey)
// @namespace https://movix.cash
// @version 1.4.0
// @description Extension proxy pour Live TV Movix - Contourne CORS, injecte les headers et extrait les sources Nexus - version userscript Tampermonkey
// @author Movix
// @match http://localhost/*
// @match http://127.0.0.1/*
// @match https://localhost/*
// @match https://127.0.0.1/*
// @match https://movix.cash/*
// @match https://*.movix.cash/*
// @match https://movix.club/*
// @match https://*.movix.club/*
// @match https://movix.cloud/*
// @match https://*.movix.cloud/*
// @match https://movix.tax/*
// @match https://*.movix.tax/*
// @match https://movix.golf/*
// @match https://*.movix.golf/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant unsafeWindow
// @connect *
// @run-at document-start
// ==/UserScript==
/* Auto-generated by userscript/build-userscript.mjs. Do not edit directly. */
/* eslint-disable */
(() => {
"use strict";
const USERSCRIPT_MANIFEST = {
name: "Movix Proxy Extension",
version: "1.4.0",
description:
"Extension proxy pour Live TV Movix - Contourne CORS, injecte les headers et extrait les sources Nexus",
};
const pageWindow =
typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const storagePrefix = "movix_userscript:";
const runtimeListeners = {
onInstalled: [],
onStartup: [],
onMessage: [],
};
const dynamicRules = [];
const nativeWindowFetch =
typeof pageWindow.fetch === "function"
? pageWindow.fetch.bind(pageWindow)
: null;
const NativeXMLHttpRequest =
typeof pageWindow.XMLHttpRequest === "function"
? pageWindow.XMLHttpRequest
: null;
const proxiedMediaObjectUrls = new WeakMap();
function getGMRequest() {
if (typeof GM_xmlhttpRequest === "function") {
return GM_xmlhttpRequest;
}
if (
typeof GM !== "undefined" &&
GM &&
typeof GM.xmlHttpRequest === "function"
) {
return GM.xmlHttpRequest.bind(GM);
}
throw new Error("GM_xmlhttpRequest est indisponible.");
}
async function gmGetValueCompat(key, fallbackValue) {
if (typeof GM_getValue === "function") {
const value = await GM_getValue(key, fallbackValue);
return value === undefined ? fallbackValue : value;
}
if (typeof GM !== "undefined" && GM && typeof GM.getValue === "function") {
const value = await GM.getValue(key, fallbackValue);
return value === undefined ? fallbackValue : value;
}
return fallbackValue;
}
async function gmSetValueCompat(key, value) {
if (typeof GM_setValue === "function") {
return GM_setValue(key, value);
}
if (typeof GM !== "undefined" && GM && typeof GM.setValue === "function") {
return GM.setValue(key, value);
}
}
async function gmDeleteValueCompat(key) {
if (typeof GM_deleteValue === "function") {
return GM_deleteValue(key);
}
if (
typeof GM !== "undefined" &&
GM &&
typeof GM.deleteValue === "function"
) {
return GM.deleteValue(key);
}
}
function parseRawHeaders(rawHeaders) {
const parsed = {};
for (const line of String(rawHeaders || "").split(/\r?\n/)) {
const separatorIndex = line.indexOf(":");
if (separatorIndex === -1) continue;
const name = line.slice(0, separatorIndex).trim();
const value = line.slice(separatorIndex + 1).trim();
if (!name) continue;
parsed[name.toLowerCase()] = value;
}
return parsed;
}
function normalizeHeaders(input) {
if (!input) return {};
if (typeof Headers !== "undefined" && input instanceof Headers) {
return Object.fromEntries(input.entries());
}
if (Array.isArray(input)) {
return Object.fromEntries(input);
}
return Object.fromEntries(
Object.entries(input).map(([key, value]) => [key, String(value)]),
);
}
function escapeRegExp(value) {
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
}
function wildcardToRegExp(pattern) {
return new RegExp(
"^" +
String(pattern || "*")
.split("*")
.map((part) => escapeRegExp(part))
.join(".*") +
"$",
"i",
);
}
function matchesRule(rule, url, resourceType = "xmlhttprequest") {
if (!rule || !rule.condition) return false;
const resourceTypes = rule.condition.resourceTypes;
if (Array.isArray(resourceTypes) && resourceTypes.length > 0) {
if (!resourceTypes.includes(resourceType)) {
return false;
}
}
const urlFilter = rule.condition.urlFilter || "*";
if (!wildcardToRegExp(urlFilter).test(url)) {
return false;
}
return true;
}
function hasMatchingRequestRule(url, resourceType = "xmlhttprequest") {
return dynamicRules.some((rule) => {
return (
rule?.action?.type === "modifyHeaders" &&
Array.isArray(rule?.action?.requestHeaders) &&
rule.action.requestHeaders.length > 0 &&
matchesRule(rule, url, resourceType)
);
});
}
function shouldProxyNetworkRequest(url, resourceType = "xmlhttprequest") {
const normalizedUrl = String(url || "").trim();
if (!normalizedUrl) return false;
if (
/^(?:blob:|data:|javascript:|about:|chrome-extension:|moz-extension:)/i.test(
normalizedUrl,
)
) {
return false;
}
return hasMatchingRequestRule(normalizedUrl, resourceType);
}
function applyDynamicRequestHeaders(
url,
headers,
resourceType = "xmlhttprequest",
) {
const merged = { ...headers };
for (const rule of dynamicRules) {
if (
rule?.action?.type !== "modifyHeaders" ||
!Array.isArray(rule?.action?.requestHeaders)
) {
continue;
}
if (!matchesRule(rule, url, resourceType)) {
continue;
}
for (const headerRule of rule.action.requestHeaders) {
if (!headerRule?.header) continue;
if (headerRule.operation === "set") {
merged[headerRule.header] = headerRule.value;
}
}
}
return merged;
}
function toArrayBuffer(value) {
if (value instanceof ArrayBuffer) {
return value;
}
if (ArrayBuffer.isView(value)) {
return value.buffer.slice(
value.byteOffset,
value.byteOffset + value.byteLength,
);
}
if (typeof value === "string") {
return new TextEncoder().encode(value).buffer;
}
return new ArrayBuffer(0);
}
function serializeBody(body) {
if (body == null) return undefined;
if (typeof body === "string") return body;
if (body instanceof URLSearchParams) return body.toString();
if (body instanceof FormData) return body;
if (body instanceof Blob) return body;
if (body instanceof ArrayBuffer) return body;
if (ArrayBuffer.isView(body)) return body;
return String(body);
}
class SimpleHeaders {
constructor(init = {}) {
this._map = new Map();
for (const [key, value] of Object.entries(init)) {
this.set(key, value);
}
}
get(name) {
return this._map.get(String(name).toLowerCase()) ?? null;
}
has(name) {
return this._map.has(String(name).toLowerCase());
}
set(name, value) {
this._map.set(String(name).toLowerCase(), String(value));
}
entries() {
return this._map.entries();
}
}
class GMFetchResponse {
constructor(url, response) {
this.status = response.status || 0;
this.statusText = response.statusText || "";
this.ok = this.status >= 200 && this.status < 300;
this.url = response.finalUrl || url;
this.headers = new SimpleHeaders(
parseRawHeaders(response.responseHeaders),
);
this._buffer = toArrayBuffer(
response.response ?? response.responseText ?? "",
);
}
async arrayBuffer() {
return this._buffer.slice(0);
}
async text() {
return new TextDecoder().decode(this._buffer);
}
async json() {
return JSON.parse(await this.text());
}
}
function gmFetch(input, init = {}) {
const requestUrl =
typeof input === "string" ? input : String(input && input.url);
const method = String(init.method || "GET").toUpperCase();
const baseHeaders = normalizeHeaders(init.headers);
const headers = applyDynamicRequestHeaders(
requestUrl,
baseHeaders,
"xmlhttprequest",
);
const body = serializeBody(init.body);
const request = getGMRequest();
return new Promise((resolve, reject) => {
let finished = false;
let abortHandler = null;
let handle = null;
const cleanup = () => {
if (abortHandler && init.signal) {
init.signal.removeEventListener("abort", abortHandler);
}
};
const fail = (error) => {
if (finished) return;
finished = true;
cleanup();
reject(error instanceof Error ? error : new Error(String(error)));
};
const succeed = (response) => {
if (finished) return;
finished = true;
cleanup();
resolve(new GMFetchResponse(requestUrl, response));
};
try {
handle = request({
method,
url: requestUrl,
headers,
data: body,
responseType: "arraybuffer",
timeout: init.timeout ?? 30000,
onload: (response) => succeed(response),
onerror: (response) =>
fail(
new Error(
response?.error ||
response?.statusText ||
"GM_xmlhttpRequest a échoué",
),
),
ontimeout: () => fail(new Error("Requête expirée")),
onabort: () =>
fail(new DOMException("La requête a été annulée", "AbortError")),
});
} catch (error) {
fail(error);
return;
}
if (init.signal) {
if (init.signal.aborted) {
try {
handle?.abort?.();
} catch {}
fail(new DOMException("La requête a été annulée", "AbortError"));
return;
}
abortHandler = () => {
try {
handle?.abort?.();
} catch {}
fail(new DOMException("La requête a été annulée", "AbortError"));
};
init.signal.addEventListener("abort", abortHandler, { once: true });
}
});
}
function createEventPayload(type, extra = {}) {
return {
type,
target: extra.target || null,
currentTarget: extra.currentTarget || extra.target || null,
lengthComputable: Boolean(extra.lengthComputable),
loaded: Number(extra.loaded || 0),
total: Number(extra.total || 0),
};
}
class MovixXMLHttpRequest {
constructor() {
this._native = NativeXMLHttpRequest ? new NativeXMLHttpRequest() : null;
this._useNative = Boolean(this._native);
this._listeners = new Map();
this._headers = {};
this._method = "GET";
this._url = "";
this._requestHandle = null;
this._responseHeadersRaw = "";
this._responseType = "";
this._timeout = 0;
this._withCredentials = false;
this.readyState = 0;
this.status = 0;
this.statusText = "";
this.response = null;
this.responseText = "";
this.responseURL = "";
this.onreadystatechange = null;
this.onload = null;
this.onerror = null;
this.onabort = null;
this.ontimeout = null;
this.onloadend = null;
this.onprogress = null;
this.upload = this._native ? this._native.upload : undefined;
if (this._native) {
this._bindNativeEvents();
}
}
get responseType() {
return this._responseType;
}
set responseType(value) {
this._responseType = String(value || "");
if (this._useNative && this._native) {
try {
this._native.responseType = this._responseType;
} catch {}
}
}
get timeout() {
return this._timeout;
}
set timeout(value) {
this._timeout = Number(value || 0);
if (this._useNative && this._native) {
this._native.timeout = this._timeout;
}
}
get withCredentials() {
return this._withCredentials;
}
set withCredentials(value) {
this._withCredentials = Boolean(value);
if (this._useNative && this._native) {
this._native.withCredentials = this._withCredentials;
}
}
addEventListener(type, listener) {
if (!this._listeners.has(type)) {
this._listeners.set(type, new Set());
}
this._listeners.get(type).add(listener);
}
removeEventListener(type, listener) {
this._listeners.get(type)?.delete(listener);
}
dispatchEvent(event) {
this._dispatch(event?.type || "event", event || {});
return true;
}
open(method, url, async = true, user, password) {
this._method = String(method || "GET").toUpperCase();
this._url = String(url || "");
this._headers = {};
this._requestHandle = null;
this._responseHeadersRaw = "";
this.response = null;
this.responseText = "";
this.responseURL = "";
this.status = 0;
this.statusText = "";
this.readyState = 0;
this._useNative =
Boolean(this._native) &&
!shouldProxyNetworkRequest(this._url, "xmlhttprequest");
if (this._useNative && this._native) {
this._native.open(method, url, async, user, password);
this._native.timeout = this._timeout;
this._native.withCredentials = this._withCredentials;
if (this._responseType) {
try {
this._native.responseType = this._responseType;
} catch {}
}
return;
}
this.readyState = MovixXMLHttpRequest.OPENED;
this._dispatch("readystatechange");
}
setRequestHeader(name, value) {
if (this._useNative && this._native) {
this._native.setRequestHeader(name, value);
return;
}
this._headers[name] = String(value);
}
getAllResponseHeaders() {
if (this._useNative && this._native) {
return this._native.getAllResponseHeaders();
}
return this._responseHeadersRaw || "";
}
getResponseHeader(name) {
if (this._useNative && this._native) {
return this._native.getResponseHeader(name);
}
return (
parseRawHeaders(this._responseHeadersRaw)[String(name).toLowerCase()] ||
null
);
}
overrideMimeType(mimeType) {
if (
this._useNative &&
this._native &&
typeof this._native.overrideMimeType === "function"
) {
this._native.overrideMimeType(mimeType);
}
}
send(body = null) {
if (this._useNative && this._native) {
this._native.send(body);
return;
}
const request = getGMRequest();
const payload = serializeBody(body);
this.readyState = MovixXMLHttpRequest.OPENED;
this._dispatch("readystatechange");
this._requestHandle = request({
method: this._method,
url: this._url,
headers: applyDynamicRequestHeaders(
this._url,
this._headers,
"xmlhttprequest",
),
data: payload,
timeout: this._timeout || 30000,
responseType: "arraybuffer",
onprogress: (event) => {
this.readyState = MovixXMLHttpRequest.LOADING;
this._dispatch("readystatechange");
this._dispatch(
"progress",
createEventPayload("progress", {
target: this,
currentTarget: this,
loaded: event?.loaded || 0,
total: event?.total || 0,
lengthComputable: Boolean(event?.total),
}),
);
},
onload: (response) => {
const buffer = toArrayBuffer(
response.response ?? response.responseText ?? "",
);
this.status = response.status || 0;
this.statusText = response.statusText || "";
this.responseURL = response.finalUrl || this._url;
this._responseHeadersRaw = response.responseHeaders || "";
this.readyState = MovixXMLHttpRequest.HEADERS_RECEIVED;
this._dispatch("readystatechange");
this.readyState = MovixXMLHttpRequest.LOADING;
this._applyProxyResponse(buffer);
this._dispatch("readystatechange");
this.readyState = MovixXMLHttpRequest.DONE;
this._dispatch("readystatechange");
this._dispatch(
"load",
createEventPayload("load", {
target: this,
currentTarget: this,
loaded: buffer.byteLength,
total: buffer.byteLength,
lengthComputable: true,
}),
);
this._dispatch("loadend");
},
onerror: (response) => {
this.status = response?.status || 0;
this.statusText =
response?.statusText || response?.error || "Erreur réseau";
this.readyState = MovixXMLHttpRequest.DONE;
this._dispatch("readystatechange");
this._dispatch("error");
this._dispatch("loadend");
},
ontimeout: () => {
this.readyState = MovixXMLHttpRequest.DONE;
this._dispatch("readystatechange");
this._dispatch("timeout");
this._dispatch("loadend");
},
onabort: () => {
this.readyState = MovixXMLHttpRequest.DONE;
this._dispatch("readystatechange");
this._dispatch("abort");
this._dispatch("loadend");
},
});
}
abort() {
if (this._useNative && this._native) {
this._native.abort();
return;
}
try {
this._requestHandle?.abort?.();
} catch {}
this.readyState = MovixXMLHttpRequest.DONE;
this._dispatch("readystatechange");
this._dispatch("abort");
this._dispatch("loadend");
}
_applyProxyResponse(buffer) {
const contentType = this.getResponseHeader("content-type") || "";
if (this._responseType === "arraybuffer") {
this.response = buffer.slice(0);
this.responseText = "";
return;
}
if (this._responseType === "blob") {
this.response = new Blob([buffer], { type: contentType });
this.responseText = "";
return;
}
const text = new TextDecoder().decode(buffer);
this.responseText = text;
if (this._responseType === "json") {
try {
this.response = JSON.parse(text);
} catch {
this.response = null;
}
return;
}
this.response = text;
}
_bindNativeEvents() {
const eventNames = [
"readystatechange",
"load",
"error",
"abort",
"timeout",
"loadend",
"progress",
];
for (const eventName of eventNames) {
this._native["on" + eventName] = (event) => {
this._syncFromNative();
this._dispatch(eventName, event || {});
};
}
}
_syncFromNative() {
if (!this._native) return;
this.readyState = this._native.readyState;
this.status = this._native.status;
this.statusText = this._native.statusText;
this.responseURL = this._native.responseURL || "";
try {
this.response = this._native.response;
} catch {}
try {
this.responseText = this._native.responseText;
} catch {}
}
_dispatch(type, eventData = {}) {
const event =
eventData && typeof eventData === "object" && eventData.type
? eventData
: createEventPayload(type, {
...eventData,
target: this,
currentTarget: this,
});
if (!event.target) event.target = this;
if (!event.currentTarget) event.currentTarget = this;
const handler = this["on" + type];
if (typeof handler === "function") {
try {
handler.call(this, event);
} catch (error) {
console.error("[Movix Userscript] XHR handler error:", error);
}
}
const listeners = this._listeners.get(type);
if (listeners) {
for (const listener of [...listeners]) {
try {
listener.call(this, event);
} catch (error) {
console.error("[Movix Userscript] XHR listener error:", error);
}
}
}
}
}
MovixXMLHttpRequest.UNSENT = 0;
MovixXMLHttpRequest.OPENED = 1;
MovixXMLHttpRequest.HEADERS_RECEIVED = 2;
MovixXMLHttpRequest.LOADING = 3;
MovixXMLHttpRequest.DONE = 4;
function patchPageFetch() {
const patchedFetch = function (input, init) {
const requestUrl =
typeof input === "string"
? input
: input && input.url
? String(input.url)
: "";
if (
requestUrl &&
shouldProxyNetworkRequest(requestUrl, "xmlhttprequest")
) {
return gmFetch(input, init || {});
}
if (nativeWindowFetch) {
return nativeWindowFetch(input, init);
}
return gmFetch(input, init || {});
};
pageWindow.fetch = patchedFetch;
}
function patchPageXHR() {
if (!NativeXMLHttpRequest) {
return;
}
pageWindow.XMLHttpRequest = MovixXMLHttpRequest;
}
function isDirectMediaUrl(url) {
return /\.(?:mp4|m4v|webm|mov)(?:[?#]|$)/i.test(String(url || ""));
}
async function createProxiedMediaObjectUrl(url) {
const response = await gmFetch(url, {});
const buffer = await response.arrayBuffer();
const contentType = response.headers.get("content-type") || "video/mp4";
return URL.createObjectURL(new Blob([buffer], { type: contentType }));
}
function patchMediaElementSource() {
if (typeof pageWindow.HTMLMediaElement !== "function") {
return;
}
const mediaProto = pageWindow.HTMLMediaElement.prototype;
const srcDescriptor = Object.getOwnPropertyDescriptor(mediaProto, "src");
if (
!srcDescriptor ||
typeof srcDescriptor.get !== "function" ||
typeof srcDescriptor.set !== "function"
) {
return;
}
Object.defineProperty(mediaProto, "src", {
configurable: true,
enumerable: srcDescriptor.enumerable,
get() {
return srcDescriptor.get.call(this);
},
set(value) {
const nextUrl = String(value || "");
if (
!nextUrl ||
!isDirectMediaUrl(nextUrl) ||
!shouldProxyNetworkRequest(nextUrl, "media")
) {
srcDescriptor.set.call(this, value);
return;
}
const mediaElement = this;
const previousObjectUrl = proxiedMediaObjectUrls.get(mediaElement);
if (previousObjectUrl) {
try {
URL.revokeObjectURL(previousObjectUrl);
} catch {}
proxiedMediaObjectUrls.delete(mediaElement);
}
createProxiedMediaObjectUrl(nextUrl)
.then((objectUrl) => {
proxiedMediaObjectUrls.set(mediaElement, objectUrl);
srcDescriptor.set.call(mediaElement, objectUrl);
if (mediaElement.autoplay) {
mediaElement.play().catch(() => {});
}
})
.catch((error) => {
console.error("[Movix Userscript] Media proxy failed:", error);
srcDescriptor.set.call(mediaElement, nextUrl);
});
},
});
}
const storageLocal = {
async get(keys, callback) {
const result = {};
if (keys == null) {
callback?.(result);
return result;
}
if (typeof keys === "string") {
result[keys] = await gmGetValueCompat(storagePrefix + keys, undefined);
callback?.(result);
return result;
}
if (Array.isArray(keys)) {
for (const key of keys) {
result[key] = await gmGetValueCompat(storagePrefix + key, undefined);
}
callback?.(result);
return result;
}
if (typeof keys === "object") {
for (const [key, defaultValue] of Object.entries(keys)) {
result[key] = await gmGetValueCompat(
storagePrefix + key,
defaultValue,
);
}
callback?.(result);
return result;
}
callback?.(result);
return result;
},
async set(items, callback) {
for (const [key, value] of Object.entries(items || {})) {
await gmSetValueCompat(storagePrefix + key, value);
}
callback?.();
},
async remove(keys, callback) {
const list = Array.isArray(keys) ? keys : [keys];
for (const key of list) {
await gmDeleteValueCompat(storagePrefix + key);
}
callback?.();
},
};
const chrome = {
runtime: {
getManifest() {
return USERSCRIPT_MANIFEST;
},
getURL(resource) {
return resource;
},
onInstalled: {
addListener(listener) {
runtimeListeners.onInstalled.push(listener);
},
},
onStartup: {
addListener(listener) {
runtimeListeners.onStartup.push(listener);
},
},
onMessage: {
addListener(listener) {
runtimeListeners.onMessage.push(listener);
},
},
async sendMessage(message) {
for (const listener of runtimeListeners.onMessage) {
let resolved = false;
const response = await new Promise((resolve, reject) => {
try {
const maybeResult = listener(message, {}, (value) => {
resolved = true;
resolve(value);
});
if (maybeResult && typeof maybeResult.then === "function") {
maybeResult.then(resolve, reject);
return;
}
if (maybeResult !== true && !resolved) {
resolve(maybeResult);
}
} catch (error) {
reject(error);
}
});
if (response !== undefined) {
return response;
}
}
return undefined;
},
},
storage: {
local: storageLocal,
},
declarativeNetRequest: {
async getDynamicRules() {
return dynamicRules.map((rule) => ({ ...rule }));
},
async updateDynamicRules({ addRules = [], removeRuleIds = [] } = {}) {
if (Array.isArray(removeRuleIds) && removeRuleIds.length > 0) {
for (const ruleId of removeRuleIds) {
const index = dynamicRules.findIndex((rule) => rule.id === ruleId);
if (index !== -1) {
dynamicRules.splice(index, 1);
}
}
}
if (Array.isArray(addRules) && addRules.length > 0) {
for (const rule of addRules) {
const existingIndex = dynamicRules.findIndex(
(existingRule) => existingRule.id === rule.id,
);
if (existingIndex !== -1) {
dynamicRules.splice(existingIndex, 1, { ...rule });
} else {
dynamicRules.push({ ...rule });
}
}
}
},
},
};
patchPageFetch();
patchPageXHR();
patchMediaElementSource();
globalThis.chrome = chrome;
const fetch = gmFetch;
// --- BEGIN extension/Chrome/extractors.js ---
/**
* 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.cash";
// 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, 7200000),
};
// ===== Utility Functions =====
// Dean Edwards packer signature — split to avoid Chrome Web Store code scanner false positives
const PACKER_MARKER = "ev" + "al(func" + "tion(p,a,c,k,e,";
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 decoder.
*
* PURPOSE: Third-party video hosting sites (Fsvid, Vidzy, etc.) serve their
* player configuration inside a "packed" JavaScript block produced by Dean
* Edwards' Packer (http://dean.edwards.name/packer/). The packed format
* encodes the original script as a template string plus a dictionary of
* keywords separated by '|'. This function reconstructs the original
* human-readable script so we can extract the video source URL from it.
*
* HOW IT WORKS (step by step):
* 1. A helper converts a number to a base-N string (like toString(36) but
* supporting bases larger than 36 by using a-z then A-Z).
* 2. Each placeholder token in the template (a single base-N number) is
* replaced by the corresponding keyword from the dictionary.
* 3. The result is the original, readable JavaScript source.
*
* @param {string} packedScript - The template string with placeholder tokens
* @param {number} radix - The numeric base used for token encoding
* @param {number} keywordCount - Total number of keywords (used to iterate)
* @param {string[]} keywords - Array of replacement words, indexed by their
* base-N token value
* @returns {string} The decoded, human-readable JavaScript source
*/
function decodeDeanEdwardsPacker(
packedScript,
radix,
keywordCount,
keywords,
) {
// Convert a number to its base-N string representation.
// For digits 0-9 we use '0'-'9', for 10-35 we use 'a'-'z',
// and for 36+ we use uppercase letters (charCode 36+29=65 = 'A').
function numberToBaseNString(number) {
const quotient = Math.floor(number / radix);
const remainder = number % radix;
const digit =
remainder > 35
? String.fromCharCode(remainder + 29) // 36→'A', 37→'B', etc.
: remainder.toString(36); // 0-9, a-z
return (quotient > 0 ? numberToBaseNString(quotient) : "") + digit;
}
// Build a lookup table: for each token index, map its base-N string
// representation to the corresponding keyword (or keep the token itself
// if the keyword slot is empty).
const lookupTable = {};
for (let i = keywordCount - 1; i >= 0; i--) {
const token = numberToBaseNString(i);
lookupTable[token] = keywords[i] || token;
}
// Replace every word-boundary-delimited token in the packed script
// with its decoded keyword from the lookup table.
const decodedScript = packedScript.replace(/\b\w+\b/g, function (token) {
return lookupTable[token] !== undefined ? lookupTable[token] : token;
});
return decodedScript;
}
/**
* Extract the original JavaScript source from HTML that contains a
* Dean Edwards Packed script block.
*
* PURPOSE: Some third-party video embed pages (Fsvid, Vidzy, etc.) inline
* their player configuration inside a Dean Edwards packed script block.
* This function locates that block in the raw HTML, extracts the
* four parameters (packed template, radix, count, keyword list), and feeds
* them to decodeDeanEdwardsPacker() to recover the original script.
*
* The recovered script typically contains the .m3u8 or .mp4 video URL
* which we then extract with a simple regex in the calling code.
*
* @param {string} html - The full HTML source of the embed page
* @returns {string|null} The decoded JavaScript source, or null if no
* packed block was found
*/
function decodePackedScriptFromHtml(html) {
// Step 1: Locate the packed script marker in the HTML
const packerMarker = PACKER_MARKER;
const markerIndex = html.indexOf(packerMarker);
if (markerIndex === -1) return null;
// Step 2: Find the .split('|') call that marks the end of the keyword
// list — this tells us where the packed block ends
let splitIndex = html.indexOf(".split('|')", markerIndex);
if (splitIndex === -1)
splitIndex = html.indexOf('.split("|")', markerIndex);
if (splitIndex === -1) return null;
// Step 3: Extract the substring containing the packed call arguments
const packedSection = html.substring(markerIndex, splitIndex + 15);
// Step 4: Parse the four arguments from the packed call.
// The format is: }('PACKED_TEMPLATE', RADIX, COUNT, 'KW1|KW2|...'.split('|'))
// We use a regex that handles escaped quotes inside the strings.
const singleQuotePattern =
/\}\s*\(\s*'((?:[^'\\]|\\.)*)'\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*'((?:[^'\\]|\\.)*)'\s*\.split/s;
const doubleQuotePattern =
/\}\s*\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*"((?:[^"\\]|\\.)*)"\s*\.split/s;
const match =
packedSection.match(singleQuotePattern) ||
packedSection.match(doubleQuotePattern);
if (!match) return null;
// Step 5: Unescape string literals (e.g. \' becomes ', \" becomes ")
const packedTemplate = match[1].replace(/\\'/g, "'").replace(/\\"/g, '"');
const radix = parseInt(match[2]);
const keywordCount = parseInt(match[3]);
const keywords = match[4].split("|");
// Step 6: Decode and return the original script
return decodeDeanEdwardsPacker(
packedTemplate,
radix,
keywordCount,
keywords,
);
}
/**
* 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}`);
if (!fsvidUrl || !fsvidUrl.toLowerCase().includes("fsvid")) {
console.warn("[EXT-FSVID] Invalid URL, skipping");
return { success: false, error: "Fsvid: Invalid URL" };
}
const cacheKey = md5Hash(fsvidUrl);
const cached = caches.fsvid.get(cacheKey);
if (cached) {
console.log("[EXT-FSVID] Cache hit");
return { ...cached, 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://fs13.lol/",
"https://french-stream.one/",
"https://fstream.info/",
];
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(fsvidUrl, { 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}`);
// Check if the page has packed script
const hasEval = html.includes(PACKER_MARKER);
console.log(`[EXT-FSVID] Contains eval packer: ${hasEval}`);
if (!hasEval) {
// Try to find m3u8 directly in page (some fsvid pages have it in plain)
const directM3u8 = html.match(
/["'](https?:\/\/[^"']*\.m3u8[^"']*)["']/,
);
if (directM3u8) {
console.log(
`[EXT-FSVID] Found direct M3U8 (no packer needed): ${directM3u8[1]}`,
);
const m3u8Url = directM3u8[1].replace(/\\\//g, "/");
const result = { m3u8Url, success: true, source: "fsvid" };
caches.fsvid.set(cacheKey, result);
return result;
}
console.error("[EXT-FSVID] No eval packer and no direct M3U8 found");
console.log(
"[EXT-FSVID] HTML snippet (first 500 chars):",
html.substring(0, 500),
);
return { success: false, error: "Fsvid: No packed script found" };
}
// Pass full HTML to decodePackedScriptFromHtml (handles escaped quotes)
const deobfuscated = decodePackedScriptFromHtml(html);
if (!deobfuscated) {
console.error("[EXT-FSVID] Deobfuscation returned null");
// Log area around eval for debugging
const evalIdx = html.indexOf(PACKER_MARKER);
if (evalIdx !== -1) {
console.log(
"[EXT-FSVID] Packer snippet:",
html.substring(evalIdx, evalIdx + 200),
);
}
return { success: false, error: "Fsvid: Deobfuscation failed" };
}
console.log(`[EXT-FSVID] Deobfuscated length: ${deobfuscated.length}`);
console.log(
"[EXT-FSVID] Deobfuscated snippet:",
deobfuscated.substring(0, 300),
);
let m3u8Url = null;
const patterns = [
/sources:\s*\[\s*\{[^}]*?src:\s*["']([^"']+\.m3u8[^"']*)["']/,
/src:\s*["']([^"']+\.m3u8[^"']*)["']/,
/file:\s*["']([^"']+\.m3u8[^"']*)["']/,
/["'](https?:\/\/[^"']*\.m3u8[^"']*)["']/,
];
for (const pat of patterns) {
const m = deobfuscated.match(pat);
if (m) {
m3u8Url = m[1];
console.log(`[EXT-FSVID] M3U8 found with pattern ${pat}: ${m3u8Url}`);
break;
}
}
if (!m3u8Url) {
console.error("[EXT-FSVID] No M3U8 URL found in deobfuscated script");
console.log("[EXT-FSVID] Full deobfuscated:", deobfuscated);
return { success: false, error: "Fsvid: M3U8 not found in script" };
}
m3u8Url = m3u8Url.replace(/\\\//g, "/");
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 cacheKey = md5Hash(vidzyUrl);
const cached = caches.vidzy.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://vidzy.org/",
"user-agent": "Mozilla/5.0 Chrome/140.0.0.0",
};
const resp = await fetch(vidzyUrl, {
headers,
signal: controller.signal,
});
clearTimeout(timer);
if (!resp.ok)
return { success: false, error: `Vidzy: HTTP ${resp.status}` };
const html = await resp.text();
// Pass full HTML to decodePackedScriptFromHtml (handles escaped quotes)
const deobfuscated = decodePackedScriptFromHtml(html);
if (!deobfuscated)
return { success: false, error: "Vidzy: Deobfuscation failed" };
// Try multiple M3U8 patterns
const patterns = [
/file:\s*["']([^"']+\.m3u8[^"']*)['"]/,
/sources:\s*\[["']([^"']+\.m3u8[^"']*)['"]/,
/["']([^"']*\.m3u8[^"']*)['"]/,
];
let m3u8Url = null;
for (const pat of patterns) {
const m = deobfuscated.match(pat);
if (m) {
m3u8Url = m[1];
break;
}
}
if (!m3u8Url)
return { success: false, error: "Vidzy: M3U8 not found in script" };
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)
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) return { success: false, error: "Sibnet: MP4 not found" };
let mp4Url = mp4Match[1];
if (!mp4Url.startsWith("http")) {
mp4Url = `https://video.sibnet.ru${mp4Url}`;
}
// 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.
clearTimeout(timer);
try {
const mp4Resp = await fetch(mp4Url, {
headers: {
accept: "*/*",
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}`);
}
} 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 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 };
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
// Normalize URL
let normalized = uqloadUrl.replace(
/uqload\.(cx|com|net|co)/gi,
"uqload.bz",
);
// Validate and format
const parts = normalized.split("/");
const base = parts.slice(0, -1).join("/") || "https://uqload.bz";
let videoId = parts[parts.length - 1];
if (!videoId.includes(".html")) videoId += ".html";
if (!videoId.includes("embed-")) videoId = "embed-" + videoId;
const fullUrl = `${base}/${videoId}`;
const headers = {
"User-Agent": "Mozilla/5.0 Chrome/91.0.0.0",
Accept: "text/html,*/*",
};
// Try embed and non-embed versions
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;
}
}
clearTimeout(timer);
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" };
// Préférer le HLS master.m3u8 (multi-bitrate) au mp4 single-quality
const m3u8Matches =
html.match(/https?:\/\/[^"'\s]+\/master\.m3u8/g) ||
html.match(/https?:\/\/[^"'\s]+\.m3u8/g);
let videoUrl = m3u8Matches?.[0];
if (!videoUrl) {
const mp4Matches = html.match(/https?:\/\/.+\/v\.mp4/g);
videoUrl = mp4Matches?.[0];
}
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" };
}
}
/**
* 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);
console.error("[EXT-DOODSTREAM] pass_md5 pattern not found");
return { success: false, error: "DoodStream: pass_md5 not found" };
}
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 SeekStreaming (embed4me / embedseek) embed
*/
async function extractSeekStreaming(seekUrl) {
console.log(`[EXT-SEEKSTREAMING] Extracting from: ${seekUrl}`);
// Extract video ID
let videoId = null;
const decoded = decodeURIComponent(seekUrl);
if (decoded.includes("#")) {
videoId = decoded.split("#").pop().trim();
} else if (decoded.toLowerCase().includes("/embed/")) {
videoId = decoded.replace(/\/$/, "").split("/").pop().trim();
} else {
try {
const parsed = new URL(decoded);
if (parsed.hash) videoId = parsed.hash.replace("#", "").trim();
else if (parsed.pathname && parsed.pathname !== "/") {
videoId = parsed.pathname.replace(/\/$/, "").split("/").pop().trim();
}
} catch {}
}
if (!videoId) {
return {
success: false,
error: "SeekStreaming: Could not extract video ID",
};
}
const cacheKey = md5Hash(videoId);
const cached = caches.seekstreaming.get(cacheKey);
if (cached) return { ...cached, fromCache: true };
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
// Determine API domain from URL
let apiDomain = "lpayer.embed4me.com";
try {
apiDomain = new URL(decoded).host;
} catch {}
const apiUrl = `https://${apiDomain}/api/v1/video?id=${videoId}&w=1920&h=1080&r=`;
const headers = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
Accept: "*/*",
"Accept-Language": "en-US,en;q=0.5",
Referer: `https://${apiDomain}/`,
Origin: `https://${apiDomain}`,
};
const resp = await fetch(apiUrl, { headers, signal: controller.signal });
clearTimeout(timer);
if (!resp.ok)
return {
success: false,
error: `SeekStreaming: API HTTP ${resp.status}`,
};
const encryptedText = await resp.text();
// Decrypt AES-CBC response
const decryptedRaw = await decryptAesCbc(
encryptedText,
SEEKSTREAMING_AES_KEY_RAW,
SEEKSTREAMING_AES_IV_RAW,
);
if (!decryptedRaw) {
return {
success: false,
error: "SeekStreaming: AES decryption failed",
};
}
const data = JSON.parse(decryptedRaw);
const cfUrl = data.cf || "";
const sourceUrl = data.source || "";
if (!cfUrl && !sourceUrl) {
return {
success: false,
error: "SeekStreaming: No video source found",
};
}
// Prefer CF (CDN) URL
const videoUrl = cfUrl || sourceUrl;
const result = {
hlsUrl: videoUrl,
success: true,
source: "seekstreaming",
// Also provide both URLs
cfUrl: cfUrl || undefined,
ipUrl: sourceUrl || undefined,
};
caches.seekstreaming.set(cacheKey, result);
return result;
} catch (e) {
console.error("[EXT-SEEKSTREAMING] Error:", e);
return {
success: false,
error: e.message || "SeekStreaming extraction failed",
};
}
}
// ===== 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\.(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) => {
const lower = url.toLowerCase();
return (
lower.includes("embedseek.com") ||
lower.includes("embed4me.com") ||
lower.includes("seekstreaming")
);
},
};
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) → fs13.lol (required by fsvid to serve content)
// - CDN/M3U8 (s1.fsvid.lol, s2.fsvid.lol, etc.) → fsvid.lol (required by CDN)
let fsvidHeaders;
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 fs13.lol referer
if (hostname === "fsvid.lol") {
fsvidHeaders = {
Referer: "https://fs13.lol/",
Origin: "https://fs13.lol",
};
} else {
fsvidHeaders = {
Referer: "https://fsvid.lol/",
Origin: "https://fsvid.lol",
};
}
} catch {
fsvidHeaders = {
Referer: "https://fsvid.lol/",
Origin: "https://fsvid.lol",
};
}
}
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: { Referer: "https://uqload.bz/", Origin: "https://uqload.bz" },
doodstream: {
Referer: referer || "https://d0000d.com/",
Origin: referer ? new URL(referer).origin : "https://d0000d.com",
},
seekstreaming: {
Referer: referer || "https://lpayer.embed4me.com/",
Origin: referer
? new URL(referer).origin
: "https://lpayer.embed4me.com",
},
cinep: {
Referer: "https://cinepulse.lol/",
Origin: "https://cinepulse.lol",
},
};
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 === "sibnet" ? "*://*.sibnet.ru/*" : `*://${parsedUrl.hostname}/*`;
return { domainPattern, headers: hdrs };
} catch (e) {
console.error(`[EXT-EXTRACT] Failed to setup headers for ${type}:`, e);
return null;
}
}
// Cache helpers exposed on the Extractors namespace
function getCacheSizes() {
const result = {};
for (const [name, cache] of Object.entries(caches)) {
result[name] = cache._cache ? cache._cache.size : 0;
}
return result;
}
function clearCaches(type) {
if (type) {
if (caches[type] && caches[type]._cache) caches[type]._cache.clear();
} else {
for (const cache of Object.values(caches)) {
if (cache._cache) 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,
EXTRACT_FN,
EMBED_PATTERNS,
getCacheSizes,
clearCaches,
};
}
// --- END extension/Chrome/extractors.js ---
// --- BEGIN extension/Chrome/background.js ---
const VAVOO_BASE_URL = "https://tvvoo.hayd.uk/cfg-fr";
const WITV_BASE_URL = "https://witv.team";
const SOSPLAY_BASE_URL = "https://streamonsport.art";
const LIVETV_BASE_URL = "https://livetv882.me/frx/";
const LIVETV_EMBED_ORIGIN = "https://livetv882.me";
const LIVETV_EMBED_REFERER = LIVETV_BASE_URL;
// Backend API URL for got-scraping based extraction
const API_BASE_URL = "https://api.movix.cash";
const STREAM_PROXY_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";
// Import extractors module
const Extractors = globalThis.MovixExtractors;
// Cache for Wiflix channels with their page slugs
let wiflixChannelCache = {};
// User extraction preferences (synced from site via SET_EXTRACTION_PREFS)
const DEFAULT_EXTRACTION_PREFS = {
version: 1,
m3u8: {
voe: true,
fsvid: true,
vidzy: true,
vidmoly: true,
sibnet: true,
uqload: true,
doodstream: true,
seekstreaming: true,
},
livetv: {
linkzy: true,
wiflix: true,
sosplay: true,
livetv: true,
matches: true,
},
};
let extractionPrefs = DEFAULT_EXTRACTION_PREFS;
// Extension enabled state
let extensionEnabled = true;
// Stats tracking (enriched with per-type counters)
let sessionStats = {
extractions: 0,
corsFixed: 0,
cached: 0,
byType: {
voe: 0,
fsvid: 0,
vidzy: 0,
vidmoly: 0,
sibnet: 0,
uqload: 0,
doodstream: 0,
seekstreaming: 0,
},
};
// Load initial state
(async () => {
const storedEnabled = await gmGetValueCompat(
"movix_extensionEnabled",
null,
);
if (storedEnabled !== null) extensionEnabled = storedEnabled !== false;
const storedStats = await gmGetValueCompat("movix_stats", null);
if (storedStats) {
sessionStats = { ...sessionStats, ...storedStats };
if (!sessionStats.byType) {
sessionStats.byType = {
voe: 0,
fsvid: 0,
vidzy: 0,
vidmoly: 0,
sibnet: 0,
uqload: 0,
doodstream: 0,
seekstreaming: 0,
};
}
}
const storedPrefs = await gmGetValueCompat("movix_extraction_prefs", null);
if (storedPrefs) extractionPrefs = storedPrefs;
})();
// Initial setup
chrome.runtime.onInstalled.addListener(() => {
setupRules();
});
chrome.runtime.onStartup.addListener(() => {
setupRules();
// Reset session stats on startup
sessionStats = {
extractions: 0,
corsFixed: 0,
cached: 0,
byType: {
voe: 0,
fsvid: 0,
vidzy: 0,
vidmoly: 0,
sibnet: 0,
uqload: 0,
doodstream: 0,
seekstreaming: 0,
},
};
gmSetValueCompat("movix_stats", sessionStats);
});
// Configure DNR rules for CORS and Headers
async function setupRules() {
// Clear existing dynamic rules to prevent accumulation
const existingRules = await chrome.declarativeNetRequest.getDynamicRules();
const ruleIds = existingRules.map((rule) => rule.id);
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: ruleIds,
});
// Reset rule counter when rules are cleared
ruleIdCounter = 100;
const rules = [
// 1. Allow CORS for everything (Response Headers)
{
id: 1,
priority: 1,
action: {
type: "modifyHeaders",
responseHeaders: [
{
header: "Access-Control-Allow-Origin",
operation: "set",
value: "*",
},
{
header: "Access-Control-Allow-Methods",
operation: "set",
value: "GET, POST, OPTIONS, HEAD, PUT, DELETE, PATCH",
},
{
header: "Access-Control-Allow-Headers",
operation: "set",
value: "*",
},
],
},
condition: {
urlFilter: "*",
initiatorDomains: [
"localhost",
"127.0.0.1",
"movix.cash",
"movix.club",
"movix.cloud",
"movix.tax",
"movix.golf",
],
resourceTypes: [
"xmlhttprequest",
"other",
"media",
"image",
"script",
"stylesheet",
"font",
"websocket",
],
},
},
];
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: rules,
});
}
/**
* Map a catalogId (e.g. "wiflix_sport") to a livetv source key.
* Returns null if unknown — unknown keys are allowed by default.
*/
function getLiveTvSourceKey(catalogId) {
if (!catalogId || typeof catalogId !== "string") return null;
if (catalogId.startsWith("linkzy_")) return "linkzy";
if (catalogId.startsWith("wiflix_")) return "wiflix";
if (catalogId.startsWith("sosplay_")) return "sosplay";
if (catalogId.startsWith("livetv_")) return "livetv";
if (catalogId.startsWith("matches_")) return "matches";
return null;
}
function isLiveTvAllowed(catalogId) {
const key = getLiveTvSourceKey(catalogId);
if (!key) return true; // Unknown source → allow by default
return extractionPrefs.livetv[key] !== false;
}
function isEmbedAllowed(type) {
if (!type) return true;
return extractionPrefs.m3u8[type] !== false;
}
// Handle messages
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleMessage(message)
.then(sendResponse)
.catch((err) => sendResponse({ error: err.message }));
return true; // Keep channel open for async response
});
async function handleMessage(message) {
const { action, payload } = message;
// Handle toggle action (always allowed)
if (action === "TOGGLE_EXTENSION") {
extensionEnabled = payload.enabled;
if (extensionEnabled) {
await setupRules();
} else {
// Remove all DNR rules when disabled
const existingRules =
await chrome.declarativeNetRequest.getDynamicRules();
const ruleIds = existingRules.map((rule) => rule.id);
if (ruleIds.length > 0) {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: ruleIds,
});
}
}
return { success: true, enabled: extensionEnabled };
}
// Handle stats request (always allowed)
if (action === "GET_STATS") {
return sessionStats;
}
// Block all other actions if disabled
if (!extensionEnabled) {
return { error: "Extension is disabled" };
}
switch (action) {
case "GET_MANIFEST":
return await getManifest();
case "GET_CATALOG": {
const catalogId = payload?.id || "";
if (!isLiveTvAllowed(catalogId)) {
return { metas: [], disabled_by_user: true };
}
return await getCatalog(payload.type, payload.id, payload?.accessKey);
}
case "GET_STREAM": {
const channelId = payload?.id || "";
if (!isLiveTvAllowed(channelId)) {
return {
error: "disabled_by_user",
source: getLiveTvSourceKey(channelId),
};
}
return await getStream(
payload.type,
payload.id,
payload?.accessKey,
payload,
);
}
case "PROXY_HTTP":
return await proxyHttpRequest(payload.url, payload.headers);
// === Nexus M3U8 Extraction (runs locally in extension, no server needed) ===
case "EXTRACT_M3U8": {
const { url: embedUrl, type: hintedType } = payload || {};
const detectedType =
hintedType ||
(Extractors.detectEmbedType
? Extractors.detectEmbedType(embedUrl)
: null);
if (detectedType && !isEmbedAllowed(detectedType)) {
return {
success: false,
error: "disabled_by_user",
type: detectedType,
};
}
sessionStats.extractions++;
if (detectedType && sessionStats.byType) {
sessionStats.byType[detectedType] =
(sessionStats.byType[detectedType] || 0) + 1;
}
await gmSetValueCompat("movix_stats", sessionStats);
return await handleExtractM3u8(payload);
}
case "EXTRACT_ALL_M3U8": {
const filteredSources = (payload?.sources || []).filter((source) => {
const srcUrl =
typeof source === "string"
? source
: source?.link || source?.url || "";
const srcType = Extractors.detectEmbedType
? Extractors.detectEmbedType(srcUrl)
: null;
return !srcType || isEmbedAllowed(srcType);
});
sessionStats.extractions++;
await gmSetValueCompat("movix_stats", sessionStats);
return await handleExtractAllM3u8({
...payload,
sources: filteredSources,
});
}
case "DETECT_EMBEDS":
return handleDetectEmbeds(payload);
case "SETUP_HEADERS": {
const headerInfo = await Extractors.setupHeadersForService(
payload.type,
payload.url,
);
if (headerInfo) {
await addHeadersRule(headerInfo.domainPattern, headerInfo.headers);
console.log(
`[NEXUS] DNR headers set for ${payload.type}: ${headerInfo.domainPattern}`,
);
return { success: true };
}
return { success: false, error: "Could not setup headers" };
}
case "SET_EXTRACTION_PREFS": {
const incoming = payload?.prefs;
if (
incoming &&
incoming.version === 1 &&
incoming.m3u8 &&
incoming.livetv
) {
extractionPrefs = {
version: 1,
m3u8: { ...DEFAULT_EXTRACTION_PREFS.m3u8, ...incoming.m3u8 },
livetv: { ...DEFAULT_EXTRACTION_PREFS.livetv, ...incoming.livetv },
};
await gmSetValueCompat("movix_extraction_prefs", extractionPrefs);
return { success: true };
}
return { success: false, error: "Invalid prefs shape" };
}
case "GET_EXTRACTION_PREFS":
return extractionPrefs;
case "GET_CACHE_STATS": {
if (typeof Extractors.getCacheSizes === "function") {
return Extractors.getCacheSizes();
}
return {};
}
case "CLEAR_EXTRACTION_CACHE": {
if (typeof Extractors.clearCaches === "function") {
Extractors.clearCaches(payload?.type);
return { success: true };
}
return { success: false, error: "Cache API unavailable" };
}
default:
throw new Error(`Unknown action: ${action}`);
}
}
// Helper to proxy HTTP requests via extension (to bypass Mixed Content)
async function proxyHttpRequest(url, headers = {}) {
try {
if (headers && Object.keys(headers).length > 0) {
try {
const parsedUrl = new URL(url);
const rulePattern = `*://${parsedUrl.host}${parsedUrl.pathname}*`;
await addHeadersRule(rulePattern, headers);
} catch (ruleError) {
console.warn(
"[PROXY_HTTP] Failed to add DNR headers rule:",
ruleError,
);
}
}
const response = await fetch(url, { headers });
const buffer = await response.arrayBuffer();
// Convert ArrayBuffer to Base64
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = btoa(binary);
return {
data: base64,
contentType: response.headers.get("content-type"),
status: response.status,
finalUrl: response.url,
};
} catch (e) {
console.error("Proxy HTTP error:", e);
return { error: e.message };
}
}
// === NEXUS M3U8 EXTRACTION HANDLERS ===
/**
* Handle single embed extraction request
* payload: { type: 'voe'|'fsvid'|..., url: 'https://...' }
*/
async function handleExtractM3u8(payload) {
const { type, url } = payload;
if (!url) return { success: false, error: "Missing URL" };
// Auto-detect type if not provided
const embedType = type || Extractors.detectEmbedType(url);
if (!embedType) return { success: false, error: "Unknown embed type" };
// Set up DNR headers BEFORE extraction so the fetch request succeeds
try {
const headerInfo = await Extractors.setupHeadersForService(
embedType,
url,
);
if (headerInfo) {
await addHeadersRule(headerInfo.domainPattern, headerInfo.headers);
console.log(
`[NEXUS] Pre-extraction DNR headers set for ${embedType}: ${headerInfo.domainPattern}`,
);
}
} catch (e) {
console.warn("[NEXUS] Failed to set pre-extraction headers:", e);
}
console.log(`[NEXUS] Extracting ${embedType} from: ${url}`);
const result = await Extractors.extractSingle(embedType, url);
// Set up DNR headers for the extracted URL so the page player can use it
if (result.success) {
const videoUrl = result.hlsUrl || result.m3u8Url;
// If the video URL is different from the page URL (likely), set headers for it too
if (videoUrl && videoUrl !== url) {
const headerInfo = await Extractors.setupHeadersForService(
embedType,
videoUrl,
);
if (headerInfo) {
await addHeadersRule(headerInfo.domainPattern, headerInfo.headers);
console.log(
`[NEXUS] DNR headers set for ${embedType}: ${headerInfo.domainPattern}`,
);
}
}
}
return result;
}
/**
* Handle parallel extraction of all supported embeds from a sources list
* payload: { sources: ['url1', 'url2', ...] or [{link:'url', player:'name'}, ...] }
*/
async function handleExtractAllM3u8(payload) {
const { sources } = payload;
if (!sources || !Array.isArray(sources) || sources.length === 0) {
return { success: false, error: "No sources provided", results: [] };
}
console.log(`[NEXUS] Extracting all from ${sources.length} sources`);
// Set up DNR headers for all sources BEFORE extraction
try {
const detected = Extractors.detectSupportedEmbeds(sources);
for (const item of detected) {
const headerInfo = await Extractors.setupHeadersForService(
item.type,
item.url,
);
if (headerInfo) {
await addHeadersRule(headerInfo.domainPattern, headerInfo.headers);
}
}
console.log(
`[NEXUS] Pre-extraction headers set for ${detected.length} sources`,
);
} catch (e) {
console.warn(
"[NEXUS] Failed to set pre-extraction headers for batch:",
e,
);
}
const results = await Extractors.extractAll(sources);
// Set up DNR headers for all successful extractions (video URLs)
for (const result of results) {
if (result.success) {
const videoUrl = result.hlsUrl || result.m3u8Url;
if (videoUrl) {
const headerInfo = await Extractors.setupHeadersForService(
result.type,
videoUrl,
);
if (headerInfo) {
await addHeadersRule(headerInfo.domainPattern, headerInfo.headers);
}
}
}
}
const successCount = results.filter((r) => r.success).length;
return {
success: successCount > 0,
total: results.length,
successCount,
results,
};
}
/**
* Handle embed type detection only (no extraction)
* payload: { sources: ['url1', 'url2', ...] }
*/
function handleDetectEmbeds(payload) {
const { sources } = payload;
if (!sources || !Array.isArray(sources)) return { embeds: [] };
return { embeds: Extractors.detectSupportedEmbeds(sources) };
}
// === API LOGIC ===
function getMovixFrontendOrigin() {
try {
const currentOrigin = pageWindow.location?.origin;
const currentHostname = pageWindow.location?.hostname ?? "";
if (
currentHostname === "movix.cash" ||
currentHostname.endsWith(".movix.cash") ||
currentHostname === "movix.club" ||
currentHostname.endsWith(".movix.club") ||
currentHostname === "movix.cloud" ||
currentHostname.endsWith(".movix.cloud") ||
currentHostname === "movix.tax" ||
currentHostname.endsWith(".movix.tax") ||
currentHostname === "movix.golf" ||
currentHostname.endsWith(".movix.golf")
) {
return (currentOrigin || "https://movix.cash").replace(/\/$/, "");
}
} catch {}
return "https://movix.cash";
}
function buildBackendApiHeaders(accessKey, extraHeaders = {}) {
const frontendOrigin = getMovixFrontendOrigin();
const headers = {
Accept: "application/json",
Origin: frontendOrigin,
Referer: `${frontendOrigin}/`,
...extraHeaders,
};
if (accessKey) {
headers["x-access-key"] = accessKey;
}
return headers;
}
async function getManifest() {
console.log("Fetching manifest from Vavoo...");
const vavooData = await fetchSafe(
`${VAVOO_BASE_URL}/manifest.json`,
"Vavoo",
);
const manifest = {
id: "org.stremio.merged",
version: "1.0.0",
name: "Live TV (Extension)",
description: "TV sources via Extension",
catalogs: [],
resources: ["catalog", "meta", "stream"],
types: ["tv"],
idPrefixes: [],
};
// Add Vavoo catalogs
if (vavooData) {
if (vavooData.catalogs) manifest.catalogs.push(...vavooData.catalogs);
if (vavooData.idPrefixes) {
manifest.idPrefixes.push(...vavooData.idPrefixes);
} else {
manifest.idPrefixes.push("vavoo_");
}
}
// Add Wiflix (WITV) catalogs
const wiflixCatalogs = [
{ type: "tv", id: "wiflix_sport", name: "⚽ Sport" },
{ type: "tv", id: "wiflix_cinema", name: "🎥 Cinéma" },
{ type: "tv", id: "wiflix_generaliste", name: "📺 Généraliste" },
{ type: "tv", id: "wiflix_documentaire", name: "🌍 Documentaire" },
{ type: "tv", id: "wiflix_enfants", name: "🎈 Enfants" },
{ type: "tv", id: "wiflix_info", name: "📰 Info" },
{ type: "tv", id: "wiflix_musique", name: "🎵 Musique" },
];
manifest.catalogs.push(...wiflixCatalogs);
manifest.idPrefixes.push("wiflix_");
// Add Bolaloca catalog (compat prefix sosplay_)
const sosplayCatalogs = [
{ type: "tv", id: "sosplay_chaines", name: "📺 Chaînes (Bolaloca)" },
];
manifest.catalogs.push(...sosplayCatalogs);
manifest.idPrefixes.push("sosplay_");
const livetvCatalogs = [
{ type: "tv", id: "livetv_live", name: "🔴 En direct" },
];
manifest.catalogs.push(...livetvCatalogs);
manifest.idPrefixes.push("livetv_");
manifest.catalogs.push(
{ type: "tv", id: "livetv_all", name: "📅 Tous les sports" },
{ type: "tv", id: "livetv_football", name: "⚽ Football" },
{ type: "tv", id: "livetv_hockey", name: "🏒 Hockey" },
{ type: "tv", id: "livetv_basketball", name: "🏀 Basketball" },
{ type: "tv", id: "livetv_tennis", name: "🎾 Tennis" },
{ type: "tv", id: "livetv_volleyball", name: "🏐 Volley-ball" },
{ type: "tv", id: "livetv_handball", name: "🤾 Handball" },
{ type: "tv", id: "livetv_rugby", name: "🏉 Rugby" },
{ type: "tv", id: "livetv_combat", name: "🥊 Sports de combat" },
{ type: "tv", id: "livetv_motorsport", name: "🏎️ Sports mecaniques" },
{ type: "tv", id: "livetv_winter", name: "🎿 Sports d'hiver" },
{ type: "tv", id: "livetv_athletics", name: "🏃 Athletisme" },
{ type: "tv", id: "livetv_other", name: "🏟️ Autres sports" },
);
return manifest;
}
async function getCatalog(type, catalogId, accessKey = null) {
// Check if this is a Wiflix catalog
if (catalogId.startsWith("wiflix_")) {
return await getWiflixCatalog(catalogId);
}
// Check if this is a Bolaloca/LiveTV catalog resolved by backend
if (catalogId.startsWith("sosplay_")) {
console.log(`[SOSPLAY] Fetching catalog via Backend: ${catalogId}`);
const response = await fetch(
`${API_BASE_URL}/api/livetv/catalog/tv/${catalogId}`,
{
headers: buildBackendApiHeaders(accessKey),
},
);
if (!response.ok)
throw new Error(`Backend API error: ${response.status}`);
return await response.json();
}
if (catalogId.startsWith("livetv_")) {
console.log(`[LIVETV] Fetching catalog via Backend: ${catalogId}`);
const response = await fetch(
`${API_BASE_URL}/api/livetv/catalog/tv/${catalogId}`,
{
headers: buildBackendApiHeaders(accessKey),
},
);
if (!response.ok)
throw new Error(`Backend API error: ${response.status}`);
return await response.json();
}
// Default: Vavoo catalog
const url = `${VAVOO_BASE_URL}/catalog/${type}/${catalogId}.json`;
const catalog = await fetchSafe(url, "Catalog " + catalogId);
if (!catalog) throw new Error("Catalog fetch failed");
return catalog;
}
// Wiflix catalog categories mapping (URL paths)
const WITV_CATEGORIES = {
wiflix_sport: "/chaines-live/sport/",
wiflix_cinema: "/chaines-live/cinema/",
wiflix_generaliste: "/chaines-live/generaliste/",
wiflix_documentaire: "/chaines-live/documentaire/",
wiflix_enfants: "/chaines-live/enfants/",
wiflix_info: "/chaines-live/info/",
wiflix_musique: "/chaines-live/musique/",
};
// Scrape Wiflix channels from WITV website
async function getWiflixCatalog(catalogId) {
const categoryPath = WITV_CATEGORIES[catalogId];
if (!categoryPath) {
throw new Error(`Unknown Wiflix catalog: ${catalogId}`);
}
console.log(`[WITV] Fetching catalog for: ${catalogId}`);
try {
const categoryUrl = `${WITV_BASE_URL}${categoryPath}`;
console.log(`[WITV] Category URL: ${categoryUrl}`);
const response = await fetch(categoryUrl, {
headers: {
"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,image/webp,*/*;q=0.8",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch category page: ${response.status}`);
}
const html = await response.text();
console.log(`[WITV] Category page length: ${html.length}`);
// Parse channels from HTML
const channels = [];
const cardRegex =
/<div[^>]*class="[^"]*holographic-card[^"]*"[^>]*>[\s\S]*?<a[^>]*href="[^"]*\/(\d+)-[^"]*"[^>]*>[\s\S]*?<[^>]*class="[^"]*ann-short_price[^"]*"[^>]*>([^<]+)</gi;
let match;
while ((match = cardRegex.exec(html)) !== null) {
const id = match[1];
const name = match[2].trim();
if (id && name && !channels.find((c) => c.id === `wiflix_${id}`)) {
const slugMatch = html.match(
new RegExp(`href="([^"]*/${id}-[^"\.]+\.html)"`),
);
const pageSlug = slugMatch ? slugMatch[1] : null;
const channel = {
id: `wiflix_${id}`,
type: "tv",
name: name,
poster: null,
genres: [catalogId.replace("wiflix_", "")],
_pageSlug: pageSlug,
_categoryPath: categoryPath,
};
channels.push(channel);
wiflixChannelCache[channel.id] = channel;
}
}
// Pattern 2 fallback
if (channels.length === 0) {
console.log("[WITV] Pattern 1 failed, trying fallback pattern...");
const fallbackRegex =
/href="[^"]*\/(\d+)-([^"]+)\.html"[^>]*>[\s\S]*?<[^>]*class="[^"]*ann-short_price[^"]*"[^>]*>([^<]+)</gi;
while ((match = fallbackRegex.exec(html)) !== null) {
const id = match[1];
const name = match[3].trim();
if (id && name && !channels.find((c) => c.id === `wiflix_${id}`)) {
const slugMatch = html.match(
new RegExp(`href="([^"]*/${id}-[^"\.]+\.html)"`),
);
const pageSlug = slugMatch ? slugMatch[1] : null;
const channel = {
id: `wiflix_${id}`,
type: "tv",
name: name,
poster: null,
genres: [catalogId.replace("wiflix_", "")],
_pageSlug: pageSlug,
_categoryPath: categoryPath,
};
channels.push(channel);
wiflixChannelCache[channel.id] = channel;
}
}
}
// Pattern 3: Ultra-simple
if (channels.length === 0) {
console.log("[WITV] Pattern 2 failed, trying ultra-simple pattern...");
console.log("[WITV] HTML preview:", html.substring(0, 2000));
const simpleRegex = /href="[^"]*\/(\d+)-([^"\.]+)/gi;
const seenIds = new Set();
while ((match = simpleRegex.exec(html)) !== null) {
const id = match[1];
const slug = match[2];
const name = slug
.replace(/-/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
if (id && !seenIds.has(id)) {
seenIds.add(id);
const channel = {
id: `wiflix_${id}`,
type: "tv",
name: name,
poster: null,
genres: [catalogId.replace("wiflix_", "")],
_pageSlug: `${id}-${slug}.html`,
_categoryPath: categoryPath,
};
channels.push(channel);
wiflixChannelCache[channel.id] = channel;
}
}
}
console.log(`[WITV] Found ${channels.length} channels in ${catalogId}`);
return { metas: channels };
} catch (error) {
console.error("[WITV] Error fetching catalog:", error);
throw error;
}
}
async function getStream(type, channelId, accessKey = null, options = {}) {
// Check if this is a Wiflix channel
if (channelId.startsWith("wiflix_")) {
return await getWiflixStream(channelId, accessKey);
}
// Check if this is a Sosplay channel
if (channelId.startsWith("sosplay_")) {
return await getSosplayStream(channelId, accessKey);
}
if (channelId.startsWith("livetv_")) {
return await getLiveTvStream(channelId, accessKey, options);
}
// Default: Vavoo stream
const url = `${VAVOO_BASE_URL}/stream/${type}/${channelId}.json`;
const streamData = await fetchSafe(url, "Vavoo Stream");
if (streamData && streamData.streams) {
for (const stream of streamData.streams) {
if (stream.url) {
await addUserAgentRule(stream.url, "VAVOO/2.6");
}
}
}
if (!streamData) throw new Error("Stream fetch failed");
return streamData;
}
// Find the channel page URL from cache or by searching
async function findWiflixChannelPageUrl(channelId) {
if (wiflixChannelCache[channelId]) {
const channel = wiflixChannelCache[channelId];
if (channel._pageSlug) {
const pageUrl = channel._pageSlug.startsWith("http")
? channel._pageSlug
: `${WITV_BASE_URL}${channel._categoryPath}${channel._pageSlug.replace(/^\//, "")}`;
console.log(`[WITV] Found channel page URL in cache: ${pageUrl}`);
return pageUrl;
}
}
const id = channelId.replace("wiflix_", "");
console.log(`[WITV] Channel ${channelId} not in cache, searching...`);
for (const [catId, categoryPath] of Object.entries(WITV_CATEGORIES)) {
try {
const categoryUrl = `${WITV_BASE_URL}${categoryPath}`;
const response = await fetch(categoryUrl, {
headers: {
"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",
},
});
if (!response.ok) continue;
const html = await response.text();
const regex = new RegExp(`href="([^"]*/${id}-[^"\.]+\.html)"`, "i");
const match = html.match(regex);
if (match) {
const channelPath = match[1];
const pageUrl = channelPath.startsWith("http")
? channelPath
: `${WITV_BASE_URL}${channelPath}`;
console.log(`[WITV] Found channel page URL via search: ${pageUrl}`);
return pageUrl;
}
} catch (e) {
console.warn(`[WITV] Error searching in ${catId}: ${e.message}`);
}
}
return null;
}
// Wiflix (WITV) stream extraction
async function getWiflixStream(channelId, accessKey = null) {
console.log(`[WITV] Extracting stream for channel: ${channelId}`);
try {
const channelPageUrl = await findWiflixChannelPageUrl(channelId);
if (!channelPageUrl) {
throw new Error(`Could not find page URL for ${channelId}`);
}
console.log(`[WITV] Channel page URL: ${channelPageUrl}`);
const pageResponse = await fetch(channelPageUrl, {
headers: {
"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",
Referer: WITV_BASE_URL + "/",
},
});
if (!pageResponse.ok) {
throw new Error(`Failed to fetch channel page: ${pageResponse.status}`);
}
const pageHtml = await pageResponse.text();
console.log(`[WITV] Channel page length: ${pageHtml.length}`);
const iframeMatch = pageHtml.match(/<iframe[^>]*src=["']([^"']+)["']/i);
if (!iframeMatch) {
console.error("[WITV] No iframe found on channel page");
throw new Error("No iframe found on page");
}
let embedSrc = iframeMatch[1];
console.log(`[WITV] Found embed iframe: ${embedSrc}`);
// Type 1: witv-player.php
if (embedSrc.includes("witv-player.php")) {
const playerUrl = embedSrc.startsWith("http")
? embedSrc
: `${WITV_BASE_URL}${embedSrc}`;
console.log(
`[WITV] Type 1: witv-player detected, fetching ${playerUrl}`,
);
const playerResponse = await fetch(playerUrl, {
headers: {
"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",
Referer: channelPageUrl,
},
});
if (!playerResponse.ok) {
throw new Error(
`Failed to fetch player page: ${playerResponse.status}`,
);
}
const playerHtml = await playerResponse.text();
let m3u8Url = null;
const streamMatch = playerHtml.match(
/var\s+streamUrl\s*=\s*["']([^"']+)["']/,
);
if (streamMatch) {
m3u8Url = streamMatch[1];
} else {
const fileMatch = playerHtml.match(
/file:\s*["']([^"']+\.m3u8[^"']*)["']/,
);
if (fileMatch) {
m3u8Url = fileMatch[1];
} else {
const genericMatch = playerHtml.match(
/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/,
);
if (genericMatch) {
m3u8Url = genericMatch[1];
}
}
}
if (!m3u8Url) {
throw new Error("Could not extract stream URL from witv-player");
}
await addWiflixHeadersRule(m3u8Url);
return {
streams: [
{
title: "Orca",
url: m3u8Url,
originalUrl: m3u8Url,
behaviorHints: { notWebReady: false },
},
],
};
}
// Type 2: livehdtv.com
if (embedSrc.includes("livehdtv.com")) {
const cacheKey = `witv_livehdtv_${channelId}`;
const cachedStream = await getFromCache(cacheKey);
if (cachedStream) {
console.log(`[WITV] Found valid stream in cache for ${channelId}`);
await addWiflixHeadersRule(
cachedStream.url,
"https://www.livehdtv.com/",
);
return { streams: [cachedStream] };
}
console.log(
`[WITV] Type 2: livehdtv detected, using backend stream API...`,
);
const apiUrl = `${API_BASE_URL}/api/livetv/stream/tv/${channelId}`;
console.log(`[WITV] Calling backend stream API: ${apiUrl}`);
try {
const apiResponse = await fetch(apiUrl, {
headers: buildBackendApiHeaders(accessKey),
});
if (!apiResponse.ok) {
throw new Error(`Backend API error: ${apiResponse.status}`);
}
const apiData = await apiResponse.json();
if (apiData.error) {
throw new Error(apiData.error);
}
if (!apiData.streams || apiData.streams.length === 0) {
throw new Error("No streams returned from backend");
}
const stream = apiData.streams[0];
const m3u8Url = stream.originalUrl || stream.url;
await addWiflixHeadersRule(m3u8Url, "https://www.livehdtv.com/");
// Verify stream availability
console.log("[WITV] Verifying stream availability...");
let retries = 0;
const maxRetries = 20;
while (retries < maxRetries) {
try {
const checkResponse = await fetch(m3u8Url, {
method: "GET",
headers: {
Origin: "https://www.livehdtv.com",
Referer: "https://www.livehdtv.com/",
},
});
if (checkResponse.ok) {
console.log(`[WITV] Stream verified after ${retries} retries`);
break;
}
} catch (e) {
// retry
}
await new Promise((resolve) => setTimeout(resolve, 500));
retries++;
}
const streamData = {
title: "Orca",
url: m3u8Url,
originalUrl: m3u8Url,
behaviorHints: { notWebReady: false },
};
await saveToCache(cacheKey, streamData, 1);
return { streams: [streamData] };
} catch (apiError) {
console.error(
`[WITV] Backend API failed, trying direct fetch fallback:`,
apiError.message,
);
const livehdtvResponse = await fetch(embedSrc, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
Referer: "https://www.livehdtv.com/",
},
});
if (!livehdtvResponse.ok) {
throw new Error(
`Failed to fetch livehdtv page: ${livehdtvResponse.status}`,
);
}
const livehdtvHtml = await livehdtvResponse.text();
const innerIframeMatch = livehdtvHtml.match(
/<iframe[^>]*src=["']([^"']+)["']/i,
);
if (!innerIframeMatch) {
throw new Error("No inner iframe found in livehdtv page");
}
let tokenPhpUrl = innerIframeMatch[1];
if (!tokenPhpUrl.startsWith("http")) {
tokenPhpUrl = `https://www.livehdtv.com${tokenPhpUrl}`;
}
const tokenResponse = await fetch(tokenPhpUrl, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
Referer: embedSrc,
},
});
if (!tokenResponse.ok) {
throw new Error(
`Failed to fetch token.php: ${tokenResponse.status}`,
);
}
const tokenHtml = await tokenResponse.text();
const fileMatch = tokenHtml.match(
/file:\s*["']([^"']+\.m3u8[^"']*)["']/,
);
if (!fileMatch) {
throw new Error("Could not extract m3u8 from token.php");
}
const m3u8Url = fileMatch[1];
await addWiflixHeadersRule(m3u8Url, "https://www.livehdtv.com/");
return {
streams: [
{
title: "Orca",
url: m3u8Url,
originalUrl: m3u8Url,
behaviorHints: { notWebReady: false },
},
],
};
}
}
// Type 3: Unknown embed - try generic extraction
console.log(
`[WITV] Unknown embed type, attempting generic extraction from: ${embedSrc}`,
);
const unknownUrl = embedSrc.startsWith("http")
? embedSrc
: `${WITV_BASE_URL}${embedSrc}`;
const unknownResponse = await fetch(unknownUrl, {
headers: {
"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",
Referer: channelPageUrl,
},
});
if (!unknownResponse.ok) {
throw new Error(
`Failed to fetch unknown embed: ${unknownResponse.status}`,
);
}
const unknownHtml = await unknownResponse.text();
const m3u8Match = unknownHtml.match(
/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/,
);
if (!m3u8Match) {
throw new Error("Could not extract stream URL from unknown embed");
}
const m3u8Url = m3u8Match[1];
await addWiflixHeadersRule(m3u8Url);
return {
streams: [
{
title: "Orca",
url: m3u8Url,
originalUrl: m3u8Url,
behaviorHints: { notWebReady: false },
},
],
};
} catch (error) {
console.error("[WITV] Error extracting stream:", error);
throw error;
}
}
// Add DNR rule for Wiflix headers
async function addWiflixHeadersRule(
urlPattern,
referer = "https://witv.team/",
) {
try {
const url = new URL(urlPattern);
const domainPattern = `*://${url.hostname}/*`;
const origin = referer.endsWith("/") ? referer.slice(0, -1) : referer;
await addHeadersRule(domainPattern, {
Origin: origin,
Referer: referer,
});
} catch (e) {
console.error("[WITV] Failed to add headers rule:", e);
}
}
async function getBackendIframeSourceStream(
channelId,
accessKey = null,
logPrefix = "LIVE",
options = {},
) {
const requestUrl = new URL(
`${API_BASE_URL}/api/livetv/stream/tv/${channelId}`,
);
if (options.mode === "sources") {
requestUrl.searchParams.set("mode", "sources");
}
if (Number.isInteger(options.sourceIndex) && options.sourceIndex >= 0) {
requestUrl.searchParams.set("sourceIndex", String(options.sourceIndex));
}
const response = await fetch(requestUrl.toString(), {
headers: buildBackendApiHeaders(accessKey),
});
if (!response.ok) throw new Error(`Backend API error: ${response.status}`);
const data = await response.json();
if (options.mode === "sources") {
console.log(
`[${logPrefix}] Backend returned ${data.sources?.length || 0} source(s) for ${channelId}`,
);
return data;
}
if (data.streams && data.streams.length > 0) {
const normalizedStreams = [];
const isLiveTvRequest =
channelId.startsWith("livetv_") || logPrefix === "LIVETV";
for (const stream of data.streams) {
if (stream._isEmbed) {
const url = stream.originalUrl || stream.url;
if (isLiveTvRequest && url?.startsWith("http")) {
await addLiveTvEmbedHeadersRule(url);
}
normalizedStreams.push({
...stream,
url: url || stream.url,
originalUrl: url || stream.originalUrl,
referer: isLiveTvRequest ? LIVETV_EMBED_REFERER : stream.referer,
behaviorHints: { notWebReady: false },
});
continue;
}
const url = stream.originalUrl || stream.url;
if (!url || !url.startsWith("http")) continue;
if (isLiveTvRequest) {
await addLiveTvHeadersRule(url, stream.userAgent);
} else {
await addSosplayHeadersRule(url, stream.referer, stream.userAgent);
}
normalizedStreams.push({
...stream,
url,
originalUrl: url,
referer: isLiveTvRequest ? LIVETV_EMBED_REFERER : stream.referer,
behaviorHints: { notWebReady: false },
});
}
data.streams = normalizedStreams;
}
console.log(
`[${logPrefix}] Backend returned ${data.streams?.length || 0} stream(s) for ${channelId}`,
);
return data;
}
async function getSosplayStream(channelId, accessKey = null) {
console.log(`[SOSPLAY] Fetching stream logic via Extension: ${channelId}`);
try {
return await getBackendIframeSourceStream(
channelId,
accessKey,
"BOLALOCA",
);
let slug = channelId.replace("sosplay_", "");
let channelPageUrl = `${SOSPLAY_BASE_URL}/regardertv-${slug}-streaming-direct`;
console.log(`[SOSPLAY] Fetching channel page: ${channelPageUrl}`);
const pageResponse = await fetch(channelPageUrl, {
headers: {
"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",
Referer: SOSPLAY_BASE_URL,
},
});
if (!pageResponse.ok) {
console.log(
`[SOSPLAY] Failed to fetch channel page: ${pageResponse.status}`,
);
}
const pageHtml = await pageResponse.text();
const serverRegex =
/class="[^"]*change-video[^"]*"[^>]*data-embed="([^"]+)"[^>]*>([\s\S]*?)<\/span>/gi;
let match;
const servers = [];
while ((match = serverRegex.exec(pageHtml)) !== null) {
const rawName = match[2];
const cleanName = rawName.replace(/<[^>]+>/g, "").trim();
if (match[1] && cleanName) {
const idMatch = match[1].match(/id=(\d+)\/(\d+)/);
servers.push({
embedPath: match[1],
name: cleanName,
channelNum: idMatch ? idMatch[1] : null,
serverNum: idMatch ? idMatch[2] : null,
});
}
}
console.log(
`[SOSPLAY] Found ${servers.length} servers: ${servers.map((s) => s.name).join(", ")}`,
);
const allStreams = [];
const allEmbeds = [];
for (const server of servers) {
try {
console.log(`[SOSPLAY] Trying server: ${server.name}`);
const partUrl = `${SOSPLAY_BASE_URL}${server.embedPath}`;
const partResponse = await fetch(partUrl, {
headers: {
"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",
Referer: channelPageUrl,
},
});
const partHtml = await partResponse.text();
const tyIframeMatch = partHtml.match(/<iframe[^>]+src=["']([^"']+)/i);
if (!tyIframeMatch) {
console.warn(
`[SOSPLAY] No iframe in /part/ page for ${server.name}`,
);
continue;
}
let tyPageUrl = tyIframeMatch[1];
if (tyPageUrl.startsWith("//")) tyPageUrl = "https:" + tyPageUrl;
if (tyPageUrl.startsWith("/"))
tyPageUrl = SOSPLAY_BASE_URL + tyPageUrl;
const tyPageResponse = await fetch(tyPageUrl, {
headers: {
"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",
Referer: partUrl,
},
});
const tyPageHtml = await tyPageResponse.text();
const playerIframeMatch = tyPageHtml.match(
/<iframe[^>]+src=["']([^"']+)/i,
);
if (!playerIframeMatch) {
console.warn(
`[SOSPLAY] No player iframe in ty page for ${server.name}`,
);
continue;
}
let playerUrl = playerIframeMatch[1];
if (playerUrl.startsWith("//")) playerUrl = "https:" + playerUrl;
try {
const playerDomain = new URL(playerUrl).hostname;
await addHeadersRule(`*://${playerDomain}/*`, {
Referer: tyPageUrl,
Origin: new URL(tyPageUrl).origin,
});
} catch (e) {
console.warn(
`[SOSPLAY] Could not add pre-fetch DNR rule for ${server.name}:`,
e,
);
}
const playerResponse = await fetch(playerUrl, {
headers: {
"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",
},
});
const playerHtml = await playerResponse.text();
let m3u8Url = null;
const isHoca =
server.name.toLowerCase().includes("hoca") ||
playerUrl.includes("hoca");
if (isHoca) {
m3u8Url = decodeHocaStream(playerHtml);
} else {
m3u8Url = extractM3u8FromPackedHtml(playerHtml);
}
if (m3u8Url) {
console.log(
`[SOSPLAY] Found stream for ${server.name}: ${m3u8Url}`,
);
await addSosplayHeadersRule(m3u8Url, playerUrl);
allStreams.push({
title: `Sosplay - ${server.name}`,
url: m3u8Url,
originalUrl: m3u8Url,
behaviorHints: { notWebReady: false },
_referer: playerUrl,
userAgent: STREAM_PROXY_USER_AGENT,
});
} else {
console.warn(
`[SOSPLAY] Failed to decode stream for ${server.name}`,
);
}
} catch (serverError) {
console.warn(
`[SOSPLAY] Error with server ${server.name}:`,
serverError.message || serverError,
);
continue;
}
}
if (allStreams.length > 0) {
console.log(
`[SOSPLAY] Total streams found locally: ${allStreams.length}`,
);
return { streams: allStreams };
}
// Fallback: Use backend API
console.log(
"[SOSPLAY] Local extraction failed, falling back to Backend API",
);
const response = await fetch(
`${API_BASE_URL}/api/livetv/stream/tv/${channelId}`,
);
if (!response.ok)
throw new Error(`Backend API error: ${response.status}`);
const data = await response.json();
if (data.streams && data.streams.length > 0) {
for (const stream of data.streams) {
const url = stream.originalUrl || stream.url;
if (url && url.startsWith("http")) {
await addSosplayHeadersRule(url, stream.referer);
stream.url = url;
stream.behaviorHints = { notWebReady: false };
}
}
}
return data;
} catch (error) {
console.error("[SOSPLAY] Error fetching stream:", error);
throw error;
}
}
async function getLiveTvStream(channelId, accessKey = null, options = {}) {
console.log(`[LIVETV] Fetching stream logic via Extension: ${channelId}`);
try {
const sourceIndex =
Number.isInteger(options?.sourceIndex) && options?.sourceIndex >= 0
? options.sourceIndex
: null;
const mode = options?.mode === "sources" ? "sources" : "stream";
const decodedPath = decodeLiveTvChannelPath(channelId);
if (!decodedPath) {
throw new Error(`Could not decode channel path for ${channelId}`);
}
const eventUrl = absolutizeLiveTvUrl(
decodedPath,
LIVETV_BASE_URL,
LIVETV_BASE_URL,
);
if (!eventUrl) {
throw new Error(`Could not build event URL for ${channelId}`);
}
const eventPage = await fetchLiveTvText(eventUrl, LIVETV_BASE_URL);
if (!eventPage?.html) {
throw new Error(`Could not fetch event page ${eventUrl}`);
}
const webplayerEntries = extractLiveTvWebplayerEntries(
eventPage.html,
eventPage.finalUrl || eventUrl,
);
console.log(
`[LIVETV] Extracted ${webplayerEntries.length} webplayer link(s) locally for ${channelId}`,
);
if (mode === "sources") {
return {
sources: buildLiveTvSourceOptions(webplayerEntries),
};
}
const selectedEntries =
sourceIndex === null
? webplayerEntries
: webplayerEntries.filter((_, index) => index === sourceIndex);
if (selectedEntries.length === 0) {
return await getBackendIframeSourceStream(
channelId,
accessKey,
"LIVETV",
options,
);
}
const allStreams = [];
const allEmbeds = [];
for (const entry of selectedEntries) {
const candidateUrls = dedupeLiveTvItems(
[entry.exportUrl, entry.webplayerUrl].filter(Boolean),
(url) => url,
);
let resolved = { streams: [], embeds: [] };
for (const candidateUrl of candidateUrls) {
resolved = await resolveLiveTvMediaFromUrl(
candidateUrl,
eventPage.finalUrl || eventUrl,
6,
new Set(),
);
if (resolved.streams.length > 0 || resolved.embeds.length > 0) {
break;
}
}
for (const [streamIndex, stream] of resolved.streams.entries()) {
await addLiveTvHeadersRule(stream.url);
allStreams.push({
title:
resolved.streams.length > 1
? `${entry.title} ${streamIndex + 1}`
: entry.title,
url: stream.url,
originalUrl: stream.url,
behaviorHints: { notWebReady: false },
_referer: LIVETV_EMBED_REFERER,
userAgent: STREAM_PROXY_USER_AGENT,
});
}
for (const [embedIndex, embed] of resolved.embeds.entries()) {
await addLiveTvEmbedHeadersRule(embed.url);
allEmbeds.push({
title:
resolved.embeds.length > 1
? `${entry.title} Embed ${embedIndex + 1}`
: `${entry.title} Embed`,
url: embed.url,
originalUrl: embed.url,
referer: LIVETV_EMBED_REFERER,
behaviorHints: { notWebReady: false },
_referer: LIVETV_EMBED_REFERER,
userAgent: STREAM_PROXY_USER_AGENT,
_isEmbed: true,
});
}
}
const uniqueStreams = dedupeLiveTvItems(
allStreams,
(stream) => `${stream.url}__${stream._referer || ""}`,
);
const uniqueEmbeds = dedupeLiveTvItems(
allEmbeds,
(embed) => `${embed.url}__${embed._referer || ""}`,
);
if (uniqueStreams.length > 0 || uniqueEmbeds.length > 0) {
console.log(
`[LIVETV] Resolved ${uniqueStreams.length} direct stream(s) and ${uniqueEmbeds.length} embed(s) locally for ${channelId}`,
);
return {
streams: uniqueStreams.length > 0 ? uniqueStreams : uniqueEmbeds,
};
}
console.log(
"[LIVETV] Local extraction failed, falling back to Backend API",
);
return await getBackendIframeSourceStream(
channelId,
accessKey,
"LIVETV",
options,
);
} catch (error) {
console.warn(
`[LIVETV] Local extraction error for ${channelId}:`,
error.message || error,
);
return await getBackendIframeSourceStream(
channelId,
accessKey,
"LIVETV",
options,
);
}
}
function decodeLiveTvChannelPath(channelId) {
try {
const encodedPath = String(channelId || "").replace(/^livetv_/, "");
const normalized = encodedPath.replace(/-/g, "+").replace(/_/g, "/");
const paddingLength = (4 - (normalized.length % 4 || 4)) % 4;
return atob(`${normalized}${"=".repeat(paddingLength)}`);
} catch (error) {
return null;
}
}
function absolutizeLiveTvUrl(
rawUrl,
currentUrl = "",
fallbackBase = LIVETV_BASE_URL,
) {
if (!rawUrl) return null;
const normalized = String(rawUrl)
.trim()
.replace(/&amp;/gi, "&")
.replace(/\\u0026/g, "&")
.replace(/\\\//g, "/")
.replace(/\s+/g, "");
if (!normalized) return null;
if (normalized.startsWith("//")) {
const protocol = String(currentUrl || fallbackBase).startsWith("http://")
? "http:"
: "https:";
return `${protocol}${normalized}`;
}
try {
return new URL(normalized, currentUrl || fallbackBase).href;
} catch (error) {
return null;
}
}
function dedupeLiveTvItems(items, getKey) {
const seen = new Set();
const deduped = [];
for (const item of items) {
const key = getKey(item);
if (!key || seen.has(key)) continue;
seen.add(key);
deduped.push(item);
}
return deduped;
}
function stripLiveTvHtml(value) {
return String(value || "")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/gi, " ")
.replace(/&amp;/gi, "&")
.replace(/&quot;/gi, '"')
.replace(/&#39;/gi, "'")
.replace(/&#(\d+);/g, (_, code) => {
const value = Number.parseInt(code, 10);
return Number.isFinite(value) ? String.fromCodePoint(value) : "";
})
.replace(/&#x([0-9a-f]+);/gi, (_, code) => {
const value = Number.parseInt(code, 16);
return Number.isFinite(value) ? String.fromCodePoint(value) : "";
})
.replace(/\s+/g, " ")
.trim();
}
function buildLiveTvExportUrl(webplayerUrl, eventUrl = "") {
try {
const parsed = new URL(webplayerUrl, eventUrl || LIVETV_BASE_URL);
if (/\/export\/webplayer\.iframe\.php$/i.test(parsed.pathname)) {
return parsed.href;
}
if (!/\/webplayer(?:2)?\.php$/i.test(parsed.pathname)) {
return parsed.href;
}
let cdnHost = parsed.hostname;
if (!cdnHost.startsWith("cdn.")) {
const eventHost = new URL(eventUrl || LIVETV_BASE_URL).hostname.replace(
/^www\./i,
"",
);
cdnHost = `cdn.${eventHost}`;
}
parsed.protocol = "https:";
parsed.hostname = cdnHost;
parsed.pathname = "/export/webplayer.iframe.php";
return parsed.href;
} catch (error) {
return webplayerUrl;
}
}
function extractLiveTvWebplayerEntries(html, eventUrl) {
const entries = [];
const rawHtml = String(html || "");
const rowPattern = /<table[^>]+class=["']lnktbj["'][\s\S]*?<\/table>/gi;
for (const rowMatch of rawHtml.matchAll(rowPattern)) {
const rowHtml = rowMatch[0];
const hrefMatch = rowHtml.match(
/href=["']([^"']*\/webplayer(?:2)?\.php[^"']*)["']/i,
);
if (!hrefMatch) continue;
const webplayerUrl = absolutizeLiveTvUrl(
hrefMatch[1],
eventUrl,
LIVETV_BASE_URL,
);
if (!webplayerUrl) continue;
let streamType = "";
try {
streamType = new URL(webplayerUrl).searchParams.get("t") || "";
} catch (error) {
streamType = "";
}
if (streamType.toLowerCase() === "acestream") {
continue;
}
const language =
stripLiveTvHtml(
rowHtml.match(/<img[^>]+title=["']([^"']+)["']/i)?.[1] || "",
) || "Stream";
const bitrate = stripLiveTvHtml(
rowHtml.match(/class=["']bitrate["'][^>]*>([\s\S]*?)<\/td>/i)?.[1] ||
"",
);
const hoster = stripLiveTvHtml(
rowHtml.match(/class=["']lnktyt["'][^>]*>([\s\S]*?)<\/td>/i)?.[1] || "",
);
const title =
[language, hoster, bitrate].filter(Boolean).join(" - ") || language;
entries.push({
title,
language,
bitrate,
hoster,
sourceType: streamType,
webplayerUrl,
exportUrl: buildLiveTvExportUrl(webplayerUrl, eventUrl),
});
}
if (entries.length === 0) {
for (const hrefMatch of rawHtml.matchAll(
/href=["']([^"']*\/webplayer(?:2)?\.php[^"']*)["']/gi,
)) {
const webplayerUrl = absolutizeLiveTvUrl(
hrefMatch[1],
eventUrl,
LIVETV_BASE_URL,
);
if (!webplayerUrl) continue;
entries.push({
title: "Stream",
language: "",
bitrate: "",
hoster: "",
sourceType: "",
webplayerUrl,
exportUrl: buildLiveTvExportUrl(webplayerUrl, eventUrl),
});
}
}
if (entries.length === 0) {
const onclickPattern =
/show_webplayer\('([^']+)'\s*,\s*'([^']+)'\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*'([^']+)'\)/gi;
for (const match of rawHtml.matchAll(onclickPattern)) {
const [, type, contentId, eventId, linkId, countryId, streamId, lang] =
match;
if (String(type).toLowerCase() === "acestream") continue;
const webplayerUrl = absolutizeLiveTvUrl(
`/webplayer2.php?t=${encodeURIComponent(type)}&c=${encodeURIComponent(contentId)}&lang=${encodeURIComponent(lang)}&eid=${eventId}&lid=${linkId}&ci=${countryId}&si=${streamId}`,
eventUrl,
LIVETV_BASE_URL,
);
if (!webplayerUrl) continue;
entries.push({
title: stripLiveTvHtml(type) || "Stream",
language: stripLiveTvHtml(lang) || "",
bitrate: "",
hoster: "",
sourceType: stripLiveTvHtml(type) || "",
webplayerUrl,
exportUrl: buildLiveTvExportUrl(webplayerUrl, eventUrl),
});
}
}
return dedupeLiveTvItems(
entries,
(entry) => `${entry.exportUrl}__${entry.webplayerUrl}`,
);
}
function buildLiveTvSourceOptions(entries) {
return entries.map((entry, index) => ({
index,
title: entry.title || `Source ${index + 1}`,
language: entry.language || "",
bitrate: entry.bitrate || "",
hoster: entry.hoster || "",
sourceType: entry.sourceType || "",
}));
}
function shouldIgnoreLiveTvIframeUrl(rawUrl) {
const normalizedUrl = String(rawUrl || "").trim();
if (!normalizedUrl) {
return true;
}
if (/^(?:about:blank|javascript:|data:)/i.test(normalizedUrl)) {
return true;
}
try {
const parsed = new URL(normalizedUrl, LIVETV_BASE_URL);
const hostname = parsed.hostname.toLowerCase();
const pathname = parsed.pathname.toLowerCase();
const search = parsed.search.toLowerCase();
const combined = `${hostname}${pathname}${search}`;
if (
hostname === "ads.livetv882.me" ||
hostname.startsWith("ads.") ||
hostname.startsWith("ad.")
) {
return true;
}
if (pathname.includes("getbanner.php") || search.includes("zone_id=")) {
return true;
}
if (
/(?:^|[./_-])(banner|ads?|popunder|popup)(?:[./_-]|$)/i.test(combined)
) {
return true;
}
} catch (error) {
return false;
}
return false;
}
function shouldTreatLiveTvIframeAsTerminalEmbed(
rawUrl,
fallbackBase = LIVETV_BASE_URL,
) {
const normalizedUrl = String(rawUrl || "").trim();
if (!normalizedUrl) {
return false;
}
if (/\.(m3u8|mpd)(?:[?#]|$)/i.test(normalizedUrl)) {
return false;
}
try {
const parsed = new URL(normalizedUrl, fallbackBase || LIVETV_BASE_URL);
const fallbackHost = new URL(fallbackBase || LIVETV_BASE_URL).hostname
.replace(/^www\./i, "")
.toLowerCase();
const hostname = parsed.hostname.replace(/^www\./i, "").toLowerCase();
if (!hostname || !fallbackHost) {
return false;
}
return (
hostname !== fallbackHost && !hostname.endsWith(`.${fallbackHost}`)
);
} catch (error) {
return false;
}
}
function isLiveTvExportIframePage(rawUrl) {
try {
const parsed = new URL(rawUrl, LIVETV_BASE_URL);
return /\/export\/webplayer\.iframe\.php$/i.test(parsed.pathname);
} catch (error) {
return false;
}
}
function shouldFollowLiveTvIframeForExtraction(iframeUrl, pageUrl) {
try {
const page = new URL(pageUrl, LIVETV_BASE_URL);
if (!isLiveTvExportIframePage(page.href)) {
return false;
}
if ((page.searchParams.get("t") || "").toLowerCase() !== "alieztv") {
return false;
}
const iframe = new URL(iframeUrl, page.href);
const hostname = iframe.hostname.replace(/^www\./i, "").toLowerCase();
const pathname = iframe.pathname.toLowerCase();
return hostname === "emb.apl395.me" && pathname === "/player/live.php";
} catch (error) {
return false;
}
}
function extractLiveTvIframeUrls(html, currentUrl) {
const iframeUrls = [];
const iframePattern = /<iframe[^>]+src=["']([^"']+)["']/gi;
for (const match of String(html || "").matchAll(iframePattern)) {
const iframeUrl = absolutizeLiveTvUrl(
match[1],
currentUrl,
LIVETV_BASE_URL,
);
if (iframeUrl && !shouldIgnoreLiveTvIframeUrl(iframeUrl)) {
iframeUrls.push(iframeUrl);
}
}
return dedupeLiveTvItems(iframeUrls, (url) => url);
}
function extractLiveTvDirectStreams(html, referer) {
const streams = [];
const cleaned = String(html || "").replace(/\\\//g, "/");
const addCandidate = (rawUrl, candidateReferer = referer) => {
const absoluteUrl = absolutizeLiveTvUrl(
rawUrl,
candidateReferer,
LIVETV_BASE_URL,
);
if (!absoluteUrl) return;
if (!/\.(m3u8|mpd)(?:[?#]|$)/i.test(absoluteUrl)) return;
streams.push({ url: absoluteUrl, referer: candidateReferer });
};
const packedStreamUrl = extractM3u8FromPackedHtml(cleaned);
if (packedStreamUrl) {
addCandidate(packedStreamUrl, referer);
}
const hocaStreamUrl = decodeHocaStream(cleaned);
if (hocaStreamUrl) {
addCandidate(hocaStreamUrl, referer);
}
for (const match of cleaned.matchAll(
/pl\.init\(\s*['"]([^'"]+)['"]\s*\)/gi,
)) {
addCandidate(match[1], referer);
}
for (const match of cleaned.matchAll(
/manifestUrl\s*:\s*['"]([^'"]+)['"]/gi,
)) {
addCandidate(match[1], referer);
}
for (const match of cleaned.matchAll(
/(?:source|file|src)\s*[:=]\s*['"]([^'"]+\.(?:m3u8|mpd)[^'"]*)['"]/gi,
)) {
addCandidate(match[1], referer);
}
for (const match of cleaned.matchAll(
/['"]((?:https?:)?\/\/[^'"]+\.(?:m3u8|mpd)[^'"]*)['"]/gi,
)) {
addCandidate(match[1], referer);
}
return dedupeLiveTvItems(
streams,
(stream) => `${stream.url}__${stream.referer || ""}`,
);
}
async function addLiveTvHeadersRule(
targetUrl,
userAgent = STREAM_PROXY_USER_AGENT,
) {
try {
const url = new URL(targetUrl);
const rulePattern = `*://${url.host}${url.pathname}*`;
await addHeadersRule(rulePattern, {
Origin: LIVETV_EMBED_ORIGIN,
Referer: LIVETV_EMBED_REFERER,
"User-Agent": userAgent || STREAM_PROXY_USER_AGENT,
});
} catch (error) {
console.warn("[LIVETV] Failed to add page headers rule:", error);
}
}
async function addLiveTvEmbedHeadersRule(targetUrl) {
try {
const url = new URL(targetUrl);
const rulePattern = `*://${url.host}${url.pathname}*`;
await addHeadersRule(rulePattern, {
Origin: LIVETV_EMBED_ORIGIN,
Referer: LIVETV_EMBED_REFERER,
});
} catch (error) {
console.warn("[LIVETV] Failed to add embed headers rule:", error);
}
}
async function fetchLiveTvText(url, referer = "") {
try {
const absoluteUrl = absolutizeLiveTvUrl(url, referer, LIVETV_BASE_URL);
if (!absoluteUrl) return null;
await addLiveTvHeadersRule(absoluteUrl);
const response = await fetch(absoluteUrl, {
method: "GET",
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",
"User-Agent": STREAM_PROXY_USER_AGENT,
},
cache: "no-cache",
redirect: "follow",
});
if (!response.ok) {
console.warn(
`[LIVETV] Fetch failed for ${absoluteUrl}: ${response.status} ${response.statusText}`,
);
return null;
}
return {
html: await response.text(),
finalUrl: response.url || absoluteUrl,
};
} catch (error) {
console.warn(`[LIVETV] Fetch error for ${url}:`, error.message || error);
return null;
}
}
async function resolveLiveTvMediaFromUrl(
startUrl,
referer,
depth = 4,
visited = new Set(),
) {
const absoluteUrl = absolutizeLiveTvUrl(startUrl, referer, LIVETV_BASE_URL);
if (!absoluteUrl || visited.has(absoluteUrl)) {
return { streams: [], embeds: [] };
}
visited.add(absoluteUrl);
if (/\.(m3u8|mpd)(?:[?#]|$)/i.test(absoluteUrl)) {
return {
streams: [{ url: absoluteUrl, referer: referer || absoluteUrl }],
embeds: [],
};
}
const page = await fetchLiveTvText(absoluteUrl, referer);
if (!page?.html) {
return { streams: [], embeds: [] };
}
let streams = extractLiveTvDirectStreams(
page.html,
page.finalUrl || absoluteUrl,
);
let embeds = [];
const currentPageUrl = page.finalUrl || absoluteUrl;
const iframeUrls = extractLiveTvIframeUrls(
page.html,
page.finalUrl || absoluteUrl,
);
if (depth <= 0) {
return {
streams,
embeds: iframeUrls.map((url) => ({
url,
referer: LIVETV_EMBED_REFERER,
})),
};
}
for (const iframeUrl of iframeUrls) {
if (!shouldFollowLiveTvIframeForExtraction(iframeUrl, currentPageUrl)) {
embeds.push({ url: iframeUrl, referer: LIVETV_EMBED_REFERER });
continue;
}
console.log(`[LIVETV] Following iframe locally: ${iframeUrl}`);
const nested = await resolveLiveTvMediaFromUrl(
iframeUrl,
page.finalUrl || absoluteUrl,
depth - 1,
visited,
);
streams = streams.concat(nested.streams);
embeds = embeds.concat(nested.embeds);
if (nested.streams.length === 0 && nested.embeds.length === 0) {
embeds.push({ url: iframeUrl, referer: LIVETV_EMBED_REFERER });
}
}
return {
streams: dedupeLiveTvItems(
streams,
(stream) => `${stream.url}__${stream.referer || ""}`,
),
embeds: dedupeLiveTvItems(
embeds,
(embed) => `${embed.url}__${embed.referer || ""}`,
),
};
}
async function addSosplayHeadersRule(
urlPattern,
customReferer = null,
customUserAgent = STREAM_PROXY_USER_AGENT,
) {
try {
const url = new URL(urlPattern);
const pathNoExt = url.pathname.replace(/\.[^/.]+$/, "");
const rulePattern = `*://${url.host}${pathNoExt}*`;
const referer = customReferer || "https://dishtrainer.net/";
let origin;
try {
origin = new URL(referer).origin;
} catch {
origin = "https://dishtrainer.net";
}
const userAgent = customUserAgent || STREAM_PROXY_USER_AGENT;
await addHeadersRule(rulePattern, {
Origin: origin,
Referer: referer,
"User-Agent": userAgent,
});
} catch (e) {
console.error("[SOSPLAY] Failed to add headers rule:", e);
}
}
// === UTILS ===
function decodeHocaStream(html) {
try {
const atobMatch = html.match(/atob\(['"]([^'"]+)['"]\)/);
if (atobMatch) {
try {
const decoded = atob(atobMatch[1]);
if (decoded.includes(".m3u8")) return decoded;
} catch (e) {}
}
const urlArrayMatch = html.match(/return\s*\(\[([^\]]+)\]\.join/);
if (urlArrayMatch) {
try {
const chars = urlArrayMatch[1].match(/"([^"]*)"/g);
if (chars) {
let url = chars.map((c) => c.replace(/"/g, "")).join("");
url = url.replace(/\\\//g, "/");
if (url.includes(".m3u8")) return url;
}
} catch (e) {}
}
const srcMatch = html.match(
/(?:source|src|file)\s*[:=]\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)/i,
);
if (srcMatch) {
return srcMatch[1].replace(/\\\//g, "/");
}
const m3u8Match = html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)/);
if (m3u8Match) {
return m3u8Match[1].replace(/\\\//g, "/");
}
const packerResult = extractM3u8FromPackedHtml(html);
if (packerResult) return packerResult;
return null;
} catch (error) {
console.error("[SOSPLAY-HOCA] Error:", error.message || error);
return null;
}
}
/**
* Dean Edwards Packer decoder (see extractors.js for full documentation).
*
* Converts a number to its base-N string, builds a keyword lookup table,
* then replaces every placeholder token in the packed template with the
* corresponding keyword to recover the original readable JavaScript.
*
* @param {string} packedScript - Template with placeholder tokens
* @param {number} radix - Numeric base for token encoding
* @param {number} keywordCount - Number of keywords
* @param {string[]} keywords - Replacement words indexed by token value
* @returns {string} Decoded JavaScript source
*/
function decodeDeanEdwardsPacker(
packedScript,
radix,
keywordCount,
keywords,
) {
function numberToBaseNString(number) {
const quotient = Math.floor(number / radix);
const remainder = number % radix;
const digit =
remainder > 35
? String.fromCharCode(remainder + 29)
: remainder.toString(36);
return (quotient > 0 ? numberToBaseNString(quotient) : "") + digit;
}
const lookupTable = {};
for (let i = keywordCount - 1; i >= 0; i--) {
const token = numberToBaseNString(i);
lookupTable[token] = keywords[i] || token;
}
return packedScript.replace(/\b\w+\b/g, function (token) {
return lookupTable[token] !== undefined ? lookupTable[token] : token;
});
}
/**
* Extract M3U8 video URLs from HTML that contains Dean Edwards Packed
* script blocks (used by WigiStream / live TV embed pages).
*
* The function searches for all packed blocks in the HTML, decodes each
* one, and looks for .m3u8 stream URLs in the decoded output.
*
* @param {string} html - Raw HTML source of the embed page
* @returns {string|null} The best M3U8 URL found, or null
*/
function extractM3u8FromPackedHtml(html) {
try {
// Strategy 1: Try to match complete packed blocks with a regex.
// Split to avoid Chrome Web Store code scanner false positives.
const packerBlockPattern = new RegExp(
"ev" +
"al\\(function\\(p,a,c,k,e,(?:d|r)\\)\\{.*?return p\\}\\(\\s*['\"]([\\s\\S]*?)['\"]\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*['\"]([\\s\\S]*?)['\"]\\s*\\.split\\(['\"]\\|['\"]\\)\\s*(?:,\\s*0\\s*,\\s*\\{\\})?\\s*\\)\\)",
"gs",
);
const regexMatches = [...html.matchAll(packerBlockPattern)];
// Strategy 2: If the regex didn't match (malformed or unusual formatting),
// manually locate each packed block by searching for the marker string
// and parsing the arguments by hand.
if (regexMatches.length === 0) {
const markerPositions = [];
let searchPos = 0;
const marker = "ev" + "al(func" + "tion(p,a,c,k,e,";
while (true) {
const index = html.indexOf(marker, searchPos);
if (index === -1) break;
markerPositions.push(index);
searchPos = index + 1;
}
for (const position of markerPositions) {
// Take a chunk of HTML starting at the marker (5000 chars should
// be enough to contain the full packed block arguments)
const chunk = html.substring(position, position + 5000);
// Find the .split('|') that terminates the keyword list
const splitIdx = chunk.indexOf(".split('|')");
const splitIdx2 = chunk.indexOf('.split("|")');
const keywordListEnd = splitIdx !== -1 ? splitIdx : splitIdx2;
if (keywordListEnd === -1) continue;
// Extract the keyword string (everything between the last quote
// before .split and the .split itself)
let keywordStringStart = chunk.lastIndexOf("'", keywordListEnd - 1);
if (keywordStringStart === -1)
keywordStringStart = chunk.lastIndexOf('"', keywordListEnd - 1);
if (keywordStringStart === -1) continue;
const keywords = chunk
.substring(keywordStringStart + 1, keywordListEnd)
.split("|");
// Find the template string: starts after }(' or }("
const templateMarker1 = chunk.indexOf("}('");
const templateMarker2 = chunk.indexOf('}("');
const templateStart =
templateMarker1 !== -1 ? templateMarker1 : templateMarker2;
if (templateStart === -1) continue;
const quoteChar = chunk[templateStart + 2];
const templateBegin = templateStart + 3;
const templateEnd = chunk.indexOf(quoteChar + ",", templateBegin);
if (templateEnd === -1) continue;
const packedTemplate = chunk.substring(templateBegin, templateEnd);
// Parse the radix and count numbers that follow the template
const afterTemplate = chunk.substring(templateEnd + 2);
const numbersMatch = afterTemplate.match(/^\s*(\d+)\s*,\s*(\d+)\s*,/);
if (!numbersMatch) continue;
const radix = parseInt(numbersMatch[1]);
const keywordCount = parseInt(numbersMatch[2]);
const decodedScript = decodeDeanEdwardsPacker(
packedTemplate,
radix,
keywordCount,
keywords,
);
// Check if the decoded script contains video player code
const hasVideoContent =
decodedScript &&
(decodedScript.includes(".m3u8") ||
decodedScript.includes("hls") ||
decodedScript.includes("Clappr"));
if (hasVideoContent) {
const m3u8Url = findBestM3u8Url(decodedScript);
if (m3u8Url) return m3u8Url;
}
}
}
// Process regex-matched packed blocks
for (const packerMatch of regexMatches) {
const packedTemplate = packerMatch[1];
const radix = parseInt(packerMatch[2]);
const keywordCount = parseInt(packerMatch[3]);
const keywords = packerMatch[4].split("|");
const decodedScript = decodeDeanEdwardsPacker(
packedTemplate,
radix,
keywordCount,
keywords,
);
const hasVideoContent =
decodedScript.includes("Clappr") ||
decodedScript.includes(".m3u8") ||
decodedScript.includes("hls");
if (!hasVideoContent) continue;
const m3u8Url = findBestM3u8Url(decodedScript);
if (m3u8Url) return m3u8Url;
}
// Fallback: try to find M3U8 URLs directly in the raw HTML
const directSrcMatch = html.match(/src:\s*["']([^"']+\.m3u8[^"']*)/i);
if (directSrcMatch) return directSrcMatch[1].replace(/\\\//g, "/");
const directStreamMatch = html.match(
/["'](https?:\/\/[^"']*\.m3u8[^"']*)/,
);
if (directStreamMatch) return directStreamMatch[1].replace(/\\\//g, "/");
const varSrcMatch = html.match(/var\s+src\s*=\s*["']([^"']+)/i);
if (varSrcMatch && varSrcMatch[1].includes(".m3u8")) {
return varSrcMatch[1].replace(/\\\//g, "/");
}
return null;
} catch (error) {
return null;
}
}
/**
* Find the best M3U8 URL from decoded script content.
* Prefers backup/CDN URLs over primary ones.
*
* @param {string} scriptContent - Decoded JavaScript source
* @returns {string|null} Best M3U8 URL found, or null
*/
function findBestM3u8Url(scriptContent) {
const m3u8Pattern = /["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/g;
let match;
const urls = [];
while ((match = m3u8Pattern.exec(scriptContent)) !== null) {
urls.push(match[1].replace(/\\\//g, "/"));
}
if (urls.length === 0) return null;
// Prefer backup/live CDN URLs, then primary CDN, then first found
const backupUrl = urls.find(
(u) => u.includes("vuunov") || u.includes("live"),
);
const primaryUrl = urls.find(
(u) => u.includes("shop") || u.includes("srvagu"),
);
return backupUrl || primaryUrl || urls[0];
}
async function fetchSafe(url, context = "") {
try {
const options = {
method: "GET",
headers: {
Accept: "application/json",
"Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
},
cache: "no-cache",
};
const response = await fetch(url, options);
if (!response.ok) {
console.error(
`Fetch failed for ${context || url}: ${response.status} ${response.statusText}`,
);
return null;
}
return await response.json();
} catch (e) {
console.error(`Fetch error for ${context || url}:`, e);
return null;
}
}
// Cache helpers
async function getFromCache(key) {
const result = await chrome.storage.local.get(key);
const entry = result[key];
if (!entry) return null;
if (Date.now() > entry.expiry) {
chrome.storage.local.remove(key);
return null;
}
return entry.data;
}
async function saveToCache(key, data, ttlMinutes) {
const expiry = Date.now() + ttlMinutes * 60 * 1000;
await chrome.storage.local.set({ [key]: { data, expiry } });
}
// DNR Helper
let ruleIdCounter = 100;
async function addUserAgentRule(urlPattern, userAgent) {
return addHeadersRule(urlPattern, { "User-Agent": userAgent });
}
async function addHeadersRule(urlPattern, headers) {
const existingRules = await chrome.declarativeNetRequest.getDynamicRules();
const existingIds = new Set(existingRules.map((r) => r.id));
let id = ruleIdCounter;
while (existingIds.has(id)) {
id++;
}
ruleIdCounter = id + 1;
const requestHeaders = Object.entries(headers).map(([header, value]) => ({
header: header,
operation: "set",
value: value,
}));
const rule = {
id: id,
priority: 10,
action: {
type: "modifyHeaders",
requestHeaders: requestHeaders,
},
condition: {
urlFilter: urlPattern,
resourceTypes: [
"xmlhttprequest",
"media",
"websocket",
"other",
"sub_frame",
"main_frame",
],
},
};
try {
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [rule],
});
} catch (e) {
console.error("Failed to add dynamic rule:", e);
}
}
// --- END extension/Chrome/background.js ---
function postExtensionResponse(messageId, success, payload) {
pageWindow.postMessage(
success
? {
source: "MOVIX_EXTENSION",
messageId,
success: true,
data: payload,
}
: {
source: "MOVIX_EXTENSION",
messageId,
success: false,
error:
payload instanceof Error
? payload.message
: payload?.error || String(payload || "Erreur inconnue"),
},
"*",
);
}
async function requestAction(action, payload) {
try {
const result = await handleMessage({ action, payload });
if (result && result.error) {
return { success: false, error: result.error };
}
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function exposePageApi() {
pageWindow.__MOVIX_EXTENSION_INSTALLED = true;
pageWindow.hasMovixUserscript = true;
pageWindow.hasMovixNexusExtractor = true;
if (document.documentElement) {
document.documentElement.dataset.movixExtension = "true";
}
pageWindow.movixExtractM3u8 = async function movixExtractM3u8(type, url) {
const result = await requestAction("EXTRACT_M3U8", { type, url });
return result.success
? result.data
: { success: false, error: result.error || "Extraction impossible" };
};
pageWindow.movixExtractAllM3u8 = async function movixExtractAllM3u8(
sources,
) {
const result = await requestAction("EXTRACT_ALL_M3U8", { sources });
return result.success
? result.data
: {
success: false,
error: result.error || "Extraction multiple impossible",
results: [],
};
};
pageWindow.movixDetectEmbeds = async function movixDetectEmbeds(sources) {
const result = await requestAction("DETECT_EMBEDS", { sources });
return result.success ? result.data : { embeds: [] };
};
pageWindow.movixSetupHeaders = async function movixSetupHeaders(type, url) {
const result = await requestAction("SETUP_HEADERS", { type, url });
return result.success
? result.data
: { success: false, error: result.error || "Configuration impossible" };
};
pageWindow.dispatchEvent(new CustomEvent("movix-extension-loaded"));
}
pageWindow.addEventListener("message", async (event) => {
if (
event.source !== pageWindow ||
!event.data ||
event.data.source !== "MOVIX_WEB" ||
event.data.type !== "EXTENSION_REQUEST"
) {
return;
}
const { action, payload, messageId } = event.data;
const result = await requestAction(action, payload);
if (result.success) {
postExtensionResponse(messageId, true, result.data);
} else {
postExtensionResponse(
messageId,
false,
result.error || "Erreur inconnue",
);
}
});
exposePageApi();
Promise.resolve()
.then(() => (typeof setupRules === "function" ? setupRules() : undefined))
.catch((error) => {
console.error("[Movix Userscript] setupRules error:", error);
});
})();