@vyla-entertainment/sdk 1.21.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vyla-entertainment/sdk",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "scripts": {
5
5
  "test": "node test/index.js",
6
6
  "test:all": "node test/index.js all"
package/src/config.js CHANGED
@@ -18,6 +18,25 @@ export const SOURCES = [
18
18
  }],
19
19
  },
20
20
 
21
+ {
22
+ key: '4movies',
23
+ label: '4Movies',
24
+ sourceFile: '4movies',
25
+ proxyParam: '4m',
26
+ timeout: 25000,
27
+ jitter: 500,
28
+ retries: 2,
29
+ multiUrl: true,
30
+ skipProxy: true,
31
+ cdnHeaders: [{
32
+ pattern: /4movie\.in|aapanel\.devcorp\.me/i,
33
+ headers: {
34
+ Referer: 'https://4movie.in/',
35
+ Origin: 'https://4movie.in',
36
+ },
37
+ }],
38
+ },
39
+
21
40
  {
22
41
  key: '123anime',
23
42
  label: '123Anime',
@@ -56,6 +75,37 @@ export const SOURCES = [
56
75
  }],
57
76
  },
58
77
 
78
+ {
79
+ key: 'aether',
80
+ label: 'Aether',
81
+ sourceFile: 'aether',
82
+ proxyParam: 'aet',
83
+ timeout: 25000,
84
+ jitter: 500,
85
+ retries: 2,
86
+ multiUrl: true,
87
+ skipProxy: true,
88
+ cdnHeaders: [{
89
+ pattern: /raccoonunbutteredbagel\.zip|1x2\.space|kaxate\.fun|mechanicalserenity\.xyz|reelvault\.click|visionaryfounderslab\.site|goldenkite\.top|aether\.ist/i,
90
+ headers: {
91
+ Referer: 'https://aether.st/',
92
+ Origin: 'https://aether.st',
93
+ },
94
+ }],
95
+ },
96
+
97
+ {
98
+ key: 'castle',
99
+ label: 'Castle',
100
+ sourceFile: 'castle',
101
+ proxyParam: 'cst',
102
+ timeout: 25000,
103
+ jitter: 500,
104
+ retries: 2,
105
+ multiUrl: true,
106
+ skipProxy: true,
107
+ },
108
+
59
109
  {
60
110
  key: 'cinemaarmy',
61
111
  label: 'CinemaArmy',
@@ -151,6 +201,25 @@ export const SOURCES = [
151
201
  }],
152
202
  },
153
203
 
204
+ {
205
+ key: 'framextv',
206
+ label: 'FrameXTV',
207
+ sourceFile: 'framextv',
208
+ proxyParam: 'fxt',
209
+ timeout: 25000,
210
+ jitter: 500,
211
+ retries: 2,
212
+ multiUrl: true,
213
+ skipProxy: true,
214
+ cdnHeaders: [{
215
+ pattern: /framextv\.cc|streamos\.top|workers\.dev|films365\.org/i,
216
+ headers: {
217
+ Referer: 'https://framextv.cc/',
218
+ Origin: 'https://framextv.cc',
219
+ },
220
+ }],
221
+ },
222
+
154
223
  {
155
224
  key: 'frost',
156
225
  label: 'FrostStream',
@@ -278,6 +347,25 @@ export const SOURCES = [
278
347
  },
279
348
  },
280
349
 
