// ==UserScript== // @name Movix Proxy Extension (Tampermonkey) // @namespace https://movix.cash // @version 1.4.9 // @description Extension proxy pour Live TV Movix - Contourne CORS, injecte les headers et extrait les sources Nexus - version userscript Tampermonkey // @author Movix // @updateURL https://github.com/movixcorp/MovixOpenSource/raw/refs/heads/main/userscript/movix.user.js // @downloadURL https://github.com/movixcorp/MovixOpenSource/raw/refs/heads/main/userscript/movix.user.js // @match http://localhost/* // @match http://127.0.0.1/* // @match https://localhost/* // @match https://127.0.0.1/* // @match https://movix.cash/* // @match https://*.movix.cash/* // @match https://movix.club/* // @match https://*.movix.club/* // @match https://movix.cloud/* // @match https://*.movix.cloud/* // @match https://movix.tax/* // @match https://*.movix.tax/* // @match https://movix.golf/* // @match https://movix.chat/* // @match https://*.movix.chat/* // @match https://*.movix.golf/* // @match https://movix.date/* // @match https://*.movix.date/* // @match https://movix.show/* // @match https://*.movix.show/* // @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.9", 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) { // DNR urlFilter semantics: `*` is a wildcard, everything else is a // SUBSTRING match (not anchored). A bare filter like "/cfall/s" must match // anywhere in the URL, so it must NOT be wrapped in ^...$ — otherwise the // FCTV Referer rule never fires and the CDN 403s ("deny by referer"). 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.show"; // 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,"; const PACKER_SIGNATURE_PATTERN = new RegExp( "ev" + "al\\s*\\(\\s*function\\s*\\(\\s*p\\s*,\\s*a\\s*,\\s*c\\s*,\\s*k\\s*,\\s*e\\s*,\\s*d\\s*\\)", ); const UQLOAD_ROOT_DOMAINS = Object.freeze([ "uqload.is", "uqload.bz", "uqload.cx", "uqload.com", "uqload.net", "uqload.org", "uqload.to", "uqload.io", "uqload.co", ]); function getUqloadRootDomain(hostname) { const host = String(hostname || "") .toLowerCase() .replace(/\.$/, ""); return ( UQLOAD_ROOT_DOMAINS.find( (root) => host === root || host.endsWith(`.${root}`), ) || null ); } function parseAllowedUqloadUrl(rawUrl) { let parsed; try { parsed = new URL(String(rawUrl || "").trim()); } catch { throw new Error("Invalid Uqload URL"); } if ( parsed.protocol !== "https:" || parsed.username || parsed.password || (parsed.port && parsed.port !== "443") || !getUqloadRootDomain(parsed.hostname) ) { throw new Error("Invalid Uqload URL"); } return parsed; } function normalizeUqloadEmbedUrl(rawUrl) { const parsed = parseAllowedUqloadUrl(rawUrl); const lastPart = parsed.pathname.split("/").filter(Boolean).pop() || ""; const videoId = lastPart .replace(/^embed-/i, "") .replace(/\.html$/i, ""); if (!/^[a-z0-9_-]+$/i.test(videoId)) { throw new Error("Invalid Uqload URL"); } return `${parsed.origin}/embed-${videoId}.html`; } function getUqloadSiteOrigin(rawUrl) { const parsed = parseAllowedUqloadUrl(rawUrl); return `https://${getUqloadRootDomain(parsed.hostname)}`; } function md5Hash(str) { // Simple hash for cache keys (not cryptographic, just for dedup) let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return "h_" + Math.abs(hash).toString(36); } /** * Follow redirects and extract final HTML */ async function fetchWithRedirects( url, headers, maxRedirects = 3, timeoutMs = 3000, ) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { let currentUrl = url; let html = ""; const resp = await fetch(currentUrl, { headers, signal: controller.signal, redirect: "follow", }); html = await resp.text(); currentUrl = resp.url || currentUrl; for (let i = 0; i < maxRedirects; i++) { // Check if we have the content we need if ( /type=["']\s*application\/json\s*["']/.test(html) && html.includes("]*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 while tolerating formatter whitespace. const markerMatch = PACKER_SIGNATURE_PATTERN.exec(html); if (!markerMatch) return null; const markerIndex = markerMatch.index; // 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("|"); if ( radix < 2 || radix > 62 || keywordCount < 0 || keywordCount > 10000 || keywordCount > keywords.length ) { return null; } // Step 6: Decode and return the original script return decodeDeanEdwardsPacker( packedTemplate, radix, keywordCount, keywords, ); } function extractUqloadMediaUrl(html) { const candidates = []; const collect = (value) => { const normalized = String(value || "").replace(/\\\//g, "/"); for (const match of normalized.matchAll(/https:\/\/[^\s"'\\<>]+/gi)) { const candidate = match[0].replace(/[),;]+$/, ""); try { parseAllowedUqloadUrl(candidate); candidates.push(candidate); } catch { // Ignore URLs outside the Uqload domain allowlist. } } }; collect(html); const decoded = decodePackedScriptFromHtml(html); if (decoded) collect(decoded); return ( candidates.find((url) => /\/master\.m3u8(?:[?#]|$)/i.test(url)) || candidates.find((url) => /\.m3u8(?:[?#]|$)/i.test(url)) || candidates.find((url) => /\/v\.mp4(?:[?#]|$)/i.test(url)) || null ); } /** * Extract JSON from VOE HTML */ function extractJsonFromHtml(html) { // Pattern 1: script type=application/json let match = html.match( /]*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: "*/*", // Only resolve the redirect chain — don't download the video range: "bytes=0-0", referer: "https://video.sibnet.ru/", "user-agent": "Mozilla/5.0 Chrome/145.0.0.0", }, redirect: "follow", }); // response.url contains the final URL after all redirects if (mp4Resp.url && mp4Resp.url !== mp4Url) { mp4Url = mp4Resp.url; console.log(`[EXT-SIBNET] Followed redirect to: ${mp4Url}`); } // Stop any body download (in case the server ignored the Range header) try { await mp4Resp.body?.cancel(); } catch { /* already closed */ } } catch (e) { console.warn( "[EXT-SIBNET] Could not follow redirect, using original URL:", e, ); } const result = { m3u8Url: mp4Url, success: true, source: "sibnet" }; caches.sibnet.set(cacheKey, result); return result; } catch (e) { console.error("[EXT-SIBNET] Error:", e); return { success: false, error: e.message || "Sibnet extraction failed" }; } } /** * Extract HLS or MP4 from Uqload embed */ async function extractUqload(uqloadUrl) { console.log(`[EXT-UQLOAD] Extracting from: ${uqloadUrl}`); const cacheKey = md5Hash(uqloadUrl); const cached = caches.uqload.get(cacheKey); if (cached) return { ...cached, fromCache: true }; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); try { const fullUrl = normalizeUqloadEmbedUrl(uqloadUrl); const siteOrigin = getUqloadSiteOrigin(fullUrl); const headers = { "User-Agent": "Mozilla/5.0 Chrome/91.0.0.0", Accept: "text/html,*/*", Referer: `${siteOrigin}/`, Origin: siteOrigin, }; // Try embed and non-embed versions without leaving the validated host. const urls = [fullUrl, fullUrl.replace("/embed-", "/")]; let html = null; for (const url of urls) { try { const resp = await fetch(url, { headers, signal: controller.signal }); if (resp.ok) { html = await resp.text(); break; } } catch { continue; } } if (!html) return { success: false, error: "Uqload: Could not fetch page" }; if (html.includes("File was deleted")) return { success: false, error: "Uqload: File was deleted" }; const videoUrl = extractUqloadMediaUrl(html); if (!videoUrl) return { success: false, error: "Uqload: video URL not found" }; const result = { m3u8Url: videoUrl, success: true, source: "uqload" }; caches.uqload.set(cacheKey, result); return result; } catch (e) { console.error("[EXT-UQLOAD] Error:", e); return { success: false, error: e.message || "Uqload extraction failed" }; } finally { clearTimeout(timer); } } /** * Extract video URL from DoodStream embed */ async function extractDoodStream(doodUrl) { console.log(`[EXT-DOODSTREAM] Extracting from: ${doodUrl}`); const cacheKey = md5Hash(doodUrl); const cached = caches.doodstream.get(cacheKey); if (cached) return { ...cached, fromCache: true }; try { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 3000); const headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", Referer: "https://d0000d.com/", }; // Step 1: Fetch the embed page const resp = await fetch(doodUrl, { headers, redirect: "follow", signal: controller.signal, }); if (!resp.ok) { clearTimeout(timer); return { success: false, error: `DoodStream: HTTP ${resp.status}` }; } const html = await resp.text(); // Step 2: Extract pass_md5 URL and token const passMatch = html.match(/\/pass_md5\/[\w-]+\/(?[\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\.(is|cx|com|bz|net|org|to|io|co)/i.test(url), doodstream: (url) => { const lower = url.toLowerCase(); return ( lower.includes("d0000d.com") || lower.includes("doodstream.com") || lower.includes("dood.") || lower.includes("myvidplay.com") || lower.includes("dsvplay.com") || lower.includes("doply.net") ); }, seekstreaming: (url) => { 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; let uqloadHeaders; if (type === "fsvid" && url) { try { const hostname = new URL(url).hostname; // CDN subdomains (s1.fsvid.lol, s2.fsvid.lol, etc.) need fsvid.lol referer // Embed pages (fsvid.lol) need 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", }; } } if (type === "uqload" && url) { try { const origin = getUqloadSiteOrigin(url); uqloadHeaders = { Referer: `${origin}/`, Origin: origin }; } catch { return null; } } const headerMap = { voe: { Referer: "https://voe.sx/", Origin: "https://voe.sx" }, fsvid: fsvidHeaders || { Referer: "https://fsvid.lol/", Origin: "https://fsvid.lol", }, vidzy: { Referer: "https://vidzy.org/", Origin: "https://vidzy.org" }, vidmoly: { Referer: "https://voirdrama.to/", Origin: "https://voirdrama.to", }, sibnet: { Referer: "https://video.sibnet.ru/", Origin: "https://video.sibnet.ru", }, uqload: uqloadHeaders, doodstream: { Referer: referer || "https://d0000d.com/", Origin: referer ? new URL(referer).origin : "https://d0000d.com", }, seekstreaming: { Referer: referer || "https://lpayer.embed4me.com/", Origin: referer ? new URL(referer).origin : "https://lpayer.embed4me.com", }, cinep: { Referer: "https://purstream.mx/", Origin: "https://purstream.mx", }, }; 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 WITV_BASE_URL = "https://witv.football"; const SOSPLAY_BASE_URL = "https://streamonsport.art"; const LIVETV_BASE_URL = "https://livetv901.me/frx/"; const LIVETV_EMBED_ORIGIN = "https://livetv901.me"; const LIVETV_EMBED_REFERER = LIVETV_BASE_URL; // Backend API URL for got-scraping based extraction. // Dev override: on localhost (Vite dev on :3000) hit the local backend (:25565). const API_BASE_URL = typeof location !== "undefined" && (location.hostname === "localhost" || location.hostname === "127.0.0.1") ? "http://localhost:25565" : "https://api.movix.show"; 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: { wiflix: true, sosplay: true, livetv: true, matches: true, }, }; let extractionPrefs = DEFAULT_EXTRACTION_PREFS; // Extension enabled state let extensionEnabled = true; // Stats tracking (enriched with per-type counters) let sessionStats = { extractions: 0, corsFixed: 0, cached: 0, byType: { voe: 0, fsvid: 0, vidzy: 0, vidmoly: 0, sibnet: 0, uqload: 0, doodstream: 0, seekstreaming: 0, }, }; // Load initial state (async () => { const storedEnabled = await gmGetValueCompat( "movix_extensionEnabled", null, ); if (storedEnabled !== null) extensionEnabled = storedEnabled !== false; const storedStats = await gmGetValueCompat("movix_stats", null); if (storedStats) { sessionStats = { ...sessionStats, ...storedStats }; if (!sessionStats.byType) { sessionStats.byType = { voe: 0, fsvid: 0, vidzy: 0, vidmoly: 0, sibnet: 0, uqload: 0, doodstream: 0, seekstreaming: 0, }; } } const storedPrefs = await gmGetValueCompat("movix_extraction_prefs", null); if (storedPrefs) extractionPrefs = storedPrefs; })(); // Initial setup chrome.runtime.onInstalled.addListener(() => { setupRules(); }); chrome.runtime.onStartup.addListener(() => { setupRules(); // Reset session stats on startup sessionStats = { extractions: 0, corsFixed: 0, cached: 0, byType: { voe: 0, fsvid: 0, vidzy: 0, vidmoly: 0, sibnet: 0, uqload: 0, doodstream: 0, seekstreaming: 0, }, }; gmSetValueCompat("movix_stats", sessionStats); }); // Configure DNR rules for CORS and Headers async function setupRules() { // Clear existing dynamic rules to prevent accumulation const existingRules = await chrome.declarativeNetRequest.getDynamicRules(); const ruleIds = existingRules.map((rule) => rule.id); await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: ruleIds, }); // Reset rule counter when rules are cleared ruleIdCounter = 100; const rules = [ // 1. Allow CORS for everything (Response Headers) { id: 1, priority: 1, action: { type: "modifyHeaders", responseHeaders: [ { header: "Access-Control-Allow-Origin", operation: "set", value: "*", }, { header: "Access-Control-Allow-Methods", operation: "set", value: "GET, POST, OPTIONS, HEAD, PUT, DELETE, PATCH", }, { header: "Access-Control-Allow-Headers", operation: "set", value: "*", }, ], }, condition: { urlFilter: "*", initiatorDomains: [ "localhost", "127.0.0.1", "movix.cash", "movix.club", "movix.cloud", "movix.tax", "movix.golf", "movix.chat", "movix.date", "movix.show", ], 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("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" }; } // FCTV (matches) native: inject the player Referer on the CDN segments. case "SETUP_FCTV_HEADERS": { const okFctv = await setupFctvHeadersRule(payload?.referer); return { success: okFctv }; } // FCTV (matches) native: resolve ONE server locally (IP-bound) -> m3u8. case "RESOLVE_FCTV": { const fctvUrl = await resolveFctvStream(payload || {}); if (fctvUrl) return { success: true, url: fctvUrl }; return { success: false, error: "fctv resolve failed" }; } case "SET_EXTRACTION_PREFS": { const incoming = payload?.prefs; if ( incoming && incoming.version === 1 && incoming.m3u8 && incoming.livetv ) { extractionPrefs = { version: 1, m3u8: { ...DEFAULT_EXTRACTION_PREFS.m3u8, ...incoming.m3u8 }, livetv: { ...DEFAULT_EXTRACTION_PREFS.livetv, ...incoming.livetv }, }; await gmSetValueCompat("movix_extraction_prefs", extractionPrefs); return { success: true }; } return { success: false, error: "Invalid prefs shape" }; } case "GET_EXTRACTION_PREFS": return extractionPrefs; case "GET_CACHE_STATS": { if (typeof Extractors.getCacheSizes === "function") { return Extractors.getCacheSizes(); } return {}; } case "CLEAR_EXTRACTION_CACHE": { if (typeof Extractors.clearCaches === "function") { Extractors.clearCaches(payload?.type); return { success: true }; } return { success: false, error: "Cache API unavailable" }; } default: throw new Error(`Unknown action: ${action}`); } } // Helper to proxy HTTP requests via extension (to bypass Mixed Content) async function proxyHttpRequest(url, headers = {}) { try { if (headers && Object.keys(headers).length > 0) { try { const parsedUrl = new URL(url); const rulePattern = `*://${parsedUrl.host}${parsedUrl.pathname}*`; await addHeadersRule(rulePattern, headers); } catch (ruleError) { console.warn( "[PROXY_HTTP] Failed to add DNR headers rule:", ruleError, ); } } const response = await fetch(url, { headers }); const buffer = await response.arrayBuffer(); // Convert ArrayBuffer to Base64 let binary = ""; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } const base64 = btoa(binary); return { data: base64, contentType: response.headers.get("content-type"), status: response.status, finalUrl: response.url, }; } catch (e) { console.error("Proxy HTTP error:", e); return { error: e.message }; } } // === NEXUS M3U8 EXTRACTION HANDLERS === /** * Handle single embed extraction request * payload: { type: 'voe'|'fsvid'|..., url: 'https://...' } */ async function handleExtractM3u8(payload) { const { type, url } = payload; if (!url) return { success: false, error: "Missing URL" }; // Auto-detect type if not provided const embedType = type || Extractors.detectEmbedType(url); if (!embedType) return { success: false, error: "Unknown embed type" }; // Set up DNR headers BEFORE extraction so the fetch request succeeds try { const headerInfo = await Extractors.setupHeadersForService( embedType, url, ); if (headerInfo) { await addHeadersRule(headerInfo.domainPattern, headerInfo.headers); console.log( `[NEXUS] Pre-extraction DNR headers set for ${embedType}: ${headerInfo.domainPattern}`, ); } } catch (e) { console.warn("[NEXUS] Failed to set pre-extraction headers:", e); } console.log(`[NEXUS] Extracting ${embedType} from: ${url}`); const result = await Extractors.extractSingle(embedType, url); // Set up DNR headers for the extracted URL so the page player can use it if (result.success) { const videoUrl = result.hlsUrl || result.m3u8Url; // If the video URL is different from the page URL (likely), set headers for it too if (videoUrl && videoUrl !== url) { const headerInfo = await Extractors.setupHeadersForService( embedType, videoUrl, ); if (headerInfo) { await addHeadersRule(headerInfo.domainPattern, headerInfo.headers); console.log( `[NEXUS] DNR headers set for ${embedType}: ${headerInfo.domainPattern}`, ); } } } return result; } /** * Handle parallel extraction of all supported embeds from a sources list * payload: { sources: ['url1', 'url2', ...] or [{link:'url', player:'name'}, ...] } */ async function handleExtractAllM3u8(payload) { const { sources } = payload; if (!sources || !Array.isArray(sources) || sources.length === 0) { return { success: false, error: "No sources provided", results: [] }; } console.log(`[NEXUS] Extracting all from ${sources.length} sources`); // Set up DNR headers for all sources BEFORE extraction try { const detected = Extractors.detectSupportedEmbeds(sources); for (const item of detected) { const headerInfo = await Extractors.setupHeadersForService( item.type, item.url, ); if (headerInfo) { await addHeadersRule(headerInfo.domainPattern, headerInfo.headers); } } console.log( `[NEXUS] Pre-extraction headers set for ${detected.length} sources`, ); } catch (e) { console.warn( "[NEXUS] Failed to set pre-extraction headers for batch:", e, ); } const results = await Extractors.extractAll(sources); // Set up DNR headers for all successful extractions (video URLs) for (const result of results) { if (result.success) { const videoUrl = result.hlsUrl || result.m3u8Url; if (videoUrl) { const headerInfo = await Extractors.setupHeadersForService( result.type, videoUrl, ); if (headerInfo) { await addHeadersRule(headerInfo.domainPattern, headerInfo.headers); } } } } const successCount = results.filter((r) => r.success).length; return { success: successCount > 0, total: results.length, successCount, results, }; } /** * Handle embed type detection only (no extraction) * payload: { sources: ['url1', 'url2', ...] } */ function handleDetectEmbeds(payload) { const { sources } = payload; if (!sources || !Array.isArray(sources)) return { embeds: [] }; return { embeds: Extractors.detectSupportedEmbeds(sources) }; } // === API LOGIC === function getMovixFrontendOrigin() { try { const currentOrigin = pageWindow.location?.origin; const currentHostname = pageWindow.location?.hostname ?? ""; if ( currentHostname === "movix.cash" || currentHostname.endsWith(".movix.cash") || currentHostname === "movix.club" || currentHostname.endsWith(".movix.club") || currentHostname === "movix.cloud" || currentHostname.endsWith(".movix.cloud") || currentHostname === "movix.tax" || currentHostname.endsWith(".movix.tax") || currentHostname === "movix.golf" || currentHostname === "movix.chat" || currentHostname.endsWith(".movix.chat") || currentHostname === "movix.date" || currentHostname.endsWith(".movix.date") || currentHostname === "movix.show" || currentHostname.endsWith(".movix.show") || currentHostname.endsWith(".movix.golf") ) { return (currentOrigin || "https://movix.show").replace(/\/$/, ""); } } catch {} return "https://movix.show"; } 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() { 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 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: route to backend (handles matches_, TV Direct, etc.) console.log(`[CATALOG] 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(); } // 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 = /]*class="[^"]*holographic-card[^"]*"[^>]*>[\s\S]*?]*href="[^"]*\/(\d+)-[^"]*"[^>]*>[\s\S]*?<[^>]*class="[^"]*ann-short_price[^"]*"[^>]*>([^<]+) 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[^"]*"[^>]*>([^<]+) 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: route to backend (handles matches_, TV Direct, etc.) console.log(`[STREAM] Fetching stream via Backend: ${channelId}`); const response = await fetch( `${API_BASE_URL}/api/livetv/stream/tv/${channelId}`, { headers: buildBackendApiHeaders(accessKey), }, ); if (!response.ok) throw new Error(`Backend API error: ${response.status}`); return await response.json(); } // 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(/]*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( /]*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.football/", ) { 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(/]+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( /]+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(/&/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(/ /gi, " ") .replace(/&/gi, "&") .replace(/"/gi, '"') .replace(/'/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 = /]+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(/]+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.livetv901.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 = /]+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 }); } // === FCTV (matches): Referer rule + IP-bound local resolver =============== const FCTV_HEADERS_RULE_ID = 60; async function setupFctvHeadersRule(referer) { if (!referer) return false; const ref = referer.endsWith("/") ? referer : referer + "/"; let origin; try { origin = new URL(ref).origin; } catch { origin = ref.replace(/\/+$/, ""); } try { await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [FCTV_HEADERS_RULE_ID], addRules: [ { id: FCTV_HEADERS_RULE_ID, priority: 20, action: { type: "modifyHeaders", requestHeaders: [ { header: "Referer", operation: "set", value: ref }, { header: "Origin", operation: "set", value: origin }, { header: "User-Agent", operation: "set", value: STREAM_PROXY_USER_AGENT }, ], }, condition: { urlFilter: "/cfall/s", resourceTypes: ["xmlhttprequest", "media", "other"], }, }, ], }); return true; } catch (e) { console.error("[FCTV] Failed to add headers rule:", e); return false; } } const FCTV_API_BASE = "https://apis-data-defra10.tcore131ybdf.ru"; const FCTV_TOKEN_KEYSTREAM_HEX = "15764bab80a419c6abdd5518f3db0ea95bb3b9a2e2b519ce5c159af6917e2000c2d680ae30706a3aba1c9c25786c7c28774eecf20450a3cf414ca17f6472798cfa557c7a8705b7861f06e84f827f8a24676eeab77ce504bfc335b79609b9"; function fctvRot47(s) { let out = ""; for (let i = 0; i < s.length; i++) { const k = s.charCodeAt(i); out += k < 33 || k > 126 ? s[i] : String.fromCharCode(33 + ((k - 33 + 47) % 94)); } return out; } function fctvReadVarint(buf, off) { let result = 0n, shift = 0n, cur = off; while (cur < buf.length) { const b = buf[cur++]; result |= BigInt(b & 0x7f) << shift; if ((b & 0x80) === 0) return { value: result, offset: cur }; shift += 7n; if (shift > 70n) break; } throw new Error("varint"); } function fctvDecode(buf, depth = 0) { const fields = []; let off = 0; while (off < buf.length) { let tag; try { tag = fctvReadVarint(buf, off); } catch { break; } off = tag.offset; const field = Number(tag.value >> 3n); const wt = Number(tag.value & 7n); const e = { field, wireType: wt }; if (wt === 0) { const p = fctvReadVarint(buf, off); off = p.offset; } else if (wt === 1) { off += 8; } else if (wt === 2) { const pl = fctvReadVarint(buf, off); off = pl.offset; const len = Number(pl.value); const bytes = buf.subarray(off, off + len); off += len; let text = ""; try { text = new TextDecoder().decode(bytes); } catch {} let printable = 0; for (let i = 0; i < text.length; i++) { const c = text.charCodeAt(i); if ((c >= 32 && c <= 126) || c >= 160) printable++; } if (text && printable / text.length > 0.7) e.value = text; if (depth < 8 && bytes.length) { try { const ch = fctvDecode(bytes, depth + 1); if (ch.length) e.children = ch; } catch {} } } else if (wt === 5) { off += 4; } else break; fields.push(e); } return fields; } const fctvField = (fields, f) => (fields || []).find((e) => e.field === f); const fctvChildren = (fields, f) => { const x = fctvField(fields, f); return (x && x.children) || []; }; function fctvMakeToken(rbSession) { const ks = []; for (let i = 0; i < FCTV_TOKEN_KEYSTREAM_HEX.length; i += 2) ks.push(parseInt(FCTV_TOKEN_KEYSTREAM_HEX.substr(i, 2), 16)); const pt = new TextEncoder().encode(rbSession); const n = Math.min(pt.length, ks.length); let bin = ""; for (let i = 0; i < n; i++) bin += String.fromCharCode(pt[i] ^ ks[i]); return encodeURIComponent(btoa(bin) + "a"); } async function resolveFctvStream(opts) { const { matchId, streamId, siteType, sportType, referer, apiBase } = opts || {}; if (!matchId || !streamId) return null; const base = apiBase || FCTV_API_BASE; const u = new URL(base + "/api/stream/detail"); u.searchParams.set("streamId", String(streamId)); u.searchParams.set("siteType", String(siteType || 2001)); u.searchParams.set("continent", "EU"); u.searchParams.set("country", "FR"); u.searchParams.set("digit", "seth"); u.searchParams.set("matchId", String(matchId)); u.searchParams.set("sportType", String(sportType || 1)); const resp = await fetch(u.toString(), { headers: { Accept: "*/*" } }); const rbSession = resp.headers.get("rb-session"); const buf = new Uint8Array(await resp.arrayBuffer()); const root = fctvDecode(buf); const body = fctvChildren(root, 10); const inner = fctvChildren(body, 2).length ? fctvChildren(body, 2) : body; const maskedField = fctvField(inner, 4); const masked = maskedField && typeof maskedField.value === "string" ? maskedField.value : ""; if (!masked || !rbSession) return null; let parsed; try { parsed = new URL(fctvRot47(masked).slice(8)); } catch { return null; } const token = fctvMakeToken(rbSession); if (referer) await setupFctvHeadersRule(referer); return `${parsed.origin}/token-${token}${parsed.pathname}${parsed.search}`; } 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); }); })();