nekostream 2026.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/isolate.js ADDED
@@ -0,0 +1,178 @@
1
+ (() => {
2
+ const rootSelectors = ['.watch-player', '.player-section', '.video-container', '.art-video-player', '#player', '.player-wrapper'];
3
+ const anchorSelectors = [
4
+ '.watch-player .jwplayer', '.player-section .jwplayer', '.video-container .jwplayer',
5
+ '.player-wrapper.jw-active', '.player-wrapper .jwplayer', '.jwplayer', '.jw-video',
6
+ '.video-js', '.vjs-tech', '.player-section video', '.video-container video',
7
+ '.watch-player video', 'iframe[src*="anime47"]', 'iframe[src*="nonprofit"]',
8
+ 'iframe[src*="vlogphim"]', 'iframe[src*="anime3s"]', 'video', '.art-video-player'
9
+ ];
10
+ const containerSelectors = [
11
+ '.watch-player', '.player-section', '.video-container', '.player-wrapper.jw-active',
12
+ '.player-wrapper', '.jw-player-wrapper', '.jwplayer', 'main.q-page.watch-page',
13
+ 'main.q-page', 'main', '.art-video-player', '#player', '.player'
14
+ ];
15
+
16
+ const firstMatch = (selectors) => {
17
+ for (const selector of selectors) {
18
+ const found = document.querySelector(selector);
19
+ if (found instanceof HTMLElement) return found;
20
+ }
21
+ return null;
22
+ };
23
+
24
+ const firstMatchWithin = (root, selectors) => {
25
+ if (!(root instanceof HTMLElement)) return null;
26
+ for (const selector of selectors) {
27
+ const found = root.querySelector(selector);
28
+ if (found instanceof HTMLElement) return found;
29
+ }
30
+ return null;
31
+ };
32
+
33
+ const stretch = (el) => {
34
+ if (!(el instanceof HTMLElement)) return;
35
+ el.style.setProperty('width', '100%', 'important');
36
+ el.style.setProperty('height', '100%', 'important');
37
+ el.style.setProperty('max-width', '100%', 'important');
38
+ el.style.setProperty('max-height', '100%', 'important');
39
+ el.style.setProperty('margin', '0', 'important');
40
+ el.style.setProperty('padding', '0', 'important');
41
+ el.style.setProperty('padding-bottom', '0', 'important');
42
+ el.style.setProperty('padding-top', '0', 'important');
43
+ el.style.setProperty('top', '0', 'important');
44
+ el.style.setProperty('left', '0', 'important');
45
+ el.style.setProperty('transform', 'none', 'important');
46
+ el.style.setProperty('background', '#000', 'important');
47
+ };
48
+
49
+ const hideSiblingBranches = (node, anchorNode) => {
50
+ if (!(node instanceof HTMLElement)) return;
51
+ let current = node;
52
+
53
+ // Hide siblings upwards from root to body
54
+ while (current?.parentElement) {
55
+ const parent = current.parentElement;
56
+ for (const sibling of Array.from(parent.children)) {
57
+ if (!(sibling instanceof HTMLElement) || sibling === current) continue;
58
+ sibling.style.setProperty('display', 'none', 'important');
59
+ }
60
+ if (parent === document.body) break;
61
+ current = parent;
62
+ }
63
+
64
+ // Hide siblings downwards from anchor to root
65
+ if (anchorNode && anchorNode instanceof HTMLElement && node.contains(anchorNode)) {
66
+ let currChild = anchorNode;
67
+ while (currChild && currChild !== node && currChild.parentElement) {
68
+ const parent = currChild.parentElement;
69
+ if (!parent.classList.contains('jwplayer') && !parent.classList.contains('jw-wrapper')) {
70
+ for (const sibling of Array.from(parent.children)) {
71
+ if (!(sibling instanceof HTMLElement) || sibling === currChild) continue;
72
+ sibling.style.setProperty('display', 'none', 'important');
73
+ sibling.style.setProperty('visibility', 'hidden', 'important');
74
+ sibling.style.setProperty('opacity', '0', 'important');
75
+ sibling.style.setProperty('pointer-events', 'none', 'important');
76
+ sibling.style.setProperty('width', '0', 'important');
77
+ sibling.style.setProperty('height', '0', 'important');
78
+ }
79
+ }
80
+ currChild = parent;
81
+ }
82
+ }
83
+ };
84
+
85
+ const isolateOnce = () => {
86
+ const root = firstMatch(rootSelectors);
87
+ const anchor = (root && firstMatchWithin(root, anchorSelectors)) || firstMatch(anchorSelectors);
88
+ if (!(anchor instanceof HTMLElement)) return false;
89
+
90
+ let toKeep = root;
91
+ if (!(toKeep instanceof HTMLElement)) {
92
+ for (const selector of containerSelectors) {
93
+ const candidate = anchor.closest(selector);
94
+ if (candidate instanceof HTMLElement && candidate !== document.body) {
95
+ toKeep = candidate;
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ if (!(toKeep instanceof HTMLElement)) toKeep = anchor;
101
+
102
+ toKeep.style.setProperty('position', 'fixed', 'important');
103
+ toKeep.style.setProperty('inset', '0', 'important');
104
+ toKeep.style.setProperty('width', '100vw', 'important');
105
+ toKeep.style.setProperty('height', '100vh', 'important');
106
+ toKeep.style.setProperty('max-width', '100vw', 'important');
107
+ toKeep.style.setProperty('max-height', '100vh', 'important');
108
+ toKeep.style.setProperty('overflow', 'hidden', 'important');
109
+ toKeep.style.setProperty('background', '#000', 'important');
110
+ toKeep.style.setProperty('z-index', '2147483000', 'important');
111
+
112
+ stretch(anchor);
113
+ stretch(toKeep);
114
+ toKeep.querySelectorAll('video, iframe, .jwplayer, .jw-player-wrapper, .jw-holder, .jw-media, .jw-video, .video-js, .vjs-tech')
115
+ .forEach((el) => stretch(el));
116
+
117
+ document.documentElement.style.setProperty('overflow', 'hidden', 'important');
118
+ document.body.style.setProperty('overflow', 'hidden', 'important');
119
+ document.body.style.setProperty('background', '#000', 'important');
120
+
121
+ hideSiblingBranches(toKeep, anchor);
122
+ return true;
123
+ };
124
+
125
+ const hideOverlayAds = () => {
126
+ const collapse = (node) => {
127
+ if (!(node instanceof HTMLElement)) return;
128
+ node.style.setProperty('display', 'none', 'important');
129
+ node.style.setProperty('visibility', 'hidden', 'important');
130
+ node.style.setProperty('opacity', '0', 'important');
131
+ node.style.setProperty('width', '0', 'important');
132
+ node.style.setProperty('height', '0', 'important');
133
+ node.style.setProperty('pointer-events', 'none', 'important');
134
+ };
135
+
136
+ document.querySelectorAll('#avs-pause-ad, .avs-pause-ad, .avs-pause-ad-box, [id*="avs-pause" i], [class*="avs-pause" i]')
137
+ .forEach(collapse);
138
+
139
+ document.querySelectorAll('[id*="adblock" i], [class*="adblock" i], [id*="anti-ad" i], [class*="anti-ad" i]')
140
+ .forEach(collapse);
141
+
142
+ document.querySelectorAll('.jwplayer, .jw-wrapper, .jw-media').forEach(el => {
143
+ el.style.setProperty('background', '#000', 'important');
144
+ });
145
+ };
146
+
147
+ let matchedCount = 0;
148
+ let attempts = 0;
149
+ const maxAttempts = 120;
150
+
151
+ const observer = new MutationObserver(() => {
152
+ if (isolateOnce()) matchedCount += 1;
153
+ });
154
+ observer.observe(document.documentElement, { childList: true, subtree: true });
155
+
156
+ const timer = window.setInterval(() => {
157
+ attempts += 1;
158
+ if (isolateOnce()) matchedCount += 1;
159
+ if (attempts >= maxAttempts || matchedCount >= 3) {
160
+ window.clearInterval(timer);
161
+ observer.disconnect();
162
+ }
163
+ }, 500);
164
+
165
+ let adHideTimer = null;
166
+ const startAdCleanupLoop = () => {
167
+ if (adHideTimer) return;
168
+ hideOverlayAds();
169
+ isolateOnce();
170
+ adHideTimer = window.setInterval(() => {
171
+ hideOverlayAds();
172
+ isolateOnce();
173
+ }, 1000);
174
+ };
175
+
176
+ window.setTimeout(startAdCleanupLoop, 2000);
177
+ isolateOnce();
178
+ })();
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "nekostream",
3
+ "version": "2026.0.1",
4
+ "description": "Bettter version of NekoStream ",
5
+ "keywords": [
6
+ "anime",
7
+ "scraper",
8
+ "animevietsub",
9
+ "animetvn",
10
+ "anime47",
11
+ "nekostream",
12
+ "cli",
13
+ "video-player",
14
+ "streaming",
15
+ "vietsub",
16
+ "anime-cli"
17
+ ],
18
+ "main": "dist/index.js",
19
+ "bin": {
20
+ "nekostream": "./bin/nekostream.js"
21
+ },
22
+ "scripts": {
23
+ "start": "tsx index.ts",
24
+ "build": "tsc",
25
+ "prepublishOnly": "npm run build",
26
+ "postinstall": "playwright install chromium"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "bin",
31
+ "player-main.js",
32
+ "player.html",
33
+ "isolate.js",
34
+ "isolate.css"
35
+ ],
36
+ "dependencies": {
37
+ "@cliqz/adblocker-playwright": "^1.34.0",
38
+ "@ghostery/adblocker-playwright": "^2.17.1",
39
+ "@inquirer/prompts": "^8.4.2",
40
+ "artplayer": "^5.3.0",
41
+ "chalk": "^4.1.2",
42
+ "cheerio": "^1.2.0",
43
+ "cross-fetch": "^4.1.0",
44
+ "discord-rpc": "^4.0.1",
45
+ "electron": "^34.0.0",
46
+ "hls.js": "^1.6.0",
47
+ "ora": "^5.4.1",
48
+ "playwright": "^1.59.1",
49
+ "playwright-extra": "^4.3.6",
50
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
51
+ "tsx": "^4.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/discord-rpc": "^4.0.10",
55
+ "@types/node": "^22.0.0",
56
+ "@types/prompts": "^2.4.9",
57
+ "eruda": "^3.4.3",
58
+ "typescript": "^5.0.0"
59
+ }
60
+ }
package/player-main.js ADDED
@@ -0,0 +1,225 @@
1
+ const { app, BrowserWindow, session, ipcMain } = require('electron')
2
+ const path = require('path')
3
+
4
+ let mainWindow = null
5
+
6
+ function createWindow() {
7
+ const streamData = process.env.NEKOSTREAM_CLI_STREAM
8
+ if (!streamData) {
9
+ console.error('No stream data provided.')
10
+ app.quit()
11
+ return
12
+ }
13
+
14
+ const streamInfo = JSON.parse(Buffer.from(streamData, 'base64').toString('utf-8'))
15
+
16
+ mainWindow = new BrowserWindow({
17
+ width: 1280,
18
+ height: 720,
19
+ frame: false,
20
+ autoHideMenuBar: true,
21
+ backgroundColor: '#000000',
22
+ webPreferences: {
23
+ nodeIntegration: true,
24
+ contextIsolation: false,
25
+ webSecurity: false, // Required for some streams (CORS bypass)
26
+ webviewTag: true,
27
+ autoplayPolicy: 'no-user-gesture-required'
28
+ }
29
+ })
30
+
31
+ const playerSession = session.fromPartition('persist:player-session')
32
+ const sessions = [session.defaultSession, playerSession]
33
+
34
+ sessions.forEach(sess => {
35
+ // Network-level ad-blocking (like browser extensions)
36
+ sess.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, callback) => {
37
+ const url = details.url;
38
+ const lowerUrl = url.toLowerCase();
39
+
40
+ // ── Whitelist: never block requests originating FROM or loaded in an animevietsub tab ──
41
+ // AVS fetches probe URLs (googlesyndication, doubleclick, etc.) to detect adblock.
42
+ // If we cancel those probes, the site shows the "Phát hiện trình chặn quảng cáo" overlay.
43
+ // We must let these probes succeed so AVS thinks ads loaded normally.
44
+ const referer = (details.referrer || '').toLowerCase();
45
+ const initiator = (details.initiator || '').toLowerCase();
46
+ const fromAvs = referer.includes('animevietsub') || initiator.includes('animevietsub') || lowerUrl.includes('animevietsub');
47
+
48
+ let isAvsTab = false;
49
+ try {
50
+ const { webContents } = require('electron');
51
+ const wc = webContents.fromId(details.webContentsId);
52
+ if (wc) {
53
+ const tabUrl = wc.getURL().toLowerCase();
54
+ if (tabUrl.includes('animevietsub')) {
55
+ isAvsTab = true;
56
+ }
57
+ }
58
+ } catch (e) {}
59
+
60
+ if (fromAvs || isAvsTab) {
61
+ return callback({ cancel: false });
62
+ }
63
+
64
+ const adKeywords = [
65
+ 'in88', 'quayhu', 'nohu', 'bet188', '188bet', 'w88', 'fun88', 'm88', 'fb88',
66
+ 'bk8', 'cmd368', 'letou', 'kyna', 'vwin', 'lixi88', 'loto188', 'kubet', 'ku-bet',
67
+ 'thabet', 'tha-bet', 'ae888', 'fi88', '12bet', 'junk', 'popunder', 'pop-under',
68
+ 'histats.com', 'clickadu', 'exoclick', 'juicyads', 'adsterra', 'yandex.ru',
69
+ 'adskeeper', 'mgid.com', 'ad-maven', 'onclickads', 'propellerads', 'cloudflareinsights.com'
70
+ ];
71
+
72
+ const shouldBlock = adKeywords.some(keyword => lowerUrl.includes(keyword)) ||
73
+ lowerUrl.includes('doubleclick.net') ||
74
+ lowerUrl.includes('googleads') ||
75
+ lowerUrl.includes('googlesyndication') ||
76
+ (lowerUrl.includes('banner') && (lowerUrl.includes('casino') || lowerUrl.includes('tai-xiu') || lowerUrl.includes('keo-nha-cai')));
77
+
78
+ if (shouldBlock) {
79
+ return callback({ cancel: true });
80
+ }
81
+
82
+ return callback({ cancel: false });
83
+ });
84
+
85
+ // Strip Content-Security-Policy and X-Frame-Options to allow script injection (Eruda) and cross-origin iframe handling
86
+ sess.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
87
+ const responseHeaders = { ...details.responseHeaders }
88
+ for (const key of Object.keys(responseHeaders)) {
89
+ const lowerKey = key.toLowerCase()
90
+ if (
91
+ lowerKey === 'content-security-policy' ||
92
+ lowerKey === 'content-security-policy-report-only' ||
93
+ lowerKey === 'x-frame-options'
94
+ ) {
95
+ delete responseHeaders[key]
96
+ }
97
+ }
98
+ callback({ cancel: false, responseHeaders })
99
+ })
100
+
101
+ // Always inject Referer/Origin headers (needed for iframe streams like googleapiscdn)
102
+ if (streamInfo.headers) {
103
+ const filter = { urls: ['*://*/*'] }
104
+ sess.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
105
+ const headers = { ...details.requestHeaders }
106
+ if (streamInfo.headers.Referer) headers['Referer'] = streamInfo.headers.Referer
107
+ if (streamInfo.headers.Origin) headers['Origin'] = streamInfo.headers.Origin
108
+ callback({ requestHeaders: headers })
109
+ })
110
+ }
111
+ })
112
+
113
+ // Load eruda script content securely in Node process
114
+ const fs = require('fs')
115
+ let erudaCode = ''
116
+ try {
117
+ const erudaPath = require.resolve('eruda/eruda.js')
118
+ erudaCode = fs.readFileSync(erudaPath, 'utf-8')
119
+ } catch (err) {
120
+ try {
121
+ const fallbackPath = path.join(__dirname, 'node_modules', 'eruda', 'eruda.js')
122
+ erudaCode = fs.readFileSync(fallbackPath, 'utf-8')
123
+ } catch (err2) {
124
+ console.error('[Main] Failed to load eruda:', err2)
125
+ }
126
+ }
127
+
128
+ mainWindow.webContents.on('did-finish-load', () => {
129
+ mainWindow?.webContents.executeJavaScript(`
130
+ window.initPlayer(${JSON.stringify(streamInfo)});
131
+ `)
132
+ })
133
+
134
+ // Listen for webview attachment, then watch all frames it creates.
135
+ // frame-created fires for EVERY frame (including dynamically injected iframes),
136
+ // and WebFrameMain.executeJavaScript() bypasses all cross-origin restrictions.
137
+ mainWindow.webContents.on('did-attach-webview', (event, guestWebContents) => {
138
+ guestWebContents.on('frame-created', (e, details) => {
139
+ const frame = details.frame
140
+
141
+ frame.once('dom-ready', () => {
142
+ const frameUrl = frame.url
143
+
144
+ const isPlayer = frameUrl.includes('googleapiscdn.com') || frameUrl.includes('googleapis.com')
145
+ if (!isPlayer) return
146
+
147
+ // Dump DOM once for diagnostic (reads the real elements so we can write precise selectors)
148
+ frame.executeJavaScript('document.documentElement.outerHTML')
149
+ .then(html => {
150
+ if (html && html.length > 200) {
151
+ require('fs').writeFileSync(
152
+ path.join(__dirname, 'dom-dump.html'), html, 'utf-8'
153
+ )
154
+ }
155
+ })
156
+ .catch(() => {})
157
+
158
+ // Inject ad-blocking CSS + DOM cleaner directly inside the player frame
159
+ frame.executeJavaScript(`
160
+ (function() {
161
+ // --- CSS block ---
162
+ const style = document.createElement('style');
163
+ style.id = '__neko_adblock__';
164
+ style.textContent = [
165
+ 'img[src*="in88"],img[src*="quayhu"],img[src*="nohu"],',
166
+ 'img[src*="188bet"],img[src*="kubet"],img[src*="w88"],img[src*="fun88"],',
167
+ '[class*="pause-ad"],[id*="pause-ad"],[class*="ads-pause"],[id*="ads-pause"],',
168
+ '[class*="overlay-ad"],[id*="overlay-ad"],[class*="popup-ad"],[id*="popup-ad"],',
169
+ '[class*="quangcao"],[id*="quangcao"],[class*="qc-"],[id*="qc-"] { display:none!important }'
170
+ ].join('');
171
+ (document.head || document.documentElement).appendChild(style);
172
+
173
+ // --- DOM cleaner ---
174
+ const AD_TEXTS = ['Đóng quảng cáo', 'Đóng và xem tiếp', 'Quảng cáo'];
175
+ const AD_SRCS = ['in88', 'quayhu', 'nohu', '188bet', 'kubet', 'w88', 'fun88'];
176
+
177
+ const clean = () => {
178
+ try {
179
+ if (!document.body) return;
180
+ document.querySelectorAll('div,section,article,button,a,span,img').forEach(el => {
181
+ const txt = (el.innerText || el.textContent || '').trim();
182
+ const src = (el.src || el.getAttribute('src') || el.getAttribute('href') || '').toLowerCase();
183
+ const isAdTxt = AD_TEXTS.some(t => txt === t || txt.startsWith(t));
184
+ const isAdSrc = AD_SRCS.some(k => src.includes(k));
185
+ if (!isAdTxt && !isAdSrc) return;
186
+
187
+ // Walk up to find fixed/absolute overlay container
188
+ let node = el;
189
+ for (let i = 0; i < 10; i++) {
190
+ const p = node.parentElement;
191
+ if (!p || p === document.body || p === document.documentElement) break;
192
+ if (p.querySelector('video') || p.querySelector('canvas')) break;
193
+ const cs = getComputedStyle(p);
194
+ if (cs.position === 'fixed' || cs.position === 'absolute') {
195
+ p.remove(); return;
196
+ }
197
+ node = p;
198
+ }
199
+ el.remove();
200
+ });
201
+ } catch(e) {}
202
+ };
203
+
204
+ clean();
205
+ setInterval(clean, 250);
206
+ })();
207
+ `).catch(() => {})
208
+ })
209
+ })
210
+ })
211
+
212
+ mainWindow.loadFile(path.join(__dirname, 'player.html'))
213
+
214
+ mainWindow.on('closed', () => {
215
+ mainWindow = null
216
+ })
217
+ }
218
+
219
+ app.whenReady().then(createWindow)
220
+
221
+ app.on('window-all-closed', () => {
222
+ if (process.platform !== 'darwin') {
223
+ app.quit()
224
+ }
225
+ })