ani-auto 1.4.4 → 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 (89) 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 +15 -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 +62 -11
  59. package/src/scraper.js +2 -2
  60. package/src/server.js +108 -0
  61. package/utils.js +21 -4
  62. package/web/index.html +13 -0
  63. package/web/package-lock.json +1420 -0
  64. package/web/package.json +24 -0
  65. package/web/src/App.vue +200 -0
  66. package/web/src/api/client.js +75 -0
  67. package/web/src/assets/styles/base.css +961 -0
  68. package/web/src/components/UpdateModal.vue +157 -0
  69. package/web/src/components/sidebar/SidebarResizable.vue +170 -0
  70. package/web/src/components/sidebar/sidebarConfig.js +24 -0
  71. package/web/src/main.js +104 -0
  72. package/web/src/router/index.js +44 -0
  73. package/web/src/stores/config.js +27 -0
  74. package/web/src/stores/download.js +107 -0
  75. package/web/src/stores/update.js +73 -0
  76. package/web/src/views/About.vue +42 -0
  77. package/web/src/views/AniListSync.vue +122 -0
  78. package/web/src/views/AnimeDetail.vue +202 -0
  79. package/web/src/views/AnimeList.vue +110 -0
  80. package/web/src/views/CFResult.vue +312 -0
  81. package/web/src/views/DaemonControl.vue +83 -0
  82. package/web/src/views/Dashboard.vue +187 -0
  83. package/web/src/views/DownloadCenter.vue +153 -0
  84. package/web/src/views/MatchFinder.vue +131 -0
  85. package/web/src/views/OAuthCallback.vue +44 -0
  86. package/web/src/views/Onboarding.vue +93 -0
  87. package/web/src/views/RunHistory.vue +122 -0
  88. package/web/src/views/Settings.vue +410 -0
  89. package/web/vite.config.js +23 -0
