@vyla-entertainment/sdk 1.20.0 → 1.21.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 (52) hide show
  1. package/package.json +3 -2
  2. package/src/config.js +10 -85
  3. package/src/sdk.d.ts +94 -121
  4. package/src/sdk.js +82 -78
  5. package/src/sources/111477.js +88 -100
  6. package/src/sources/123anime.js +120 -56
  7. package/src/sources/3eyedraven.js +82 -135
  8. package/src/sources/cinecat.js +37 -18
  9. package/src/sources/cinemaarmy.js +179 -180
  10. package/src/sources/cinesrc.js +401 -397
  11. package/src/sources/cno.js +101 -94
  12. package/src/sources/dulo.js +134 -149
  13. package/src/sources/flaxmovies.js +114 -105
  14. package/src/sources/frost.js +38 -29
  15. package/src/sources/fsonline.js +95 -47
  16. package/src/sources/hdhub.js +43 -36
  17. package/src/sources/hexa.js +60 -53
  18. package/src/sources/hyper.js +42 -35
  19. package/src/sources/kickassanime.js +117 -124
  20. package/src/sources/kisskh.js +114 -59
  21. package/src/sources/lmscript.js +72 -70
  22. package/src/sources/lookmovie.js +80 -79
  23. package/src/sources/movenerds.js +74 -124
  24. package/src/sources/movy.js +197 -209
  25. package/src/sources/net79.js +59 -55
  26. package/src/sources/peestream.js +126 -138
  27. package/src/sources/pengu.js +46 -49
  28. package/src/sources/pstream.js +73 -67
  29. package/src/sources/purstream.js +96 -104
  30. package/src/sources/rivestream.js +182 -176
  31. package/src/sources/rivo.js +58 -53
  32. package/src/sources/streamaggregator.js +114 -128
  33. package/src/sources/thugtv.js +70 -62
  34. package/src/sources/vidapi.js +122 -114
  35. package/src/sources/vidcore.js +14 -122
  36. package/src/sources/vidfast.js +14 -122
  37. package/src/sources/vidlink.js +78 -27
  38. package/src/sources/vidup.js +14 -122
  39. package/src/sources/vidvault.js +133 -187
  40. package/src/sources/vidy.js +192 -187
  41. package/src/sources/vidzee.js +189 -183
  42. package/src/sources/vixsrc.js +123 -113
  43. package/src/sources/watchout.js +60 -46
  44. package/src/sources/xdownloader.js +127 -128
  45. package/src/sources/yapgrid.js +68 -62
  46. package/src/sources/yastream.js +35 -29
  47. package/src/subtitles.js +105 -122
  48. package/src/utils/helpers.js +586 -247
  49. package/src/sources/bcine.js +0 -202
  50. package/src/sources/cinezo.js +0 -55
  51. package/src/sources/vidora.js +0 -62
  52. package/src/sources/vidrock.js +0 -145
