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/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>