ani-web 1.5.8

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.

Potentially problematic release.


This version of ani-web might be problematic. Click here for more details.

Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/client/dist/assets/AnimeInfo-C7DQp7Oo.js +1 -0
  4. package/client/dist/assets/AnimeInfo-Sb3YiXHJ.css +1 -0
  5. package/client/dist/assets/AnimeInfoPage-DJA7AJQ8.js +2 -0
  6. package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
  7. package/client/dist/assets/Button-o0l9V_NG.js +1 -0
  8. package/client/dist/assets/ErrorMessage-Ddf2zmRx.js +1 -0
  9. package/client/dist/assets/ErrorMessage-FOxXyZC9.css +1 -0
  10. package/client/dist/assets/Home-CKHJA97j.css +1 -0
  11. package/client/dist/assets/Home-Dey0azy1.js +1 -0
  12. package/client/dist/assets/Insights-BSRcCkDs.css +1 -0
  13. package/client/dist/assets/Insights-CogjPOd_.js +1 -0
  14. package/client/dist/assets/MAL-CYArH4yf.js +1 -0
  15. package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
  16. package/client/dist/assets/Player-BWFN9gud.js +9 -0
  17. package/client/dist/assets/Player-CBCYW7uG.css +1 -0
  18. package/client/dist/assets/PlayerSettings-BgStUrrP.css +1 -0
  19. package/client/dist/assets/PlayerSettings-rWZuATQf.js +1 -0
  20. package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
  21. package/client/dist/assets/RemoveConfirmationModal-CLYqyGOv.js +1 -0
  22. package/client/dist/assets/Search-DZAWgKwq.js +1 -0
  23. package/client/dist/assets/Search-lWsVQ0Ke.css +1 -0
  24. package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
  25. package/client/dist/assets/Settings-DyisJGeD.js +1 -0
  26. package/client/dist/assets/ToggleSwitch-CLnWnAuY.js +1 -0
  27. package/client/dist/assets/ToggleSwitch-DInRb7iM.css +1 -0
  28. package/client/dist/assets/Watchlist-2dVYksxq.css +1 -0
  29. package/client/dist/assets/Watchlist-CuqJISI3.js +1 -0
  30. package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
  31. package/client/dist/assets/index-BK_Zaqaw.css +1 -0
  32. package/client/dist/assets/index-CHVF4D4L.js +178 -0
  33. package/client/dist/assets/useAnimeInfoData-Cr58brCY.js +1 -0
  34. package/client/dist/assets/useIsMobile-gHo4t6g6.js +1 -0
  35. package/client/dist/assets/vendor-DdbgYKo4.js +3 -0
  36. package/client/dist/favicon.ico +0 -0
  37. package/client/dist/index.html +35 -0
  38. package/client/dist/logo.png +0 -0
  39. package/client/dist/placeholder.svg +4 -0
  40. package/client/dist/robots.txt +3 -0
  41. package/client/package.json +54 -0
  42. package/orchestrator.js +302 -0
  43. package/package.json +69 -0
  44. package/server/dist/config.js +86 -0
  45. package/server/dist/constants.json +1359 -0
  46. package/server/dist/controllers/auth.controller.js +213 -0
  47. package/server/dist/controllers/data.controller.js +126 -0
  48. package/server/dist/controllers/insights.controller.js +125 -0
  49. package/server/dist/controllers/proxy.controller.js +235 -0
  50. package/server/dist/controllers/settings.controller.js +147 -0
  51. package/server/dist/controllers/watchlist.controller.js +499 -0
  52. package/server/dist/db.js +231 -0
  53. package/server/dist/github-sync.js +279 -0
  54. package/server/dist/google.js +274 -0
  55. package/server/dist/logger.js +21 -0
  56. package/server/dist/providers/123anime.provider.js +229 -0
  57. package/server/dist/providers/allanime.provider.js +773 -0
  58. package/server/dist/providers/animepahe.provider.js +313 -0
  59. package/server/dist/providers/animeya.provider.js +799 -0
  60. package/server/dist/providers/provider.interface.js +2 -0
  61. package/server/dist/rclone.js +126 -0
  62. package/server/dist/repositories/insights.repository.js +30 -0
  63. package/server/dist/repositories/notifications.repository.js +22 -0
  64. package/server/dist/repositories/settings.repository.js +13 -0
  65. package/server/dist/repositories/shows-meta.repository.js +39 -0
  66. package/server/dist/repositories/watched-episodes.repository.js +60 -0
  67. package/server/dist/repositories/watchlist.repository.js +49 -0
  68. package/server/dist/routes/auth.routes.js +23 -0
  69. package/server/dist/routes/data.routes.js +43 -0
  70. package/server/dist/routes/insights.routes.js +11 -0
  71. package/server/dist/routes/proxy.routes.js +13 -0
  72. package/server/dist/routes/settings.routes.js +26 -0
  73. package/server/dist/routes/watchlist.routes.js +26 -0
  74. package/server/dist/server.js +179 -0
  75. package/server/dist/sync-config.js +28 -0
  76. package/server/dist/sync.js +383 -0
  77. package/server/dist/utils/db-utils.js +36 -0
  78. package/server/dist/utils/env.utils.js +70 -0
  79. package/server/package.json +54 -0
