ani-auto 1.4.3 → 2.0.0

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.
Files changed (90) hide show
  1. package/anime/LICENSE +21 -0
  2. package/anime/api/index.js +110 -0
  3. package/anime/app.js +79 -0
  4. package/anime/controllers/animeInfoController.js +48 -0
  5. package/anime/controllers/animeListController.js +21 -0
  6. package/anime/controllers/homeController.js +42 -0
  7. package/anime/controllers/playController.js +140 -0
  8. package/anime/controllers/queueController.js +20 -0
  9. package/anime/controllers/testController.js +667 -0
  10. package/anime/index.js +139 -0
  11. package/anime/middleware/cache.js +64 -0
  12. package/anime/middleware/errorHandler.js +33 -0
  13. package/anime/middleware/rateLimiter.js +73 -0
  14. package/anime/models/animeInfoModel.js +193 -0
  15. package/anime/models/animeListModel.js +69 -0
  16. package/anime/models/homeModel.js +33 -0
  17. package/anime/models/playModel.js +528 -0
  18. package/anime/models/queueModel.js +22 -0
  19. package/anime/package.json +27 -0
  20. package/anime/pnpm-lock.yaml +1774 -0
  21. package/anime/readme.md +236 -0
  22. package/anime/routes/animeInfoRoutes.js +9 -0
  23. package/anime/routes/animeListRoutes.js +9 -0
  24. package/anime/routes/homeRoutes.js +10 -0
  25. package/anime/routes/playRoutes.js +11 -0
  26. package/anime/routes/queueRoutes.js +8 -0
  27. package/anime/routes/testRoutes.js +325 -0
  28. package/anime/routes/webhookRoutes.js +23 -0
  29. package/anime/scrapers/animepahe.js +1053 -0
  30. package/anime/test-server.js +10 -0
  31. package/anime/test.sh +275 -0
  32. package/anime/utils/browser.js +172 -0
  33. package/anime/utils/config.js +209 -0
  34. package/anime/utils/dataProcessor.js +172 -0
  35. package/anime/utils/diskCache.js +228 -0
  36. package/anime/utils/flaresolverr.js +361 -0
  37. package/anime/utils/jsParser.js +19 -0
  38. package/anime/utils/redis.js +64 -0
  39. package/anime/utils/requestManager.js +706 -0
  40. package/anime/utils/urlConverter.js +88 -0
  41. package/anime/utils/webhookNotifier.js +190 -0
  42. package/cookiereader.py +291 -0
  43. package/package.json +14 -6
  44. package/src/anilist.js +19 -2
  45. package/src/api/anilist.js +32 -0
  46. package/src/api/anime.js +181 -0
  47. package/src/api/cf-bypass.js +131 -0
  48. package/src/api/config.js +134 -0
  49. package/src/api/daemon.js +62 -0
  50. package/src/api/download.js +98 -0
  51. package/src/api/match.js +58 -0
  52. package/src/api/runs.js +15 -0
  53. package/src/api/update.js +96 -0
  54. package/src/cli.js +18 -2
  55. package/src/config.js +14 -2
  56. package/src/daemon.js +1 -1
  57. package/src/db.js +38 -11
  58. package/src/engine.js +66 -13
  59. package/src/notify.js +82 -7
  60. package/src/scraper.js +2 -2
  61. package/src/server.js +108 -0
  62. package/utils.js +21 -21
  63. package/web/index.html +13 -0
  64. package/web/package-lock.json +1420 -0
  65. package/web/package.json +24 -0
  66. package/web/src/App.vue +200 -0
  67. package/web/src/api/client.js +75 -0
  68. package/web/src/assets/styles/base.css +961 -0
  69. package/web/src/components/UpdateModal.vue +157 -0
  70. package/web/src/components/sidebar/SidebarResizable.vue +170 -0
  71. package/web/src/components/sidebar/sidebarConfig.js +24 -0
  72. package/web/src/main.js +104 -0
  73. package/web/src/router/index.js +44 -0
  74. package/web/src/stores/config.js +27 -0
  75. package/web/src/stores/download.js +107 -0
  76. package/web/src/stores/update.js +73 -0
  77. package/web/src/views/About.vue +42 -0
  78. package/web/src/views/AniListSync.vue +122 -0
  79. package/web/src/views/AnimeDetail.vue +202 -0
  80. package/web/src/views/AnimeList.vue +110 -0
  81. package/web/src/views/CFResult.vue +312 -0
  82. package/web/src/views/DaemonControl.vue +83 -0
  83. package/web/src/views/Dashboard.vue +187 -0
  84. package/web/src/views/DownloadCenter.vue +153 -0
  85. package/web/src/views/MatchFinder.vue +131 -0
  86. package/web/src/views/OAuthCallback.vue +44 -0
  87. package/web/src/views/Onboarding.vue +93 -0
  88. package/web/src/views/RunHistory.vue +122 -0
  89. package/web/src/views/Settings.vue +410 -0
  90. package/web/vite.config.js +23 -0
