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/README.md +30 -0
- package/bin/nekostream.js +4 -0
- package/dist/discord.js +144 -0
- package/dist/fetch-avs.js +22 -0
- package/dist/index.js +808 -0
- package/dist/player.js +37 -0
- package/dist/prompts-wrapper.js +81 -0
- package/dist/providers.js +35 -0
- package/dist/scrapers/anilist-api.js +516 -0
- package/dist/scrapers/anilist.js +82 -0
- package/dist/scrapers/auth-service.js +1245 -0
- package/dist/scrapers/base.js +41 -0
- package/dist/scrapers/crawler.js +301 -0
- package/dist/scrapers/external-api.js +68 -0
- package/dist/scrapers/fingerprint.js +240 -0
- package/dist/scrapers/interceptor.js +288 -0
- package/dist/scrapers/jikan-api.js +60 -0
- package/dist/scrapers/providers/anime47.js +941 -0
- package/dist/scrapers/providers/animehay.js +640 -0
- package/dist/scrapers/providers/animevietsub.js +1455 -0
- package/dist/storage.js +215 -0
- package/dist/test-a47-eps.js +69 -0
- package/dist/test-a47-real.js +56 -0
- package/dist/test-a47.js +8 -0
- package/dist/ui.js +187 -0
- package/isolate.css +115 -0
- package/isolate.js +178 -0
- package/package.json +60 -0
- package/player-main.js +225 -0
- package/player.html +376 -0
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
|
+
})
|