350
+ {
351
+ key: 'meowtv',
352
+ label: 'MeowTV',
353
+ sourceFile: 'meowtv',
354
+ proxyParam: 'mtv',
355
+ timeout: 25000,
356
+ jitter: 500,
357
+ retries: 2,
358
+ multiUrl: true,
359
+ skipProxy: true,
360
+ cdnHeaders: [{
361
+ pattern: /meowtv\.ru/i,
362
+ headers: {
363
+ Referer: 'https://meowtv.ru/',
364
+ Origin: 'https://meowtv.ru',
365
+ },
366
+ }],
367
+ },
368
+
281
369
  {
282
370
  key: 'movenerds',
283
371
  label: 'MoverNerds',
@@ -481,6 +569,25 @@ export const SOURCES = [
481
569
  multiUrl: true,
482
570
  },
483
571
 
572
+ {
573
+ key: 'vidora',
574
+ label: 'Vidora',
575
+ sourceFile: 'vidora',
576
+ proxyParam: 'vd',
577
+ timeout: 25000,
578
+ jitter: 500,
579
+ retries: 2,
580
+ multiUrl: true,
581
+ skipProxy: true,
582
+ cdnHeaders: [{
583
+ pattern: /vidoraplayer\.duckdns\.org/i,
584
+ headers: {
585
+ Referer: 'https://vidoraplayer.duckdns.org/',
586
+ Origin: 'https://vidoraplayer.duckdns.org',
587
+ },
588
+ }],
589
+ },
590
+
484
591
  {
485
592
  key: 'vidup',
486
593
  label: 'VidUp',
package/src/health.js CHANGED
@@ -1,6 +1,15 @@
1
- import { HEALTH_PROBE_ID } from "./config.js";
2
1
 
3
- const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
2
+ const MOVIE_PROBE_ID = "936075";
3
+ const TV_PROBE_ID = "37854";
4
+ const TV_PROBE_SEASON = "1";
5
+ const TV_PROBE_EPISODE = "1";
6
+
7
+ const withTimeout = (promise, ms) =>
8
+ Promise.race([
9
+ promise,
10
+ new Promise(resolve => setTimeout(() => resolve(null), ms))
11
+ ]);
12
+
4
13
  async function withRetry(fn, attempts = 2, delay = 300) {
5
14
  for (let i = 0; i < attempts; i++) {
6
15
  try {
@@ -8,31 +17,62 @@ async function withRetry(fn, attempts = 2, delay = 300) {
8
17
  if (result != null) return result;
9
18
  } catch (err) {
10
19
  if (i === attempts - 1) throw err;
11
- await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5));
20
+ await new Promise(resolve =>
21
+ setTimeout(resolve, delay + Math.random() * delay * 0.5)
22
+ );
12
23
  }
13
24
  }
14
25
  return null;
15
26
  }
16
27
 
28
+ function hasStream(result) {
29
+ const urls = [
30
+ result?.url,
31
+ ...(result?.allUrls || []).map(item => item?.url)
32
+ ];
33
+
34
+ return urls.some(url =>
35
+ typeof url === "string" && url.startsWith("http")
36
+ );
37
+ }
38
+
17
39
  export async function probeSource(sdk, cfg, mod) {
18
- const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
19
- const streamArgs = {
20
- id: HEALTH_PROBE_ID,
21
- s: null,
22
- e: null,
23
- clientIP: null,
24
- absoluteBase: null,
25
- audio,
26
- sdk,
27
- config: cfg,
40
+ const audio = /dub$/.test(cfg.key) ? "dub" : "sub";
41
+ const start = Date.now();
42
+
43
+ const test = async (id, s = null, e = null) => {
44
+ const streamArgs = {
45
+ id,
46
+ s,
47
+ e,
48
+ clientIP: null,
49
+ absoluteBase: null,
50
+ audio,
51
+ sdk,
52
+ config: cfg,
53
+ };
54
+
55
+ const result = await withTimeout(
56
+ withRetry(
57
+ () => mod.getStream(streamArgs),
58
+ cfg.retries,
59
+ 300
60
+ ),
61
+ cfg.timeout
62
+ );
63
+
64
+ return hasStream(result);
28
65
  };
29
66
 
30
- const t = Date.now();
31
- const result = await withTimeout(
32
- withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
33
- cfg.timeout
34
- );
67
+ const [movie, tv] = await Promise.all([
68
+ test(MOVIE_PROBE_ID),
69
+ test(TV_PROBE_ID, TV_PROBE_SEASON, TV_PROBE_EPISODE)
70
+ ]);
35
71
 
36
- const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
37
- return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
72
+ return {
73
+ ok: movie || tv,
74
+ movie,
75
+ tv,
76
+ ms: Date.now() - start
77
+ };
38
78
  }
@@ -0,0 +1,77 @@
1
+ const BASE_URL = 'https://4movie.in';
2
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
3
+
4
+ const HEADERS = {
5
+ 'User-Agent': UA,
6
+ Referer: `${BASE_URL}/`,
7
+ Origin: BASE_URL,
8
+ Accept: 'application/json, text/plain, */*',
9
+ 'Accept-Language': 'en-US,en;q=0.9',
10
+ };
11
+
12
+ const PLAYBACK_HEADERS = {
13
+ 'User-Agent': UA,
14
+ Referer: `${BASE_URL}/`,
15
+ Origin: BASE_URL,
16
+ };
17
+
18
+ export async function getStream(args) {
19
+ const { id, s, e } = args || {};
20
+ const isTv = s != null && e != null;
21
+
22
+ let query = isTv
23
+ ? `tmdb=${id}&type=tv&season=${s}&episode=${e}`
24
+ : `tmdb=${id}&type=movie`;
25
+
26
+ if (args?.title) query += `&title=${encodeURIComponent(args.title)}`;
27
+ if (args?.year) query += `&year=${encodeURIComponent(args.year)}`;
28
+
29
+ const targetUrl = `${BASE_URL}/api/hubcloud?${query}`;
30
+
31
+ const res = await fetch(targetUrl, { headers: HEADERS }).catch(() => null);
32
+ if (!res || !res.ok) return null;
33
+
34
+ const data = await res.json().catch(() => null);
35
+ if (!data?.ok || !Array.isArray(data.streams) || !data.streams.length) return null;
36
+
37
+ const allUrls = [];
38
+ const seen = new Set();
39
+
40
+ for (const stream of data.streams) {
41
+ if (stream.rawUrl && !seen.has(stream.rawUrl)) {
42
+ seen.add(stream.rawUrl);
43
+ allUrls.push({
44
+ url: stream.rawUrl,
45
+ server: `4Movies - ${stream.name || 'Direct'}`,
46
+ quality: stream.quality || 'Auto',
47
+ type: stream.type || (stream.rawUrl.includes('.m3u8') ? 'hls' : 'mp4'),
48
+ headers: PLAYBACK_HEADERS,
49
+ });
50
+ }
51
+
52
+ if (stream.url) {
53
+ const proxyUrl = stream.url.startsWith('http')
54
+ ? stream.url
55
+ : `${BASE_URL}${stream.url.startsWith('/') ? '' : '/'}${stream.url}`;
56
+
57
+ if (!seen.has(proxyUrl)) {
58
+ seen.add(proxyUrl);
59
+ allUrls.push({
60
+ url: proxyUrl,
61
+ server: `4Movies - ${stream.name ? `${stream.name} (Proxy)` : 'Proxy'}`,
62
+ quality: stream.quality || 'Auto',
63
+ type: stream.type || 'hls',
64
+ headers: PLAYBACK_HEADERS,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ if (!allUrls.length) return null;
71
+
72
+ return {
73
+ url: allUrls[0].url,
74
+ headers: allUrls[0].headers,
75
+ allUrls,
76
+ };
77
+ }
@@ -0,0 +1,99 @@
1
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
2
+
3
+ const HEADERS = {
4
+ 'User-Agent': UA,
5
+ Origin: 'https://aether.st',
6
+ Referer: 'https://aether.st/',
7
+ Accept: 'application/json, text/plain, */*',
8
+ 'Accept-Language': 'en-US,en;q=0.9',
9
+ };
10
+
11
+ const PLAYBACK_HEADERS = {
12
+ 'User-Agent': UA,
13
+ Referer: 'https://aether.st/',
14
+ Origin: 'https://aether.st',
15
+ };
16
+
17
+ function extractStreamUrls(data, target) {
18
+ const urls = [];
19
+ if (!data) return urls;
20
+
21
+ if (Array.isArray(data.streams)) {
22
+ for (const s of data.streams) {
23
+ if (s?.url) {
24
+ urls.push({
25
+ url: s.url,
26
+ name: `${target.name} (${s.name || s.language || 'Default'})`,
27
+ quality: s.quality || 'Auto',
28
+ type: s.type || (s.url.includes('.m3u8') ? 'hls' : 'mp4'),
29
+ });
30
+ }
31
+ }
32
+ }
33
+
34
+ const singleUrl = data.directUrl || data.url || data.stream;
35
+ if (typeof singleUrl === 'string' && /^https?:\/\//.test(singleUrl)) {
36
+ urls.push({
37
+ url: singleUrl,
38
+ name: `${target.name}${data.server ? ` - ${data.server}` : ''}`,
39
+ quality: data.quality || 'Auto',
40
+ type: data.streamType || data.container || (singleUrl.includes('.m3u8') ? 'hls' : 'mp4'),
41
+ });
42
+ }
43
+
44
+ return urls;
45
+ }
46
+
47
+ export async function getStream(args) {
48
+ const { id, s, e } = args || {};
49
+ const isTv = s != null && e != null;
50
+
51
+ const endpoints = [
52
+ { name: 'Meridian', url: isTv ? `https://meridian.raccoonunbutteredbagel.zip/show/${id}/${s}/${e}` : `https://meridian.raccoonunbutteredbagel.zip/movie/${id}` },
53
+ { name: 'Rock', url: isTv ? `https://rock.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://rock.raccoonunbutteredbagel.zip/movie/${id}` },
54
+ { name: 'Shore', url: isTv ? `https://shore.raccoonunbutteredbagel.zip/show/${id}?s=${s}&e=${e}` : `https://shore.raccoonunbutteredbagel.zip/movie/${id}` },
55
+ { name: 'Filler', url: isTv ? `https://filler.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://filler.raccoonunbutteredbagel.zip/movie/${id}` },
56
+ { name: 'Tiki', url: isTv ? `https://tiki.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://tiki.raccoonunbutteredbagel.zip/movie/${id}` },
57
+ { name: 'Link', url: isTv ? `https://link.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://link.raccoonunbutteredbagel.zip/movie/${id}` },
58
+ { name: 'VFast Rapid', url: isTv ? `https://vfast.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}?server=vRapid` : `https://vfast.raccoonunbutteredbagel.zip/movie/${id}?server=vRapid` },
59
+ { name: 'VFast Blaze', url: isTv ? `https://vfast.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}?server=vBlaze` : `https://vfast.raccoonunbutteredbagel.zip/movie/${id}?server=vBlaze` },
60
+ ];
61
+
62
+ const responses = await Promise.allSettled(
63
+ endpoints.map(async target => {
64
+ const res = await fetch(target.url, { headers: HEADERS });
65
+ if (!res.ok) return null;
66
+ const data = await res.json();
67
+ return { target, data };
68
+ })
69
+ );
70
+
71
+ const allUrls = [];
72
+ const seen = new Set();
73
+
74
+ for (const res of responses) {
75
+ if (res.status !== 'fulfilled' || !res.value?.data) continue;
76
+ const { target, data } = res.value;
77
+ const found = extractStreamUrls(data, target);
78
+
79
+ for (const item of found) {
80
+ if (!item?.url || seen.has(item.url)) continue;
81
+ seen.add(item.url);
82
+ allUrls.push({
83
+ url: item.url,
84
+ server: `Aether - ${item.name}`,
85
+ quality: item.quality || 'Auto',
86
+ type: item.type || (item.url.includes('.m3u8') ? 'hls' : 'mp4'),
87
+ headers: PLAYBACK_HEADERS,
88
+ });
89
+ }
90
+ }
91
+
92
+ if (!allUrls.length) return null;
93
+
94
+ return {
95
+ url: allUrls[0].url,
96
+ headers: allUrls[0].headers,
97
+ allUrls,
98
+ };
99
+ }
@@ -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
+ }
@@ -0,0 +1,126 @@
1
+ const FRAMEX_BASE = 'https://api.framextv.cc';
2
+ const STREAMOS_BASE = 'https://api.streamos.top';
3
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
4
+
5
+ const FRAMEX_HEADERS = {
6
+ 'User-Agent': UA,
7
+ Referer: 'https://framextv.cc/',
8
+ Origin: 'https://framextv.cc',
9
+ Accept: 'application/json, text/plain, */*',
10
+ 'Accept-Language': 'en-US,en;q=0.9',
11
+ };
12
+
13
+ const STREAMOS_HEADERS = {
14
+ 'User-Agent': UA,
15
+ Referer: 'https://streamos.top/',
16
+ Origin: 'https://streamos.top',
17
+ };
18
+
19
+ const STREAMOS_SERVERS = [
20
+ 'auto',
21
+ 'miami',
22
+ 'boise',
23
+ 'vegas',
24
+ 'reno',
25
+ 'oakland',
26
+ 'seattle',
27
+ 'phoenix',
28
+ 'atlanta',
29
+ 'portland',
30
+ 'austin',
31
+ 'dallas',
32
+ 'tampa',
33
+ 'orlando',
34
+ 'munich',
35
+ 'berlin',
36
+ 'france',
37
+ 'india',
38
+ 'mexico',
39
+ 'delhi',
40
+ ];
41
+
42
+ export async function getStream(args) {
43
+ const { id, s, e } = args || {};
44
+ const isTv = s != null && e != null;
45
+
46
+ const framexQuery = isTv
47
+ ? `type=tv&id=${id}&season=${s}&episode=${e}&codecs=hevc`
48
+ : `type=movie&id=${id}&codecs=hevc`;
49
+
50
+ const framexUrls = [
51
+ `${FRAMEX_BASE}/api/stream?${framexQuery}`,
52
+ `${FRAMEX_BASE}/api/stream?${framexQuery}&provider=sparky`,
53
+ `${FRAMEX_BASE}/api/stream?${framexQuery}&provider=titan`,
54
+ ];
55
+
56
+ const streamOsBaseQuery = isTv
57
+ ? `id=${id}&season=${s}&episode=${e}&kind=tv`
58
+ : `id=${id}&kind=movie`;
59
+
60
+ const allUrls = [];
61
+ const seen = new Set();
62
+
63
+ const framexPromises = Promise.all(
64
+ framexUrls.map(url =>
65
+ fetch(url, { headers: FRAMEX_HEADERS })
66
+ .then(r => (r.ok ? r.json() : null))
67
+ .catch(() => null)
68
+ )
69
+ );
70
+
71
+ const streamOsChecks = Promise.all(
72
+ STREAMOS_SERVERS.slice(0, 8).map(async server => {
73
+ const serverParam = server === 'auto' ? '' : `&server=${server}`;
74
+ const url = `${STREAMOS_BASE}/api/relay/hls?${streamOsBaseQuery}${serverParam}`;
75
+ try {
76
+ const res = await fetch(url, { method: 'HEAD', headers: STREAMOS_HEADERS });
77
+ if (res.ok) {
78
+ return {
79
+ url,
80
+ server: `StreamOS - ${server.toUpperCase()}`,
81
+ quality: '1080p',
82
+ type: 'hls',
83
+ headers: STREAMOS_HEADERS,
84
+ };
85
+ }
86
+ } catch { }
87
+ return null;
88
+ })
89
+ );
90
+
91
+ const [framexResults, streamOsResults] = await Promise.all([framexPromises, streamOsChecks]);
92
+
93
+ for (const data of framexResults) {
94
+ if (!data?.success || !Array.isArray(data.sources)) continue;
95
+ for (const src of data.sources) {
96
+ if (!src?.url || seen.has(src.url)) continue;
97
+ seen.add(src.url);
98
+ allUrls.push({
99
+ url: src.url,
100
+ server: `FrameX - ${src.server || data.provider || 'Direct'}`,
101
+ quality: src.quality || 'Auto',
102
+ type: src.type || (src.url.includes('.m3u8') ? 'hls' : 'mp4'),
103
+ headers: {
104
+ 'User-Agent': UA,
105
+ Referer: 'https://framextv.cc/',
106
+ Origin: 'https://framextv.cc',
107
+ },
108
+ });
109
+ }
110
+ }
111
+
112
+ for (const stream of streamOsResults) {
113
+ if (stream && !seen.has(stream.url)) {
114
+ seen.add(stream.url);
115
+ allUrls.push(stream);
116
+ }
117
+ }
118
+
119
+ if (!allUrls.length) return null;
120
+
121
+ return {
122
+ url: allUrls[0].url,
123
+ headers: allUrls[0].headers,
124
+ allUrls,
125
+ };
126
+ }
@@ -0,0 +1,140 @@
1
+ const BASE_URL = 'https://api.meowtv.ru';
2
+ const DEC_API = 'https://enc-dec.app/api';
3
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
4
+
5
+ const HEADERS = {
6
+ 'User-Agent': UA,
7
+ Origin: 'https://meowtv.ru',
8
+ Referer: 'https://meowtv.ru/',
9
+ Accept: 'application/json, text/plain, */*',
10
+ 'Accept-Language': 'en-US,en;q=0.9',
11
+ };
12
+
13
+ const SERVERS = [
14
+ { id: 'acme', label: 'Acme' },
15
+ { id: 'dcloud', label: 'Dcloud' },
16
+ { id: 'tik', label: 'TCloud' },
17
+ { id: 'ipcloud', label: 'IPcloud' },
18
+ { id: 'turkce', label: 'Türkçe', movieOnly: true },
19
+ { id: 'hindi', label: 'Hindi' },
20
+ { id: 'english', label: 'English' },
21
+ ];
22
+
23
+ async function decryptMeow(data) {
24
+ try {
25
+ const res = await fetch(`${DEC_API}/dec-meowtv`, {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ 'User-Agent': UA,
30
+ },
31
+ body: JSON.stringify({ data }),
32
+ });
33
+ if (!res.ok) return null;
34
+ const json = await res.json();
35
+ return json?.status === 200 ? json.result : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function extractUrls(result) {
42
+ const urls = [];
43
+ if (!result) return urls;
44
+ if (typeof result === 'string' && /^https?:\/\//.test(result)) {
45
+ urls.push({ url: result, quality: 'Auto', type: result.includes('.m3u8') ? 'hls' : 'mp4' });
46
+ } else if (result?.url && typeof result.url === 'string') {
47
+ urls.push({
48
+ url: result.url,
49
+ quality: result.quality || 'Auto',
50
+ type: result.type || (result.url.includes('.m3u8') ? 'hls' : 'mp4'),
51
+ });
52
+ } else if (Array.isArray(result?.sources)) {
53
+ for (const s of result.sources) {
54
+ if (s?.url) {
55
+ urls.push({
56
+ url: s.url,
57
+ quality: s.quality || 'Auto',
58
+ type: s.type || (s.url.includes('.m3u8') ? 'hls' : 'mp4'),
59
+ });
60
+ }
61
+ }
62
+ } else if (result?.stream) {
63
+ const sUrl = typeof result.stream === 'string' ? result.stream : result.stream?.url;
64
+ if (sUrl) {
65
+ urls.push({
66
+ url: sUrl,
67
+ quality: result.stream?.quality || 'Auto',
68
+ type: sUrl.includes('.m3u8') ? 'hls' : 'mp4',
69
+ });
70
+ }
71
+ }
72
+ return urls;
73
+ }
74
+
75
+ export async function getStream(args) {
76
+ const { id, s, e } = args || {};
77
+ const isTv = s != null && e != null;
78
+
79
+ const ticketRes = await fetch(`${BASE_URL}/streams/ticket`, {
80
+ method: 'POST',
81
+ headers: HEADERS,
82
+ }).then(r => (r.ok ? r.json() : null)).catch(() => null);
83
+
84
+ const ticket = ticketRes?.ticket;
85
+ if (!ticket) return null;
86
+
87
+ const requestHeaders = {
88
+ ...HEADERS,
89
+ 'X-Stream-Ticket': ticket,
90
+ };
91
+
92
+ const targetServers = isTv ? SERVERS.filter(srv => !srv.movieOnly) : SERVERS;
93
+
94
+ const responses = await Promise.allSettled(
95
+ targetServers.map(async server => {
96
+ const url = isTv
97
+ ? `${BASE_URL}/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(server.id)}`
98
+ : `${BASE_URL}/streams/movie/${id}?s=${encodeURIComponent(server.id)}`;
99
+
100
+ const res = await fetch(url, { headers: requestHeaders });
101
+ if (!res.ok) return null;
102
+ const data = await res.json();
103
+ const decrypted = await decryptMeow(data);
104
+ return { server, decrypted };
105
+ })
106
+ );
107
+
108
+ const allUrls = [];
109
+ const seen = new Set();
110
+
111
+ for (const res of responses) {
112
+ if (res.status !== 'fulfilled' || !res.value?.decrypted) continue;
113
+ const { server, decrypted } = res.value;
114
+ const found = extractUrls(decrypted);
115
+
116
+ for (const item of found) {
117
+ if (!item?.url || seen.has(item.url)) continue;
118
+ seen.add(item.url);
119
+ allUrls.push({
120
+ url: item.url,
121
+ server: `MeowTV - ${server.label}`,
122
+ quality: item.quality || 'Auto',
123
+ type: item.type || (item.url.includes('.m3u8') ? 'hls' : 'mp4'),
124
+ headers: {
125
+ Referer: 'https://meowtv.ru/',
126
+ Origin: 'https://meowtv.ru',
127
+ 'User-Agent': UA,
128
+ },
129
+ });
130
+ }
131
+ }
132
+
133
+ if (!allUrls.length) return null;
134
+
135
+ return {
136
+ url: allUrls[0].url,
137
+ headers: allUrls[0].headers,
138
+ allUrls,
139
+ };
140
+ }
@@ -0,0 +1,94 @@
1
+ const BASE_URL = 'https://vidoraplayer.duckdns.org';
2
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
3
+
4
+ const HEADERS = {
5
+ 'User-Agent': UA,
6
+ Referer: `${BASE_URL}/`,
7
+ Origin: BASE_URL,
8
+ Accept: 'application/x-ndjson, text/plain, */*',
9
+ 'Accept-Language': 'en-US,en;q=0.9',
10
+ };
11
+
12
+ function parseNdjson(text) {
13
+ if (!text) return [];
14
+ const normalized = text.replace(/\}\s*\{/g, '}\n{');
15
+ const lines = normalized.split('\n');
16
+ const items = [];
17
+ for (const raw of lines) {
18
+ const line = raw.trim();
19
+ if (!line) continue;
20
+ try {
21
+ items.push(JSON.parse(line));
22
+ } catch { }
23
+ }
24
+ return items;
25
+ }
26
+
27
+ export async function getStream(args) {
28
+ const { id, s, e } = args || {};
29
+ const isTv = s != null && e != null;
30
+
31
+ const query = isTv
32
+ ? `type=tv&id=${id}&season=${s}&episode=${e}`
33
+ : `type=movie&id=${id}`;
34
+
35
+ const urls = [
36
+ `${BASE_URL}/api/sources?${query}`,
37
+ `${BASE_URL}/api/sources?${query}&gen=2`,
38
+ ];
39
+
40
+ const responses = await Promise.all(
41
+ urls.map(url =>
42
+ fetch(url, { headers: HEADERS })
43
+ .then(r => (r.ok ? r.text() : ''))
44
+ .catch(() => '')
45
+ )
46
+ );
47
+
48
+ const allUrls = [];
49
+ const seen = new Set();
50
+
51
+ for (const text of responses) {
52
+ const events = parseNdjson(text);
53
+ for (const item of events) {
54
+ const list = [];
55
+ if (item?.event === 'source' && item?.source) {
56
+ list.push(item.source);
57
+ } else if (item?.event === 'done' && Array.isArray(item?.sources)) {
58
+ list.push(...item.sources);
59
+ } else if (item?.url) {
60
+ list.push(item);
61
+ }
62
+
63
+ for (const src of list) {
64
+ if (!src?.url) continue;
65
+ const fullUrl = src.url.startsWith('http')
66
+ ? src.url
67
+ : `${BASE_URL}${src.url.startsWith('/') ? '' : '/'}${src.url}`;
68
+
69
+ if (!seen.has(fullUrl)) {
70
+ seen.add(fullUrl);
71
+ allUrls.push({
72
+ url: fullUrl,
73
+ server: `Vidora - ${src.label || src.server || src.providerGroup || 'Default'}`,
74
+ quality: src.quality || 'Auto',
75
+ type: src.type || (fullUrl.includes('.m3u8') ? 'hls' : 'mp4'),
76
+ headers: {
77
+ Referer: `${BASE_URL}/`,
78
+ Origin: BASE_URL,
79
+ 'User-Agent': UA,
80
+ },
81
+ });
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ if (!allUrls.length) return null;
88
+
89
+ return {
90
+ url: allUrls[0].url,
91
+ headers: allUrls[0].headers,
92
+ allUrls,
93
+ };
94
+ }