@vyla-entertainment/sdk 1.20.0 → 1.22.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 (58) hide show
  1. package/package.json +3 -2
  2. package/src/config.js +99 -67
  3. package/src/health.js +60 -20
  4. package/src/sdk.d.ts +94 -121
  5. package/src/sdk.js +82 -78
  6. package/src/sources/111477.js +88 -100
  7. package/src/sources/123anime.js +120 -56
  8. package/src/sources/3eyedraven.js +82 -135
  9. package/src/sources/4movies.js +77 -0
  10. package/src/sources/aether.js +99 -0
  11. package/src/sources/castle.js +415 -0
  12. package/src/sources/cinecat.js +37 -18
  13. package/src/sources/cinemaarmy.js +179 -180
  14. package/src/sources/cinesrc.js +401 -397
  15. package/src/sources/cno.js +101 -94
  16. package/src/sources/dulo.js +134 -149
  17. package/src/sources/flaxmovies.js +114 -105
  18. package/src/sources/framextv.js +126 -0
  19. package/src/sources/frost.js +38 -29
  20. package/src/sources/fsonline.js +95 -47
  21. package/src/sources/hdhub.js +43 -36
  22. package/src/sources/hexa.js +60 -53
  23. package/src/sources/hyper.js +42 -35
  24. package/src/sources/kickassanime.js +117 -124
  25. package/src/sources/kisskh.js +114 -59
  26. package/src/sources/lmscript.js +72 -70
  27. package/src/sources/lookmovie.js +80 -79
  28. package/src/sources/meowtv.js +140 -0
  29. package/src/sources/movenerds.js +74 -124
  30. package/src/sources/movy.js +197 -209
  31. package/src/sources/net79.js +59 -55
  32. package/src/sources/peestream.js +126 -138
  33. package/src/sources/pengu.js +46 -49
  34. package/src/sources/pstream.js +73 -67
  35. package/src/sources/purstream.js +96 -104
  36. package/src/sources/rivestream.js +182 -176
  37. package/src/sources/rivo.js +58 -53
  38. package/src/sources/streamaggregator.js +114 -128
  39. package/src/sources/thugtv.js +70 -62
  40. package/src/sources/vidapi.js +122 -114
  41. package/src/sources/vidcore.js +14 -122
  42. package/src/sources/vidfast.js +14 -122
  43. package/src/sources/vidlink.js +78 -27
  44. package/src/sources/vidora.js +74 -42
  45. package/src/sources/vidup.js +14 -122
  46. package/src/sources/vidvault.js +133 -187
  47. package/src/sources/vidy.js +192 -187
  48. package/src/sources/vidzee.js +189 -183
  49. package/src/sources/vixsrc.js +123 -113
  50. package/src/sources/watchout.js +60 -46
  51. package/src/sources/xdownloader.js +127 -128
  52. package/src/sources/yapgrid.js +68 -62
  53. package/src/sources/yastream.js +35 -29
  54. package/src/subtitles.js +105 -122
  55. package/src/utils/helpers.js +586 -247
  56. package/src/sources/bcine.js +0 -202
  57. package/src/sources/cinezo.js +0 -55
  58. package/src/sources/vidrock.js +0 -145
