MovixOpenSource/app/tests/mediaProxyRuntime.test.mjs
Movix 74b9f9448d fix: lecteurs, proxies locaux et mise à jour Android 2.5.4
- Corrige l’extraction Uqload dans les extensions Chrome/Firefox et le userscript
- Ajoute un proxy média local Android avec streaming binaire
- Corrige Vidzy, Fsvid, Bravo et les autres sources protégées sur mobile
- Préserve correctement les en-têtes Origin, Referer et Range
- Ajoute la réécriture locale des playlists HLS et de leurs segments
- Corrige la reprise de l’installation après l’autorisation des sources inconnues
- Corrige la détection d’une installation annulée ou terminée
- Passe l’application en version 2.5.4, build 13
- Rend cette mise à jour obligatoire et regroupe toutes les notes de version
- Régénère le userscript mobile et l’APK Android signé
- Ajoute des tests de régression Kotlin

Co-authored-by: GPT-5.6 Sol <codex@openai.com>
2026-07-27 15:18:25 +02:00

163 lines
4.8 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { test } from 'node:test';
import vm from 'node:vm';
import ts from 'typescript';
async function loadBridgeRuntimeBuilder() {
const sourceUrl = new URL('../src/injection/bridge-runtime.ts', import.meta.url);
let source = await readFile(sourceUrl, 'utf8');
source = source.replace(
/import\s+\{\s*MEDIA_ENTRY_PATH_SOURCE\s*\}\s+from\s+['"]\.\/mediaProxyRouting['"];\s*/,
`const MEDIA_ENTRY_PATH_SOURCE = ${JSON.stringify(String.raw`\.(?:m3u8|mp4|m4v|m4s|mpd|ts|aac|m4a|vtt|srt)(?:$|[?#])`)};\n`,
);
const transpiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ES2022,
},
fileName: sourceUrl.pathname,
});
const dataUrl = `data:text/javascript;base64,${Buffer.from(transpiled.outputText).toString('base64')}`;
return import(dataUrl);
}
function createRuntimeHarness(buildBridgeRuntime, { rejectOpen = false } = {}) {
const posted = [];
const nativeFetches = [];
const listeners = new Map();
class CustomEvent {
constructor(type, init = {}) {
this.type = type;
this.detail = init.detail;
}
}
const window = {
__MOVIX_BRIDGE_READY: false,
addEventListener(type, handler) {
if (!listeners.has(type)) listeners.set(type, new Set());
listeners.get(type).add(handler);
},
dispatchEvent(event) {
for (const handler of listeners.get(event.type) || []) handler(event);
},
fetch: async url => {
nativeFetches.push(String(url));
return {
status: 206,
statusText: 'Partial Content',
url: String(url),
headers: new Map([
['content-type', 'application/vnd.apple.mpegurl'],
['content-range', 'bytes 0-2/3'],
]),
arrayBuffer: async () => Uint8Array.from([1, 2, 3]).buffer,
text: async () => 'LOCAL',
};
},
};
window.ReactNativeWebView = {
postMessage(raw) {
const message = JSON.parse(raw);
posted.push(message);
queueMicrotask(() => {
if (message.type === 'GM_OPEN_MEDIA_PROXY') {
window.dispatchEvent(new CustomEvent('__MOVIX_BRIDGE_RESPONSE', {
detail: rejectOpen
? { id: message.id, success: false, error: 'unavailable' }
: {
id: message.id,
success: true,
value: 'http://127.0.0.1:28123/p/opaque-session',
},
}));
return;
}
window.dispatchEvent(new CustomEvent('__MOVIX_BRIDGE_RESPONSE', {
detail: {
id: message.id,
success: true,
status: 200,
statusText: 'OK',
headers: { 'content-type': 'application/octet-stream' },
body: 'BAUG',
finalUrl: message.url,
},
}));
});
},
};
const context = vm.createContext({
window,
CustomEvent,
Uint8Array,
ArrayBuffer,
URLSearchParams,
Promise,
console,
atob,
btoa,
setTimeout: () => 1,
clearTimeout: () => {},
});
vm.runInContext(buildBridgeRuntime(), context);
return { window, posted, nativeFetches };
}
function gmRequest(window, details) {
return new Promise((resolve, reject) => {
window.GM_xmlhttpRequest({
responseType: 'arraybuffer',
...details,
onload: resolve,
onerror: reject,
});
});
}
test('protected media uses the native loopback proxy without a Base64 GM_FETCH', async () => {
const { buildBridgeRuntime } = await loadBridgeRuntimeBuilder();
const harness = createRuntimeHarness(buildBridgeRuntime);
const response = await gmRequest(harness.window, {
method: 'GET',
url: 'https://u14.vidzy.cc/movie/master.m3u8?token=abc',
headers: {
Origin: 'https://vidzy.org',
Referer: 'https://vidzy.org/',
},
});
assert.deepEqual(harness.posted.map(entry => entry.type), [
'GM_OPEN_MEDIA_PROXY',
]);
assert.deepEqual(harness.nativeFetches, [
'http://127.0.0.1:28123/p/opaque-session',
]);
assert.deepEqual([...new Uint8Array(response.response)], [1, 2, 3]);
});
test('falls back to GM_FETCH when the native proxy is unavailable', async () => {
const { buildBridgeRuntime } = await loadBridgeRuntimeBuilder();
const harness = createRuntimeHarness(buildBridgeRuntime, { rejectOpen: true });
const response = await gmRequest(harness.window, {
method: 'GET',
url: 'https://r1.fsvid.lol/movie/master.m3u8',
headers: {
Origin: 'https://fsvid.lol',
Referer: 'https://fsvid.lol/',
},
});
assert.deepEqual(harness.posted.map(entry => entry.type), [
'GM_OPEN_MEDIA_PROXY',
'GM_FETCH',
]);
assert.deepEqual([...new Uint8Array(response.response)], [4, 5, 6]);
});