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/player.html
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>NekoStream CLI Player</title>
|
|
7
|
+
<style>
|
|
8
|
+
body, html {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
background-color: #000;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
font-family: sans-serif;
|
|
16
|
+
}
|
|
17
|
+
#player-container {
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
}
|
|
21
|
+
webview {
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
border: none;
|
|
25
|
+
}
|
|
26
|
+
#close-btn, .dev-btn {
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 20px;
|
|
29
|
+
z-index: 9999;
|
|
30
|
+
background: rgba(0, 0, 0, 0.5);
|
|
31
|
+
color: white;
|
|
32
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
33
|
+
padding: 8px 16px;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
transition: background 0.2s;
|
|
38
|
+
-webkit-app-region: no-drag;
|
|
39
|
+
}
|
|
40
|
+
#close-btn {
|
|
41
|
+
right: 20px;
|
|
42
|
+
}
|
|
43
|
+
#close-btn:hover {
|
|
44
|
+
background: rgba(255, 0, 0, 0.7);
|
|
45
|
+
}
|
|
46
|
+
.dev-btn:hover {
|
|
47
|
+
background: rgba(37, 99, 235, 0.7);
|
|
48
|
+
}
|
|
49
|
+
.drag-region {
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 0;
|
|
52
|
+
left: 0;
|
|
53
|
+
right: 80px;
|
|
54
|
+
height: 40px;
|
|
55
|
+
-webkit-app-region: drag;
|
|
56
|
+
z-index: 9998;
|
|
57
|
+
}
|
|
58
|
+
#loading-overlay {
|
|
59
|
+
position: absolute;
|
|
60
|
+
top: 0;
|
|
61
|
+
left: 0;
|
|
62
|
+
width: 100%;
|
|
63
|
+
height: 100%;
|
|
64
|
+
background: #000;
|
|
65
|
+
z-index: 9997;
|
|
66
|
+
display: none;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
color: #fff;
|
|
71
|
+
transition: opacity 0.5s ease;
|
|
72
|
+
}
|
|
73
|
+
.spinner {
|
|
74
|
+
width: 40px;
|
|
75
|
+
height: 40px;
|
|
76
|
+
border: 4px solid rgba(255, 255, 255, 0.1);
|
|
77
|
+
border-top: 4px solid #E11D48;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
animation: spin 1s linear infinite;
|
|
80
|
+
margin-bottom: 20px;
|
|
81
|
+
}
|
|
82
|
+
@keyframes spin {
|
|
83
|
+
0% { transform: rotate(0deg); }
|
|
84
|
+
100% { transform: rotate(360deg); }
|
|
85
|
+
}
|
|
86
|
+
.loading-text {
|
|
87
|
+
font-size: 16px;
|
|
88
|
+
color: #aaa;
|
|
89
|
+
letter-spacing: 1px;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
92
|
+
<script src="./node_modules/hls.js/dist/hls.min.js"></script>
|
|
93
|
+
<script src="./node_modules/artplayer/dist/artplayer.js"></script>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<div class="drag-region"></div>
|
|
97
|
+
<button id="close-btn" onclick="window.close()">✕ Đóng</button>
|
|
98
|
+
<div id="loading-overlay">
|
|
99
|
+
<div class="spinner"></div>
|
|
100
|
+
<div class="loading-text">Đang tải và làm sạch trình phát...</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div id="player-container"></div>
|
|
103
|
+
|
|
104
|
+
<script>
|
|
105
|
+
const { ipcRenderer } = require('electron');
|
|
106
|
+
|
|
107
|
+
// Keyboard shortcut: Escape to close
|
|
108
|
+
document.addEventListener('keydown', (e) => {
|
|
109
|
+
if (e.key === 'Escape') window.close();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
window.initPlayer = function(streamInfo) {
|
|
114
|
+
const container = document.getElementById('player-container');
|
|
115
|
+
|
|
116
|
+
if (streamInfo.type === 'iframe') {
|
|
117
|
+
const overlay = document.getElementById('loading-overlay');
|
|
118
|
+
overlay.style.display = 'flex';
|
|
119
|
+
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
overlay.style.opacity = '0';
|
|
122
|
+
setTimeout(() => { overlay.style.display = 'none'; }, 500);
|
|
123
|
+
}, 5500);
|
|
124
|
+
|
|
125
|
+
const isGooglePlayer = streamInfo.url.includes('googleapiscdn.com') || streamInfo.url.includes('googleapis.com');
|
|
126
|
+
|
|
127
|
+
const path = require('path');
|
|
128
|
+
const url = require('url');
|
|
129
|
+
const preloadPath = url.pathToFileURL(path.resolve(__dirname, 'webview-preload.js')).href;
|
|
130
|
+
|
|
131
|
+
// For Google Player: MUST load referer page first so the player thinks it's in an iframe
|
|
132
|
+
// (googleapiscdn checks window.self !== window.top and refuses to play if at top level)
|
|
133
|
+
// The preload script runs in BOTH the referer page AND the injected iframe subframe
|
|
134
|
+
// via nodeIntegrationInSubFrames=yes
|
|
135
|
+
let webviewSrc = streamInfo.url;
|
|
136
|
+
if (isGooglePlayer && streamInfo.headers && streamInfo.headers.Referer) {
|
|
137
|
+
webviewSrc = streamInfo.headers.Referer;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log('[Renderer] Webview src:', webviewSrc, '| preload:', preloadPath);
|
|
141
|
+
|
|
142
|
+
const webview = document.createElement('webview');
|
|
143
|
+
webview.setAttribute('partition', 'persist:player-session');
|
|
144
|
+
webview.setAttribute('useragent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36');
|
|
145
|
+
webview.setAttribute('allowpopups', 'false');
|
|
146
|
+
webview.setAttribute('disablewebsecurity', '');
|
|
147
|
+
webview.setAttribute('webpreferences', 'webSecurity=no, contextIsolation=no, nodeIntegration=yes, nodeIntegrationInSubFrames=yes');
|
|
148
|
+
webview.setAttribute('preload', preloadPath);
|
|
149
|
+
webview.setAttribute('src', webviewSrc); // src LAST so preload registers first
|
|
150
|
+
|
|
151
|
+
let authInjected = false;
|
|
152
|
+
let playerInjected = false;
|
|
153
|
+
|
|
154
|
+
webview.addEventListener('dom-ready', () => {
|
|
155
|
+
try {
|
|
156
|
+
console.log('[Renderer Webview] dom-ready for:', webview.getURL());
|
|
157
|
+
|
|
158
|
+
// Step 1: inject auth localStorage if needed
|
|
159
|
+
if (!authInjected && streamInfo.localStorageState && typeof streamInfo.localStorageState === 'object') {
|
|
160
|
+
const entries = Object.entries(streamInfo.localStorageState);
|
|
161
|
+
if (entries.length > 0) {
|
|
162
|
+
authInjected = true;
|
|
163
|
+
const injectScript = entries
|
|
164
|
+
.map(([k, v]) => `try{localStorage.setItem(${JSON.stringify(k)},${JSON.stringify(v)})}catch(e){}`)
|
|
165
|
+
.join(';');
|
|
166
|
+
webview.executeJavaScript(injectScript)
|
|
167
|
+
.then(() => { webview.reload(); })
|
|
168
|
+
.catch((err) => console.error('[Renderer Webview] LocalStorage inject failed:', err));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Step 2: For Google Player, replace page body with a fullscreen iframe pointing to the actual stream
|
|
174
|
+
// This makes googleapiscdn think it's embedded (window.self !== window.top = true inside the iframe)
|
|
175
|
+
if (isGooglePlayer && !playerInjected) {
|
|
176
|
+
playerInjected = true;
|
|
177
|
+
console.log('[Renderer Webview] Injecting Google Player iframe into referer page...');
|
|
178
|
+
const streamUrl = JSON.stringify(streamInfo.url);
|
|
179
|
+
webview.executeJavaScript(`
|
|
180
|
+
(function() {
|
|
181
|
+
window.stop();
|
|
182
|
+
document.documentElement.innerHTML = '<head><style>*{margin:0;padding:0;overflow:hidden;background:#000}</style></head><body></body>';
|
|
183
|
+
const f = document.createElement('iframe');
|
|
184
|
+
f.id = '__neko_player__';
|
|
185
|
+
f.src = ${streamUrl};
|
|
186
|
+
f.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;border:none';
|
|
187
|
+
f.setAttribute('allowfullscreen','true');
|
|
188
|
+
document.body.appendChild(f);
|
|
189
|
+
console.log('[Injected] Google Player iframe appended:', f.src);
|
|
190
|
+
})();
|
|
191
|
+
`)
|
|
192
|
+
.then(() => {
|
|
193
|
+
console.log('[Renderer Webview] Player iframe injected OK');
|
|
194
|
+
|
|
195
|
+
// Start a reliable ad-cleaner that accesses iframe.contentDocument directly.
|
|
196
|
+
// This works because webSecurity=no disables the same-origin policy for the webview.
|
|
197
|
+
const AD_TEXTS = ['Đóng quảng cáo', 'Đóng và xem tiếp', 'Quảng cáo'];
|
|
198
|
+
const AD_SRCS = ['in88', 'quayhu', 'nohu', '188bet', 'kubet', 'w88', 'fun88'];
|
|
199
|
+
const ADBLOCK_STYLE_ID = '__neko_adblock__';
|
|
200
|
+
|
|
201
|
+
const cleanAdOverlay = () => {
|
|
202
|
+
webview.executeJavaScript(`
|
|
203
|
+
(function() {
|
|
204
|
+
try {
|
|
205
|
+
const frame = document.getElementById('__neko_player__') || document.querySelector('iframe');
|
|
206
|
+
if (!frame) return;
|
|
207
|
+
const doc = frame.contentDocument || frame.contentWindow && frame.contentWindow.document;
|
|
208
|
+
if (!doc || !doc.body) return;
|
|
209
|
+
|
|
210
|
+
// Inject persistent blocking CSS once
|
|
211
|
+
if (!doc.getElementById('${ADBLOCK_STYLE_ID}')) {
|
|
212
|
+
const s = doc.createElement('style');
|
|
213
|
+
s.id = '${ADBLOCK_STYLE_ID}';
|
|
214
|
+
s.textContent = [
|
|
215
|
+
'img[src*="in88"],img[src*="quayhu"],img[src*="nohu"],img[src*="188bet"],',
|
|
216
|
+
'img[src*="kubet"],img[src*="w88"],img[src*="fun88"],',
|
|
217
|
+
'[class*="pause-ad"],[id*="pause-ad"],[class*="ads-pause"],[id*="ads-pause"],',
|
|
218
|
+
'[class*="overlay-ad"],[id*="overlay-ad"] { display:none!important; }'
|
|
219
|
+
].join('');
|
|
220
|
+
(doc.head || doc.documentElement).appendChild(s);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Walk every element looking for ad text / ad image src
|
|
224
|
+
const AD_TEXTS = ${JSON.stringify(AD_TEXTS)};
|
|
225
|
+
const AD_SRCS = ${JSON.stringify(AD_SRCS)};
|
|
226
|
+
doc.querySelectorAll('div,button,a,span,img').forEach(el => {
|
|
227
|
+
const txt = (el.innerText || el.textContent || '').trim();
|
|
228
|
+
const src = (el.src || el.getAttribute('src') || el.getAttribute('href') || '').toLowerCase();
|
|
229
|
+
const isAdText = AD_TEXTS.some(t => txt === t);
|
|
230
|
+
const isAdSrc = AD_SRCS.some(k => src.includes(k));
|
|
231
|
+
if (!isAdText && !isAdSrc) return;
|
|
232
|
+
|
|
233
|
+
// Walk up to find the overlay container (fixed/absolute positioned)
|
|
234
|
+
let node = el;
|
|
235
|
+
for (let i = 0; i < 8; i++) {
|
|
236
|
+
const p = node.parentElement;
|
|
237
|
+
if (!p || p === doc.body || p === doc.documentElement) break;
|
|
238
|
+
if (p.querySelector('video') || p.querySelector('canvas')) break;
|
|
239
|
+
const cs = window.getComputedStyle(p);
|
|
240
|
+
if (cs.position === 'fixed' || cs.position === 'absolute') {
|
|
241
|
+
p.remove(); return;
|
|
242
|
+
}
|
|
243
|
+
node = p;
|
|
244
|
+
}
|
|
245
|
+
el.remove();
|
|
246
|
+
});
|
|
247
|
+
} catch(e) { /* cross-origin not yet ready, retry */ }
|
|
248
|
+
})();
|
|
249
|
+
`).catch(() => {});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Run immediately and then every 300ms
|
|
253
|
+
setTimeout(cleanAdOverlay, 800);
|
|
254
|
+
setInterval(cleanAdOverlay, 300);
|
|
255
|
+
|
|
256
|
+
// === DOM DIAGNOSTIC DUMP ===
|
|
257
|
+
// Fires once after 3s to capture ad structure for analysis.
|
|
258
|
+
// Writes to dom-dump.html in the project directory.
|
|
259
|
+
const fs = require('fs');
|
|
260
|
+
const dumpPath = require('path').join(__dirname, 'dom-dump.html');
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
webview.executeJavaScript(`
|
|
263
|
+
(function() {
|
|
264
|
+
try {
|
|
265
|
+
const frame = document.getElementById('__neko_player__') || document.querySelector('iframe');
|
|
266
|
+
if (!frame) return 'NO_FRAME';
|
|
267
|
+
const doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document);
|
|
268
|
+
if (!doc || !doc.body) return 'NO_DOC';
|
|
269
|
+
return doc.documentElement.outerHTML;
|
|
270
|
+
} catch(e) { return 'ERROR: ' + e.message; }
|
|
271
|
+
})();
|
|
272
|
+
`).then(html => {
|
|
273
|
+
if (html && typeof html === 'string' && html.length > 100) {
|
|
274
|
+
fs.writeFileSync(dumpPath, html, 'utf-8');
|
|
275
|
+
console.log('[DOM Dump] Written to dom-dump.html, size:', html.length, 'chars');
|
|
276
|
+
} else {
|
|
277
|
+
console.warn('[DOM Dump] Got unexpected result:', html);
|
|
278
|
+
}
|
|
279
|
+
}).catch(e => console.error('[DOM Dump] Error:', e));
|
|
280
|
+
}, 3000);
|
|
281
|
+
})
|
|
282
|
+
.catch((err) => console.error('[Renderer Webview] Player inject failed:', err));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Step 3: For other providers (Anime47), run UI isolation
|
|
287
|
+
if (!isGooglePlayer) {
|
|
288
|
+
const fs = require('fs');
|
|
289
|
+
const path = require('path');
|
|
290
|
+
try {
|
|
291
|
+
const css = fs.readFileSync(path.join(__dirname, 'isolate.css'), 'utf-8');
|
|
292
|
+
const js = fs.readFileSync(path.join(__dirname, 'isolate.js'), 'utf-8');
|
|
293
|
+
webview.insertCSS(css);
|
|
294
|
+
webview.executeJavaScript(js);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error('[Renderer Webview] Isolation inject failed:', e);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error('[Renderer Webview] dom-ready error:', e);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
container.appendChild(webview);
|
|
305
|
+
} else {
|
|
306
|
+
// Use Artplayer for m3u8 / mp4
|
|
307
|
+
const artType = streamInfo.type === 'hls' ? 'm3u8' : streamInfo.type;
|
|
308
|
+
const art = new Artplayer({
|
|
309
|
+
container: container,
|
|
310
|
+
url: streamInfo.url,
|
|
311
|
+
type: artType,
|
|
312
|
+
title: streamInfo.provider || 'NekoStream CLI',
|
|
313
|
+
poster: '',
|
|
314
|
+
volume: 0.5,
|
|
315
|
+
isLive: false,
|
|
316
|
+
muted: false,
|
|
317
|
+
autoplay: false, // Must be false for customType, we play manually inside MANIFEST_PARSED
|
|
318
|
+
pip: true,
|
|
319
|
+
autoSize: true,
|
|
320
|
+
autoMini: true,
|
|
321
|
+
setting: true,
|
|
322
|
+
loop: false,
|
|
323
|
+
flip: true,
|
|
324
|
+
playbackRate: true,
|
|
325
|
+
aspectRatio: true,
|
|
326
|
+
fullscreen: true,
|
|
327
|
+
fullscreenWeb: true,
|
|
328
|
+
subtitleOffset: true,
|
|
329
|
+
miniProgressBar: true,
|
|
330
|
+
mutex: true,
|
|
331
|
+
backdrop: true,
|
|
332
|
+
playsInline: true,
|
|
333
|
+
autoPlayback: true,
|
|
334
|
+
airplay: true,
|
|
335
|
+
theme: '#ff0055',
|
|
336
|
+
customType: {
|
|
337
|
+
m3u8: function(video, url) {
|
|
338
|
+
if (Hls.isSupported()) {
|
|
339
|
+
const hls = new Hls({
|
|
340
|
+
xhrSetup: function(xhr, url) {
|
|
341
|
+
// Note: headers are already intercepted in main process
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
hls.loadSource(url);
|
|
345
|
+
hls.attachMedia(video);
|
|
346
|
+
hls.on(Hls.Events.MANIFEST_PARSED, function() {
|
|
347
|
+
art.play().catch(() => {});
|
|
348
|
+
});
|
|
349
|
+
// Handle HLS errors so it doesn't just spin
|
|
350
|
+
hls.on(Hls.Events.ERROR, function(event, data) {
|
|
351
|
+
if (data.fatal) {
|
|
352
|
+
switch(data.type) {
|
|
353
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
354
|
+
hls.startLoad();
|
|
355
|
+
break;
|
|
356
|
+
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
357
|
+
hls.recoverMediaError();
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
hls.destroy();
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
366
|
+
video.src = url;
|
|
367
|
+
art.play().catch(() => {});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
</script>
|
|
375
|
+
</body>
|
|
376
|
+
</html>
|