From a57d848e48a57eeb89ab01a681fb96d6d616607b Mon Sep 17 00:00:00 2001 From: siritami <102145692+FiorenMas@users.noreply.github.com> Date: Thu, 21 May 2026 15:22:28 +0700 Subject: [PATCH 1/4] AVS: Fix new cf block --- plugins/vietnamese/AnimeVietsub.ts | 47 +- public/static/src/vi/animevietsub/player.js | 498 +++++++++++++++++++- 2 files changed, 507 insertions(+), 38 deletions(-) diff --git a/plugins/vietnamese/AnimeVietsub.ts b/plugins/vietnamese/AnimeVietsub.ts index c5c90211..d40b6b0b 100644 --- a/plugins/vietnamese/AnimeVietsub.ts +++ b/plugins/vietnamese/AnimeVietsub.ts @@ -12,7 +12,7 @@ class AnimeVietsubPlugin implements Plugin.PluginBase { name = 'AnimeVietsub'; icon = 'src/vi/animevietsub/icon.png'; site = 'https://animevietsub.site'; - version = '1.0.10'; + version = '1.0.11'; customJS = 'src/vi/animevietsub/player.js'; @@ -591,33 +591,16 @@ class AnimeVietsubPlugin implements Plugin.PluginBase { const rawJson = pdMatch[1].replace(/\\\//g, '/'); const pd = JSON.parse(rawJson); - // Case A: iframe player at storage.googleapiscdn.com + // Case A: iframe player at stream.googleapiscdn.com + // The player page is behind Cloudflare managed challenge, so + // fetchText cannot reach it. Embed the iframe directly and let + // the WebView handle the Cloudflare challenge + JWPlayer boot. if ( pd.playTech === 'iframe' && typeof pd.link === 'string' && pd.link.includes('googleapiscdn.com') ) { - try { - const playerHtml = await fetchText(pd.link, { - headers: { Referer: this.site + '/' }, - }); - const idM = playerHtml.match(/const\s+id\s*=\s*"([0-9a-f]+)"/); - const tokM = playerHtml.match(/const\s+avsToken\s*=\s*"([^"]+)"/); - if (idM && tokM) { - const videoId = idM[1]; - const token = tokM[1]; - const base = - pd.link.match(/^(https?:\/\/[^/]+)/)?.[1] || - 'https://storage.googleapiscdn.com'; - const m3u8 = `${base}/playlist/${videoId}/playlist.m3u8?token=${encodeURIComponent(token)}&plain=1`; - return this.buildPlayerHtml({ - m3u8, - referer: pd.link, - }); - } - } catch (_) { - // - } + return this.buildPlayerHtml({ iframe: pd.link }); } // Case B: api / all with sources array @@ -675,12 +658,10 @@ class AnimeVietsubPlugin implements Plugin.PluginBase { } } - // When embed is off, don't fall back to hash/iframe - if (!this.enableEmbed) { - return '

Không tìm thấy nguồn m3u8 cho tập phim này. Bật "Bật embed" trong cài đặt plugin để dùng fallback.

'; - } - // ── 2. Fallback: extract data-hash/data-id for AJAX via customJS ── + // The customJS handles the AJAX response: if the server returns an + // iframe URL (e.g. googleapiscdn), it uses hidden-iframe token + // extraction → direct m3u8 playback. Always allow this path. const $ = loadCheerio(html); this.checkCommonBlocked($); const cleanPath = chapterPath.split('?')[0].split('#')[0]; @@ -710,7 +691,10 @@ class AnimeVietsubPlugin implements Plugin.PluginBase { } // ── 3. Last resort: embed the episode page in an iframe ── - return this.buildPlayerHtml({ iframe: url }); + if (this.enableEmbed) { + return this.buildPlayerHtml({ iframe: url }); + } + return '

Không tìm thấy nguồn video cho tập phim này.

'; } // ── Helper: build the HTML container for customJS ── @@ -734,8 +718,9 @@ class AnimeVietsubPlugin implements Plugin.PluginBase { if (opts.id) attrs.push(`data-id="${esc(opts.id)}"`); if (opts.referer) attrs.push(`data-referer="${esc(opts.referer)}"`); if (opts.site) attrs.push(`data-site="${esc(opts.site)}"`); + attrs.push(`data-embed-enabled="${this.enableEmbed ? '1' : '0'}"`); - const mode = opts.m3u8 ? 'Đang ở chế độ m3u8' : 'Đang ở chế độ embed'; + const mode = opts.m3u8 ? 'Đang ở chế độ m3u8' : 'Đang tải video…'; return [ `
Đang tải video...

', '
', '', - `

${mode}

`, + `

${mode}

`, '', ].join('\n'); } diff --git a/public/static/src/vi/animevietsub/player.js b/public/static/src/vi/animevietsub/player.js index 7bc4d796..3d678b07 100644 --- a/public/static/src/vi/animevietsub/player.js +++ b/public/static/src/vi/animevietsub/player.js @@ -20,6 +20,49 @@ var inner = document.getElementById('avs-player-inner'); if (!inner) return; + var embedEnabled = container.getAttribute('data-embed-enabled') === '1'; + var modeLabel = document.getElementById('avs-mode-label'); + var _debugLog = []; + + // ─── Native fetch bridge (bypasses CORS via React Native) ──────── + var _nfCallbacks = {}; + window._nativeFetchResolve = function (id, status, text, headers) { + if (_nfCallbacks[id]) { + _nfCallbacks[id].resolve({ status: status, text: text, headers: headers || {} }); + delete _nfCallbacks[id]; + } + }; + window._nativeFetchReject = function (id, err) { + if (_nfCallbacks[id]) { + _nfCallbacks[id].reject(new Error(err)); + delete _nfCallbacks[id]; + } + }; + function nativeFetch(url, headers) { + if (!window.ReactNativeWebView) { + return fetch(url, { credentials: 'include', headers: headers }) + .then(function (r) { + var h = {}; + r.headers.forEach(function (v, k) { h[k.toLowerCase()] = v; }); + return r.text().then(function (t) { return { status: r.status, text: t, headers: h }; }); + }); + } + return new Promise(function (resolve, reject) { + var id = 'nf_' + Date.now() + '_' + Math.random().toString(36).substr(2); + _nfCallbacks[id] = { resolve: resolve, reject: reject }; + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'native-fetch', + data: { url: url, requestId: id, headers: headers || {}, responseType: 'text' } + })); + setTimeout(function () { + if (_nfCallbacks[id]) { + _nfCallbacks[id].reject(new Error('Timeout')); + delete _nfCallbacks[id]; + } + }, 30000); + }); + } + // ─── Priority 1: direct m3u8 URL (from parseChapter extraction) ── var m3u8 = container.getAttribute('data-m3u8'); if (m3u8) { @@ -44,12 +87,23 @@ // ─── Priority 3: iframe embed ──────────────────────────────────── var iframeSrc = container.getAttribute('data-iframe'); if (iframeSrc) { + if (iframeSrc.indexOf('googleapiscdn.com') !== -1) { + console.log('[AVS] googleapiscdn detected, trying token extraction…'); + tryGoogleApisCdn(iframeSrc, inner); + return; + } + if (!embedEnabled) { + showError('Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.'); + if (modeLabel) modeLabel.textContent = ''; + return; + } console.log('[AVS] Embedding iframe:', iframeSrc.substring(0, 80)); inner.innerHTML = ''; + if (modeLabel) modeLabel.textContent = 'Đang ở chế độ embed'; return; } @@ -114,11 +168,22 @@ function handlePlayerResponse(json) { // iframe player if (json.playTech === 'iframe' && typeof json.link === 'string') { + if (json.link.indexOf('googleapiscdn.com') !== -1) { + console.log('[AVS] AJAX returned googleapiscdn, trying token extraction…'); + tryGoogleApisCdn(json.link, inner); + return; + } + if (!embedEnabled) { + showError('Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.'); + if (modeLabel) modeLabel.textContent = ''; + return; + } inner.innerHTML = ''; + if (modeLabel) modeLabel.textContent = 'Đang ở chế độ embed'; return; } @@ -135,12 +200,16 @@ buildVideoPlayer(inner, [{ file: link, type: 'hls' }]); } else if (/\.(mp4|webm)(\?|$)/i.test(link)) { buildVideoPlayer(inner, [{ file: link }]); - } else { + } else if (embedEnabled) { inner.innerHTML = ''; + if (modeLabel) modeLabel.textContent = 'Đang ở chế độ embed'; + } else { + showError('Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.'); + if (modeLabel) modeLabel.textContent = ''; } return; } @@ -148,14 +217,429 @@ showError('Định dạng phát không được hỗ trợ.'); } + // ─── googleapiscdn: two-layer m3u8 decryption ───────────────── + // + // Layer 1: AES-GCM decrypt concatenated _t params → intermediate m3u8 + // Layer 2: AES-CTR decrypt segment URLs → real CDN URLs + // + // If any phase fails, fall back to iframe embed. + function tryGoogleApisCdn(playerUrl, target) { + + // Create hidden iframe to solve Cloudflare challenge + var iframe = document.createElement('iframe'); + iframe.style.cssText = + 'position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;'; + iframe.src = playerUrl; + (document.body || document.documentElement).appendChild(iframe); + + // Wait for Cloudflare, then try fetch + var cfWait = 4000; // 4s for CF challenge + debugLog('Đợi CF ' + cfWait + 'ms…'); + setTimeout(function () { + debugLog('CF done, fetching page…'); + fetchPlayerPage(playerUrl, target, iframe); + }, cfWait); + } + + function fetchPlayerPage(playerUrl, target, iframe) { + // Use native fetch bridge to bypass CORS + // React Native reads cookies from CookieManager (shared with WebView) + // so Cloudflare cf_clearance cookie is included automatically + nativeFetch(playerUrl, { Referer: playerUrl }) + .then(function (res) { + if (res.status !== 200) throw new Error('HTTP ' + res.status + ' (len=' + (res.text || '').length + ')'); + var html = res.text; + debugLog('Page OK, size=' + html.length); + cleanupIframe(iframe); + + // Extract avsToken from inline script + var tokenMatch = html.match( + /const\s+avsToken\s*=\s*"([^"]+)"/ + ); + if (!tokenMatch) { + debugLog('No avsToken! First 150: ' + html.substring(0, 150)); + return; + } + var avsToken = tokenMatch[1]; + debugLog('Token: ' + avsToken.substring(0, 30) + '…'); + + // Extract video hash from URL + var hashMatch = playerUrl.match(/\/player\/([0-9a-f]+)/); + if (!hashMatch) { + fallbackToEmbed(playerUrl, target); + return; + } + var videoHash = hashMatch[1]; + + // Fetch m3u8 with token + debugLog('Fetching m3u8…'); + + var baseUrl = playerUrl.match(/^(https?:\/\/[^/]+)/)[1]; + var m3u8Url = baseUrl + + '/playlist/' + videoHash + + '/playlist.m3u8?token=' + encodeURIComponent(avsToken); + + return nativeFetch(m3u8Url, { Referer: playerUrl }).then(function (m3u8Res) { + var m3u8Text = m3u8Res.text; + var m3u8Headers = m3u8Res.headers || {}; + debugLog('m3u8 OK, size=' + m3u8Text.length); + processEncryptedM3u8(m3u8Text, m3u8Headers, avsToken, playerUrl, target); + }); + }) + .catch(function (err) { + cleanupIframe(iframe); + debugLog('Fetch fail: ' + err.message); + }); + } + + // ─── Crypto helpers (ported from avs-loader.min.js v1.12.16) ────── + function b64urlDecode(str) { + var s = str.replace(/-/g, '+').replace(/_/g, '/'); + s += '=='.slice(0, (4 - s.length % 4) % 4); + var binary = atob(s); + var bytes = new Uint8Array(binary.length); + for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes; + } + + function lcgNext(state) { + return (Math.imul(state, 1664525) + 1013904223) >>> 0; + } + + function stringUnshuffle(str, seed) { + var chars = str.split(''); + var len = chars.length; + var state = (parseInt(seed.substring(0, 8), 16) >>> 0); + var swaps = []; + for (var i = len - 1; i > 0; i--) { + state = lcgNext(state); + swaps.push([i, state % (i + 1)]); + } + for (var k = swaps.length - 1; k >= 0; k--) { + var a = swaps[k][0], b = swaps[k][1]; + var tmp = chars[a]; chars[a] = chars[b]; chars[b] = tmp; + } + return chars.join(''); + } + + function createPRNG(seed) { + var hash = 2166136261; + for (var i = 0; i < seed.length; i++) { + hash = ((hash ^ (seed.charCodeAt(i) & 255)) >>> 0); + hash = (Math.imul(hash, 16777619)) >>> 0; + } + var state = (hash >>> 0) || 1; + return function () { + state ^= (state << 13); state >>>= 0; + state ^= (state >>> 17); state >>>= 0; + state ^= (state << 5); + return (state >>>= 0); + }; + } + + function descramble(data, permKey, permSalt) { + var input = data instanceof Uint8Array ? data : new Uint8Array(data); + var len = input.length; + var output = new Uint8Array(len); + if (len === 0) return output.buffer; + var rng = createPRNG(permKey + '|' + permSalt); + var perm = new Uint32Array(len); + for (var i = 0; i < len; i++) perm[i] = i; + for (var i = len - 1; i > 0; i--) { + var j = rng() % (i + 1); + var t = perm[i]; perm[i] = perm[j]; perm[j] = t; + } + var xorState = 0; + for (var i = 0; i < len; i++) { + if (!(i & 3)) xorState = rng(); + output[perm[i]] = input[i] ^ ((xorState >>> (8 * (i & 3))) & 255); + } + return output.buffer; + } + + // ─── Two-layer m3u8 decryption ────────────────────────────────── + function processEncryptedM3u8(m3u8Text, m3u8Headers, avsToken, playerUrl, target) { + debugLog('Decrypting (2-layer)…'); + + // Parse JWT → jti + var jwtParts = avsToken.split('.'); + var payload; + try { + payload = JSON.parse(atob(jwtParts[1])); + } catch (e) { + debugLog('JWT parse failed'); + fallbackToEmbed(playerUrl, target); + return; + } + var jti = payload.jti; + debugLog('JTI: ' + jti.substring(0, 20) + '…'); + + // Extract jtiOdd (every odd-indexed char → 64-char hex string) + var jtiOdd = ''; + for (var k = 0; k < jti.length; k++) { + if (k % 2 === 1) jtiOdd += jti[k]; + } + + // Read session params from m3u8 response headers + var cn = m3u8Headers['x-edge-tag'] || ''; + var sk = m3u8Headers['x-cache-node'] || ''; + var ts = m3u8Headers['x-request-trace'] || '0'; + var uid = ''; + try { uid = decodeURIComponent(m3u8Headers['x-proxy-digest'] || 'anon'); } catch (e) { uid = 'anon'; } + debugLog('cn=' + cn.substring(0, 16) + ' sk=' + sk.substring(0, 16) + ' ts=' + ts); + + if (!cn || !sk) { + // Try envelope header (base64url-encoded JSON with cn, sk, ts, uid) + var envHeader = m3u8Headers['x-avs-envelope'] || m3u8Headers['x-stream-envelope'] || ''; + if (envHeader) { + try { + var envJson = JSON.parse(new TextDecoder().decode(b64urlDecode(envHeader))); + cn = envJson.cn || cn; + sk = envJson.sk || sk; + ts = envJson.ts || ts; + uid = envJson.uid || uid; + } catch (e) { /* envelope parse failed */ } + } + } + + if (!cn || !sk) { + debugLog('No cn/sk → cannot decrypt'); + fallbackToEmbed(playerUrl, target); + return; + } + + // ── Layer 1: AES-GCM batch decrypt of _t values ── + var lines = m3u8Text.split('\n'); + var tValues = []; + var headerLines = []; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (line.startsWith('#') || line.trim() === '') { + if (!line.match(/^#EXTINF:/) && !line.match(/^#EXT-X-ENDLIST/) && !line.match(/^#EXT-X-KEY/)) { + headerLines.push(line); + } + } else { + var tm = line.match(/[?&]_t=([^&\s]+)/); + if (tm) tValues.push(tm[1]); + } + } + + debugLog('_t segments: ' + tValues.length); + + if (tValues.length === 0) { + debugLog('No _t params in m3u8'); + fallbackToEmbed(playerUrl, target); + return; + } + + // Concatenate and unshuffle + var concatenated = tValues.join(''); + var unshuffled = stringUnshuffle(concatenated, sk); + + // Base64url decode to get the encrypted blob + var encryptedBlob; + try { + encryptedBlob = b64urlDecode(unshuffled); + } catch (e) { + debugLog('b64 decode failed: ' + e.message); + fallbackToEmbed(playerUrl, target); + return; + } + + debugLog('Blob: ' + encryptedBlob.length + ' bytes'); + + // Derive AES-GCM key: HMAC-SHA256(key=b64decode(cn), data="uid:ts:sk:0") + var cnBytes = b64urlDecode(cn); + var hmacDataStr = uid + ':' + ts + ':' + sk + ':0'; + var hmacData = new TextEncoder().encode(hmacDataStr); + var iv = cnBytes.slice(0, 12); + + crypto.subtle.importKey( + 'raw', cnBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] + ).then(function (hmacKey) { + return crypto.subtle.sign('HMAC', hmacKey, hmacData); + }).then(function (gcmKeyBuf) { + return crypto.subtle.importKey( + 'raw', gcmKeyBuf, { name: 'AES-GCM' }, false, ['decrypt'] + ); + }).then(function (gcmKey) { + debugLog('AES-GCM decrypt ' + encryptedBlob.length + 'B…'); + return crypto.subtle.decrypt( + { name: 'AES-GCM', iv: iv }, gcmKey, encryptedBlob + ); + }).then(function (rawResult) { + // Apply descrambler (crypto harden is active) + var descrambled = descramble(rawResult, sk, ts); + var intermediateM3u8 = new TextDecoder().decode(descrambled); + debugLog('Intermediate: ' + intermediateM3u8.length + 'ch, first80=' + intermediateM3u8.substring(0, 80)); + + if (intermediateM3u8.length < 10) { + debugLog('Intermediate too short'); + fallbackToEmbed(playerUrl, target); + return; + } + + // ── Layer 2: AES-CTR decrypt segment URLs ── + decryptSegmentUrls(intermediateM3u8, headerLines, jtiOdd, playerUrl, target); + }).catch(function (err) { + debugLog('Layer 1 (GCM) failed: ' + (err && err.message || err)); + fallbackToEmbed(playerUrl, target); + }); + } + + function decryptSegmentUrls(intermediateM3u8, headerLines, jtiOdd, playerUrl, target) { + var lines = intermediateM3u8.split('\n'); + var segments = []; + + // Parse /hls/{fileId}.ts?e=...&i=... or absolute URLs with same pattern + var hlsRe = /\/hls\/([0-9a-f]{24})\.ts[^#\s]*/; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line.startsWith('#') || line === '') continue; + + var m = line.match(hlsRe); + if (m) { + // Extract query params from URL + var qIdx = line.indexOf('?'); + var params = {}; + if (qIdx !== -1) { + line.substring(qIdx + 1).split('&').forEach(function (p) { + var eq = p.indexOf('='); + if (eq !== -1) params[p.substring(0, eq)] = p.substring(eq + 1); + }); + } + segments.push({ + fileId: m[1], + e: params.e || '', + i: parseInt(params.i || '0', 10), + lineIdx: i + }); + } + } + + debugLog('Segments to decrypt: ' + segments.length); + + if (segments.length === 0) { + // Intermediate m3u8 might already have direct URLs (no /hls/ pattern) + debugLog('No /hls/ segments in intermediate m3u8'); + fallbackToEmbed(playerUrl, target); + return; + } + + // Derive AES-CTR key per fileId (usually all same) + var hmacKeyBytes = new TextEncoder().encode(jtiOdd); + var keyCache = {}; + + function deriveCtrKey(fileId) { + if (keyCache[fileId]) return Promise.resolve(keyCache[fileId]); + var signData = new TextEncoder().encode('url-cipher|' + fileId); + return crypto.subtle.importKey( + 'raw', hmacKeyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] + ).then(function (k) { + return crypto.subtle.sign('HMAC', k, signData); + }).then(function (buf) { + return crypto.subtle.importKey('raw', buf, { name: 'AES-CTR' }, false, ['decrypt']); + }).then(function (ctrKey) { + keyCache[fileId] = ctrKey; + return ctrKey; + }); + } + + // Decrypt all segments + var promises = segments.map(function (seg) { + return deriveCtrKey(seg.fileId).then(function (ctrKey) { + var encrypted = b64urlDecode(seg.e); + var counter = new Uint8Array(16); + var idx = seg.i; + counter[12] = (idx >>> 24) & 0xff; + counter[13] = (idx >>> 16) & 0xff; + counter[14] = (idx >>> 8) & 0xff; + counter[15] = idx & 0xff; + return crypto.subtle.decrypt( + { name: 'AES-CTR', counter: counter, length: 64 }, ctrKey, encrypted + ).then(function (dec) { + return { lineIdx: seg.lineIdx, url: new TextDecoder().decode(dec) }; + }); + }); + }); + + Promise.all(promises).then(function (results) { + var validCount = 0; + var outLines = lines.slice(); // copy intermediate lines + + results.forEach(function (r) { + if (r.url.indexOf('http') === 0) { + outLines[r.lineIdx] = r.url; + validCount++; + } + }); + + debugLog('Decrypted ' + validCount + '/' + results.length + ' URLs'); + if (validCount === 0) { + fallbackToEmbed(playerUrl, target); + return; + } + + // Build clean m3u8: header lines + decrypted segment lines + var cleanLines = []; + for (var i = 0; i < headerLines.length; i++) cleanLines.push(headerLines[i]); + for (var i = 0; i < outLines.length; i++) { + var l = outLines[i]; + if (!l) continue; + if (l.indexOf('#EXT-X-KEY:') === 0 && l.indexOf('urn:avs:shield') !== -1) continue; + // Skip /hls/ placeholder lines that weren't decrypted + if (l.match(/\/hls\/[0-9a-f]{24}\.ts/)) continue; + cleanLines.push(l); + } + + var cleanM3u8 = cleanLines.join('\n'); + var blob = new Blob([cleanM3u8], { type: 'application/vnd.apple.mpegurl' }); + var blobUrl = URL.createObjectURL(blob); + + if (modeLabel) modeLabel.textContent = 'Đang ở chế độ m3u8'; + buildVideoPlayer(target, [{ file: blobUrl, type: 'hls' }]); + }).catch(function (err) { + debugLog('Layer 2 (CTR) failed: ' + (err && err.message || err)); + fallbackToEmbed(playerUrl, target); + }); + } + + function fallbackToEmbed(playerUrl, target) { + if (!embedEnabled) { + console.log('[AVS] Embed disabled, showing error'); + showError('Không thể giải mã video. Bật "Bật embed" trong cài đặt plugin để xem qua iframe.'); + if (modeLabel) modeLabel.textContent = ''; + return; + } + console.log('[AVS] Falling back to iframe embed'); + target.innerHTML = + ''; + if (modeLabel) modeLabel.textContent = 'Đang ở chế độ embed'; + } + + function cleanupIframe(iframe) { + try { iframe.src = 'about:blank'; } catch (e) {} + setTimeout(function () { + try { iframe.remove(); } catch (e) {} + }, 200); + } + // ─── Utilities ────────────────────────────────────────────────── - function showError(msg) { - if (inner) { - inner.innerHTML = - '

' + - msg + - '

'; + function debugLog(msg) { + _debugLog.push(msg); + console.log('[AVS] ' + msg); + var el = document.getElementById('avs-debug-log'); + if (!el && inner) { + inner.innerHTML = '
'; + el = document.getElementById('avs-debug-log'); } + if (el) el.textContent = _debugLog.join('\n'); + } + function showError(msg) { + debugLog('ERROR: ' + msg); } function escapeAttr(s) { From 92955dc5a5426b556350de6ad858b1ddea20f4b8 Mon Sep 17 00:00:00 2001 From: Fioren <102145692+FiorenMas@users.noreply.github.com> Date: Thu, 21 May 2026 23:22:28 +0700 Subject: [PATCH 2/4] change to new api --- public/static/src/vi/animevietsub/player.js | 44 ++++----------------- 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/public/static/src/vi/animevietsub/player.js b/public/static/src/vi/animevietsub/player.js index 3d678b07..34451b89 100644 --- a/public/static/src/vi/animevietsub/player.js +++ b/public/static/src/vi/animevietsub/player.js @@ -24,43 +24,15 @@ var modeLabel = document.getElementById('avs-mode-label'); var _debugLog = []; - // ─── Native fetch bridge (bypasses CORS via React Native) ──────── - var _nfCallbacks = {}; - window._nativeFetchResolve = function (id, status, text, headers) { - if (_nfCallbacks[id]) { - _nfCallbacks[id].resolve({ status: status, text: text, headers: headers || {} }); - delete _nfCallbacks[id]; - } - }; - window._nativeFetchReject = function (id, err) { - if (_nfCallbacks[id]) { - _nfCallbacks[id].reject(new Error(err)); - delete _nfCallbacks[id]; - } - }; + // ─── Fetch helper (uses reader.fetch proxy to bypass CORS) ────── function nativeFetch(url, headers) { - if (!window.ReactNativeWebView) { - return fetch(url, { credentials: 'include', headers: headers }) - .then(function (r) { - var h = {}; - r.headers.forEach(function (v, k) { h[k.toLowerCase()] = v; }); - return r.text().then(function (t) { return { status: r.status, text: t, headers: h }; }); - }); - } - return new Promise(function (resolve, reject) { - var id = 'nf_' + Date.now() + '_' + Math.random().toString(36).substr(2); - _nfCallbacks[id] = { resolve: resolve, reject: reject }; - window.ReactNativeWebView.postMessage(JSON.stringify({ - type: 'native-fetch', - data: { url: url, requestId: id, headers: headers || {}, responseType: 'text' } - })); - setTimeout(function () { - if (_nfCallbacks[id]) { - _nfCallbacks[id].reject(new Error('Timeout')); - delete _nfCallbacks[id]; - } - }, 30000); - }); + var fetchFn = (window.reader && window.reader.fetch) || fetch; + return fetchFn(url, { credentials: 'include', headers: headers }) + .then(function (r) { + var h = {}; + r.headers.forEach(function (v, k) { h[k.toLowerCase()] = v; }); + return r.text().then(function (t) { return { status: r.status, text: t, headers: h }; }); + }); } // ─── Priority 1: direct m3u8 URL (from parseChapter extraction) ── From c151efb52b30110bea1ab43ec28522d85c6ec557 Mon Sep 17 00:00:00 2001 From: Fioren <102145692+FiorenMas@users.noreply.github.com> Date: Thu, 21 May 2026 23:25:00 +0700 Subject: [PATCH 3/4] lint --- public/static/src/vi/animevietsub/player.js | 380 +++++++++++++------- 1 file changed, 249 insertions(+), 131 deletions(-) diff --git a/public/static/src/vi/animevietsub/player.js b/public/static/src/vi/animevietsub/player.js index 34451b89..be5c7164 100644 --- a/public/static/src/vi/animevietsub/player.js +++ b/public/static/src/vi/animevietsub/player.js @@ -27,12 +27,17 @@ // ─── Fetch helper (uses reader.fetch proxy to bypass CORS) ────── function nativeFetch(url, headers) { var fetchFn = (window.reader && window.reader.fetch) || fetch; - return fetchFn(url, { credentials: 'include', headers: headers }) - .then(function (r) { + return fetchFn(url, { credentials: 'include', headers: headers }).then( + function (r) { var h = {}; - r.headers.forEach(function (v, k) { h[k.toLowerCase()] = v; }); - return r.text().then(function (t) { return { status: r.status, text: t, headers: h }; }); - }); + r.headers.forEach(function (v, k) { + h[k.toLowerCase()] = v; + }); + return r.text().then(function (t) { + return { status: r.status, text: t, headers: h }; + }); + }, + ); } // ─── Priority 1: direct m3u8 URL (from parseChapter extraction) ── @@ -65,7 +70,9 @@ return; } if (!embedEnabled) { - showError('Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.'); + showError( + 'Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.', + ); if (modeLabel) modeLabel.textContent = ''; return; } @@ -141,12 +148,16 @@ // iframe player if (json.playTech === 'iframe' && typeof json.link === 'string') { if (json.link.indexOf('googleapiscdn.com') !== -1) { - console.log('[AVS] AJAX returned googleapiscdn, trying token extraction…'); + console.log( + '[AVS] AJAX returned googleapiscdn, trying token extraction…', + ); tryGoogleApisCdn(json.link, inner); return; } if (!embedEnabled) { - showError('Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.'); + showError( + 'Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.', + ); if (modeLabel) modeLabel.textContent = ''; return; } @@ -180,7 +191,9 @@ 'allowfullscreen allow="autoplay; fullscreen; encrypted-media">'; if (modeLabel) modeLabel.textContent = 'Đang ở chế độ embed'; } else { - showError('Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.'); + showError( + 'Nguồn này chỉ hỗ trợ embed. Bật "Bật embed" trong cài đặt plugin.', + ); if (modeLabel) modeLabel.textContent = ''; } return; @@ -196,7 +209,6 @@ // // If any phase fails, fall back to iframe embed. function tryGoogleApisCdn(playerUrl, target) { - // Create hidden iframe to solve Cloudflare challenge var iframe = document.createElement('iframe'); iframe.style.cssText = @@ -219,15 +231,16 @@ // so Cloudflare cf_clearance cookie is included automatically nativeFetch(playerUrl, { Referer: playerUrl }) .then(function (res) { - if (res.status !== 200) throw new Error('HTTP ' + res.status + ' (len=' + (res.text || '').length + ')'); + if (res.status !== 200) + throw new Error( + 'HTTP ' + res.status + ' (len=' + (res.text || '').length + ')', + ); var html = res.text; debugLog('Page OK, size=' + html.length); cleanupIframe(iframe); // Extract avsToken from inline script - var tokenMatch = html.match( - /const\s+avsToken\s*=\s*"([^"]+)"/ - ); + var tokenMatch = html.match(/const\s+avsToken\s*=\s*"([^"]+)"/); if (!tokenMatch) { debugLog('No avsToken! First 150: ' + html.substring(0, 150)); return; @@ -247,16 +260,27 @@ debugLog('Fetching m3u8…'); var baseUrl = playerUrl.match(/^(https?:\/\/[^/]+)/)[1]; - var m3u8Url = baseUrl + - '/playlist/' + videoHash + - '/playlist.m3u8?token=' + encodeURIComponent(avsToken); - - return nativeFetch(m3u8Url, { Referer: playerUrl }).then(function (m3u8Res) { - var m3u8Text = m3u8Res.text; - var m3u8Headers = m3u8Res.headers || {}; - debugLog('m3u8 OK, size=' + m3u8Text.length); - processEncryptedM3u8(m3u8Text, m3u8Headers, avsToken, playerUrl, target); - }); + var m3u8Url = + baseUrl + + '/playlist/' + + videoHash + + '/playlist.m3u8?token=' + + encodeURIComponent(avsToken); + + return nativeFetch(m3u8Url, { Referer: playerUrl }).then( + function (m3u8Res) { + var m3u8Text = m3u8Res.text; + var m3u8Headers = m3u8Res.headers || {}; + debugLog('m3u8 OK, size=' + m3u8Text.length); + processEncryptedM3u8( + m3u8Text, + m3u8Headers, + avsToken, + playerUrl, + target, + ); + }, + ); }) .catch(function (err) { cleanupIframe(iframe); @@ -267,7 +291,7 @@ // ─── Crypto helpers (ported from avs-loader.min.js v1.12.16) ────── function b64urlDecode(str) { var s = str.replace(/-/g, '+').replace(/_/g, '/'); - s += '=='.slice(0, (4 - s.length % 4) % 4); + s += '=='.slice(0, (4 - (s.length % 4)) % 4); var binary = atob(s); var bytes = new Uint8Array(binary.length); for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); @@ -281,15 +305,18 @@ function stringUnshuffle(str, seed) { var chars = str.split(''); var len = chars.length; - var state = (parseInt(seed.substring(0, 8), 16) >>> 0); + var state = parseInt(seed.substring(0, 8), 16) >>> 0; var swaps = []; for (var i = len - 1; i > 0; i--) { state = lcgNext(state); swaps.push([i, state % (i + 1)]); } for (var k = swaps.length - 1; k >= 0; k--) { - var a = swaps[k][0], b = swaps[k][1]; - var tmp = chars[a]; chars[a] = chars[b]; chars[b] = tmp; + var a = swaps[k][0], + b = swaps[k][1]; + var tmp = chars[a]; + chars[a] = chars[b]; + chars[b] = tmp; } return chars.join(''); } @@ -297,14 +324,16 @@ function createPRNG(seed) { var hash = 2166136261; for (var i = 0; i < seed.length; i++) { - hash = ((hash ^ (seed.charCodeAt(i) & 255)) >>> 0); - hash = (Math.imul(hash, 16777619)) >>> 0; + hash = (hash ^ (seed.charCodeAt(i) & 255)) >>> 0; + hash = Math.imul(hash, 16777619) >>> 0; } - var state = (hash >>> 0) || 1; + var state = hash >>> 0 || 1; return function () { - state ^= (state << 13); state >>>= 0; - state ^= (state >>> 17); state >>>= 0; - state ^= (state << 5); + state ^= state << 13; + state >>>= 0; + state ^= state >>> 17; + state >>>= 0; + state ^= state << 5; return (state >>>= 0); }; } @@ -319,7 +348,9 @@ for (var i = 0; i < len; i++) perm[i] = i; for (var i = len - 1; i > 0; i--) { var j = rng() % (i + 1); - var t = perm[i]; perm[i] = perm[j]; perm[j] = t; + var t = perm[i]; + perm[i] = perm[j]; + perm[j] = t; } var xorState = 0; for (var i = 0; i < len; i++) { @@ -330,7 +361,13 @@ } // ─── Two-layer m3u8 decryption ────────────────────────────────── - function processEncryptedM3u8(m3u8Text, m3u8Headers, avsToken, playerUrl, target) { + function processEncryptedM3u8( + m3u8Text, + m3u8Headers, + avsToken, + playerUrl, + target, + ) { debugLog('Decrypting (2-layer)…'); // Parse JWT → jti @@ -357,20 +394,31 @@ var sk = m3u8Headers['x-cache-node'] || ''; var ts = m3u8Headers['x-request-trace'] || '0'; var uid = ''; - try { uid = decodeURIComponent(m3u8Headers['x-proxy-digest'] || 'anon'); } catch (e) { uid = 'anon'; } - debugLog('cn=' + cn.substring(0, 16) + ' sk=' + sk.substring(0, 16) + ' ts=' + ts); + try { + uid = decodeURIComponent(m3u8Headers['x-proxy-digest'] || 'anon'); + } catch (e) { + uid = 'anon'; + } + debugLog( + 'cn=' + cn.substring(0, 16) + ' sk=' + sk.substring(0, 16) + ' ts=' + ts, + ); if (!cn || !sk) { // Try envelope header (base64url-encoded JSON with cn, sk, ts, uid) - var envHeader = m3u8Headers['x-avs-envelope'] || m3u8Headers['x-stream-envelope'] || ''; + var envHeader = + m3u8Headers['x-avs-envelope'] || m3u8Headers['x-stream-envelope'] || ''; if (envHeader) { try { - var envJson = JSON.parse(new TextDecoder().decode(b64urlDecode(envHeader))); + var envJson = JSON.parse( + new TextDecoder().decode(b64urlDecode(envHeader)), + ); cn = envJson.cn || cn; sk = envJson.sk || sk; ts = envJson.ts || ts; uid = envJson.uid || uid; - } catch (e) { /* envelope parse failed */ } + } catch (e) { + /* envelope parse failed */ + } } } @@ -387,7 +435,11 @@ for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line.startsWith('#') || line.trim() === '') { - if (!line.match(/^#EXTINF:/) && !line.match(/^#EXT-X-ENDLIST/) && !line.match(/^#EXT-X-KEY/)) { + if ( + !line.match(/^#EXTINF:/) && + !line.match(/^#EXT-X-ENDLIST/) && + !line.match(/^#EXT-X-KEY/) + ) { headerLines.push(line); } } else { @@ -426,40 +478,69 @@ var hmacData = new TextEncoder().encode(hmacDataStr); var iv = cnBytes.slice(0, 12); - crypto.subtle.importKey( - 'raw', cnBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] - ).then(function (hmacKey) { - return crypto.subtle.sign('HMAC', hmacKey, hmacData); - }).then(function (gcmKeyBuf) { - return crypto.subtle.importKey( - 'raw', gcmKeyBuf, { name: 'AES-GCM' }, false, ['decrypt'] - ); - }).then(function (gcmKey) { - debugLog('AES-GCM decrypt ' + encryptedBlob.length + 'B…'); - return crypto.subtle.decrypt( - { name: 'AES-GCM', iv: iv }, gcmKey, encryptedBlob - ); - }).then(function (rawResult) { - // Apply descrambler (crypto harden is active) - var descrambled = descramble(rawResult, sk, ts); - var intermediateM3u8 = new TextDecoder().decode(descrambled); - debugLog('Intermediate: ' + intermediateM3u8.length + 'ch, first80=' + intermediateM3u8.substring(0, 80)); - - if (intermediateM3u8.length < 10) { - debugLog('Intermediate too short'); - fallbackToEmbed(playerUrl, target); - return; - } + crypto.subtle + .importKey('raw', cnBytes, { name: 'HMAC', hash: 'SHA-256' }, false, [ + 'sign', + ]) + .then(function (hmacKey) { + return crypto.subtle.sign('HMAC', hmacKey, hmacData); + }) + .then(function (gcmKeyBuf) { + return crypto.subtle.importKey( + 'raw', + gcmKeyBuf, + { name: 'AES-GCM' }, + false, + ['decrypt'], + ); + }) + .then(function (gcmKey) { + debugLog('AES-GCM decrypt ' + encryptedBlob.length + 'B…'); + return crypto.subtle.decrypt( + { name: 'AES-GCM', iv: iv }, + gcmKey, + encryptedBlob, + ); + }) + .then(function (rawResult) { + // Apply descrambler (crypto harden is active) + var descrambled = descramble(rawResult, sk, ts); + var intermediateM3u8 = new TextDecoder().decode(descrambled); + debugLog( + 'Intermediate: ' + + intermediateM3u8.length + + 'ch, first80=' + + intermediateM3u8.substring(0, 80), + ); - // ── Layer 2: AES-CTR decrypt segment URLs ── - decryptSegmentUrls(intermediateM3u8, headerLines, jtiOdd, playerUrl, target); - }).catch(function (err) { - debugLog('Layer 1 (GCM) failed: ' + (err && err.message || err)); - fallbackToEmbed(playerUrl, target); - }); + if (intermediateM3u8.length < 10) { + debugLog('Intermediate too short'); + fallbackToEmbed(playerUrl, target); + return; + } + + // ── Layer 2: AES-CTR decrypt segment URLs ── + decryptSegmentUrls( + intermediateM3u8, + headerLines, + jtiOdd, + playerUrl, + target, + ); + }) + .catch(function (err) { + debugLog('Layer 1 (GCM) failed: ' + ((err && err.message) || err)); + fallbackToEmbed(playerUrl, target); + }); } - function decryptSegmentUrls(intermediateM3u8, headerLines, jtiOdd, playerUrl, target) { + function decryptSegmentUrls( + intermediateM3u8, + headerLines, + jtiOdd, + playerUrl, + target, + ) { var lines = intermediateM3u8.split('\n'); var segments = []; @@ -475,16 +556,19 @@ var qIdx = line.indexOf('?'); var params = {}; if (qIdx !== -1) { - line.substring(qIdx + 1).split('&').forEach(function (p) { - var eq = p.indexOf('='); - if (eq !== -1) params[p.substring(0, eq)] = p.substring(eq + 1); - }); + line + .substring(qIdx + 1) + .split('&') + .forEach(function (p) { + var eq = p.indexOf('='); + if (eq !== -1) params[p.substring(0, eq)] = p.substring(eq + 1); + }); } segments.push({ fileId: m[1], e: params.e || '', i: parseInt(params.i || '0', 10), - lineIdx: i + lineIdx: i, }); } } @@ -505,16 +589,30 @@ function deriveCtrKey(fileId) { if (keyCache[fileId]) return Promise.resolve(keyCache[fileId]); var signData = new TextEncoder().encode('url-cipher|' + fileId); - return crypto.subtle.importKey( - 'raw', hmacKeyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] - ).then(function (k) { - return crypto.subtle.sign('HMAC', k, signData); - }).then(function (buf) { - return crypto.subtle.importKey('raw', buf, { name: 'AES-CTR' }, false, ['decrypt']); - }).then(function (ctrKey) { - keyCache[fileId] = ctrKey; - return ctrKey; - }); + return crypto.subtle + .importKey( + 'raw', + hmacKeyBytes, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'], + ) + .then(function (k) { + return crypto.subtle.sign('HMAC', k, signData); + }) + .then(function (buf) { + return crypto.subtle.importKey( + 'raw', + buf, + { name: 'AES-CTR' }, + false, + ['decrypt'], + ); + }) + .then(function (ctrKey) { + keyCache[fileId] = ctrKey; + return ctrKey; + }); } // Decrypt all segments @@ -527,59 +625,74 @@ counter[13] = (idx >>> 16) & 0xff; counter[14] = (idx >>> 8) & 0xff; counter[15] = idx & 0xff; - return crypto.subtle.decrypt( - { name: 'AES-CTR', counter: counter, length: 64 }, ctrKey, encrypted - ).then(function (dec) { - return { lineIdx: seg.lineIdx, url: new TextDecoder().decode(dec) }; - }); + return crypto.subtle + .decrypt( + { name: 'AES-CTR', counter: counter, length: 64 }, + ctrKey, + encrypted, + ) + .then(function (dec) { + return { lineIdx: seg.lineIdx, url: new TextDecoder().decode(dec) }; + }); }); }); - Promise.all(promises).then(function (results) { - var validCount = 0; - var outLines = lines.slice(); // copy intermediate lines + Promise.all(promises) + .then(function (results) { + var validCount = 0; + var outLines = lines.slice(); // copy intermediate lines - results.forEach(function (r) { - if (r.url.indexOf('http') === 0) { - outLines[r.lineIdx] = r.url; - validCount++; - } - }); + results.forEach(function (r) { + if (r.url.indexOf('http') === 0) { + outLines[r.lineIdx] = r.url; + validCount++; + } + }); - debugLog('Decrypted ' + validCount + '/' + results.length + ' URLs'); - if (validCount === 0) { - fallbackToEmbed(playerUrl, target); - return; - } + debugLog('Decrypted ' + validCount + '/' + results.length + ' URLs'); + if (validCount === 0) { + fallbackToEmbed(playerUrl, target); + return; + } - // Build clean m3u8: header lines + decrypted segment lines - var cleanLines = []; - for (var i = 0; i < headerLines.length; i++) cleanLines.push(headerLines[i]); - for (var i = 0; i < outLines.length; i++) { - var l = outLines[i]; - if (!l) continue; - if (l.indexOf('#EXT-X-KEY:') === 0 && l.indexOf('urn:avs:shield') !== -1) continue; - // Skip /hls/ placeholder lines that weren't decrypted - if (l.match(/\/hls\/[0-9a-f]{24}\.ts/)) continue; - cleanLines.push(l); - } + // Build clean m3u8: header lines + decrypted segment lines + var cleanLines = []; + for (var i = 0; i < headerLines.length; i++) + cleanLines.push(headerLines[i]); + for (var i = 0; i < outLines.length; i++) { + var l = outLines[i]; + if (!l) continue; + if ( + l.indexOf('#EXT-X-KEY:') === 0 && + l.indexOf('urn:avs:shield') !== -1 + ) + continue; + // Skip /hls/ placeholder lines that weren't decrypted + if (l.match(/\/hls\/[0-9a-f]{24}\.ts/)) continue; + cleanLines.push(l); + } - var cleanM3u8 = cleanLines.join('\n'); - var blob = new Blob([cleanM3u8], { type: 'application/vnd.apple.mpegurl' }); - var blobUrl = URL.createObjectURL(blob); + var cleanM3u8 = cleanLines.join('\n'); + var blob = new Blob([cleanM3u8], { + type: 'application/vnd.apple.mpegurl', + }); + var blobUrl = URL.createObjectURL(blob); - if (modeLabel) modeLabel.textContent = 'Đang ở chế độ m3u8'; - buildVideoPlayer(target, [{ file: blobUrl, type: 'hls' }]); - }).catch(function (err) { - debugLog('Layer 2 (CTR) failed: ' + (err && err.message || err)); - fallbackToEmbed(playerUrl, target); - }); + if (modeLabel) modeLabel.textContent = 'Đang ở chế độ m3u8'; + buildVideoPlayer(target, [{ file: blobUrl, type: 'hls' }]); + }) + .catch(function (err) { + debugLog('Layer 2 (CTR) failed: ' + ((err && err.message) || err)); + fallbackToEmbed(playerUrl, target); + }); } function fallbackToEmbed(playerUrl, target) { if (!embedEnabled) { console.log('[AVS] Embed disabled, showing error'); - showError('Không thể giải mã video. Bật "Bật embed" trong cài đặt plugin để xem qua iframe.'); + showError( + 'Không thể giải mã video. Bật "Bật embed" trong cài đặt plugin để xem qua iframe.', + ); if (modeLabel) modeLabel.textContent = ''; return; } @@ -593,9 +706,13 @@ } function cleanupIframe(iframe) { - try { iframe.src = 'about:blank'; } catch (e) {} + try { + iframe.src = 'about:blank'; + } catch (e) {} setTimeout(function () { - try { iframe.remove(); } catch (e) {} + try { + iframe.remove(); + } catch (e) {} }, 200); } @@ -605,7 +722,8 @@ console.log('[AVS] ' + msg); var el = document.getElementById('avs-debug-log'); if (!el && inner) { - inner.innerHTML = '
'; + inner.innerHTML = + '
'; el = document.getElementById('avs-debug-log'); } if (el) el.textContent = _debugLog.join('\n'); From a071623b082547c4b71fb76696a9af88bc32669d Mon Sep 17 00:00:00 2001 From: Fioren <102145692+FiorenMas@users.noreply.github.com> Date: Thu, 21 May 2026 23:31:24 +0700 Subject: [PATCH 4/4] reduce cf wait --- public/static/src/vi/animevietsub/player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/static/src/vi/animevietsub/player.js b/public/static/src/vi/animevietsub/player.js index be5c7164..bbb4cb51 100644 --- a/public/static/src/vi/animevietsub/player.js +++ b/public/static/src/vi/animevietsub/player.js @@ -217,7 +217,7 @@ (document.body || document.documentElement).appendChild(iframe); // Wait for Cloudflare, then try fetch - var cfWait = 4000; // 4s for CF challenge + var cfWait = 1000; // 1s for CF challenge debugLog('Đợi CF ' + cfWait + 'ms…'); setTimeout(function () { debugLog('CF done, fetching page…');