@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.
- package/package.json +3 -2
- package/src/config.js +10 -85
- package/src/sdk.d.ts +94 -121
- package/src/sdk.js +82 -78
- package/src/sources/111477.js +88 -100
- package/src/sources/123anime.js +120 -56
- package/src/sources/3eyedraven.js +82 -135
- package/src/sources/cinecat.js +37 -18
- package/src/sources/cinemaarmy.js +179 -180
- package/src/sources/cinesrc.js +401 -397
- package/src/sources/cno.js +101 -94
- package/src/sources/dulo.js +134 -149
- package/src/sources/flaxmovies.js +114 -105
- package/src/sources/frost.js +38 -29
- package/src/sources/fsonline.js +95 -47
- package/src/sources/hdhub.js +43 -36
- package/src/sources/hexa.js +60 -53
- package/src/sources/hyper.js +42 -35
- package/src/sources/kickassanime.js +117 -124
- package/src/sources/kisskh.js +114 -59
- package/src/sources/lmscript.js +72 -70
- package/src/sources/lookmovie.js +80 -79
- package/src/sources/movenerds.js +74 -124
- package/src/sources/movy.js +197 -209
- package/src/sources/net79.js +59 -55
- package/src/sources/peestream.js +126 -138
- package/src/sources/pengu.js +46 -49
- package/src/sources/pstream.js +73 -67
- package/src/sources/purstream.js +96 -104
- package/src/sources/rivestream.js +182 -176
- package/src/sources/rivo.js +58 -53
- package/src/sources/streamaggregator.js +114 -128
- package/src/sources/thugtv.js +70 -62
- package/src/sources/vidapi.js +122 -114
- package/src/sources/vidcore.js +14 -122
- package/src/sources/vidfast.js +14 -122
- package/src/sources/vidlink.js +78 -27
- package/src/sources/vidup.js +14 -122
- package/src/sources/vidvault.js +133 -187
- package/src/sources/vidy.js +192 -187
- package/src/sources/vidzee.js +189 -183
- package/src/sources/vixsrc.js +123 -113
- package/src/sources/watchout.js +60 -46
- package/src/sources/xdownloader.js +127 -128
- package/src/sources/yapgrid.js +68 -62
- package/src/sources/yastream.js +35 -29
- package/src/subtitles.js +105 -122
- package/src/utils/helpers.js +586 -247
- package/src/sources/bcine.js +0 -202
- package/src/sources/cinezo.js +0 -55
- package/src/sources/vidora.js +0 -62
- package/src/sources/vidrock.js +0 -145
package/src/utils/helpers.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const genres =
|
|
168
|
-
const isAnime = genres.some(g => g.id === 16) && ((
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|