From 72707a02de26fb477a369c211a134a876d81915d Mon Sep 17 00:00:00 2001
From: Fioren <102145692+FiorenMas@users.noreply.github.com>
Date: Fri, 10 Jul 2026 10:18:42 +0700
Subject: [PATCH 1/2] up
---
plugins/vietnamese/NguonC/index.ts | 26 ++++++++------------------
1 file changed, 8 insertions(+), 18 deletions(-)
diff --git a/plugins/vietnamese/NguonC/index.ts b/plugins/vietnamese/NguonC/index.ts
index f9899c79..a3a80f5f 100644
--- a/plugins/vietnamese/NguonC/index.ts
+++ b/plugins/vietnamese/NguonC/index.ts
@@ -8,6 +8,7 @@ import { ContentType } from '@libs/pluginMetadata';
import { storage } from '@libs/storage';
import { load } from 'cheerio';
+
const SITE = 'https://phim.nguonc.com';
const API_BASE = SITE + '/api';
@@ -16,7 +17,7 @@ class NguonCPlugin implements Plugin.PluginBase {
name = 'NguonC';
icon = 'src/vi/nguonc/icon.png';
site = SITE;
- version = '1.0.9';
+ version = '1.0.10';
customJS = 'src/vi/nguonc/player.js';
contentType = ContentType.VIDEO;
@@ -286,14 +287,15 @@ class NguonCPlugin implements Plugin.PluginBase {
const html = await fetchText(url);
const $ = load(html);
const div = $('#player');
+ const dataObf = div.attr('data-obf')!;
const obf = JSON.parse(
- Buffer.from(div.attr('data-obf')!, 'base64').toString(),
+ Buffer.from(dataObf, 'base64').toString(),
) as {
sUb: string;
hD: string;
- kX: string;
+ kX?: string;
};
- return obf;
+ return { hD: obf.hD, dataObf };
}
// ---------- buildPlayerHtml ----------
@@ -318,23 +320,11 @@ class NguonCPlugin implements Plugin.PluginBase {
``,
);
} else {
- /*
- if (opts.m3u8) {
- // không phải m3u8 của nguonc nên chưa rõ cách bypass
- metas.push('', '');
- metas.push(
- ``,
- );
- } else {
- */
metas.push('');
+ const iframeObf = await this.getM3u8DataFromEmbed(opts.embed!);
const attrs: string[] = ['id="nguonc-player-container"'];
attrs.push(`data-iframe="${encodeHtmlEntities(opts.embed)}"`);
- // trying fetch iframe
- const iframeObf = await this.getM3u8DataFromEmbed(opts.embed!);
- attrs.push(`data-s="${encodeHtmlEntities(iframeObf?.sUb)}"`);
- attrs.push(`data-h="${encodeHtmlEntities(iframeObf?.hD)}"`);
- attrs.push(`data-k="${encodeHtmlEntities(iframeObf?.kX)}"`);
+ attrs.push(`data-obf="${encodeHtmlEntities(iframeObf?.dataObf || '')}"`);
metas.push(`
`);
}
From e0ee36bee14f2cee311aed421f67d85506e2ae31 Mon Sep 17 00:00:00 2001
From: Fioren <102145692+FiorenMas@users.noreply.github.com>
Date: Fri, 10 Jul 2026 10:19:06 +0700
Subject: [PATCH 2/2] up
---
plugins/vietnamese/NguonC/webview/index.ts | 256 +++++++++------------
1 file changed, 112 insertions(+), 144 deletions(-)
diff --git a/plugins/vietnamese/NguonC/webview/index.ts b/plugins/vietnamese/NguonC/webview/index.ts
index 8bea5a88..e9f8f1b9 100644
--- a/plugins/vietnamese/NguonC/webview/index.ts
+++ b/plugins/vietnamese/NguonC/webview/index.ts
@@ -1,165 +1,133 @@
/* eslint-disable */
///
-function hexToBytes(hexString: string) {
- const bytes = new Uint8Array(hexString.length / 2);
- for (let i = 0; i < hexString.length; i += 2) {
- bytes[i / 2] = parseInt(hexString.substr(i, 2), 16);
+/**
+ * NguonC - WebView Video Player
+ *
+ * Decryption: HMAC-SHA256(key="stream-derive-v1", data=videoHash)[0:32] → AES-GCM key
+ * Then AES-GCM decrypt with that key + IV from the #ENC-AESGCM header.
+ * Fallback: iframe if decryption fails.
+ */
+
+const hexToBytes = (hex: string) => {
+ const bytes = new Uint8Array(hex.length / 2);
+ for (let i = 0; i < hex.length; i += 2) {
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
-}
-function base64ToBytes(base64String: string) {
- const binaryString = atob(base64String);
- const bytes = new Uint8Array(binaryString.length);
- for (let i = 0; i < binaryString.length; i++) {
- bytes[i] = binaryString.charCodeAt(i);
+};
+
+const b64Decode = (b64: string) => {
+ const bin = atob(b64);
+ const bytes = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) {
+ bytes[i] = bin.charCodeAt(i);
}
return bytes;
-}
-
-async function initPlayer() {
- const container = document.getElementById('nguonc-player-container');
- if (!container || !window.LNReaderPlayer) return;
- const iframeUrl = container.getAttribute('data-iframe');
- const s = container.getAttribute('data-s');
- const h = container.getAttribute('data-h');
- const k = container.getAttribute('data-k');
- if (!iframeUrl) return;
- const urlObj = new URL(iframeUrl);
- const req = await window.reader.fetch(`${urlObj.origin}/${s}.m3u8`, {
- method: 'GET',
- headers: {
- Referer: urlObj.origin,
- },
- referrer: urlObj.origin,
- });
- const m3u8Content = await req.text();
- const lines = m3u8Content.split('\n');
- let ivHex = '';
- let encryptedData = '';
+};
- for (let line of lines) {
- line = line.trim();
- if (line.startsWith('#ENC-AESGCM')) {
- const ivMatch = line.match(/iv=([a-fA-F0-9]+)/);
- if (ivMatch) ivHex = ivMatch[1];
- } else if (line && !line.startsWith('#')) {
- encryptedData = line;
- }
+async function decryptM3u8(
+ encryptedBytes: Uint8Array,
+ ivBytes: Uint8Array,
+ videoHash: string,
+): Promise {
+ try {
+ const enc = new TextEncoder();
+ const hmacKey = await crypto.subtle.importKey(
+ 'raw',
+ enc.encode('stream-derive-v1'),
+ { name: 'HMAC', hash: 'SHA-256' },
+ false,
+ ['sign'],
+ );
+ const hmacResult = await crypto.subtle.sign(
+ 'HMAC',
+ hmacKey,
+ enc.encode(videoHash),
+ );
+ const aesKey = await crypto.subtle.importKey(
+ 'raw',
+ new Uint8Array(hmacResult).slice(0, 32),
+ { name: 'AES-GCM' },
+ false,
+ ['decrypt'],
+ );
+ const decrypted = await crypto.subtle.decrypt(
+ { name: 'AES-GCM', iv: ivBytes },
+ aesKey,
+ encryptedBytes,
+ );
+ return new TextDecoder().decode(decrypted);
+ } catch {
+ return null;
}
+}
- if (!ivHex || !encryptedData) {
- throw new Error('Định dạng tệp mã hóa không hợp lệ.');
- }
+function createProxyFragLoader(origin: string) {
+ return class {
+ stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0, loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, first: 0, end: 0 } };
+ constructor() {}
+ destroy() {}
+ abort() {}
+ load(ctx: any, _cfg: any, cbs: any) {
+ this.stats.loading.start = performance.now();
+ window.reader.fetch(ctx.url, { method: 'GET', headers: { Referer: origin }, referrer: origin })
+ .then((resp: any) => { if (!resp.ok) throw new Error('HTTP ' + resp.status); this.stats.loading.first = performance.now(); return resp.arrayBuffer(); })
+ .then((buf: any) => { this.stats.loading.end = performance.now(); this.stats.loaded = buf.byteLength; this.stats.total = buf.byteLength; cbs.onSuccess({ data: buf }, this.stats, ctx, null); })
+ .catch((err: any) => { this.stats.loading.end = performance.now(); cbs.onError({ code: 0, text: err.message }, ctx, null, this.stats); });
+ }
+ };
+}
- const ivBytes = hexToBytes(ivHex);
- const encryptedBytes = base64ToBytes(encryptedData);
+(async () => {
+ const player = window.LNReaderPlayer;
+ if (!player) return;
- const ciphertext = encryptedBytes.slice(0, -16);
- const authTag = encryptedBytes.slice(-16);
+ const container = document.getElementById('nguonc-player-container');
+ const embedUrl = container?.getAttribute('data-iframe') || '';
+ const dataObf = container?.getAttribute('data-obf') || '';
- const encryptionKey = k;
- if (!encryptionKey) {
- throw new Error('Thiếu khóa xác thực mã hóa.');
+ if (!embedUrl || !dataObf) {
+ player.playIframe(embedUrl);
+ return;
}
- const textEncoder = new TextEncoder();
- const keyBytes = textEncoder.encode(encryptionKey).slice(0, 32);
- const cryptoKey = await crypto.subtle.importKey(
- 'raw',
- keyBytes,
- {
- name: 'AES-GCM',
- },
- false,
- ['decrypt'],
- );
-
- const encryptedBuffer = new Uint8Array(ciphertext.length + authTag.length);
- encryptedBuffer.set(ciphertext);
- encryptedBuffer.set(authTag, ciphertext.length);
+ try {
+ const embedOrigin = new URL(embedUrl).origin;
+ const streamData = JSON.parse(atob(dataObf));
+ const m3u8Url = `${embedOrigin}/${streamData.sUb}`;
- const algorithm = {
- name: 'AES-GCM',
- iv: ivBytes,
- tagLength: 128,
- };
+ player.log('[NGC] Fetching m3u8...');
+ const resp = await window.reader.fetch(m3u8Url, { method: 'GET' });
+ const encryptedText = await resp.text();
- const decryptedBuffer = await crypto.subtle.decrypt(
- algorithm,
- cryptoKey,
- encryptedBuffer,
- );
- const textDecoder = new TextDecoder();
- const m3u8 = textDecoder.decode(decryptedBuffer);
- const blob = new Blob([m3u8], {
- type: 'application/vnd.apple.mpegurl',
- });
- const url = URL.createObjectURL(blob);
- let ProxyFragLoader = function (config: any) {
- // @ts-ignore
- this._config = config;
- // @ts-ignore
- this.stats = {
- aborted: false,
- loaded: 0,
- retry: 0,
- total: 0,
- chunkCount: 0,
- bwEstimate: 0,
- loading: { start: 0, first: 0, end: 0 },
- parsing: { start: 0, end: 0 },
- buffering: { start: 0, first: 0, end: 0 },
- };
- // @ts-ignore
- this.context = null;
- // @ts-ignore
- this._controller = null;
- };
- ProxyFragLoader.prototype.destroy = function () {
- this.abort();
- };
- ProxyFragLoader.prototype.abort = function () {
- if (this._controller) {
- this._controller.abort();
- this._controller = null;
+ let ivHex = '';
+ let encryptedData = '';
+ for (const line of encryptedText.split('\n')) {
+ const trimmed = line.trim();
+ if (trimmed.startsWith('#ENC-AESGCM')) {
+ const m = trimmed.match(/iv=([a-fA-F0-9]+)/);
+ if (m) ivHex = m[1];
+ } else if (trimmed && !trimmed.startsWith('#')) {
+ encryptedData = trimmed;
+ }
}
- };
- // @ts-ignore
- ProxyFragLoader.prototype.load = function (ctx, cfg, cbs) {
- this.context = ctx;
- var self = this;
- self.stats.loading.start = performance.now();
- window.reader
- .fetch(ctx.url, {
- method: 'GET',
- headers: {
- Referer: urlObj.origin,
- },
- referrer: urlObj.origin,
- })
- .then(function (resp) {
- if (!resp.ok) throw new Error('HTTP ' + resp.status);
- self.stats.loading.first = performance.now();
- return resp.arrayBuffer();
- })
- .then(function (buf) {
- self.stats.loading.end = performance.now();
- self.stats.loaded = buf.byteLength;
- self.stats.total = buf.byteLength;
- cbs.onSuccess({ data: buf }, self.stats, ctx, null);
- })
- .catch(function (err) {
- if (err.name === 'AbortError') return;
- self.stats.loading.end = performance.now();
- cbs.onError({ code: 0, text: err.message }, ctx, null, self.stats);
- });
- };
+ if (!ivHex || !encryptedData) throw new Error('Invalid m3u8 format');
- window.LNReaderPlayer.playHls(url, {
- fLoader: ProxyFragLoader,
- });
-}
+ const m3u8Text = await decryptM3u8(
+ b64Decode(encryptedData),
+ hexToBytes(ivHex),
+ streamData.hD,
+ );
-initPlayer();
+ if (!m3u8Text) throw new Error('Decryption failed');
+
+ player.log('[NGC] Playing decrypted m3u8 (' + m3u8Text.length + ' chars)');
+ const blob = new Blob([m3u8Text], { type: 'application/vnd.apple.mpegurl' });
+ player.playHls(URL.createObjectURL(blob), { fLoader: createProxyFragLoader(embedOrigin) });
+ } catch (err: any) {
+ player.log('[NGC] Error: ' + (err?.message || err));
+ player.playIframe(embedUrl);
+ }
+})();