@@ -0,0 +1,799 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.AnimeyaProvider = void 0;
40
+ const cheerio = __importStar(require("cheerio"));
41
+ const logger_1 = __importDefault(require("../logger"));
42
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, healthiest/537.36) Chrome/120.0.0.0 Safari/537.36';
43
+ const DEFAULT_CORS_HEADERS = {
44
+ Referer: 'https://animeya.cc',
45
+ Origin: 'https://animeya.cc',
46
+ 'User-Agent': UA,
47
+ };
48
+ class AnimeyaProvider {
49
+ name = 'Animeya';
50
+ cache;
51
+ constructor(cache) {
52
+ this.cache = cache;
53
+ }
54
+ async fetchText(url, referer) {
55
+ const res = await fetch(url, {
56
+ headers: {
57
+ 'User-Agent': UA,
58
+ Referer: referer || 'https://animeya.cc',
59
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
60
+ },
61
+ signal: AbortSignal.timeout(30000),
62
+ });
63
+ return res.text();
64
+ }
65
+ extractM3u8FromText(text) {
66
+ const matches = text.match(/https?:\/\/[^\s"'<>]+\.m3u8[^\s"'<>]*/gi) || [];
67
+ return Array.from(new Set(matches.map((m) => m.replace(/\\\//g, '/'))));
68
+ }
69
+ async extractEpisodeHls(url) {
70
+ if (!url) {
71
+ return {
72
+ sourceUrl: url,
73
+ hls: [],
74
+ inspected: [],
75
+ cors: true,
76
+ headers: DEFAULT_CORS_HEADERS,
77
+ note: 'Missing url',
78
+ };
79
+ }
80
+ const inspected = [url];
81
+ const hls = new Set();
82
+ try {
83
+ const html = await this.fetchText(url);
84
+ this.extractM3u8FromText(html).forEach((u) => hls.add(u));
85
+ const $ = cheerio.load(html);
86
+ const scriptBlob = $('script')
87
+ .map((_, s) => $(s).html() || '')
88
+ .get()
89
+ .join('\n');
90
+ this.extractM3u8FromText(scriptBlob).forEach((u) => hls.add(u));
91
+ $('iframe[src], script[src], source[src], video source[src], a[href]').each((_, el) => {
92
+ const raw = $(el).attr('src') || $(el).attr('href');
93
+ if (!raw)
94
+ return;
95
+ if (!/^https?:\/\//i.test(raw))
96
+ return;
97
+ if (/\.(js|css|png|jpg|jpeg|svg|woff2?|ttf|mp4)(\?|$)/i.test(raw))
98
+ return;
99
+ inspected.push(raw);
100
+ });
101
+ for (const candidate of Array.from(new Set(inspected)).slice(0, 12)) {
102
+ if (candidate === url)
103
+ continue;
104
+ try {
105
+ const page = await this.fetchText(candidate, url);
106
+ this.extractM3u8FromText(page).forEach((u) => hls.add(u));
107
+ }
108
+ catch {
109
+ // ignore
110
+ }
111
+ }
112
+ }
113
+ catch {
114
+ // ignore
115
+ }
116
+ return {
117
+ sourceUrl: url,
118
+ hls: Array.from(hls),
119
+ inspected: Array.from(new Set(inspected)),
120
+ cors: true,
121
+ headers: DEFAULT_CORS_HEADERS,
122
+ };
123
+ }
124
+ async fetchRetry(url, options = {}, retries = 3) {
125
+ let lastErr = null;
126
+ for (let i = 0; i < retries; i++) {
127
+ try {
128
+ const res = await fetch(url, {
129
+ ...options,
130
+ signal: AbortSignal.timeout(30000),
131
+ headers: { ...options.headers, 'User-Agent': UA },
132
+ });
133
+ if (res.ok)
134
+ return res;
135
+ if (res.status === 404)
136
+ throw new Error('Status 404');
137
+ lastErr = new Error(`Status ${res.status}`);
138
+ }
139
+ catch (e) {
140
+ if (e instanceof Error && e.message === 'Status 404')
141
+ throw e;
142
+ lastErr = e;
143
+ }
144
+ if (i < retries - 1)
145
+ await new Promise((r) => setTimeout(r, 1000));
146
+ }
147
+ throw lastErr;
148
+ }
149
+ parseRSCStream(html) {
150
+ const streamMap = new Map();
151
+ const regex = /self\.__next_f\.push\(\[(\d+|0),"((?:[^"\\]|\\.)*)"\]\)/g;
152
+ let m;
153
+ while ((m = regex.exec(html)) !== null) {
154
+ let raw = m[2];
155
+ try {
156
+ raw = JSON.parse(`"${raw}"`);
157
+ }
158
+ catch {
159
+ raw = raw.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\\\/g, '\\');
160
+ }
161
+ if (typeof raw !== 'string')
162
+ continue;
163
+ const idx = raw.indexOf(':');
164
+ if (idx === -1)
165
+ continue;
166
+ const id = raw.substring(0, idx);
167
+ const val = raw.substring(idx + 1);
168
+ try {
169
+ streamMap.set(id, val.trim().startsWith('[') || val.trim().startsWith('{') ? JSON.parse(val) : val);
170
+ }
171
+ catch {
172
+ streamMap.set(id, val);
173
+ }
174
+ }
175
+ return streamMap;
176
+ }
177
+ resolveRSC(obj, streamMap, depth = 0) {
178
+ if (depth > 20 || !obj)
179
+ return obj;
180
+ if (typeof obj === 'string' && obj.startsWith('$L')) {
181
+ const id = obj.substring(2);
182
+ const resolved = streamMap.get(id);
183
+ if (resolved) {
184
+ return this.resolveRSC(resolved, streamMap, depth + 1);
185
+ }
186
+ return obj;
187
+ }
188
+ if (Array.isArray(obj)) {
189
+ return obj.map((item) => this.resolveRSC(item, streamMap, depth));
190
+ }
191
+ if (typeof obj === 'object') {
192
+ const record = obj;
193
+ const newObj = {};
194
+ for (const key in record) {
195
+ newObj[key] = this.resolveRSC(record[key], streamMap, depth);
196
+ }
197
+ return newObj;
198
+ }
199
+ return obj;
200
+ }
201
+ deepSearch(obj, pred, results = []) {
202
+ if (!obj || typeof obj !== 'object')
203
+ return results;
204
+ try {
205
+ const record = obj;
206
+ if (pred(record))
207
+ results.push(record);
208
+ if (Array.isArray(obj)) {
209
+ for (const x of obj)
210
+ this.deepSearch(x, pred, results);
211
+ }
212
+ else {
213
+ for (const k in record)
214
+ this.deepSearch(record[k], pred, results);
215
+ }
216
+ }
217
+ catch {
218
+ // ignore
219
+ }
220
+ return results;
221
+ }
222
+ extractCard(node) {
223
+ try {
224
+ if (!node.href || typeof node.href !== 'string' || !node.href.startsWith('/watch/'))
225
+ return null;
226
+ const slug = node.href.split('/watch/')[1];
227
+ if (!slug)
228
+ return null;
229
+ // Reject slugs that look like random IDs/hashes (e.g., NFwLCK4XiFNCHARLX)
230
+ // Real slugs on Animeya usually have multiple words and dashes.
231
+ if (!slug.includes('-') && slug.length > 12)
232
+ return null;
233
+ const props = { slug, title: 'Unknown', cover: '', type: 'TV' };
234
+ const coverNodes = this.deepSearch(node, (o) => !!((o?.cover &&
235
+ typeof o.cover === 'object' &&
236
+ (typeof o.cover.extraLarge === 'string' ||
237
+ typeof o.cover.large === 'string' ||
238
+ typeof o.cover.medium === 'string')) ||
239
+ typeof o?.image === 'string' ||
240
+ typeof o?.bannerImage === 'string'));
241
+ const coverNode = coverNodes[0];
242
+ if (coverNode?.cover && typeof coverNode.cover === 'object') {
243
+ const c = coverNode.cover;
244
+ props.cover = c.extraLarge || c.large || c.medium || '';
245
+ }
246
+ if (!props.cover && typeof coverNode?.image === 'string')
247
+ props.cover = coverNode.image;
248
+ if (!props.cover && typeof coverNode?.bannerImage === 'string')
249
+ props.cover = coverNode.bannerImage;
250
+ const titleNodes = this.deepSearch(node, (o) => !!((o?.title &&
251
+ typeof o.title === 'object' &&
252
+ (typeof o.title.english === 'string' ||
253
+ typeof o.title.romaji === 'string' ||
254
+ typeof o.title.native === 'string')) ||
255
+ typeof o?.name === 'string'));
256
+ const titleNode = titleNodes[0];
257
+ if (titleNode?.title && typeof titleNode.title === 'object') {
258
+ const t = titleNode.title;
259
+ props.title =
260
+ t.english || t.romaji || t.native || 'Unknown';
261
+ }
262
+ else if (typeof titleNode?.name === 'string') {
263
+ props.title = titleNode.name;
264
+ }
265
+ if (!props.title || props.title === 'Unknown') {
266
+ // Try to find any string that might be a title
267
+ const potentialTitles = this.deepSearch(node, (o) => typeof o?.children === 'string');
268
+ if (potentialTitles.length > 0) {
269
+ props.title = potentialTitles[0].children;
270
+ }
271
+ }
272
+ if (!props.cover) {
273
+ const serialized = JSON.stringify(node).replace(/\\\//g, '/');
274
+ const m = serialized.match(/https?:\/\/[^"\s]+anilistcdn[^"\s]+\.(?:jpg|jpeg|png|webp)/i);
275
+ if (m)
276
+ props.cover = m[0];
277
+ }
278
+ // Try to find episode count in badge
279
+ const badgeNodes = this.deepSearch(node, (o) => !!(o?.['data-slot'] === 'badge' && Array.isArray(o?.children)));
280
+ if (badgeNodes.length > 0) {
281
+ const bn = badgeNodes[0];
282
+ const count = bn.children.find((c) => typeof c === 'number');
283
+ if (typeof count === 'number')
284
+ props.episodes = count;
285
+ }
286
+ if (!props.cover && !props.title)
287
+ return null;
288
+ return props;
289
+ }
290
+ catch {
291
+ return null;
292
+ }
293
+ }
294
+ cleanText(value) {
295
+ return (value || '').replace(/\s+/g, ' ').trim();
296
+ }
297
+ collectSubtitleTracks(value, fallbackLang = 'Subtitles') {
298
+ const collected = [];
299
+ const seen = new Set();
300
+ const walk = (node, inheritedLang) => {
301
+ if (!node)
302
+ return;
303
+ if (Array.isArray(node)) {
304
+ for (const item of node)
305
+ walk(item, inheritedLang);
306
+ return;
307
+ }
308
+ if (typeof node !== 'object')
309
+ return;
310
+ const record = node;
311
+ const url = (record.url ||
312
+ record.src ||
313
+ record.file ||
314
+ record.subtitleUrl ||
315
+ record.subUrl);
316
+ if (typeof url === 'string' && url.trim()) {
317
+ const lang = String(record.lang || record.language || record.label || inheritedLang || fallbackLang).trim() || fallbackLang;
318
+ const label = String(record.label || record.name || lang).trim() || lang;
319
+ const key = `${lang}|${label}|${url}`.toLowerCase();
320
+ if (!seen.has(key)) {
321
+ seen.add(key);
322
+ collected.push({
323
+ label,
324
+ url: url.trim(),
325
+ lang,
326
+ kind: record.kind,
327
+ file: typeof record.file === 'string' ? record.file.trim() : url.trim(),
328
+ });
329
+ }
330
+ }
331
+ for (const key of ['subtitles', 'subtitle', 'tracks', 'captions']) {
332
+ const child = record[key];
333
+ if (child)
334
+ walk(child, String(record.lang || record.language || record.label || inheritedLang || fallbackLang));
335
+ }
336
+ };
337
+ walk(value);
338
+ return collected;
339
+ }
340
+ async search(options) {
341
+ const query = options.query || '';
342
+ if (!query)
343
+ return [];
344
+ const performSearch = async (q) => {
345
+ const url = `https://animeya.cc/browser?search=${encodeURIComponent(q)}`;
346
+ const res = await this.fetchRetry(url);
347
+ const html = await res.text();
348
+ const rscMap = this.parseRSCStream(html);
349
+ const results = [];
350
+ const seen = new Set();
351
+ for (const rawObj of rscMap.values()) {
352
+ const obj = this.resolveRSC(rawObj, rscMap);
353
+ // Prioritize finding the 'medias' array which contains the actual search results
354
+ const mediasLists = this.deepSearch(obj, (o) => Array.isArray(o?.medias));
355
+ for (const listNode of mediasLists) {
356
+ const medias = listNode.medias;
357
+ for (const media of medias) {
358
+ const slug = media.slug;
359
+ if (slug && !seen.has(slug)) {
360
+ // Reject slugs that look like random IDs/hashes
361
+ if (!slug.includes('-') && slug.length > 12)
362
+ continue;
363
+ seen.add(slug);
364
+ const titleNode = media.title;
365
+ const title = titleNode?.english ||
366
+ titleNode?.romaji ||
367
+ titleNode?.native ||
368
+ 'Unknown';
369
+ const coverNode = media.coverImage;
370
+ const cover = coverNode?.extraLarge ||
371
+ coverNode?.large ||
372
+ coverNode?.medium ||
373
+ '';
374
+ const episodeCount = media.episodeCount || media.episodes || 0;
375
+ const episodes = Array.from({ length: episodeCount }, (_, i) => String(i + 1));
376
+ results.push({
377
+ _id: slug,
378
+ id: slug,
379
+ name: title,
380
+ englishName: title,
381
+ thumbnail: cover,
382
+ type: media.format || 'TV',
383
+ availableEpisodesDetail: {
384
+ sub: episodes,
385
+ dub: episodes,
386
+ },
387
+ });
388
+ }
389
+ }
390
+ }
391
+ // Fallback to old extraction if medias array wasn't found
392
+ if (results.length === 0) {
393
+ this.deepSearch(obj, (o) => !!(o?.href && typeof o.href === 'string' && o.href.startsWith('/watch/'))).forEach((n) => {
394
+ const c = this.extractCard(n);
395
+ if (c && !seen.has(c.slug)) {
396
+ seen.add(c.slug);
397
+ const episodes = Array.from({ length: c.episodes || 1 }, (_, i) => String(i + 1));
398
+ results.push({
399
+ _id: c.slug,
400
+ id: c.slug,
401
+ name: c.title,
402
+ englishName: c.title,
403
+ thumbnail: c.cover,
404
+ type: c.type || 'TV',
405
+ availableEpisodesDetail: {
406
+ sub: episodes,
407
+ dub: episodes,
408
+ },
409
+ });
410
+ }
411
+ });
412
+ }
413
+ }
414
+ return results;
415
+ };
416
+ try {
417
+ let results = await performSearch(query);
418
+ // Fallback Level 1: Remove "Season X" or "Xth Season"
419
+ if (results.length === 0 && (query.includes('Season') || query.includes('season'))) {
420
+ const fallbackQuery = query
421
+ .replace(/\s+(?:Season|season)\s+\d+/gi, '')
422
+ .replace(/\s+\d+(?:st|nd|rd|th)\s+(?:Season|season)/gi, '')
423
+ .trim();
424
+ if (fallbackQuery && fallbackQuery !== query) {
425
+ results = await performSearch(fallbackQuery);
426
+ }
427
+ }
428
+ // Fallback Level 2: Remove everything after ":" or "(" or "-"
429
+ if (results.length === 0) {
430
+ const fallbackQuery = query.split(/[:(-]/)[0].trim();
431
+ if (fallbackQuery && fallbackQuery !== query) {
432
+ results = await performSearch(fallbackQuery);
433
+ }
434
+ }
435
+ // Fallback Level 3: Most aggressive - remove Season info AND everything after symbols
436
+ if (results.length === 0) {
437
+ const fallbackQuery = query
438
+ .replace(/\s+(?:Season|season)\s+\d+/gi, '')
439
+ .replace(/\s+\d+(?:st|nd|rd|th)\s+(?:Season|season)/gi, '')
440
+ .split(/[:(-]/)[0]
441
+ .trim();
442
+ if (fallbackQuery && fallbackQuery !== query) {
443
+ results = await performSearch(fallbackQuery);
444
+ }
445
+ }
446
+ return results;
447
+ }
448
+ catch (error) {
449
+ logger_1.default.error({ err: error }, 'Animeya search failed');
450
+ return [];
451
+ }
452
+ }
453
+ async getInfoInternal(slug) {
454
+ const res = await this.fetchRetry(`https://animeya.cc/watch/${slug}`);
455
+ const html = await res.text();
456
+ const rscMap = this.parseRSCStream(html);
457
+ const details = {
458
+ id: slug,
459
+ title: slug,
460
+ cover: '',
461
+ description: '',
462
+ episodes: [],
463
+ };
464
+ const htmlTitle = html.match(/<title>([^<]+)<\/title>/i)?.[1]?.trim() || '';
465
+ const ogImage = html.match(/<meta\s+property=["']og:image["']\s+content=["']([^"']+)["']/i)?.[1]?.trim() || '';
466
+ const ogDescription = html
467
+ .match(/<meta\s+property=["']og:description["']\s+content=["']([^"']+)["']/i)?.[1]
468
+ ?.trim() || '';
469
+ const metaDescription = html.match(/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i)?.[1]?.trim() || '';
470
+ const notFoundPage = /404:\s*This page could not be found\./i.test(htmlTitle) ||
471
+ /404:\s*This page could not be found\./i.test(html);
472
+ for (const rawObj of rscMap.values()) {
473
+ const obj = this.resolveRSC(rawObj, rscMap);
474
+ const epLists = this.deepSearch(obj, (o) => !!(Array.isArray(o) &&
475
+ o.length > 0 &&
476
+ typeof o[0]?.episodeNumber === 'number'));
477
+ if (epLists.length > 0) {
478
+ for (const list of epLists) {
479
+ details.episodes.push(...list.map((ep) => ({
480
+ id: ep.id,
481
+ episodeNumber: ep.episodeNumber,
482
+ title: ep.title,
483
+ isFiller: ep.isFiller,
484
+ })));
485
+ }
486
+ }
487
+ if (details.title === slug && !notFoundPage) {
488
+ const titleNodes = this.deepSearch(obj, (o) => !!(Array.isArray(o) && o[0] === '$' && o[1] === 'title'));
489
+ if (titleNodes.length > 0) {
490
+ const node = titleNodes[0][3];
491
+ const t = node?.children;
492
+ if (t)
493
+ details.title = t.replace(' | Animeya', '');
494
+ }
495
+ }
496
+ if (!details.cover) {
497
+ const coverNodes = this.deepSearch(obj, (o) => !!(o?.cover &&
498
+ typeof o.cover === 'object' &&
499
+ (typeof o.cover.large === 'string' ||
500
+ typeof o.cover.extraLarge === 'string')));
501
+ const cn = coverNodes[0];
502
+ if (cn?.cover && typeof cn.cover === 'object') {
503
+ const c = cn.cover;
504
+ details.cover = c.extraLarge || c.large || '';
505
+ }
506
+ }
507
+ if (!details.description) {
508
+ const md = this.deepSearch(obj, (o) => !!(Array.isArray(o) && o[0] === '$' && o[1] === 'meta' && o[2] === 'description'));
509
+ if (md.length > 0) {
510
+ const node = md[0][3];
511
+ details.description = node?.content || '';
512
+ }
513
+ }
514
+ }
515
+ const unique = new Map();
516
+ details.episodes.forEach((ep) => unique.set(ep.episodeNumber, ep));
517
+ details.episodes = Array.from(unique.values()).sort((a, b) => a.episodeNumber - b.episodeNumber);
518
+ if (!details.cover && ogImage)
519
+ details.cover = ogImage;
520
+ if (!details.description) {
521
+ const jsonDesc = html.match(/"description"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/i)?.[1];
522
+ if (jsonDesc) {
523
+ try {
524
+ details.description = this.cleanText(JSON.parse(`"${jsonDesc}"`));
525
+ }
526
+ catch {
527
+ details.description = this.cleanText(jsonDesc.replace(/\\n/g, ' '));
528
+ }
529
+ }
530
+ }
531
+ if (!details.description) {
532
+ details.description = this.cleanText(ogDescription || metaDescription);
533
+ }
534
+ if (notFoundPage && details.episodes.length === 0)
535
+ throw new Error('Status 404');
536
+ return details;
537
+ }
538
+ async getEpisodeSourcesInternal(episodeId) {
539
+ const trpcUrl = `https://animeya.cc/api/trpc/episode.getEpisodeFullById?batch=1&input=${encodeURIComponent(JSON.stringify({ '0': { json: parseInt(episodeId, 10) } }))}`;
540
+ const res = await this.fetchRetry(trpcUrl);
541
+ const json = (await res.json());
542
+ const firstResult = json[0];
543
+ const result = firstResult?.result;
544
+ const data = result?.data;
545
+ const episodeData = data?.json;
546
+ if (!episodeData)
547
+ throw new Error('Episode not found');
548
+ const sources = (episodeData.players || []).map((p) => ({
549
+ name: p.name || 'Unknown',
550
+ url: p.url,
551
+ type: p.type || (p.url?.includes('.m3u8') ? 'HLS' : 'EMBED'),
552
+ quality: p.quality || '720p',
553
+ langue: p.langue || 'ENG',
554
+ subType: p.subType || 'NONE',
555
+ }));
556
+ const subtitles = [
557
+ ...this.collectSubtitleTracks(episodeData.subtitles),
558
+ ...this.collectSubtitleTracks(episodeData.tracks),
559
+ ...this.collectSubtitleTracks(episodeData.players),
560
+ ...(Array.isArray(episodeData.players)
561
+ ? episodeData.players.flatMap((player) => this.collectSubtitleTracks(player?.subtitles || player?.tracks || player?.captions))
562
+ : []),
563
+ ];
564
+ return {
565
+ episode: {
566
+ id: episodeData.id,
567
+ title: episodeData.title,
568
+ number: episodeData.episodeNumber,
569
+ },
570
+ sources,
571
+ subtitles,
572
+ };
573
+ }
574
+ async getEpisodes(showId, mode) {
575
+ try {
576
+ const cacheKey = `animeya_eps_${showId}`;
577
+ const cached = this.cache.get(cacheKey);
578
+ if (cached)
579
+ return cached;
580
+ const info = await this.getInfoInternal(showId);
581
+ if (!info || !info.episodes)
582
+ return null;
583
+ const episodes = info.episodes.map((ep) => String(ep.episodeNumber));
584
+ const result = {
585
+ episodes,
586
+ description: info.description || '',
587
+ };
588
+ this.cache.set(cacheKey, result, 3600);
589
+ return result;
590
+ }
591
+ catch (error) {
592
+ logger_1.default.error({ err: error, showId }, 'Animeya getEpisodes failed');
593
+ return null;
594
+ }
595
+ }
596
+ async getStreamUrls(showId, episodeNumber, mode) {
597
+ try {
598
+ const info = await this.getInfoInternal(showId);
599
+ let episode = info.episodes.find((ep) => String(ep.episodeNumber) === episodeNumber);
600
+ if (!episode && episodeNumber === '0') {
601
+ episode = info.episodes.find((ep) => String(ep.episodeNumber) === '1');
602
+ }
603
+ if (!episode || !episode.id)
604
+ return null;
605
+ const sourcesData = await this.getEpisodeSourcesInternal(String(episode.id));
606
+ const processedSources = [];
607
+ for (const source of sourcesData.sources) {
608
+ const subType = (source.subType || '').toUpperCase();
609
+ const langue = (source.langue || '').toUpperCase();
610
+ const isSub = ['SOFT', 'HARD', 'SUB'].includes(subType);
611
+ const isDub = subType === 'DUB' || (subType === 'NONE' && langue === 'ENG');
612
+ if (mode === 'dub' && !isDub)
613
+ continue;
614
+ if (mode === 'sub' && isDub)
615
+ continue;
616
+ if (source.type === 'HLS' || source.url.includes('.m3u8')) {
617
+ processedSources.push({
618
+ sourceName: source.name,
619
+ type: 'player',
620
+ links: [
621
+ {
622
+ resolutionStr: source.quality || 'Auto',
623
+ link: source.url,
624
+ hls: true,
625
+ headers: {
626
+ Referer: 'https://animeya.cc',
627
+ 'User-Agent': UA,
628
+ },
629
+ },
630
+ ],
631
+ subtitles: sourcesData.subtitles.map((s) => ({
632
+ language: s.lang || 'English',
633
+ label: s.label || 'English',
634
+ url: s.url,
635
+ })),
636
+ actualEpisodeNumber: String(episode.episodeNumber),
637
+ });
638
+ }
639
+ else if (source.name === 'Mp4') {
640
+ try {
641
+ const embedHtml = await this.fetchText(source.url, 'https://animeya.cc/');
642
+ const match = embedHtml.match(/src:\s*"(https:\/\/.*?\.mp4)"/);
643
+ if (match) {
644
+ processedSources.push({
645
+ sourceName: source.name,
646
+ type: 'player',
647
+ links: [
648
+ {
649
+ resolutionStr: 'Default',
650
+ link: match[1],
651
+ hls: false,
652
+ headers: { Referer: 'https://www.mp4upload.com/' },
653
+ },
654
+ ],
655
+ subtitles: sourcesData.subtitles.map((s) => ({
656
+ language: s.lang || 'English',
657
+ label: s.label || 'English',
658
+ url: s.url,
659
+ })),
660
+ actualEpisodeNumber: String(episode.episodeNumber),
661
+ });
662
+ }
663
+ else {
664
+ processedSources.push({
665
+ sourceName: source.name,
666
+ type: 'iframe',
667
+ links: [{ resolutionStr: 'iframe', link: source.url, hls: false }],
668
+ actualEpisodeNumber: String(episode.episodeNumber),
669
+ });
670
+ }
671
+ }
672
+ catch {
673
+ processedSources.push({
674
+ sourceName: source.name,
675
+ type: 'iframe',
676
+ links: [{ resolutionStr: 'iframe', link: source.url, hls: false }],
677
+ actualEpisodeNumber: String(episode.episodeNumber),
678
+ });
679
+ }
680
+ }
681
+ else if (source.name === 'Ok') {
682
+ processedSources.push({
683
+ sourceName: source.name,
684
+ type: 'iframe',
685
+ links: [{ resolutionStr: 'iframe', link: source.url, hls: false }],
686
+ actualEpisodeNumber: String(episode.episodeNumber),
687
+ });
688
+ }
689
+ else if (source.type === 'EMBED' ||
690
+ source.url.includes('iframe') ||
691
+ source.url.includes('embed')) {
692
+ try {
693
+ const extracted = await this.extractEpisodeHls(source.url);
694
+ if (extracted && extracted.hls && extracted.hls.length > 0) {
695
+ processedSources.push({
696
+ sourceName: `${source.name} (Extracted)`,
697
+ type: 'player',
698
+ links: extracted.hls.map((hlsUrl) => ({
699
+ resolutionStr: 'Auto',
700
+ link: hlsUrl,
701
+ hls: true,
702
+ headers: extracted.headers,
703
+ })),
704
+ subtitles: sourcesData.subtitles.map((s) => ({
705
+ language: s.lang || 'English',
706
+ label: s.label || 'English',
707
+ url: s.url,
708
+ })),
709
+ actualEpisodeNumber: String(episode.episodeNumber),
710
+ });
711
+ }
712
+ else {
713
+ processedSources.push({
714
+ sourceName: source.name,
715
+ type: 'iframe',
716
+ links: [
717
+ {
718
+ resolutionStr: 'iframe',
719
+ link: source.url,
720
+ hls: false,
721
+ },
722
+ ],
723
+ actualEpisodeNumber: String(episode.episodeNumber),
724
+ });
725
+ }
726
+ }
727
+ catch {
728
+ processedSources.push({
729
+ sourceName: source.name,
730
+ type: 'iframe',
731
+ links: [
732
+ {
733
+ resolutionStr: 'iframe',
734
+ link: source.url,
735
+ hls: false,
736
+ },
737
+ ],
738
+ actualEpisodeNumber: String(episode.episodeNumber),
739
+ });
740
+ }
741
+ }
742
+ }
743
+ return processedSources.length > 0 ? processedSources : null;
744
+ }
745
+ catch (error) {
746
+ logger_1.default.error({ err: error, showId, episodeNumber }, 'Animeya getStreamUrls failed');
747
+ return null;
748
+ }
749
+ }
750
+ async getShowMeta(showId) {
751
+ try {
752
+ const info = await this.getInfoInternal(showId);
753
+ return {
754
+ _id: info.id,
755
+ id: info.id,
756
+ name: info.title,
757
+ englishName: info.title,
758
+ thumbnail: info.cover,
759
+ description: info.description,
760
+ availableEpisodesDetail: {
761
+ sub: info.episodes.map((ep) => String(ep.episodeNumber)),
762
+ dub: [],
763
+ },
764
+ };
765
+ }
766
+ catch (error) {
767
+ logger_1.default.error({ err: error, showId }, 'Animeya getShowMeta failed');
768
+ return null;
769
+ }
770
+ }
771
+ async getPopular(timeframe) {
772
+ return [];
773
+ }
774
+ async getSchedule(date) {
775
+ return [];
776
+ }
777
+ async getSeasonal(page) {
778
+ return [];
779
+ }
780
+ async getLatestReleases() {
781
+ return [];
782
+ }
783
+ async getSkipTimes() {
784
+ return { found: false, results: [] };
785
+ }
786
+ async getShowDetails() {
787
+ return { status: 'Unknown' };
788
+ }
789
+ async getAllmangaDetails() {
790
+ return {
791
+ Rating: 'N/A',
792
+ Season: 'N/A',
793
+ Episodes: 'N/A',
794
+ Date: 'N/A',
795
+ 'Original Broadcast': 'N/A',
796
+ };
797
+ }
798
+ }
799
+ exports.AnimeyaProvider = AnimeyaProvider;