@@ -1,247 +1,586 @@
1
- import crypto from 'node:crypto';
2
-
3
- export const UA_LIST = [
4
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
5
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
6
- 'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
7
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
8
- ];
9
-
10
- export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
11
- export const USER_AGENT = UA_LIST[0];
12
-
13
- let sharedDcKey = null;
14
-
15
- export function decryptPartner(responseText, keyEnv) {
16
- try {
17
- const sep = responseText.indexOf(':');
18
- if (sep === -1) return null;
19
- const iv = Buffer.from(responseText.slice(0, sep), 'base64');
20
- const encrypted = responseText.slice(sep + 1);
21
- if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
22
- const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
23
- return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
24
- } catch {
25
- return null;
26
- }
27
- }
28
-
29
- export async function fetchJson(url, options) {
30
- const res = await fetch(url, options);
31
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
32
- return res.json();
33
- }
34
-
35
- export async function fetchText(url, options) {
36
- const res = await fetch(url, options);
37
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
38
- return res.text();
39
- }
40
-
41
- const tmdbInfoCache = new Map();
42
- const anilistCache = new Map();
43
- const tmdbValidationCache = new Map();
44
-
45
- function pruneHelperCache(cache) {
46
- const now = Date.now();
47
- for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
48
- if (cache.size > 3000) {
49
- const overflow = cache.size - 3000;
50
- const it = cache.keys();
51
- for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
52
- }
53
- }
54
-
55
- export async function resolveImdbId(id, isTv, tmdbApiKey) {
56
- if (typeof id === 'string' && id.startsWith('tt')) return id;
57
- const apiKey = tmdbApiKey || process.env.TMDB_API_KEY;
58
- if (!apiKey) return `tt${id}`;
59
- const type = isTv ? 'tv' : 'movie';
60
- try {
61
- const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
62
- signal: AbortSignal.timeout(5000),
63
- });
64
- if (!res.ok) return `tt${id}`;
65
- const data = await res.json();
66
- return data.imdb_id || data.external_ids?.imdb_id || `tt${id}`;
67
- } catch {
68
- return `tt${id}`;
69
- }
70
- }
71
-
72
- setInterval(() => {
73
- pruneHelperCache(tmdbValidationCache);
74
- pruneHelperCache(tmdbInfoCache);
75
- pruneHelperCache(anilistCache);
76
- }, 60000).unref();
77
-
78
- function cacheGet(cache, key) {
79
- const entry = cache.get(key);
80
- if (!entry) return undefined;
81
- if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; }
82
- return entry.val;
83
- }
84
-
85
- function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
86
-
87
- export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) {
88
- const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
89
- return {
90
- id: id,
91
- s: s,
92
- e: e,
93
- clientIP: clientIP,
94
- absoluteBase: '/',
95
- audio,
96
- sdk,
97
- config: source,
98
- }
99
- }
100
-
101
- export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
102
- let apiKey = tmdbApiKey;
103
- let id = tmdbId;
104
- let type = mediaType;
105
- if (id === 'movie' || id === 'tv' || !id) {
106
- if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') {
107
- type = id || 'movie';
108
- id = apiKey;
109
- apiKey = process.env.TMDB_API_KEY;
110
- }
111
- }
112
- if (!apiKey) {
113
- apiKey = process.env.TMDB_API_KEY;
114
- }
115
- const key = `${id}-${type}`;
116
- const cached = cacheGet(tmdbValidationCache, key);
117
- if (cached !== undefined) return cached;
118
- if (!apiKey) return { valid: false, error: 'TMDB API key not configured' };
119
- try {
120
- const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
121
- if (res.ok) {
122
- const data = await res.json();
123
- const isValid = !!(data?.id && (data?.title || data?.name));
124
- const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
125
- cacheSet(tmdbValidationCache, key, result, 300000);
126
- return result;
127
- }
128
- const result = { valid: false, error: `TMDB API error: ${res.status}` };
129
- cacheSet(tmdbValidationCache, key, result, 60000);
130
- return result;
131
- } catch (error) {
132
- const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
133
- cacheSet(tmdbValidationCache, key, result, 60000);
134
- return result;
135
- }
136
- }
137
-
138
- export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
139
- let apiKey = tmdbApiKey;
140
- let id = tmdbId;
141
- let type = mediaType;
142
- let s = season;
143
- if (apiKey === 'movie' || apiKey === 'tv') {
144
- type = apiKey;
145
- id = tmdbId;
146
- apiKey = process.env.TMDB_API_KEY;
147
- } else if (id === 'movie' || id === 'tv') {
148
- type = id;
149
- id = apiKey;
150
- apiKey = process.env.TMDB_API_KEY;
151
- }
152
- if (!apiKey) {
153
- apiKey = process.env.TMDB_API_KEY;
154
- }
155
- const key = `${id}-${type}-${s || ''}`;
156
- const cached = cacheGet(tmdbInfoCache, key);
157
- if (cached !== undefined) return cached;
158
- if (!apiKey) return { isAnime: false, titles: [], year: null, imdbId: null };
159
- try {
160
- const [mainRes, seasonRes] = await Promise.all([
161
- fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
162
- s && type === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
163
- ]);
164
- let mainData = null, seasonData = null;
165
- if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
166
- if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); }
167
- const genres = mainData?.genres || [];
168
- const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
169
- const titles = [];
170
- if (seasonData?.name) titles.push(seasonData.name);
171
- const titleText = mainData?.title || mainData?.name || '';
172
- const ot = mainData?.original_title || mainData?.original_name || '';
173
- if (titleText) titles.push(titleText);
174
- if (ot && ot !== titleText) titles.push(ot);
175
- const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
176
- const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null };
177
- cacheSet(tmdbInfoCache, key, result, 600000);
178
- return result;
179
- } catch { return { isAnime: false, titles: [], year: null, imdbId: null }; }
180
- }
181
-
182
- export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
183
- const key = `${tmdbId}-${mediaType}-${season || ''}`;
184
- const cached = cacheGet(anilistCache, key);
185
- if (cached !== undefined) return cached;
186
- try {
187
- const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
188
- if (res.ok) {
189
- const id = (await res.json())?.mappings?.[0]?.anilist_id;
190
- if (id) { cacheSet(anilistCache, key, id, 600000); return id; }
191
- } else res.body?.cancel();
192
- } catch { }
193
- if (!titles.length) return null;
194
- const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
195
- let bestId = null, bestScore = -1;
196
- for (const searchTitle of titles) {
197
- try {
198
- const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) });
199
- if (!res.ok) { res.body?.cancel(); continue; }
200
- for (const entry of (await res.json())?.data?.Page?.media || []) {
201
- const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
202
- const sl = searchTitle.toLowerCase();
203
- let score = 0;
204
- if (entryTitles.some(t => t === sl)) score += 5;
205
- else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
206
- else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2;
207
- else continue;
208
- if (year && entry.startDate?.year) {
209
- const diff = Math.abs(entry.startDate.year - year);
210
- if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3;
211
- }
212
- if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
213
- if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
214
- if (score > bestScore) { bestScore = score; bestId = entry.id; }
215
- }
216
- if (bestScore >= 8) break;
217
- } catch { }
218
- }
219
- if (bestId) cacheSet(anilistCache, key, bestId, 600000);
220
- return bestId;
221
- }
222
-
223
- export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
224
- let apiKey = tmdbApiKey;
225
- let id = imdbId;
226
- let type = mediaType;
227
- if (id === 'movie' || id === 'tv') {
228
- type = id;
229
- id = apiKey;
230
- apiKey = process.env.TMDB_API_KEY;
231
- }
232
- if (!apiKey) {
233
- apiKey = process.env.TMDB_API_KEY;
234
- }
235
- const key = `imdb-${id}`;
236
- const cached = cacheGet(tmdbInfoCache, key);
237
- if (cached !== undefined) return cached;
238
- if (!apiKey) return null;
239
- try {
240
- const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
241
- let result = null;
242
- if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
243
- else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
244
- if (result) cacheSet(tmdbInfoCache, key, result, 600000);
245
- return result;
246
- } catch { return null; }
247
- }
1
+ import crypto from 'node:crypto';
2
+
3
+ export const UA_LIST = [
4
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
5
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
6
+ 'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
7
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
8
+ ];
9
+
10
+ export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
11
+ export const USER_AGENT = UA_LIST[0];
12
+
13
+ const DEFAULT_TMDB_KEY = '45dbdd5e714da212424bda0336167447';
14
+
15
+ let sharedDcKey = null;
16
+
17
+ const tmdbInfoCache = new Map();
18
+ const anilistCache = new Map();
19
+ const tmdbValidationCache = new Map();
20
+ const tmdbMetaCache = new Map();
21
+
22
+ function pruneCache(cache) {
23
+ const now = Date.now();
24
+ for (const [k, v] of cache) {
25
+ if (now - v.ts >= v.ttl) cache.delete(k);
26
+ }
27
+ if (cache.size > 3000) {
28
+ const overflow = cache.size - 3000;
29
+ const it = cache.keys();
30
+ for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
31
+ }
32
+ }
33
+
34
+ setInterval(() => {
35
+ pruneCache(tmdbValidationCache);
36
+ pruneCache(tmdbInfoCache);
37
+ pruneCache(anilistCache);
38
+ pruneCache(tmdbMetaCache);
39
+ }, 60000).unref();
40
+
41
+ function cacheGet(cache, key) {
42
+ const entry = cache.get(key);
43
+ if (!entry) return undefined;
44
+ if (Date.now() - entry.ts >= entry.ttl) {
45
+ cache.delete(key);
46
+ return undefined;
47
+ }
48
+ return entry.val;
49
+ }
50
+
51
+ function cacheSet(cache, key, val, ttl) {
52
+ cache.set(key, { val, ts: Date.now(), ttl });
53
+ }
54
+
55
+ export function resolveTmdbKey(args = {}, sdk = null) {
56
+ return (
57
+ args?.tmdbApiKey ||
58
+ args?.sdk?.tmdbApiKey ||
59
+ sdk?.tmdbApiKey ||
60
+ process.env.TMDB_API_KEY ||
61
+ DEFAULT_TMDB_KEY
62
+ );
63
+ }
64
+
65
+ export function slugify(text) {
66
+ if (!text) return '';
67
+ return String(text)
68
+ .toLowerCase()
69
+ .normalize('NFKD')
70
+ .replace(/[\u0300-\u036f]/g, '')
71
+ .replace(/[^a-z0-9]+/g, '-')
72
+ .replace(/^-|-$/g, '');
73
+ }
74
+
75
+ export function cleanTitle(text) {
76
+ if (!text) return '';
77
+ return String(text)
78
+ .toLowerCase()
79
+ .replace(/\(\d{4}\)/g, '')
80
+ .replace(/[^a-z0-9]/g, '')
81
+ .trim();
82
+ }
83
+
84
+ export function formatBytes(bytes, decimals = 2) {
85
+ if (!bytes || bytes <= 0) return '0 B';
86
+ const k = 1024;
87
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
88
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
89
+ return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`;
90
+ }
91
+
92
+ export function getRandomHex(size = 32) {
93
+ return crypto.randomBytes(size).toString('hex');
94
+ }
95
+
96
+ export async function fetchJson(url, options = {}) {
97
+ const res = await fetch(url, options);
98
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
99
+ return res.json();
100
+ }
101
+
102
+ export async function fetchText(url, options = {}) {
103
+ const res = await fetch(url, options);
104
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
105
+ return res.text();
106
+ }
107
+
108
+ export function decryptPartner(responseText, keyEnv) {
109
+ try {
110
+ const sep = responseText.indexOf(':');
111
+ if (sep === -1) return null;
112
+ const iv = Buffer.from(responseText.slice(0, sep), 'base64');
113
+ const encrypted = responseText.slice(sep + 1);
114
+ if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
115
+ const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
116
+ return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ export async function getTmdbMetadata(id, isTv, apiKey = null) {
123
+ const key = apiKey || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
124
+ const type = isTv ? 'tv' : 'movie';
125
+ const cacheKey = `${id}-${type}-${key}`;
126
+ const cached = cacheGet(tmdbMetaCache, cacheKey);
127
+ if (cached !== undefined) return cached;
128
+
129
+ const fallback = {
130
+ title: '',
131
+ originalTitle: '',
132
+ titles: [],
133
+ year: null,
134
+ releaseDate: '',
135
+ imdbId: null,
136
+ isAnime: false,
137
+ runtime: 0,
138
+ seasons: [],
139
+ };
140
+
141
+ try {
142
+ const url = `https://api.themoviedb.org/3/${type}/${id}?api_key=${key}&append_to_response=external_ids,alternative_titles`;
143
+ const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
144
+ if (!res.ok) {
145
+ cacheSet(tmdbMetaCache, cacheKey, fallback, 60000);
146
+ return fallback;
147
+ }
148
+
149
+ const data = await res.json();
150
+ const title = data.name || data.title || '';
151
+ const originalTitle = data.original_name || data.original_title || '';
152
+ const releaseDate = data.first_air_date || data.release_date || '';
153
+ const year = releaseDate ? parseInt(releaseDate.slice(0, 4), 10) : null;
154
+ const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
155
+ const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90);
156
+
157
+ const titles = [];
158
+ if (title) titles.push(title);
159
+ if (originalTitle && !titles.includes(originalTitle)) titles.push(originalTitle);
160
+
161
+ const alts = data.alternative_titles?.titles || data.alternative_titles?.results || [];
162
+ for (const alt of alts) {
163
+ const altTitle = alt.title || alt.name;
164
+ if (altTitle && !titles.includes(altTitle)) titles.push(altTitle);
165
+ }
166
+
167
+ const genres = data.genres || [];
168
+ const isAnime = genres.some(g => g.id === 16) && ((data.origin_country || []).includes('JP') || data.original_language === 'ja');
169
+
170
+ const result = {
171
+ title,
172
+ originalTitle,
173
+ titles,
174
+ year,
175
+ releaseDate,
176
+ imdbId,
177
+ isAnime,
178
+ runtime,
179
+ seasons: Array.isArray(data.seasons) ? data.seasons : [],
180
+ };
181
+
182
+ cacheSet(tmdbMetaCache, cacheKey, result, 600000);
183
+ return result;
184
+ } catch {
185
+ cacheSet(tmdbMetaCache, cacheKey, fallback, 60000);
186
+ return fallback;
187
+ }
188
+ }
189
+
190
+ export async function resolveImdbId(id, isTv, tmdbApiKey = null) {
191
+ if (typeof id === 'string' && id.startsWith('tt')) return id;
192
+ const meta = await getTmdbMetadata(id, isTv, tmdbApiKey);
193
+ return meta?.imdbId || `tt${id}`;
194
+ }
195
+
196
+ export async function getTmdbInfo(arg1, arg2, arg3, arg4) {
197
+ let apiKey = null;
198
+ let id = null;
199
+ let type = null;
200
+ let season = null;
201
+
202
+ if (arg1 === 'movie' || arg1 === 'tv') {
203
+ type = arg1;
204
+ id = arg2;
205
+ season = arg3;
206
+ apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
207
+ } else if (arg2 === 'movie' || arg2 === 'tv') {
208
+ if (typeof arg1 === 'string' && arg1.length > 20 && !/^\d+$/.test(arg1)) {
209
+ apiKey = arg1;
210
+ id = arg3;
211
+ type = arg2;
212
+ season = arg4;
213
+ } else {
214
+ id = arg1;
215
+ type = arg2;
216
+ season = arg3;
217
+ apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
218
+ }
219
+ } else if (arg3 === 'movie' || arg3 === 'tv') {
220
+ apiKey = arg1;
221
+ id = arg2;
222
+ type = arg3;
223
+ season = arg4;
224
+ } else {
225
+ id = arg1;
226
+ type = arg2 || 'movie';
227
+ season = arg3;
228
+ apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
229
+ }
230
+
231
+ if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
232
+
233
+ const isTv = type === 'tv';
234
+ const key = `${id}-${type}-${season || ''}-${apiKey}`;
235
+ const cached = cacheGet(tmdbInfoCache, key);
236
+ if (cached !== undefined) return cached;
237
+
238
+ try {
239
+ const meta = await getTmdbMetadata(id, isTv, apiKey);
240
+ let seasonTitle = null;
241
+
242
+ if (season && isTv) {
243
+ try {
244
+ const seasonRes = await fetch(`https://api.themoviedb.org/3/tv/${id}/season/${season}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
245
+ if (seasonRes.ok) {
246
+ const seasonData = await seasonRes.json();
247
+ if (seasonData?.name) seasonTitle = seasonData.name;
248
+ }
249
+ } catch { }
250
+ }
251
+
252
+ const titles = seasonTitle ? [seasonTitle, ...meta.titles] : meta.titles;
253
+ const result = {
254
+ name: meta.title,
255
+ title: meta.title,
256
+ isAnime: meta.isAnime,
257
+ titles: [...new Set(titles.filter(Boolean))],
258
+ year: meta.year,
259
+ release_date: meta.releaseDate,
260
+ first_air_date: meta.releaseDate,
261
+ imdbId: meta.imdbId,
262
+ imdb_id: meta.imdbId,
263
+ external_ids: { imdb_id: meta.imdbId },
264
+ seasons: meta.seasons,
265
+ runtime: meta.runtime,
266
+ };
267
+
268
+ cacheSet(tmdbInfoCache, key, result, 600000);
269
+ return result;
270
+ } catch {
271
+ const fallback = {
272
+ name: '',
273
+ title: '',
274
+ isAnime: false,
275
+ titles: [],
276
+ year: null,
277
+ release_date: '',
278
+ first_air_date: '',
279
+ imdbId: null,
280
+ imdb_id: null,
281
+ external_ids: { imdb_id: null },
282
+ seasons: [],
283
+ runtime: 0,
284
+ };
285
+ return fallback;
286
+ }
287
+ }
288
+
289
+ export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
290
+ let apiKey = tmdbApiKey;
291
+ let id = tmdbId;
292
+ let type = mediaType;
293
+
294
+ if (id === 'movie' || id === 'tv' || !id) {
295
+ if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') {
296
+ type = id || 'movie';
297
+ id = apiKey;
298
+ apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
299
+ }
300
+ }
301
+ if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
302
+
303
+ const key = `${id}-${type}`;
304
+ const cached = cacheGet(tmdbValidationCache, key);
305
+ if (cached !== undefined) return cached;
306
+
307
+ try {
308
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
309
+ if (res.ok) {
310
+ const data = await res.json();
311
+ const isValid = Boolean(data?.id && (data?.title || data?.name));
312
+ const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
313
+ cacheSet(tmdbValidationCache, key, result, 300000);
314
+ return result;
315
+ }
316
+ const result = { valid: false, error: `TMDB API error: ${res.status}` };
317
+ cacheSet(tmdbValidationCache, key, result, 60000);
318
+ return result;
319
+ } catch (error) {
320
+ const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
321
+ cacheSet(tmdbValidationCache, key, result, 60000);
322
+ return result;
323
+ }
324
+ }
325
+
326
+ export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
327
+ let apiKey = tmdbApiKey;
328
+ let id = imdbId;
329
+ let type = mediaType;
330
+
331
+ if (id === 'movie' || id === 'tv') {
332
+ type = id;
333
+ id = apiKey;
334
+ apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
335
+ }
336
+ if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
337
+
338
+ const key = `imdb-${id}`;
339
+ const cached = cacheGet(tmdbInfoCache, key);
340
+ if (cached !== undefined) return cached;
341
+
342
+ try {
343
+ const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
344
+ let result = null;
345
+ if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
346
+ else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
347
+ if (result) cacheSet(tmdbInfoCache, key, result, 600000);
348
+ return result;
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+
354
+ export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
355
+ const key = `${tmdbId}-${mediaType}-${season || ''}`;
356
+ const cached = cacheGet(anilistCache, key);
357
+ if (cached !== undefined) return cached;
358
+
359
+ try {
360
+ const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
361
+ if (res.ok) {
362
+ const data = await res.json();
363
+ const id = data?.mappings?.[0]?.anilist_id;
364
+ if (id) {
365
+ cacheSet(anilistCache, key, id, 600000);
366
+ return id;
367
+ }
368
+ }
369
+ } catch { }
370
+
371
+ if (!titles.length) return null;
372
+
373
+ const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
374
+ let bestId = null;
375
+ let bestScore = -1;
376
+
377
+ for (const searchTitle of titles) {
378
+ try {
379
+ const res = await fetch('https://graphql.anilist.co', {
380
+ method: 'POST',
381
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
382
+ body: JSON.stringify({ query, variables: { s: searchTitle } }),
383
+ signal: AbortSignal.timeout(6000)
384
+ });
385
+ if (!res.ok) continue;
386
+
387
+ const resJson = await res.json();
388
+ for (const entry of resJson?.data?.Page?.media || []) {
389
+ const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
390
+ const sl = searchTitle.toLowerCase();
391
+ let score = 0;
392
+
393
+ if (entryTitles.some(t => t === sl)) score += 5;
394
+ else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
395
+ else if (entryTitles.some(t => {
396
+ const w = sl.split(/\s+/).filter(x => x.length > 3);
397
+ return w.length > 0 && w.every(x => t.includes(x));
398
+ })) score += 2;
399
+ else continue;
400
+
401
+ if (year && entry.startDate?.year) {
402
+ const diff = Math.abs(entry.startDate.year - year);
403
+ if (diff === 0) score += 3;
404
+ else if (diff === 1) score += 1;
405
+ else if (diff > 2) score -= 3;
406
+ }
407
+
408
+ if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
409
+ if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
410
+
411
+ if (score > bestScore) {
412
+ bestScore = score;
413
+ bestId = entry.id;
414
+ }
415
+ }
416
+ if (bestScore >= 8) break;
417
+ } catch { }
418
+ }
419
+
420
+ if (bestId) cacheSet(anilistCache, key, bestId, 600000);
421
+ return bestId;
422
+ }
423
+
424
+ export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) {
425
+ const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
426
+ return {
427
+ id,
428
+ s,
429
+ e,
430
+ clientIP,
431
+ absoluteBase: '/',
432
+ audio,
433
+ sdk,
434
+ config: source,
435
+ tmdbApiKey: resolveTmdbKey(null, sdk),
436
+ };
437
+ }
438
+
439
+ export function formatStreamResult(streams, defaultHeaders = {}, extra = {}) {
440
+ if (!streams) return null;
441
+ const list = Array.isArray(streams) ? streams.filter(Boolean) : [streams];
442
+ if (!list.length) return null;
443
+
444
+ const allUrls = list.map(item => {
445
+ const itemUrl = item.url || item.file || item.stream || item.src;
446
+ if (!itemUrl) return null;
447
+ return {
448
+ url: itemUrl,
449
+ server: item.server || item.label || item.name || 'Server',
450
+ quality: item.quality || 'Auto',
451
+ type: item.type || (itemUrl.includes('.m3u8') ? 'hls' : (itemUrl.includes('.mpd') ? 'dash' : 'mp4')),
452
+ headers: item.headers || defaultHeaders,
453
+ ...(item.subtitles ? { subtitles: item.subtitles } : {}),
454
+ ...(item.skipProxy !== undefined ? { skipProxy: item.skipProxy } : {}),
455
+ ...(item.audio ? { audio: item.audio } : {}),
456
+ ...(item.label ? { label: item.label } : {}),
457
+ };
458
+ }).filter(Boolean);
459
+
460
+ if (!allUrls.length) return null;
461
+
462
+ return {
463
+ url: allUrls[0].url,
464
+ headers: allUrls[0].headers,
465
+ server: allUrls[0].server,
466
+ quality: allUrls[0].quality,
467
+ type: allUrls[0].type,
468
+ allUrls,
469
+ ...extra,
470
+ };
471
+ }
472
+
473
+ export async function scrapeVidcoreFamily({ id, s, e, baseUrl, apiName, serverLabel }) {
474
+ const isTv = Boolean(s && e);
475
+ const targetUrl = isTv
476
+ ? `${baseUrl}/tv/${id}/${s}/${e}/`
477
+ : `${baseUrl}/movie/${id}`;
478
+
479
+ const ua = getUA();
480
+ const baseHeaders = {
481
+ 'User-Agent': ua,
482
+ Referer: `${baseUrl}/`,
483
+ 'X-Requested-With': 'XMLHttpRequest',
484
+ };
485
+
486
+ try {
487
+ const pageRes = await fetch(targetUrl, {
488
+ headers: { 'User-Agent': ua },
489
+ signal: AbortSignal.timeout(10000),
490
+ });
491
+ if (!pageRes.ok) return null;
492
+ const html = await pageRes.text();
493
+
494
+ const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
495
+ if (!match || !match[1]) return null;
496
+ const enToken = match[1];
497
+
498
+ const s1Res = await fetch(`https://enc-dec.app/api/enc-${apiName}?text=${encodeURIComponent(enToken)}&stage=1`, {
499
+ signal: AbortSignal.timeout(8000),
500
+ });
501
+ const s1Data = await s1Res.json();
502
+ if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
503
+
504
+ const token1 = s1Data.result.token;
505
+ const stage1Url = s1Data.result.stage1;
506
+
507
+ const post1Res = await fetch(stage1Url, {
508
+ method: 'POST',
509
+ headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
510
+ signal: AbortSignal.timeout(10000),
511
+ });
512
+ if (!post1Res.ok) return null;
513
+ const stage1Text = await post1Res.text();
514
+
515
+ const s2Res = await fetch(`https://enc-dec.app/api/enc-${apiName}?text=${encodeURIComponent(stage1Text)}&stage=2`, {
516
+ signal: AbortSignal.timeout(8000),
517
+ });
518
+ const s2Data = await s2Res.json();
519
+ if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
520
+
521
+ const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
522
+ const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
523
+
524
+ const serversRes = await fetch(serversUrl, {
525
+ method: 'POST',
526
+ headers: currentHeaders,
527
+ signal: AbortSignal.timeout(10000),
528
+ });
529
+ if (!serversRes.ok) return null;
530
+ const serversEncrypted = await serversRes.text();
531
+
532
+ const decServersRes = await fetch(`https://enc-dec.app/api/dec-${apiName}`, {
533
+ method: 'POST',
534
+ headers: { 'Content-Type': 'application/json' },
535
+ body: JSON.stringify({ text: serversEncrypted }),
536
+ signal: AbortSignal.timeout(8000),
537
+ });
538
+ const decServersData = await decServersRes.json();
539
+ if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
540
+
541
+ const allUrls = [];
542
+
543
+ for (const server of decServersData.result) {
544
+ if (!server?.data) continue;
545
+ try {
546
+ const streamUrl = `${streamBaseUrl}/${server.data}`;
547
+ const streamEncRes = await fetch(streamUrl, {
548
+ method: 'POST',
549
+ headers: currentHeaders,
550
+ signal: AbortSignal.timeout(8000),
551
+ });
552
+ if (!streamEncRes.ok) continue;
553
+ const streamEncText = await streamEncRes.text();
554
+
555
+ const decStreamRes = await fetch(`https://enc-dec.app/api/dec-${apiName}`, {
556
+ method: 'POST',
557
+ headers: { 'Content-Type': 'application/json' },
558
+ body: JSON.stringify({ text: streamEncText }),
559
+ signal: AbortSignal.timeout(8000),
560
+ });
561
+ const decStreamData = await decStreamRes.json();
562
+ if (decStreamData?.status !== 200 || !decStreamData.result) continue;
563
+
564
+ const streamResult = decStreamData.result;
565
+ const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
566
+
567
+ if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
568
+ allUrls.push({
569
+ url: resultUrl,
570
+ server: `${serverLabel} - ${server.name || server.title || 'Server'}`,
571
+ label: server.name || server.title || serverLabel,
572
+ type: resultUrl.includes('.m3u8') ? 'hls' : 'mp4',
573
+ headers: {
574
+ 'User-Agent': ua,
575
+ Referer: `${baseUrl}/`,
576
+ },
577
+ });
578
+ }
579
+ } catch { }
580
+ }
581
+
582
+ return formatStreamResult(allUrls);
583
+ } catch {
584
+ return null;
585
+ }
586
+ }