@@ -0,0 +1,415 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
4
+ const CASTLE_BASE = 'https://api.hlowb.com';
5
+ const PKG = 'com.external.castle';
6
+ const CHANNEL = 'IndiaA';
7
+ const CLIENT = '1';
8
+ const LANG = 'en-US';
9
+ const SUFFIX = 'T!BgJB';
10
+ const APK_SIGN_KEY = 'ED0955EB04E67A1D9F3305B95454FED485261475';
11
+
12
+ const WORKING_HEADERS = {
13
+ 'User-Agent': 'okhttp/4.9.3',
14
+ Accept: 'application/json',
15
+ 'Accept-Language': 'en-US,en;q=0.9',
16
+ Connection: 'Keep-Alive',
17
+ Referer: CASTLE_BASE,
18
+ };
19
+
20
+ export const CASTLE_PLAYBACK_HEADERS = {
21
+ 'User-Agent':
22
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
23
+ Accept:
24
+ 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
25
+ 'Accept-Language': 'en-US,en;q=0.9',
26
+ 'Accept-Encoding': 'identity',
27
+ Connection: 'keep-alive',
28
+ 'Sec-Fetch-Dest': 'video',
29
+ 'Sec-Fetch-Mode': 'no-cors',
30
+ 'Sec-Fetch-Site': 'cross-site',
31
+ DNT: '1',
32
+ };
33
+
34
+ const QUALITY_MAP = { 1: '480p', 2: '720p', 3: '1080p' };
35
+
36
+ function safeJsonParse(text) {
37
+ const safe = text.replace(/:(\s*)(-?\d{16,})(\s*[,}\]])/g, ':$1"$2"$3');
38
+ return JSON.parse(safe);
39
+ }
40
+
41
+ class CastleScraper {
42
+ constructor(apiKey) {
43
+ this.tmdbKey =
44
+ apiKey ||
45
+ process.env.CASTLE_TMDB_API_KEY ||
46
+ process.env.TMDB_API_KEY ||
47
+ '439c478a771f35c05022f9feabcca01c';
48
+ this.securityKey = null;
49
+ }
50
+
51
+ deriveKey(apiKeyB64) {
52
+ const apiKeyBytes = Buffer.from(apiKeyB64, 'base64');
53
+ let keyMaterial = Buffer.concat([apiKeyBytes, Buffer.from(SUFFIX, 'ascii')]);
54
+ if (keyMaterial.length < 16) {
55
+ keyMaterial = Buffer.concat([
56
+ keyMaterial,
57
+ Buffer.alloc(16 - keyMaterial.length),
58
+ ]);
59
+ } else if (keyMaterial.length > 16) {
60
+ keyMaterial = keyMaterial.subarray(0, 16);
61
+ }
62
+ return keyMaterial;
63
+ }
64
+
65
+ decryptCastle(encryptedB64, securityKeyB64) {
66
+ const aesKey = this.deriveKey(securityKeyB64);
67
+ const iv = aesKey;
68
+ const encrypted = Buffer.from(encryptedB64, 'base64');
69
+ const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
70
+ decipher.setAutoPadding(true);
71
+ return Buffer.concat([
72
+ decipher.update(encrypted),
73
+ decipher.final(),
74
+ ]).toString('utf-8');
75
+ }
76
+
77
+ async request(url, init) {
78
+ const r = await fetch(url, {
79
+ ...init,
80
+ headers: { ...WORKING_HEADERS, ...(init?.headers || {}) },
81
+ });
82
+ if (!r.ok) throw new Error(`Castle request failed ${r.status} for ${url}`);
83
+ return r;
84
+ }
85
+
86
+ async extractCipher(response) {
87
+ const text = (await response.text()).trim();
88
+ if (!text) throw new Error('Empty response');
89
+ try {
90
+ const j = JSON.parse(text);
91
+ if (j && typeof j.data === 'string') return j.data.trim();
92
+ } catch { }
93
+ return text;
94
+ }
95
+
96
+ async decryptedJson(response, securityKey) {
97
+ const cipher = await this.extractCipher(response);
98
+ return safeJsonParse(this.decryptCastle(cipher, securityKey));
99
+ }
100
+
101
+ async getSecurityKey() {
102
+ if (this.securityKey) return this.securityKey;
103
+ const url = `${CASTLE_BASE}/v0.1/system/getSecurityKey/1?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}`;
104
+ const r = await this.request(url);
105
+ const data = await r.json();
106
+ if (data.code !== 200 || !data.data) {
107
+ throw new Error(`Security key API error: ${JSON.stringify(data)}`);
108
+ }
109
+ this.securityKey = data.data;
110
+ return this.securityKey;
111
+ }
112
+
113
+ async searchCastle(securityKey, keyword, page = 1, size = 30) {
114
+ const params = new URLSearchParams({
115
+ channel: CHANNEL,
116
+ clientType: CLIENT,
117
+ keyword,
118
+ lang: LANG,
119
+ mode: '1',
120
+ packageName: PKG,
121
+ page: String(page),
122
+ size: String(size),
123
+ });
124
+ const url = `${CASTLE_BASE}/film-api/v1.1.0/movie/searchByKeyword?${params}`;
125
+ return this.decryptedJson(await this.request(url), securityKey);
126
+ }
127
+
128
+ async getDetails(securityKey, movieId) {
129
+ const url = `${CASTLE_BASE}/film-api/v1.1/movie?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}&movieId=${movieId}&packageName=${PKG}`;
130
+ return this.decryptedJson(await this.request(url), securityKey);
131
+ }
132
+
133
+ async getVideo2(securityKey, movieId, episodeId, resolution = 2) {
134
+ const url = `${CASTLE_BASE}/film-api/v2.0.1/movie/getVideo2?clientType=${CLIENT}&packageName=${PKG}&channel=${CHANNEL}&lang=${LANG}`;
135
+ const body = {
136
+ mode: '1',
137
+ appMarket: 'GuanWang',
138
+ clientType: '1',
139
+ woolUser: 'false',
140
+ apkSignKey: APK_SIGN_KEY,
141
+ androidVersion: '13',
142
+ movieId: String(movieId),
143
+ episodeId: String(episodeId),
144
+ isNewUser: 'true',
145
+ resolution: String(resolution),
146
+ packageName: PKG,
147
+ };
148
+ const r = await this.request(url, {
149
+ method: 'POST',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify(body),
152
+ });
153
+ return this.decryptedJson(r, securityKey);
154
+ }
155
+
156
+ async getVideoV1(securityKey, movieId, episodeId, languageId, resolution = 2) {
157
+ const params = new URLSearchParams({
158
+ apkSignKey: APK_SIGN_KEY,
159
+ channel: CHANNEL,
160
+ clientType: CLIENT,
161
+ episodeId: String(episodeId),
162
+ lang: LANG,
163
+ languageId: String(languageId),
164
+ mode: '1',
165
+ movieId: String(movieId),
166
+ packageName: PKG,
167
+ resolution: String(resolution),
168
+ });
169
+ const url = `${CASTLE_BASE}/film-api/v1.9.1/movie/getVideo?${params}`;
170
+ return this.decryptedJson(await this.request(url), securityKey);
171
+ }
172
+
173
+ extractDataBlock(obj) {
174
+ if (obj && typeof obj.data === 'object' && obj.data) return obj.data;
175
+ return obj && typeof obj === 'object' ? obj : {};
176
+ }
177
+
178
+ qualityValue(quality) {
179
+ if (!quality) return 0;
180
+ let q = String(quality).toLowerCase();
181
+ q = q.replace(/^(sd|hd|fhd|uhd|4k)\s*/, '').replace(/p$/, '').trim();
182
+ if (q === '4k' || q === '2160') return 2160;
183
+ const known = [1440, 1080, 720, 480, 360, 240];
184
+ const n = parseInt(q, 10);
185
+ if (known.includes(n)) return n;
186
+ return n > 0 ? n : 0;
187
+ }
188
+
189
+ formatSize(size) {
190
+ if (typeof size === 'number' && size > 0) {
191
+ return size > 1_000_000_000
192
+ ? `${(size / 1_000_000_000).toFixed(2)} GB`
193
+ : `${Math.round(size / 1_000_000)} MB`;
194
+ }
195
+ return 'Unknown';
196
+ }
197
+
198
+ async getTmdbDetails(tmdbId, mediaType) {
199
+ const endpoint = mediaType === 'tv' ? 'tv' : 'movie';
200
+ const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${this.tmdbKey}&append_to_response=external_ids`;
201
+ const r = await this.request(url);
202
+ const data = await r.json();
203
+ const title = mediaType === 'tv' ? data.name : data.title;
204
+ const releaseDate =
205
+ mediaType === 'tv' ? data.first_air_date : data.release_date;
206
+ const year = releaseDate ? parseInt(String(releaseDate).split('-')[0], 10) : null;
207
+ return { title, year, tmdbId };
208
+ }
209
+
210
+ findMovieId(searchResult, tmdbInfo) {
211
+ const data = this.extractDataBlock(searchResult);
212
+ const rows = data.rows || [];
213
+ if (!rows.length) throw new Error('No search results found');
214
+ const searchTitle = (tmdbInfo.title || '').toLowerCase();
215
+
216
+ for (const item of rows) {
217
+ const itemTitle = String(item.title || item.name || '').toLowerCase();
218
+ if (
219
+ itemTitle &&
220
+ (itemTitle.includes(searchTitle) || searchTitle.includes(itemTitle))
221
+ ) {
222
+ const id = item.id || item.redirectId || item.redirectIdStr;
223
+ if (id) return String(id);
224
+ }
225
+ }
226
+ const first = rows[0];
227
+ const id = first.id || first.redirectId || first.redirectIdStr;
228
+ if (id) return String(id);
229
+ throw new Error('Could not extract movie ID from search results');
230
+ }
231
+
232
+ processVideoResponse(videoData, media, season, episode, resolution, languageInfo) {
233
+ const streams = [];
234
+ const data = this.extractDataBlock(videoData);
235
+ const videoUrl = data.videoUrl;
236
+ if (!videoUrl) return streams;
237
+
238
+ let mediaTitle = media.title || 'Unknown';
239
+ if (media.year) mediaTitle += ` (${media.year})`;
240
+ if (season && episode) {
241
+ mediaTitle = `${media.title} S${String(season).padStart(2, '0')}E${String(
242
+ episode
243
+ ).padStart(2, '0')}`;
244
+ }
245
+
246
+ const quality = QUALITY_MAP[resolution] || `${resolution}p`;
247
+
248
+ if (Array.isArray(data.videos)) {
249
+ for (const video of data.videos) {
250
+ let q = String(
251
+ video.resolutionDescription || video.resolution || quality
252
+ ).replace(/^(SD|HD|FHD)\s+/i, '');
253
+ const name = languageInfo
254
+ ? `Castle ${languageInfo} - ${q}`
255
+ : `Castle - ${q}`;
256
+ streams.push({
257
+ name,
258
+ title: mediaTitle,
259
+ url: video.url || videoUrl,
260
+ quality: q,
261
+ size: this.formatSize(video.size),
262
+ provider: 'castle',
263
+ });
264
+ }
265
+ } else {
266
+ const name = languageInfo
267
+ ? `Castle ${languageInfo} - ${quality}`
268
+ : `Castle - ${quality}`;
269
+ streams.push({
270
+ name,
271
+ title: mediaTitle,
272
+ url: videoUrl,
273
+ quality,
274
+ size: this.formatSize(data.size),
275
+ provider: 'castle',
276
+ });
277
+ }
278
+ return streams;
279
+ }
280
+
281
+ async fetchSources(tmdbId, season = 0, episode = 0) {
282
+ const mediaType = season && episode ? 'tv' : 'movie';
283
+ try {
284
+ const tmdbInfo = await this.getTmdbDetails(tmdbId, mediaType);
285
+ const securityKey = await this.getSecurityKey();
286
+
287
+ const searchTerm = tmdbInfo.year
288
+ ? `${tmdbInfo.title} ${tmdbInfo.year}`
289
+ : tmdbInfo.title;
290
+ const searchResult = await this.searchCastle(securityKey, searchTerm);
291
+ let movieId = this.findMovieId(searchResult, tmdbInfo);
292
+
293
+ let details = await this.getDetails(securityKey, movieId);
294
+
295
+ if (mediaType === 'tv') {
296
+ const data = this.extractDataBlock(details);
297
+ const seasons = data.seasons || [];
298
+ const s = seasons.find((x) => x.number === season);
299
+ if (s && s.movieId && String(s.movieId) !== movieId) {
300
+ movieId = String(s.movieId);
301
+ details = await this.getDetails(securityKey, movieId);
302
+ }
303
+ }
304
+
305
+ const data = this.extractDataBlock(details);
306
+ const episodes = data.episodes || [];
307
+
308
+ let episodeId = null;
309
+ if (mediaType === 'tv') {
310
+ const ep = episodes.find((e) => e.number === episode);
311
+ if (ep && ep.id) episodeId = String(ep.id);
312
+ } else if (episodes.length) {
313
+ episodeId = String(episodes[0].id);
314
+ }
315
+ if (!episodeId) throw new Error('Could not find episode ID');
316
+
317
+ const epObj = episodes.find((e) => String(e.id) === episodeId);
318
+ const tracks = epObj?.tracks || [];
319
+
320
+ const resolution = 2;
321
+ let allStreams = [];
322
+
323
+ for (let i = 0; i < tracks.length; i++) {
324
+ const track = tracks[i];
325
+ const langName =
326
+ track.languageName || track.abbreviate || `Lang${i + 1}`;
327
+ if (track.existIndividualVideo && track.languageId) {
328
+ try {
329
+ const videoData = await this.getVideoV1(
330
+ securityKey,
331
+ movieId,
332
+ episodeId,
333
+ String(track.languageId),
334
+ resolution
335
+ );
336
+ const langStreams = this.processVideoResponse(
337
+ videoData,
338
+ tmdbInfo,
339
+ season,
340
+ episode,
341
+ resolution,
342
+ `[${langName}]`
343
+ );
344
+ allStreams.push(...langStreams);
345
+ } catch (e) {
346
+ console.error(`[Castle] ${langName}: v1 failed - ${e?.message || e}`);
347
+ }
348
+ }
349
+ }
350
+
351
+ if (!allStreams.length) {
352
+ const videoData = await this.getVideo2(
353
+ securityKey,
354
+ movieId,
355
+ episodeId,
356
+ resolution
357
+ );
358
+ allStreams = this.processVideoResponse(
359
+ videoData,
360
+ tmdbInfo,
361
+ season,
362
+ episode,
363
+ resolution,
364
+ '[Shared]'
365
+ );
366
+ }
367
+
368
+ allStreams.sort(
369
+ (a, b) => this.qualityValue(b.quality) - this.qualityValue(a.quality)
370
+ );
371
+ return allStreams;
372
+ } catch (e) {
373
+ console.error(`[Castle] Error: ${e?.message || e}`);
374
+ return [];
375
+ }
376
+ }
377
+ }
378
+
379
+ export async function getStream(args) {
380
+ const { id, s, e, sdk } = args || {};
381
+ const scraper = new CastleScraper(sdk?.tmdbApiKey);
382
+ const tmdbId = parseInt(String(id), 10);
383
+ const season = s != null ? parseInt(String(s), 10) : 0;
384
+ const episode = e != null ? parseInt(String(e), 10) : 0;
385
+
386
+ const streams = await scraper.fetchSources(tmdbId, season, episode).catch(() => []);
387
+ if (!streams || !streams.length) return null;
388
+
389
+ const seen = new Set();
390
+ const allUrls = [];
391
+
392
+ for (const item of streams) {
393
+ if (!item?.url || seen.has(item.url)) continue;
394
+ seen.add(item.url);
395
+ allUrls.push({
396
+ url: item.url,
397
+ server: item.name || 'Castle',
398
+ quality: item.quality || 'Auto',
399
+ type: item.url.includes('.m3u8')
400
+ ? 'hls'
401
+ : item.url.includes('.mpd')
402
+ ? 'dash'
403
+ : 'mp4',
404
+ headers: CASTLE_PLAYBACK_HEADERS,
405
+ });
406
+ }
407
+
408
+ if (!allUrls.length) return null;
409
+
410
+ return {
411
+ url: allUrls[0].url,
412
+ headers: allUrls[0].headers,
413
+ allUrls,
414
+ };
415
+ }
@@ -1,19 +1,38 @@
1
- import { fetchJson } from '../utils/helpers.js';
2
-
3
- export async function getStream({ id, s, e }) {
4
- const isTv = s != null && e != null;
5
- const url = isTv
6
- ? `https://zcdn.bluevelvet.space/cdn?tmdb=${id}&type=tv&season=${s}&episode=${e}`
7
- : `https://zcdn.bluevelvet.space/cdn?tmdb=${id}&type=movie`;
8
-
9
- const res = await fetchJson(url);
10
- if (!res || !res.ready || !res.url) return null;
11
-
12
- return {
13
- url: res.url,
14
- headers: {
15
- Referer: 'https://zcdn.bluevelvet.space/',
16
- Origin: 'https://zcdn.bluevelvet.space',
17
- }
18
- };
1
+ import { fetchJson } from '../utils/helpers.js';
2
+
3
+ const BASE_URL = 'https://zcdn.bluevelvet.space';
4
+
5
+ export async function getStream(args) {
6
+ const { id, s, e } = args || {};
7
+ const isTv = s != null && e != null;
8
+
9
+ const url = isTv
10
+ ? `${BASE_URL}/cdn?tmdb=${id}&type=tv&season=${s}&episode=${e}`
11
+ : `${BASE_URL}/cdn?tmdb=${id}&type=movie`;
12
+
13
+ try {
14
+ const res = await fetchJson(url);
15
+ if (!res || !res.ready || !res.url) return null;
16
+
17
+ const headers = {
18
+ Referer: `${BASE_URL}/`,
19
+ Origin: BASE_URL,
20
+ };
21
+
22
+ const allUrls = [{
23
+ url: res.url,
24
+ server: 'CineCat',
25
+ quality: 'Auto',
26
+ type: res.url.includes('.m3u8') ? 'hls' : 'mp4',
27
+ headers,
28
+ }];
29
+
30
+ return {
31
+ url: res.url,
32
+ headers,
33
+ allUrls,
34
+ };
35
+ } catch {
36
+ return null;
37
+ }
19
38
  }