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
|
@@ -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();
|