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.
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ // Video stream interceptor and parser
3
+ // Captures HLS/DASH/MP4 streams from video players
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.parseM3U8 = parseM3U8;
6
+ exports.fetchM3U8 = fetchM3U8;
7
+ exports.extractVideoFromJS = extractVideoFromJS;
8
+ exports.waitForVideoSource = waitForVideoSource;
9
+ /**
10
+ * Parse M3U8 master playlist to extract quality options
11
+ */
12
+ function parseM3U8(content, baseUrl) {
13
+ const lines = content.split('\n').map(l => l.trim()).filter(l => l);
14
+ const qualities = [];
15
+ let isMaster = false;
16
+ for (let i = 0; i < lines.length; i++) {
17
+ const line = lines[i];
18
+ // Check if master playlist
19
+ if (line.includes('#EXT-X-STREAM-INF')) {
20
+ isMaster = true;
21
+ // Parse attributes
22
+ const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
23
+ const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
24
+ const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : undefined;
25
+ const resolution = resolutionMatch ? resolutionMatch[1] : undefined;
26
+ // Get quality from resolution or bandwidth
27
+ let quality = 'unknown';
28
+ if (resolution) {
29
+ const height = parseInt(resolution.split('x')[1]);
30
+ quality = `${height}p`;
31
+ }
32
+ else if (bandwidth) {
33
+ if (bandwidth > 4000000)
34
+ quality = '1080p';
35
+ else if (bandwidth > 2000000)
36
+ quality = '720p';
37
+ else if (bandwidth > 1000000)
38
+ quality = '480p';
39
+ else
40
+ quality = '360p';
41
+ }
42
+ // Next line should be the URL
43
+ const urlLine = lines[i + 1];
44
+ if (urlLine && !urlLine.startsWith('#')) {
45
+ const url = urlLine.startsWith('http')
46
+ ? urlLine
47
+ : new URL(urlLine, baseUrl).href;
48
+ qualities.push({
49
+ quality,
50
+ bandwidth,
51
+ resolution,
52
+ url
53
+ });
54
+ i++; // Skip URL line
55
+ }
56
+ }
57
+ }
58
+ // Sort by quality (highest first)
59
+ qualities.sort((a, b) => {
60
+ const getHeight = (q) => parseInt(q.replace('p', '')) || 0;
61
+ return getHeight(b.quality) - getHeight(a.quality);
62
+ });
63
+ return {
64
+ type: isMaster ? 'master' : 'media',
65
+ qualities,
66
+ defaultUrl: qualities[0]?.url
67
+ };
68
+ }
69
+ /**
70
+ * Fetch and parse M3U8 playlist
71
+ */
72
+ async function fetchM3U8(url, headers = {}) {
73
+ try {
74
+ const response = await fetch(url, {
75
+ headers: {
76
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
77
+ ...headers
78
+ }
79
+ });
80
+ if (!response.ok)
81
+ return null;
82
+ const content = await response.text();
83
+ const baseUrl = url.substring(0, url.lastIndexOf('/') + 1);
84
+ return parseM3U8(content, baseUrl);
85
+ }
86
+ catch (error) {
87
+ console.error('Error fetching M3U8:', error);
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Try to find video sources from page JavaScript
93
+ */
94
+ async function extractVideoFromJS(page) {
95
+ const videos = [];
96
+ try {
97
+ // Try to get video sources from various player implementations
98
+ const sources = await page.evaluate(() => {
99
+ const urls = [];
100
+ // Check video elements
101
+ document.querySelectorAll('video').forEach(video => {
102
+ if (video.src)
103
+ urls.push(video.src);
104
+ if (video.currentSrc)
105
+ urls.push(video.currentSrc);
106
+ video.querySelectorAll('source').forEach(source => {
107
+ if (source.src)
108
+ urls.push(source.src);
109
+ });
110
+ });
111
+ // Check for ArtPlayer
112
+ if (window.art?.option?.url) {
113
+ urls.push(window.art.option.url);
114
+ }
115
+ // Check for Plyr
116
+ if (window.player?.source?.sources) {
117
+ window.player.source.sources.forEach((s) => {
118
+ if (s.src)
119
+ urls.push(s.src);
120
+ });
121
+ }
122
+ // Check for Video.js
123
+ if (window.videojs) {
124
+ try {
125
+ const players = window.videojs.getPlayers();
126
+ Object.values(players).forEach((p) => {
127
+ if (p?.currentSrc)
128
+ urls.push(p.currentSrc());
129
+ if (p?.src)
130
+ urls.push(typeof p.src === 'function' ? p.src() : p.src);
131
+ });
132
+ }
133
+ catch (e) { }
134
+ }
135
+ // Check for JWPlayer - multiple detection methods
136
+ if (window.jwplayer) {
137
+ try {
138
+ // Try default instance
139
+ const jw = window.jwplayer();
140
+ if (jw?.getPlaylistItem) {
141
+ const item = jw.getPlaylistItem();
142
+ if (item?.file)
143
+ urls.push(item.file);
144
+ if (item?.sources) {
145
+ item.sources.forEach((s) => {
146
+ if (s.file)
147
+ urls.push(s.file);
148
+ });
149
+ }
150
+ }
151
+ if (jw?.getConfig) {
152
+ const config = jw.getConfig();
153
+ if (config?.file)
154
+ urls.push(config.file);
155
+ if (config?.playlist) {
156
+ config.playlist.forEach((p) => {
157
+ if (p.file)
158
+ urls.push(p.file);
159
+ if (p.sources)
160
+ p.sources.forEach((s) => s.file && urls.push(s.file));
161
+ });
162
+ }
163
+ }
164
+ }
165
+ catch (e) { }
166
+ // Try all JWPlayer instances
167
+ try {
168
+ const allJw = window.jwplayer?.instances || [];
169
+ allJw.forEach((inst) => {
170
+ try {
171
+ const item = inst?.getPlaylistItem?.();
172
+ if (item?.file)
173
+ urls.push(item.file);
174
+ if (item?.sources)
175
+ item.sources.forEach((s) => s.file && urls.push(s.file));
176
+ }
177
+ catch (e) { }
178
+ });
179
+ }
180
+ catch (e) { }
181
+ }
182
+ // Check global variables commonly used by anime sites
183
+ const globals = ['source', 'video_url', 'videoUrl', 'fileUrl', 'file', 'streamUrl', 'hlsUrl', 'mp4Url', 'playUrl'];
184
+ globals.forEach(g => {
185
+ const val = window[g];
186
+ if (typeof val === 'string' && val.includes('http'))
187
+ urls.push(val);
188
+ });
189
+ // Search in page scripts for video URLs
190
+ const scripts = document.querySelectorAll('script');
191
+ scripts.forEach(script => {
192
+ const content = script.textContent || '';
193
+ // Find m3u8 URLs
194
+ const m3u8Matches = content.match(/https?:\/\/[^\s"'<>]+\.m3u8[^\s"'<>]*/g);
195
+ if (m3u8Matches)
196
+ urls.push(...m3u8Matches);
197
+ // Find mp4 URLs - more aggressive pattern
198
+ const mp4Matches = content.match(/https?:\/\/[^\s"'<>]+\.mp4[^\s"'<>]*/g);
199
+ if (mp4Matches)
200
+ urls.push(...mp4Matches);
201
+ // Find file: "url" patterns (common in player configs)
202
+ const fileMatches = content.match(/["']file["']\s*:\s*["']([^"']+)["']/gi);
203
+ fileMatches?.forEach(m => {
204
+ const urlMatch = m.match(/["']([^"']+)["']$/i);
205
+ if (urlMatch?.[1]?.startsWith('http'))
206
+ urls.push(urlMatch[1]);
207
+ });
208
+ // Find sources array patterns
209
+ const sourcesMatch = content.match(/["']?sources["']?\s*:\s*\[([^\]]+)\]/gi);
210
+ sourcesMatch?.forEach(m => {
211
+ const urlMatches = m.match(/https?:\/\/[^\s"'<>\]]+/g);
212
+ if (urlMatches)
213
+ urls.push(...urlMatches);
214
+ });
215
+ });
216
+ // Check iframes for potential embed URLs (for logging/fallback)
217
+ document.querySelectorAll('iframe[src]').forEach(iframe => {
218
+ const src = iframe.getAttribute('src');
219
+ if (src && (src.includes('player') || src.includes('embed') || src.includes('video'))) {
220
+ // Don't add iframe URLs as direct streams, but log them
221
+ console.log('[extractVideoFromJS] Found player iframe:', src);
222
+ }
223
+ });
224
+ return urls;
225
+ });
226
+ // Deduplicate and filter
227
+ const unique = [...new Set(sources)].filter(url => {
228
+ if (!url.startsWith('http'))
229
+ return false;
230
+ // Must contain video extension or streaming patterns
231
+ return (url.includes('.m3u8') ||
232
+ url.includes('.mp4') ||
233
+ url.includes('.mpd') ||
234
+ url.includes('.webm') ||
235
+ url.includes('/hls/') ||
236
+ url.includes('/video/'));
237
+ });
238
+ console.log('[extractVideoFromJS] Found', unique.length, 'video URLs');
239
+ videos.push(...unique);
240
+ }
241
+ catch (error) {
242
+ console.error('Error extracting video from JS:', error);
243
+ }
244
+ return videos;
245
+ }
246
+ /**
247
+ * Wait for video element to have a source
248
+ */
249
+ async function waitForVideoSource(page, timeout = 10000) {
250
+ try {
251
+ const source = await page.evaluate(() => {
252
+ return new Promise((resolve) => {
253
+ const checkVideo = () => {
254
+ const video = document.querySelector('video');
255
+ if (video?.src && video.src !== '') {
256
+ resolve(video.src);
257
+ return true;
258
+ }
259
+ return false;
260
+ };
261
+ // Check immediately
262
+ if (checkVideo())
263
+ return;
264
+ // Watch for changes
265
+ const observer = new MutationObserver(() => {
266
+ if (checkVideo()) {
267
+ observer.disconnect();
268
+ }
269
+ });
270
+ observer.observe(document.body, {
271
+ childList: true,
272
+ subtree: true,
273
+ attributes: true,
274
+ attributeFilter: ['src']
275
+ });
276
+ // Timeout
277
+ setTimeout(() => {
278
+ observer.disconnect();
279
+ resolve(null);
280
+ }, 10000);
281
+ });
282
+ });
283
+ return source;
284
+ }
285
+ catch (error) {
286
+ return null;
287
+ }
288
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jikanApi = void 0;
4
+ const undici_1 = require("undici");
5
+ class JikanApiService {
6
+ CACHE_DURATION_MS = 1000 * 60 * 60 * 24; // 24 hours caching
7
+ cache = new Map();
8
+ // Rate limiting properties (Jikan applies strict limits, spacing out by 1000ms is perfectly safe)
9
+ lastRequestTime = 0;
10
+ RATE_LIMIT_DELAY_MS = 1100;
11
+ async getAnimeCover(title, titleAlt) {
12
+ const rawTitle = titleAlt ? titleAlt.split(',')[0].trim() : title.trim();
13
+ if (!rawTitle)
14
+ return null;
15
+ const cacheKey = rawTitle.toLowerCase();
16
+ // Check in-memory session cache first
17
+ const cached = this.cache.get(cacheKey);
18
+ if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION_MS) {
19
+ return cached.data.cover;
20
+ }
21
+ // Apply strict rate limiting before fetch
22
+ const now = Date.now();
23
+ const timeSinceLastRequest = now - this.lastRequestTime;
24
+ if (timeSinceLastRequest < this.RATE_LIMIT_DELAY_MS) {
25
+ await new Promise(resolve => setTimeout(resolve, this.RATE_LIMIT_DELAY_MS - timeSinceLastRequest));
26
+ }
27
+ try {
28
+ this.lastRequestTime = Date.now();
29
+ const searchUrl = `https://api.jikan.moe/v4/anime?q=${encodeURIComponent(rawTitle)}&limit=1`;
30
+ const { statusCode, body } = await (0, undici_1.request)(searchUrl, {
31
+ headers: {
32
+ 'User-Agent': 'NekoStream-Desktop-App/1.0'
33
+ }
34
+ });
35
+ if (statusCode !== 200) {
36
+ console.warn(`[JikanAPI] API returned status ${statusCode} for "${rawTitle}"`);
37
+ return null;
38
+ }
39
+ const rawBody = await body.text();
40
+ const json = JSON.parse(rawBody);
41
+ if (json.data && json.data.length > 0) {
42
+ // Prefer large webp, fallback to large jpg
43
+ const cover = json.data[0].images?.webp?.large_image_url || json.data[0].images?.jpg?.large_image_url || null;
44
+ const result = { cover };
45
+ this.cache.set(cacheKey, { data: result, timestamp: Date.now() });
46
+ console.log(`[JikanAPI] Cached cover for "${rawTitle}": ${cover}`);
47
+ return cover;
48
+ }
49
+ console.warn(`[JikanAPI] No anime found matching "${rawTitle}"`);
50
+ // Cache empty result to avoid hitting limit for unknowns
51
+ this.cache.set(cacheKey, { data: { cover: null }, timestamp: Date.now() });
52
+ return null;
53
+ }
54
+ catch (error) {
55
+ console.error('[JikanAPI] Error fetching anime cover:', error);
56
+ return null;
57
+ }
58
+ }
59
+ }
60
+ exports.jikanApi = new JikanApiService();