@@ -0,0 +1,667 @@
1
+ const cloudscraper = require("cloudscraper");
2
+ const axios = require("axios");
3
+ const vm = require("vm");
4
+ const { JSDOM } = require("jsdom");
5
+ const { CustomError } = require("../middleware/errorHandler");
6
+ const Config = require("../utils/config");
7
+
8
+ // Helper to wait
9
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
+
11
+ class TestController {
12
+ static async resolveKwik(req, res, next) {
13
+ try {
14
+ const { url } = req.query;
15
+
16
+ if (!url) {
17
+ throw new CustomError("Url is required", 400);
18
+ }
19
+
20
+ const result = await resolveKwik(url);
21
+ console.log("\nOutput snapshot:\n", result);
22
+
23
+ return res.json(result);
24
+ } catch (err) {
25
+ console.error("eval error", err && err.message);
26
+ next(err);
27
+ }
28
+ }
29
+
30
+ static async download(req, res, next) {
31
+ try {
32
+ const { url } = req.query;
33
+ if (!url) {
34
+ throw new CustomError("Url is required", 400);
35
+ }
36
+
37
+ // First, extract the actual Kwik URL from the HTML
38
+ const resolvedUrl = await extractKwikUrl(url);
39
+ if (!resolvedUrl) {
40
+ // If can't extract the URL, try the original URL
41
+ const downloadUrl = await getKwikDownloadUrl(url);
42
+ return res.json({ downloadUrl, type: "direct_download" });
43
+ }
44
+
45
+ console.log("Found Kwik URL:", resolvedUrl);
46
+
47
+ // Use the extracted URL for getting the download link
48
+ const downloadUrl = await getKwikDownloadUrl(resolvedUrl);
49
+ return res.json({
50
+ downloadUrl,
51
+ type: "redirected_download",
52
+ originalUrl: url,
53
+ resolvedUrl,
54
+ });
55
+ } catch (err) {
56
+ console.error("Error downloading:", err && err.message);
57
+ next(err);
58
+ }
59
+ }
60
+ }
61
+
62
+ // Function to extract Kwik URL from HTML content
63
+ async function extractKwikUrl(url) {
64
+ try {
65
+ console.log("[Step 1] Fetching page to extract Kwik URL:", url);
66
+
67
+ const response = await cloudscraper.get({
68
+ uri: url,
69
+ headers: {
70
+ Referer: "https://animepahe.pw/",
71
+ "User-Agent":
72
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
73
+ Accept:
74
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
75
+ "Accept-Language": "en-US,en;q=0.5",
76
+ Connection: "keep-alive",
77
+ },
78
+ resolveWithFullResponse: true,
79
+ timeout: 30000,
80
+ });
81
+
82
+ console.log("Page fetched for extraction, status:", response.statusCode);
83
+
84
+ const body = response.body;
85
+
86
+ // Pattern 1: Look for the specific script pattern with redirect href
87
+ const redirectPattern = /href\s*:\s*["']([^"']+)["']/i;
88
+ const redirectMatch = body.match(redirectPattern);
89
+ if (
90
+ redirectMatch &&
91
+ redirectMatch[1] &&
92
+ redirectMatch[1].includes(Config.iframeBaseUrl)
93
+ ) {
94
+ console.log("Found redirect URL:", redirectMatch[1]);
95
+ return redirectMatch[1];
96
+ }
97
+
98
+ // Pattern 2...
99
+ // Dynamic construction
100
+ const inputDomain = Config.iframeBaseUrl.replace(".", "\\.");
101
+ const scriptPattern = new RegExp(
102
+ `href["']\\s*,\\s*["']([^"']+\\.(?:${inputDomain}|${inputDomain.replace("\\.", "")}))[^"']*["']`,
103
+ "i",
104
+ );
105
+ const scriptMatch = body.match(scriptPattern);
106
+ if (scriptMatch && scriptMatch[1]) {
107
+ // If it's a relative URL, make it absolute
108
+ let kwikUrl = scriptMatch[1];
109
+ if (kwikUrl.startsWith("/")) {
110
+ const urlObj = new URL(url);
111
+ kwikUrl = urlObj.protocol + "//" + urlObj.host + kwikUrl;
112
+ } else if (!kwikUrl.startsWith("http")) {
113
+ kwikUrl = `https://${Config.iframeBaseUrl}${kwikUrl}`;
114
+ }
115
+ console.log("Found Kwik URL from script:", kwikUrl);
116
+ return kwikUrl;
117
+ }
118
+
119
+ // Pattern 3: Look for kwik.cx URLs in href attributes
120
+ const hrefPattern = new RegExp(
121
+ `href\\s*=\\s*["']([^"']*\\b${inputDomain}\\b[^"']*)["']`,
122
+ "gi",
123
+ );
124
+ const hrefMatches = [...body.matchAll(hrefPattern)];
125
+ if (hrefMatches.length > 0) {
126
+ // Return the first kwik URL found
127
+ let kwikUrl = hrefMatches[0][1];
128
+ if (kwikUrl.startsWith("/")) {
129
+ const urlObj = new URL(url);
130
+ kwikUrl = urlObj.protocol + "//" + urlObj.host + kwikUrl;
131
+ }
132
+ console.log("Found Kwik URL from href:", kwikUrl);
133
+ return kwikUrl;
134
+ }
135
+
136
+ // Pattern 4: Look for kwik.cx URLs in JavaScript redirects
137
+ const jsRedirectPattern = new RegExp(
138
+ `["'](https?:\\/\\/[^"']*${inputDomain}[^"']*)["']`,
139
+ "i",
140
+ );
141
+ const jsMatch = body.match(jsRedirectPattern);
142
+ if (jsMatch && jsMatch[1]) {
143
+ console.log("Found Kwik URL from JavaScript:", jsMatch[1]);
144
+ return jsMatch[1];
145
+ }
146
+
147
+ // Pattern 5: Look for the specific script pattern you mentioned
148
+ const specificPattern = new RegExp(
149
+ `href"\\s*,\\s*"([^"]*${inputDomain}[^"]*)"`,
150
+ );
151
+ const specificMatch = body.match(specificPattern);
152
+ if (specificMatch && specificMatch[1]) {
153
+ console.log("Found Kwik URL from specific pattern:", specificMatch[1]);
154
+ return specificMatch[1];
155
+ }
156
+
157
+ console.log("No Kwik URL found in the HTML content");
158
+ return null;
159
+ } catch (error) {
160
+ console.error("Error extracting Kwik URL:", error.message);
161
+ return null;
162
+ }
163
+ }
164
+
165
+ async function getKwikDownloadUrl(url) {
166
+ console.log("[Step 2] Fetching page for download link:", url);
167
+
168
+ const getResponse = await cloudscraper.get({
169
+ uri: url,
170
+ headers: {
171
+ Referer: "https://animepahe.pw/",
172
+ "User-Agent":
173
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
174
+ Accept:
175
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
176
+ "Accept-Language": "en-US,en;q=0.5",
177
+ Connection: "keep-alive",
178
+ },
179
+ resolveWithFullResponse: true,
180
+ timeout: 30000,
181
+ });
182
+
183
+ console.log("[✓] Page fetched, status:", getResponse.statusCode);
184
+
185
+ // Extract cookies
186
+ const setCookieHeaders = getResponse.headers["set-cookie"] || [];
187
+ const cookies = setCookieHeaders
188
+ .map((cookie) => cookie.split(";")[0])
189
+ .join("; ");
190
+ console.log("[Cookies]:", cookies);
191
+
192
+ const body = getResponse.body;
193
+ const scripts = [...body.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)].map(
194
+ (m) => m[1],
195
+ );
196
+
197
+ let foundAction = null;
198
+ let foundToken = null;
199
+
200
+ // JavaScript execution environment
201
+ const dom = new JSDOM(
202
+ "<!doctype html><body><div class='adSense'></div><div class='adSense'></div></body>",
203
+ );
204
+ const { window } = dom;
205
+ const { document } = window;
206
+
207
+ const $ = (sel) => {
208
+ if (typeof sel === "function") {
209
+ try {
210
+ sel.call(window);
211
+ } catch (e) {}
212
+ return $;
213
+ }
214
+ if (typeof sel !== "string")
215
+ return {
216
+ html: () => "",
217
+ attr: () => "",
218
+ click: () => $,
219
+ on: () => $,
220
+ remove: () => $,
221
+ length: 0,
222
+ };
223
+
224
+ let els = [];
225
+ const eqMatch = sel.match(/^(.+):eq\((\d+)\)$/);
226
+ if (eqMatch) {
227
+ const all = document.querySelectorAll(eqMatch[1]);
228
+ els = all[parseInt(eqMatch[2])] ? [all[parseInt(eqMatch[2])]] : [];
229
+ } else {
230
+ try {
231
+ els = Array.from(document.querySelectorAll(sel));
232
+ } catch (e) {}
233
+ }
234
+
235
+ const el = els[0];
236
+ return {
237
+ html: (v) => {
238
+ if (v !== undefined) {
239
+ const htmlStr = String(v);
240
+ const actionMatch = htmlStr.match(/action=["']([^"']+)["']/i);
241
+ if (actionMatch) foundAction = actionMatch[1];
242
+ const tokenMatch = htmlStr.match(
243
+ /name=["']_token["'][^>]*value=["']([^"']+)["']/i,
244
+ );
245
+ if (tokenMatch) foundToken = tokenMatch[1];
246
+ if (el) el.innerHTML = htmlStr;
247
+ return $;
248
+ }
249
+ return el?.innerHTML || "";
250
+ },
251
+ attr: (n, v) =>
252
+ v !== undefined
253
+ ? (el?.setAttribute(n, v), $)
254
+ : el?.getAttribute(n) || "",
255
+ click: (fn) => {
256
+ if (typeof fn === "function")
257
+ try {
258
+ fn.call(el);
259
+ } catch (e) {}
260
+ return $;
261
+ },
262
+ on: () => $,
263
+ remove: () => {
264
+ el?.remove();
265
+ return $;
266
+ },
267
+ length: els.length,
268
+ };
269
+ };
270
+ $.ajax = () => {};
271
+
272
+ const sandbox = {
273
+ window,
274
+ document,
275
+ console,
276
+ navigator: { userAgent: "Mozilla/5.0" },
277
+ MutationObserver: class {
278
+ observe() {}
279
+ },
280
+ XMLHttpRequest: function () {
281
+ this.open = this.send = this.setRequestHeader = () => {};
282
+ },
283
+ fetch: async () => ({
284
+ ok: true,
285
+ text: async () => "",
286
+ json: async () => ({}),
287
+ }),
288
+ atob: (s) => Buffer.from(s, "base64").toString("binary"),
289
+ btoa: (s) => Buffer.from(s, "binary").toString("base64"),
290
+ $,
291
+ setTimeout,
292
+ clearTimeout,
293
+ };
294
+
295
+ for (const s of scripts) {
296
+ if (s && s.length > 100) {
297
+ try {
298
+ vm.runInNewContext(s, sandbox, { timeout: 4000 });
299
+ if (foundAction && foundToken) break;
300
+ } catch (e) {}
301
+ }
302
+ }
303
+
304
+ if (!foundAction || !foundToken) {
305
+ throw new Error("⌠ Could not extract form action or token");
306
+ }
307
+
308
+ console.log("[Step 3] Extracted action:", foundAction);
309
+ console.log("[Step 3] Extracted token:", foundToken);
310
+
311
+ // Wait a bit to simulate human behavior
312
+ console.log("[Step 4] Waiting 2 seconds...");
313
+ await sleep(2000);
314
+
315
+ // Step 3: Submit POST request
316
+ console.log("[Step 5] Submitting POST request...");
317
+
318
+ // Check for serverless deployment platforms (matching browser.js logic)
319
+ const isServerless =
320
+ process.env.VERCEL ||
321
+ process.env.NETLIFY ||
322
+ process.env.AWS_LAMBDA_FUNCTION_NAME;
323
+
324
+ if (isServerless) {
325
+ // On serverless platforms, use cloudscraper to bypass Cloudflare
326
+ console.log(
327
+ "[Running on serverless platform - using cloudscraper for POST]",
328
+ );
329
+
330
+ try {
331
+ const postResponse = await cloudscraper({
332
+ method: "POST",
333
+ uri: foundAction,
334
+ form: {
335
+ _token: foundToken,
336
+ },
337
+ headers: {
338
+ "User-Agent":
339
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
340
+ Accept:
341
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
342
+ "Accept-Language": "en-US,en;q=0.5",
343
+ "Content-Type": "application/x-www-form-urlencoded",
344
+ Origin: `https://${Config.iframeBaseUrl}`,
345
+ Referer: url,
346
+ Cookie: cookies,
347
+ },
348
+ followAllRedirects: false,
349
+ followRedirect: false,
350
+ simple: false,
351
+ resolveWithFullResponse: true,
352
+ timeout: 30000,
353
+ });
354
+
355
+ console.log("[Step 6] Response status:", postResponse.statusCode);
356
+
357
+ if (postResponse.statusCode === 302 || postResponse.statusCode === 301) {
358
+ const downloadUrl = postResponse.headers.location;
359
+ console.log("Final download URL:", downloadUrl);
360
+ return downloadUrl;
361
+ } else if (postResponse.statusCode === 200) {
362
+ // Check for redirects in the body
363
+ const body = postResponse.body;
364
+ const metaMatch = body.match(
365
+ /<meta[^>]*http-equiv=["']refresh["'][^>]*content=["'][^"]*url=([^"']+)["']/i,
366
+ );
367
+ if (metaMatch) {
368
+ console.log("Found meta refresh URL:", metaMatch[1]);
369
+ return metaMatch[1];
370
+ }
371
+
372
+ const jsMatch = body.match(
373
+ /window\.location(?:\.href)?\s*=\s*["']([^"']+)["']/i,
374
+ );
375
+ if (jsMatch) {
376
+ console.log("Found JavaScript redirect URL:", jsMatch[1]);
377
+ return jsMatch[1];
378
+ }
379
+
380
+ console.log("[Response body snippet]:", body.substring(0, 800));
381
+ }
382
+ } catch (error) {
383
+ console.log("[Cloudscraper Error]:", error.message);
384
+ if (error.statusCode === 302 || error.statusCode === 301) {
385
+ const downloadUrl = error.response?.headers?.location;
386
+ if (downloadUrl) {
387
+ console.log("Final download URL (from error):", downloadUrl);
388
+ return downloadUrl;
389
+ }
390
+ }
391
+ throw error;
392
+ }
393
+ } else {
394
+ // Local development - use axios which works reliably
395
+ console.log("[Running locally - using axios for POST]");
396
+
397
+ try {
398
+ const postResponse = await axios.post(
399
+ foundAction,
400
+ `_token=${encodeURIComponent(foundToken)}`,
401
+ {
402
+ headers: {
403
+ "User-Agent":
404
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
405
+ Accept:
406
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
407
+ "Accept-Language": "en-US,en;q=0.5",
408
+ "Content-Type": "application/x-www-form-urlencoded",
409
+ Origin: `https://${Config.iframeBaseUrl}`,
410
+ Referer: url,
411
+ Cookie: cookies,
412
+ Connection: "keep-alive",
413
+ "Upgrade-Insecure-Requests": "1",
414
+ },
415
+ maxRedirects: 0,
416
+ validateStatus: (status) => status >= 200 && status < 400,
417
+ timeout: 30000,
418
+ },
419
+ );
420
+
421
+ console.log("[Step 6] Response status:", postResponse.status);
422
+
423
+ if (postResponse.status === 302 || postResponse.status === 301) {
424
+ const downloadUrl = postResponse.headers.location;
425
+ console.log("Final download URL:", downloadUrl);
426
+ return downloadUrl;
427
+ } else if (postResponse.status === 200) {
428
+ const metaMatch = postResponse.data.match(
429
+ /<meta[^>]*http-equiv=["']refresh["'][^>]*content=["'][^"]*url=([^"']+)["']/i,
430
+ );
431
+ if (metaMatch) {
432
+ console.log("Found meta refresh URL:", metaMatch[1]);
433
+ return metaMatch[1];
434
+ }
435
+
436
+ const jsMatch = postResponse.data.match(
437
+ /window\.location(?:\.href)?\s*=\s*["']([^"']+)["']/i,
438
+ );
439
+ if (jsMatch) {
440
+ console.log("Found JavaScript redirect URL:", jsMatch[1]);
441
+ return jsMatch[1];
442
+ }
443
+
444
+ console.log(
445
+ "[Response body snippet]:",
446
+ postResponse.data.substring(0, 800),
447
+ );
448
+ }
449
+ } catch (error) {
450
+ if (error.response) {
451
+ console.log("[Error Response] Status:", error.response.status);
452
+
453
+ if (error.response.status === 302 || error.response.status === 301) {
454
+ const downloadUrl = error.response.headers.location;
455
+ console.log("Final download URL (from error):", downloadUrl);
456
+ return downloadUrl;
457
+ }
458
+
459
+ console.log(
460
+ "[Error Response Body]:",
461
+ error.response.data
462
+ ? error.response.data.substring(0, 800)
463
+ : "No body",
464
+ );
465
+ } else {
466
+ console.log("[Error]:", error.message);
467
+ }
468
+ throw error;
469
+ }
470
+ }
471
+
472
+ throw new Error("Could not extract download URL");
473
+ }
474
+
475
+ async function resolveKwik(url) {
476
+ console.log(`Fetching HTML from ${url}...`);
477
+
478
+ const html = await cloudscraper.get(url, {
479
+ headers: {
480
+ Referer: "https://animepahe.pw/",
481
+ "User-Agent":
482
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
483
+ },
484
+ timeout: 20000,
485
+ });
486
+
487
+ console.log("Fetched HTML successfully.");
488
+
489
+ // collect inline script blocks
490
+ const scriptMatches = [
491
+ ...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi),
492
+ ].map((m) => m[1]);
493
+ if (!scriptMatches.length) {
494
+ console.log("No inline <script> blocks found.");
495
+ return null;
496
+ }
497
+ console.log(`Found ${scriptMatches.length} script tags.`);
498
+
499
+ // helper to extract m3u8 from a string
500
+ const findM3u8 = (s) => {
501
+ if (!s) return null;
502
+ const m = s.match(/https?:\/\/[^"'<> \n\r]+\.m3u8[^\s"'<>]*/i);
503
+ return m ? m[0] : null;
504
+ };
505
+
506
+ for (const script of scriptMatches) {
507
+ if (!script.includes("eval(")) continue; // only interested in obfuscated eval scripts
508
+
509
+ console.log("Evaluating candidate script via vm sandbox...");
510
+
511
+ // create minimal DOM with a <video> element that supports .src
512
+ const dom = new JSDOM(`<!DOCTYPE html><video id="player"></video>`);
513
+ const document = dom.window.document;
514
+ const videoEl = document.querySelector("video");
515
+
516
+ // capture storage
517
+ const captured = new Set();
518
+
519
+ // Plyr stub: capture provided source if present in options
520
+ const Plyr = function (el, opts) {
521
+ try {
522
+ if (opts && opts.sources && Array.isArray(opts.sources)) {
523
+ for (const s of opts.sources) {
524
+ if (s && typeof s.src === "string" && s.src.includes(".m3u8"))
525
+ captured.add(s.src);
526
+ }
527
+ }
528
+ } catch (e) {
529
+ /* ignore */
530
+ }
531
+ return {
532
+ on: () => {},
533
+ };
534
+ };
535
+
536
+ // Hls stub: constructor + static isSupported
537
+ const Hls = function (cfg) {
538
+ return {
539
+ loadSource: (src) => {
540
+ try {
541
+ if (typeof src === "string" && src.includes(".m3u8"))
542
+ captured.add(src);
543
+ } catch (e) {}
544
+ },
545
+ attachMedia: (m) => {
546
+ try {
547
+ // if video element has src set later, capture it
548
+ if (
549
+ m &&
550
+ m.src &&
551
+ typeof m.src === "string" &&
552
+ m.src.includes(".m3u8")
553
+ )
554
+ captured.add(m.src);
555
+ } catch (e) {}
556
+ },
557
+ on: () => {},
558
+ };
559
+ };
560
+ Hls.isSupported = () => true;
561
+
562
+ // also intercept assignments to video.src by monitoring JSDOM element after script
563
+ // Sandbox
564
+ const sandbox = {
565
+ console,
566
+ window: dom.window,
567
+ document: dom.window.document,
568
+ navigator: { userAgent: "mozilla" },
569
+ location: { href: url },
570
+ Plyr,
571
+ Hls,
572
+ setTimeout,
573
+ clearTimeout,
574
+ };
575
+
576
+ // Create context
577
+ vm.createContext(sandbox);
578
+
579
+ // Run script and also try to unwrap one level of nested evals if found
580
+ try {
581
+ // Run once
582
+ vm.runInNewContext(script, sandbox, { timeout: 2000 });
583
+ } catch (err) {
584
+ console.log("Eval failed:", err && err.message);
585
+ }
586
+
587
+ // Some pages embed further eval inside strings. Try to detect `eval(function(...` pattern and run inner body(s)
588
+ // We search the script text for eval and then try to extract common packed patterns. This is best-effort.
589
+ const innerEvalBodies = [];
590
+ // pattern to capture eval\\(function...packed...) or eval\\(p,a,c,k,e,d\\)\\(...\\)
591
+ const packedMatch = script.match(/eval\\((function[\s\S]*?)\\)\\s*;?/i);
592
+ if (packedMatch && packedMatch[1]) innerEvalBodies.push(packedMatch[1]);
593
+ // also check for common eval\\(\\(function\\(p,a,c,k,e,d\\)\\{[\\s\\S]*?\\}\\('[\\s\\S]*?'\\)\\)\\)
594
+ const genericMatches = [
595
+ ...script.matchAll(/eval\\(([\\s\S]*?)\\)\\s*;?/gi),
596
+ ];
597
+ for (const gm of genericMatches) {
598
+ if (gm[1] && !innerEvalBodies.includes(gm[1]))
599
+ innerEvalBodies.push(gm[1]);
600
+ }
601
+
602
+ for (const body of innerEvalBodies) {
603
+ try {
604
+ // attempt to run inner body directly
605
+ vm.runInNewContext(body, sandbox, { timeout: 1500 });
606
+ } catch (err) {
607
+ // ignore errors: many packed scripts expect DOM APIs we stubbed
608
+ }
609
+ }
610
+
611
+ // After execution, check multiple places for m3u8
612
+ // 1) captured set from Plyr/Hls
613
+ if (captured.size) {
614
+ const arr = Array.from(captured);
615
+ // return first
616
+ console.log("Resolved m3u8 (captured):", arr[0]);
617
+ return arr[0];
618
+ }
619
+
620
+ // 2) check video element src
621
+ try {
622
+ const vsrc = videoEl && videoEl.src;
623
+ const found = findM3u8(vsrc);
624
+ if (found) {
625
+ console.log("Resolved m3u8 (video.src):", found);
626
+ return found;
627
+ }
628
+ } catch (e) {
629
+ /* ignore */
630
+ }
631
+
632
+ // 3) check sandbox.window / sandbox.document for q or other variables
633
+ try {
634
+ const pkg = JSON.stringify(sandbox);
635
+ const found = findM3u8(pkg);
636
+ if (found) {
637
+ console.log("Resolved m3u8 (sandbox JSON):", found);
638
+ return found;
639
+ }
640
+ } catch (e) {
641
+ /* ignore */
642
+ }
643
+
644
+ // 4) finally scan the original script text for direct m3u8 (rare if obfuscated)
645
+ const fromScript = findM3u8(script);
646
+ if (fromScript) {
647
+ console.log("Resolved m3u8 (script literal):", fromScript);
648
+ return fromScript;
649
+ }
650
+
651
+ console.log(
652
+ "Could not resolve m3u8 from this script, continuing to next candidate...",
653
+ );
654
+ }
655
+
656
+ // fallback: try data-src attribute in html (in case)
657
+ const fallback = html.match(/data-src="([^"]+\.m3u8[^"]*)"/i);
658
+ if (fallback) {
659
+ console.log("FOUND data-src m3u8 (fallback):", fallback[1]);
660
+ return fallback[1];
661
+ }
662
+
663
+ console.log("Could not resolve m3u8 from any Kwik script.");
664
+ return null;
665
+ }
666
+
667
+ module.exports = TestController;