@@ -0,0 +1,528 @@
1
+ const cheerio = require('cheerio');
2
+ const vm = require('vm');
3
+ const { JSDOM } = require('jsdom');
4
+ const Config = require('../utils/config');
5
+ const DataProcessor = require('../utils/dataProcessor');
6
+ const Animepahe = require('../scrapers/animepahe');
7
+ const { getJsVariable } = require('../utils/jsParser');
8
+ const { CustomError } = require('../middleware/errorHandler');
9
+ const UrlConverter = require('../utils/urlConverter');
10
+
11
+ class PlayModel {
12
+ static async getStreamingLinks(id, episodeId, includeDownloads = true) {
13
+ const results = await Animepahe.getData('play', { id, episodeId }, false);
14
+ if (!results) throw new CustomError('Failed to fetch streaming data', 503);
15
+
16
+ if (typeof results === 'object' && !results.data) results.data = [];
17
+ if (results.data) return DataProcessor.processApiData(results);
18
+
19
+ return this.scrapePlayPage(id, episodeId, results, includeDownloads);
20
+ }
21
+
22
+ static async scrapeIframe(id, episodeId, url) {
23
+ const html = await Animepahe.fetchIframeHtml(id, episodeId, url);
24
+ if (!html) throw new CustomError('Failed to fetch iframe data', 503);
25
+
26
+ return this.extractSources(html, url);
27
+ }
28
+
29
+
30
+ static async extractSources(html, url = '') {
31
+ const scriptMatches = [...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)].map(m => m[1]);
32
+ if (!scriptMatches.length) {
33
+ console.log('No inline <script> blocks found.');
34
+ return null;
35
+ }
36
+ console.log(`Found ${scriptMatches.length} script tags.`);
37
+
38
+ const findM3u8 = (s) => {
39
+ if (!s) return null;
40
+ const m = s.match(/https?:\/\/[^"'<> \n\r]+\.m3u8[^\s"'<>]*/i);
41
+ return m ? m[0] : null;
42
+ };
43
+
44
+ for (const script of scriptMatches) {
45
+ // Optimization: fast regex check before heavy VM execution
46
+ const fromScript = findM3u8(script);
47
+ if (fromScript) {
48
+ return [{ url: fromScript, isM3U8: fromScript.includes('.m3u8') || false }];
49
+ }
50
+
51
+ if (!script.includes('eval(')) continue;
52
+
53
+ const dom = new JSDOM(`<!DOCTYPE html><video id="player"></video>`);
54
+ const document = dom.window.document;
55
+ const videoEl = document.querySelector('video');
56
+
57
+ const captured = new Set();
58
+
59
+ const Plyr = function (el, opts) {
60
+ try {
61
+ if (opts && opts.sources && Array.isArray(opts.sources)) {
62
+ for (const s of opts.sources) {
63
+ if (s && typeof s.src === 'string' && s.src.includes('.m3u8')) captured.add(s.src);
64
+ }
65
+ }
66
+ } catch (e) { /* ignore */ }
67
+ return { on: () => {}, };
68
+ };
69
+
70
+ const Hls = function (cfg) {
71
+ return {
72
+ loadSource: (src) => {
73
+ try { if (typeof src === 'string' && src.includes('.m3u8')) captured.add(src); } catch (e) {}
74
+ },
75
+ attachMedia: (m) => {
76
+ try {
77
+ if (m && m.src && typeof m.src === 'string' && m.src.includes('.m3u8')) captured.add(m.src);
78
+ } catch (e) {}
79
+ },
80
+ on: () => {},
81
+ };
82
+ };
83
+ Hls.isSupported = () => true;
84
+
85
+ const sandbox = {
86
+ console,
87
+ window: dom.window,
88
+ document: dom.window.document,
89
+ navigator: { userAgent: Config.userAgent },
90
+ location: { href: url },
91
+ Plyr,
92
+ Hls,
93
+ setTimeout,
94
+ clearTimeout,
95
+ };
96
+
97
+ vm.createContext(sandbox);
98
+
99
+ try {
100
+ vm.runInContext(script, sandbox, { timeout: 2000 });
101
+ } catch (err) {
102
+ // ignore eval errors
103
+ }
104
+
105
+ const innerEvalBodies = [];
106
+ const packedMatch = script.match(/eval\((function[\s\S]*?)\)\s*;?/i);
107
+ if (packedMatch && packedMatch[1]) innerEvalBodies.push(packedMatch[1]);
108
+
109
+ const genericMatches = [...script.matchAll(/eval\(([\s\S]*?)\)\s*;?/gi)];
110
+ for (const gm of genericMatches) {
111
+ if (gm[1] && !innerEvalBodies.includes(gm[1])) innerEvalBodies.push(gm[1]);
112
+ }
113
+
114
+ for (const body of innerEvalBodies) {
115
+ try {
116
+ vm.runInContext(body, sandbox, { timeout: 1500 });
117
+ } catch (err) {}
118
+ }
119
+
120
+ if (captured.size) {
121
+ const arr = Array.from(captured);
122
+ return [{ url: arr[0] || null, isM3U8: arr[0].includes('.m3u8') || false }];
123
+ }
124
+
125
+ try {
126
+ const vsrc = videoEl && videoEl.src;
127
+ const found = findM3u8(vsrc);
128
+ if (found) {
129
+ return [{ url: found, isM3U8: found.includes('.m3u8') || false }];
130
+ }
131
+ } catch (e) {}
132
+
133
+ try {
134
+ const pkg = JSON.stringify(sandbox);
135
+ const found = findM3u8(pkg);
136
+ if (found) {
137
+ return [{ url: found, isM3U8: found.includes('.m3u8') || false }];
138
+ }
139
+ } catch (e) {}
140
+ }
141
+
142
+ // fallback: try data-src attribute in html (in case)
143
+ const fallback = html.match(/data-src="([^"]+\.m3u8[^"]*)"/i);
144
+ if (fallback) {
145
+ console.log('FOUND data-src m3u8 (fallback):', fallback[1]);
146
+ return [{ url: fallback[1], isM3U8: fallback[1].includes('.m3u8') || false }];
147
+ }
148
+
149
+ console.log('Could not resolve m3u8 from any Kwik script.');
150
+ return null;
151
+ }
152
+
153
+ static async getDownloadLinkList($, resolveLinks = true) {
154
+ const elements = $('#pickDownload a').get();
155
+ const BATCH_SIZE = 4;
156
+ const BATCH_DELAY = 800;
157
+
158
+ const processElement = async (element) => {
159
+ const $element = $(element);
160
+ const link = $element.attr('href');
161
+ if (!link) return null;
162
+
163
+ const fullText = $element.text().trim();
164
+ const normalized = fullText
165
+ .replace(/\u00A0/g, ' ')
166
+ .replace(/\s+/g, ' ')
167
+ .trim();
168
+
169
+ const parts = normalized.split('·').map(p => p.trim()).filter(Boolean);
170
+
171
+ let fansub = null;
172
+ let filesize = null;
173
+ let isDub = false;
174
+ let resolution = null;
175
+ const quality = fullText;
176
+ let isBD = false;
177
+
178
+ const parseSizeAndEng = (text) => {
179
+ // Extract resolution
180
+ const resMatch = text.match(/(\d+)p/i);
181
+ if (resMatch) resolution = resMatch[1];
182
+
183
+ // Extract size
184
+ const sizeMatch = text.match(/\((\d+(?:\.\d+)?(?:MB|GB))\)/i);
185
+ if (sizeMatch) filesize = sizeMatch[1];
186
+ else filesize = "Unknown"; // if not found
187
+
188
+ // Extract dubbed status
189
+ if (/\beng\b/i.test(text)) {
190
+ isDub = true;
191
+ }
192
+
193
+ // Extract BD status
194
+ if (/\bBD\b/.test(text)) {
195
+ isBD = true;
196
+ }
197
+ };
198
+
199
+ if (parts.length === 1) {
200
+ parseSizeAndEng(parts[0]);
201
+ } else if (parts.length >= 2) {
202
+ fansub = parts[0];
203
+ parseSizeAndEng(parts.slice(1).join(' · '));
204
+ }
205
+
206
+ const item = {
207
+ fansub,
208
+ quality,
209
+ resolution,
210
+ filesize,
211
+ isDub,
212
+ isBD,
213
+ pahe: link,
214
+ download: null
215
+ };
216
+
217
+ if (!resolveLinks) {
218
+ return item;
219
+ }
220
+
221
+ try {
222
+ const directDownloadLink = await this.getDownloadLinks(link);
223
+ item.download = directDownloadLink.downloadUrl;
224
+ return item;
225
+ } catch (error) {
226
+ console.error(`Failed to get direct download for ${link}: ${error.message}`);
227
+ return item;
228
+ }
229
+ };
230
+
231
+ // If not resolving links, we don't need batching or delays
232
+ if (!resolveLinks) {
233
+ const results = await Promise.all(elements.map(processElement));
234
+ return results.filter(r => r);
235
+ }
236
+
237
+ const results = [];
238
+ for (let i = 0; i < elements.length; i += BATCH_SIZE) {
239
+ const batch = elements.slice(i, i + BATCH_SIZE);
240
+ const batchResults = await Promise.allSettled(batch.map(processElement));
241
+
242
+ results.push(...batchResults
243
+ .filter(r => r.status === 'fulfilled' && r.value)
244
+ .map(r => r.value)
245
+ );
246
+
247
+ if (i + BATCH_SIZE < elements.length) {
248
+ await new Promise(resolve => setTimeout(resolve, BATCH_DELAY));
249
+ }
250
+ }
251
+ return results;
252
+ }
253
+
254
+ static async getResolutionList($) {
255
+ const resolutions = [];
256
+ $('#resolutionMenu button').each((index, element) => {
257
+ const link = $(element).attr('data-src');
258
+ const resolution = $(element).attr('data-resolution');
259
+ const audio = $(element).attr('data-audio');
260
+ if (link) {
261
+ resolutions.push({
262
+ url: link || null,
263
+ resolution: resolution || null,
264
+ isDub: (audio && audio.toLowerCase() === 'eng') || false,
265
+ fanSub: $(element).attr('data-fansub') || null,
266
+ });
267
+ }
268
+ });
269
+
270
+ return resolutions;
271
+ }
272
+
273
+ static async getDownloadLinks(url) {
274
+ const results = await Animepahe.getData('download', { url }, false);
275
+ return results;
276
+ }
277
+
278
+ static async scrapePlayPage(id, episodeId, pageHtml, includeDownloads = true) {
279
+ const [session, provider] = ['session', 'provider'].map(v => getJsVariable(pageHtml, v) || null);
280
+ if (!session || !provider) throw new CustomError('Episode not found', 404);
281
+
282
+ const $ = cheerio.load(pageHtml);
283
+
284
+ const playInfo = {
285
+ ids: {
286
+ animepahe_id: parseInt($('meta[name="id"]').attr('content'), 10) || null,
287
+ mal_id: parseInt($('meta[name="anidb"]').attr('content'), 10) || null,
288
+ anilist_id: parseInt($('meta[name="anilist"]').attr('content'), 10) || null,
289
+ anime_planet_id: parseInt($('meta[name="anime-planet"]').attr('content'), 10) || null,
290
+ ann_id: parseInt($('meta[name="ann"]').attr('content'), 10) || null,
291
+ anilist: $('meta[name="anilist"]').attr('content') || null,
292
+ anime_planet: $('meta[name="anime-planet"]').attr('content') || null,
293
+ ann: $('meta[name="ann"]').attr('content') || null,
294
+ kitsu: $('meta[name="kitsu"]').attr('content') || null,
295
+ myanimelist: $('meta[name="myanimelist"]').attr('content') || null,
296
+ },
297
+ session,
298
+ provider,
299
+ episode: $('.episode-menu #episodeMenu').text().trim().replace(/\D/g, ''),
300
+ anime_title: $('div.title-wrapper > h1 > span').text().trim().replace(/^Watch\s+/i, '').replace(/\s+-\s+\d+\s+Online$/i, '') || $('h1').text().trim().replace(/^Watch\s+/i, '').replace(/\s+-\s+\d+\s+Online$/i, '').replace(/ Episode \d+$/, ''),
301
+ };
302
+
303
+ try {
304
+ const resolutions = await this.getResolutionList($);
305
+
306
+ const metadataList = await this.getDownloadLinkList($, false);
307
+
308
+ const resolutionData = resolutions.map(res => {
309
+ const match = metadataList.find(m =>
310
+ m.resolution === res.resolution &&
311
+ (m.fansub === res.fanSub || (!m.fansub && !res.fanSub)) &&
312
+ m.isDub === res.isDub
313
+ );
314
+
315
+ return {
316
+ url: res.url,
317
+ embed: res.url,
318
+ resolution: res.resolution,
319
+ isDub: res.isDub,
320
+ fanSub: res.fanSub,
321
+ isBD: match ? match.isBD : false
322
+ };
323
+ });
324
+
325
+ let allSources = await this.processHybridOptimized(id, episodeId, resolutionData);
326
+ const resolvedSourceCount = allSources.flat().filter(source => source && source.url).length;
327
+ if (resolutionData.length > 0 && resolvedSourceCount === 0) {
328
+ console.warn('[PlayModel] Parallel iframe extraction resolved 0 sources, retrying sequentially...');
329
+ allSources = await this.processSequentialFallback(id, episodeId, resolutionData);
330
+ }
331
+
332
+ // fast download links map
333
+ const fastDownloadMap = new Map();
334
+
335
+ const processedSources = allSources.flat().map(source => {
336
+ if (includeDownloads && source.url && source.isM3U8) {
337
+ const downloadResult = UrlConverter.buildDownloadUrl(
338
+ source.url,
339
+ Config.iframeBaseUrl,
340
+ {
341
+ animeTitle: playInfo.anime_title,
342
+ episode: playInfo.episode,
343
+ resolution: source.resolution,
344
+ fansub: source.fanSub,
345
+ isDub: source.isDub,
346
+ isBD: source.isBD
347
+ }
348
+ );
349
+
350
+ if (downloadResult) {
351
+ source.download = downloadResult.url;
352
+ source.downloadHeaders = downloadResult.headers;
353
+ const key = `${source.resolution}-${source.fanSub || 'default'}-${source.isDub}-${source.isBD}`;
354
+ fastDownloadMap.set(key, downloadResult);
355
+ }
356
+ }
357
+ return source;
358
+ });
359
+
360
+ playInfo.sources = processedSources;
361
+
362
+ if (includeDownloads) {
363
+ const hydratedDownloads = metadataList.map(item => {
364
+ const key = `${item.resolution}-${item.fansub || 'default'}-${item.isDub}-${item.isBD}`;
365
+ const downloadResult = fastDownloadMap.get(key);
366
+
367
+ if (downloadResult) {
368
+ item.download = downloadResult.url;
369
+ item.downloadHeaders = downloadResult.headers;
370
+ }
371
+ return item;
372
+ });
373
+
374
+ playInfo.downloads = hydratedDownloads;
375
+ console.log(`Matched ${hydratedDownloads.filter(d => d.download).length}/${metadataList.length} download links locally.`);
376
+ } else {
377
+ playInfo.downloads = [];
378
+ }
379
+
380
+ // Remove internal properties not needed in the response
381
+ playInfo.sources.forEach(s => delete s.isBD);
382
+ if (playInfo.downloads) {
383
+ playInfo.downloads.forEach(d => delete d.isBD);
384
+ }
385
+
386
+ const cdnCookies = Animepahe.getCdnCookies();
387
+ if (cdnCookies) {
388
+ playInfo.cdnCookies = cdnCookies;
389
+ }
390
+ } catch (error) {
391
+ console.error('Error in scrapePlayPage:', error);
392
+ playInfo.sources = playInfo.sources || [];
393
+ playInfo.downloads = playInfo.downloads || [];
394
+ }
395
+
396
+ return playInfo;
397
+ }
398
+
399
+ /*
400
+ * Optimized parallel approach:
401
+ * Process multiple iframe sources in parallel batches. Increased to 3 for better speed.
402
+ */
403
+ static async processHybridOptimized(id, episodeId, items) {
404
+ const results = [];
405
+ const seenUrls = new Set();
406
+ const IFRAME_TIMEOUT = 20000; // 20 seconds per iframe
407
+
408
+ const uniqueItems = items.filter(item => {
409
+ if (seenUrls.has(item.url)) {
410
+ console.log('Skipping duplicate URL:', item.url);
411
+ return false;
412
+ }
413
+ seenUrls.add(item.url);
414
+ return true;
415
+ });
416
+
417
+ console.log(`Starting parallel processing of ${uniqueItems.length} iframe sources...`);
418
+
419
+ if (uniqueItems.length === 0) {
420
+ return results;
421
+ }
422
+
423
+ // Increased from 2 to 3 for better parallelization
424
+ const maxParallel = 3;
425
+ for (let i = 0; i < uniqueItems.length; i += maxParallel) {
426
+ const batch = uniqueItems.slice(i, i + maxParallel);
427
+
428
+ console.log(`Processing batch ${Math.floor(i / maxParallel) + 1}/${Math.ceil(uniqueItems.length / maxParallel)} with ${batch.length} items...`);
429
+
430
+ const batchPromises = batch.map(async (data) => {
431
+ // Wrap each iframe scrape with a timeout
432
+ const scrapePromise = Animepahe.scrapeIframe(id, episodeId, data.url);
433
+ const timeoutPromise = new Promise((_, reject) => {
434
+ setTimeout(() => reject(new Error(`Iframe fetch timed out after ${IFRAME_TIMEOUT / 1000}s`)), IFRAME_TIMEOUT);
435
+ });
436
+
437
+ try {
438
+ const sources = await Promise.race([scrapePromise, timeoutPromise]);
439
+ if (!Array.isArray(sources) || sources.length === 0) {
440
+ throw new Error('No iframe sources resolved');
441
+ }
442
+
443
+ return sources.map(source => ({
444
+ ...source,
445
+ embed: data.embed,
446
+ resolution: data.resolution,
447
+ isDub: data.isDub,
448
+ fanSub: data.fanSub,
449
+ isBD: data.isBD || false,
450
+ }));
451
+ } catch (err) {
452
+ console.error(`Failed to process ${data.resolution}:`, err.message);
453
+
454
+ // Return partial data on timeout — at least give the embed URL
455
+ if (err.message && err.message.includes('timed out')) {
456
+ console.log(`[Timeout Fallback] Returning embed URL for ${data.resolution}`);
457
+ return [{
458
+ url: null,
459
+ embed: data.embed,
460
+ resolution: data.resolution,
461
+ isDub: data.isDub,
462
+ fanSub: data.fanSub,
463
+ isBD: data.isBD || false,
464
+ timeout: true,
465
+ }];
466
+ }
467
+ return [];
468
+ }
469
+ });
470
+
471
+ const batchResults = await Promise.all(batchPromises);
472
+ results.push(...batchResults);
473
+
474
+ // Reduced delay from 500ms to 300ms
475
+ if (i + maxParallel < uniqueItems.length) {
476
+ await new Promise(resolve => setTimeout(resolve, 300));
477
+ }
478
+ }
479
+
480
+ const successCount = results.filter(r => r && r.length > 0).length;
481
+ console.log(`Parallel processing complete: ${successCount}/${uniqueItems.length} iframe sources processed successfully`);
482
+ return results;
483
+ }
484
+
485
+ // Sequential fallback processing when needed.
486
+ static async processSequentialFallback(id, episodeId, items, delayMs = 1500) {
487
+ console.log('Switching to sequential fallback processing for remaining iframe sources...');
488
+ const results = [];
489
+
490
+ for (let i = 0; i < items.length; i++) {
491
+ const data = items[i];
492
+ try {
493
+ const sources = await Animepahe.scrapeIframe(id, episodeId, data.url);
494
+ if (!Array.isArray(sources) || sources.length === 0) {
495
+ throw new Error('No iframe sources resolved');
496
+ }
497
+
498
+ const sourcesWithMeta = sources.map(source => ({
499
+ ...source,
500
+ embed: data.embed,
501
+ resolution: data.resolution,
502
+ isDub: data.isDub,
503
+ fanSub: data.fanSub,
504
+ }));
505
+ results.push(sourcesWithMeta);
506
+
507
+ if (i < items.length - 1) {
508
+ await new Promise(resolve => setTimeout(resolve, delayMs));
509
+ }
510
+ } catch (err) {
511
+ console.error(`Failed ${data.resolution}:`, err.message);
512
+ }
513
+ }
514
+
515
+ return results;
516
+ }
517
+
518
+ // Backwards-compatibility wrappers
519
+ static async processSequential(id, episodeId, items, delayMs = 2000) {
520
+ return this.processHybridOptimized(id, episodeId, items);
521
+ }
522
+
523
+ static async processBatch(id, episodeId, items, batchSize = 3, delayMs = 500) {
524
+ return this.processHybridOptimized(id, episodeId, items);
525
+ }
526
+ }
527
+
528
+ module.exports = PlayModel;
@@ -0,0 +1,22 @@
1
+ const DataProcessor = require('../utils/dataProcessor');
2
+ const Config = require('../utils/config');
3
+ const Animepahe = require('../scrapers/animepahe');
4
+ const { CustomError } = require('../middleware/errorHandler');
5
+
6
+ class QueueModel {
7
+ static async getQueue() {
8
+ const results = await Animepahe.getData("queue");
9
+
10
+ if (!results) {
11
+ throw new CustomError('Failed to fetch queue data', 503);
12
+ }
13
+
14
+ if (typeof results === 'object' && !results.data) {
15
+ results.data = [];
16
+ }
17
+
18
+ return DataProcessor.processApiData(results, "queue", false);
19
+ }
20
+ }
21
+
22
+ module.exports = QueueModel;
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "pahe-api",
3
+ "main": "index.js",
4
+ "scripts": {
5
+ "dev": "nodemon app.js",
6
+ "start": "node app.js",
7
+ "test": "nodemon test.js"
8
+ },
9
+ "keywords": [],
10
+ "author": "aor-rex",
11
+ "license": "ISC",
12
+ "description": "",
13
+ "dependencies": {
14
+ "axios": "^1.13.5",
15
+ "cheerio": "1.0.0-rc.12",
16
+ "cloudscraper": "^4.6.0",
17
+ "cors": "^2.8.5",
18
+ "dotenv": "^16.5.0",
19
+ "express": "^4.21.2",
20
+ "jsdom": "^22.1.0",
21
+ "p-limit": "^5.0.0",
22
+ "redis": "^5.5.6"
23
+ },
24
+ "devDependencies": {
25
+ "nodemon": "^3.1.9"
26
+ }
27
+ }