MovixOpenSource/userscript/movix.user.js
Movix 7059124b24 Majs de urls + corrections scrapers + nouvelle source
Maj url dans l'app mobile, extension et userscript et backend : streamonsport, livetv.

Amélioration du scraper darkiworld/hydracker

Nouvelle source vo/vostfr qui est plutôt pas mal
Renommages des sources vo/vostfr avec le nom du doamine pour mieux se repérer
Correction de la synchronisation des données qui étais buggé sur certains navigateurs autre que chrome sur certaines versions
Amélioration de la détection du blocage dns
2026-05-06 22:54:06 +02:00

5095 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.cash/*
// @match https://*.movix.cash/*
// @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" };
const matches = html.match(/https?:\/\/.+\/v\.mp4/g);
if (!matches || matches.length === 0)
return { success: false, error: "Uqload: MP4 URL not found" };
const mp4Url = matches[0];
const result = { m3u8Url: mp4Url, 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.cash",
],
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.cash" ||
currentHostname.endsWith(".movix.cash")
) {
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);
});
})();