@vyla-entertainment/sdk 1.5.0 → 1.8.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.
@@ -13,7 +13,11 @@ const HEADERS = {
13
13
  function extract(regex, text) {
14
14
  const m = text?.match(regex);
15
15
  if (!m) return null;
16
- return m[1].replace(/\\/g, '');
16
+ let val = m[1];
17
+ val = val.replace(/\\u0026/gi, '&');
18
+ val = val.replace(/\\\//g, '/');
19
+ val = val.replace(/\\/g, '');
20
+ return val;
17
21
  }
18
22
 
19
23
  export async function getStream({ id, s, e, absoluteBase }) {
@@ -22,36 +26,40 @@ export async function getStream({ id, s, e, absoluteBase }) {
22
26
  ? `${BASE_URL}/api/tv/${id}/${s}/${e}`
23
27
  : `${BASE_URL}/api/movie/${id}`;
24
28
 
25
- const apiProxy = absoluteBase
29
+ const apiProxy = absoluteBase && absoluteBase !== '/'
26
30
  ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
27
31
  : apiUrl;
28
32
 
29
- const apiText = await fetchText(apiProxy).catch(() => null);
33
+ const apiText = await fetchText(apiProxy, { headers: HEADERS }).catch(() => null);
30
34
  const apiData = JSON.parse(apiText || '{}');
31
35
  if (!apiData.src) return null;
32
36
 
33
37
  const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`;
34
- const embedProxy = absoluteBase
38
+ const embedProxy = absoluteBase && absoluteBase !== '/'
35
39
  ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
36
40
  : embedUrl;
37
41
 
38
- const html = await fetchText(embedProxy).catch(() => null);
42
+ const html = await fetchText(embedProxy, { headers: HEADERS }).catch(() => null);
39
43
  if (!html) return null;
40
44
 
41
45
  const token = extract(/token["']\s*:\s*["']([^"']+)/, html);
42
46
  const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html);
43
47
  let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html);
48
+
44
49
  if (!token || !expires || !playlist) return null;
45
50
 
46
- playlist = playlist.replace(/\\/g, '');
51
+ if (playlist.startsWith('/')) {
52
+ playlist = `${BASE_URL}${playlist}`;
53
+ }
54
+
47
55
  const lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
48
56
  const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
49
57
 
50
- const masterProxy = absoluteBase
51
- ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
58
+ const masterProxy = absoluteBase && absoluteBase !== '/'
59
+ ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}`
52
60
  : rawMasterUrl;
53
61
 
54
- const playlistText = await fetchText(rawMasterUrl, { headers: HEADERS }).catch(() => null);
62
+ const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null);
55
63
  if (!playlistText?.includes('#EXTM3U')) return null;
56
64
 
57
65
  let finalUrl = null;
@@ -74,17 +82,23 @@ export async function getStream({ id, s, e, absoluteBase }) {
74
82
  if (!finalUrl) finalUrl = rawMasterUrl;
75
83
 
76
84
  if (!finalUrl.startsWith('http')) {
77
- const base = rawMasterUrl.split('/playlist/')[0];
78
- finalUrl = finalUrl.startsWith('/') ? `${base}${finalUrl}` : `${base}/${finalUrl}`;
85
+ const baseMatch = rawMasterUrl.match(/^(https?:\/\/[^\/]+)/);
86
+ const base = baseMatch ? baseMatch[1] : BASE_URL;
87
+ if (finalUrl.startsWith('/')) {
88
+ finalUrl = `${base}${finalUrl}`;
89
+ } else {
90
+ const pathParts = rawMasterUrl.split('/');
91
+ pathParts.pop();
92
+ finalUrl = `${pathParts.join('/')}/${finalUrl}`;
93
+ }
79
94
  }
80
95
 
81
- const alreadyProxied = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/api\?url=/.test(finalUrl);
82
-
83
96
  return {
84
97
  allUrls: [
85
98
  {
86
99
  url: finalUrl,
87
- skipProxy: alreadyProxied,
100
+ server: 'VixSrc',
101
+ skipProxy: true,
88
102
  headers: {
89
103
  ...HEADERS,
90
104
  'Referer': `${BASE_URL}/`
@@ -67,8 +67,7 @@ function cacheGet(cache, key) {
67
67
 
68
68
  function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
69
69
 
70
- export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=null) {
71
- const getAbsoluteBase = host => (host.startsWith('localhost') || host.startsWith('127.0.0.1')) ? `http://${host}` : `https://${host}`;
70
+ export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP = null) {
72
71
  const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
73
72
  return {
74
73
  id: id,
@@ -83,13 +82,25 @@ export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=nu
83
82
  }
84
83
 
85
84
  export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
86
- const key = `${tmdbId}-${mediaType}`;
85
+ let apiKey = tmdbApiKey;
86
+ let id = tmdbId;
87
+ let type = mediaType;
88
+ if (id === 'movie' || id === 'tv' || !id) {
89
+ if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') {
90
+ type = id || 'movie';
91
+ id = apiKey;
92
+ apiKey = process.env.TMDB_API_KEY;
93
+ }
94
+ }
95
+ if (!apiKey) {
96
+ apiKey = process.env.TMDB_API_KEY;
97
+ }
98
+ const key = `${id}-${type}`;
87
99
  const cached = cacheGet(tmdbValidationCache, key);
88
100
  if (cached !== undefined) return cached;
89
- const k = tmdbApiKey;
90
- if (!k) return { valid: false, error: 'TMDB API key not configured' };
101
+ if (!apiKey) return { valid: false, error: 'TMDB API key not configured' };
91
102
  try {
92
- const res = await fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}`, { signal: AbortSignal.timeout(5000) });
103
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
93
104
  if (res.ok) {
94
105
  const data = await res.json();
95
106
  const isValid = !!(data?.id && (data?.title || data?.name));
@@ -108,15 +119,30 @@ export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
108
119
  }
109
120
 
110
121
  export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
111
- const key = `${tmdbId}-${mediaType}-${season || ''}`;
122
+ let apiKey = tmdbApiKey;
123
+ let id = tmdbId;
124
+ let type = mediaType;
125
+ let s = season;
126
+ if (apiKey === 'movie' || apiKey === 'tv') {
127
+ type = apiKey;
128
+ id = tmdbId;
129
+ apiKey = process.env.TMDB_API_KEY;
130
+ } else if (id === 'movie' || id === 'tv') {
131
+ type = id;
132
+ id = apiKey;
133
+ apiKey = process.env.TMDB_API_KEY;
134
+ }
135
+ if (!apiKey) {
136
+ apiKey = process.env.TMDB_API_KEY;
137
+ }
138
+ const key = `${id}-${type}-${s || ''}`;
112
139
  const cached = cacheGet(tmdbInfoCache, key);
113
140
  if (cached !== undefined) return cached;
114
- const k = tmdbApiKey;
115
- if (!k) return { isAnime: false, titles: [], year: null, imdbId: null };
141
+ if (!apiKey) return { isAnime: false, titles: [], year: null, imdbId: null };
116
142
  try {
117
143
  const [mainRes, seasonRes] = await Promise.all([
118
- fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
119
- season && mediaType === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${tmdbId}/season/${season}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
144
+ fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
145
+ s && type === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
120
146
  ]);
121
147
  let mainData = null, seasonData = null;
122
148
  if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
@@ -125,10 +151,10 @@ export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
125
151
  const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
126
152
  const titles = [];
127
153
  if (seasonData?.name) titles.push(seasonData.name);
128
- const t = mainData?.title || mainData?.name || '';
154
+ const titleText = mainData?.title || mainData?.name || '';
129
155
  const ot = mainData?.original_title || mainData?.original_name || '';
130
- if (t) titles.push(t);
131
- if (ot && ot !== t) titles.push(ot);
156
+ if (titleText) titles.push(titleText);
157
+ if (ot && ot !== titleText) titles.push(ot);
132
158
  const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
133
159
  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 };
134
160
  cacheSet(tmdbInfoCache, key, result, 600000);
@@ -178,16 +204,26 @@ export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year
178
204
  }
179
205
 
180
206
  export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
181
- const key = `imdb-${imdbId}`;
207
+ let apiKey = tmdbApiKey;
208
+ let id = imdbId;
209
+ let type = mediaType;
210
+ if (id === 'movie' || id === 'tv') {
211
+ type = id;
212
+ id = apiKey;
213
+ apiKey = process.env.TMDB_API_KEY;
214
+ }
215
+ if (!apiKey) {
216
+ apiKey = process.env.TMDB_API_KEY;
217
+ }
218
+ const key = `imdb-${id}`;
182
219
  const cached = cacheGet(tmdbInfoCache, key);
183
220
  if (cached !== undefined) return cached;
184
- const k = tmdbApiKey;
185
- if (!k) return null;
221
+ if (!apiKey) return null;
186
222
  try {
187
- const data = await fetchJson(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${k}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
223
+ const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
188
224
  let result = null;
189
- if (mediaType === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
190
- else if (mediaType === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
225
+ if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
226
+ else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
191
227
  if (result) cacheSet(tmdbInfoCache, key, result, 600000);
192
228
  return result;
193
229
  } catch { return null; }
@@ -1,262 +0,0 @@
1
- import crypto from "crypto";
2
-
3
- export const ID = "goated";
4
- export const DOMAIN = "https://goated.cx";
5
- export const RETURN_ALL_URLS = true;
6
-
7
- const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
8
-
9
- const enc = new TextEncoder();
10
- const info = enc.encode("goated-api-v1");
11
- const salt = enc.encode("goated-stream-salt");
12
-
13
- const generateRandomIP = () => {
14
- const p = () => Math.floor(Math.random() * 255);
15
- let first = p();
16
- while ([0, 10, 127, 169, 172, 192].includes(first)) first = p();
17
- return `${first}.${p()}.${p()}.${p()}`;
18
- };
19
-
20
- let cachedSession = null;
21
- let sessionExpiresAt = 0;
22
- let cachedProviders = null;
23
- let providersExpiresAt = 0;
24
-
25
- async function getValidSession(sessionIp) {
26
- const now = Date.now();
27
- if (cachedSession && now < sessionExpiresAt) {
28
- return cachedSession;
29
- }
30
- try {
31
- const chalRes = await fetch(`${DOMAIN}/api/auth/challenge`, {
32
- headers: {
33
- "User-Agent": UA,
34
- "X-Forwarded-For": sessionIp,
35
- "X-Real-IP": sessionIp,
36
- Origin: DOMAIN,
37
- Referer: DOMAIN + "/",
38
- },
39
- signal: AbortSignal.timeout(7000),
40
- });
41
-
42
- if (!chalRes.ok) throw new Error();
43
- const chalData = await chalRes.json();
44
- const { challenge, difficulty } = chalData;
45
- const targetPrefix = "0".repeat(Math.ceil((difficulty || 16) / 4));
46
- let nonceStr = "";
47
-
48
- for (let i = 0; i < 5000000; i++) {
49
- const nonce = Math.random().toString(36).substring(2, 10);
50
- const hash = crypto.createHash("sha256").update(`${challenge}:${nonce}`).digest("hex");
51
- if (hash.startsWith(targetPrefix)) {
52
- nonceStr = nonce;
53
- break;
54
- }
55
- }
56
-
57
- if (!nonceStr) throw new Error();
58
-
59
- const res = await fetch(`${DOMAIN}/api/auth/session`, {
60
- method: "POST",
61
- headers: {
62
- "Content-Type": "application/json",
63
- "User-Agent": UA,
64
- "X-Forwarded-For": sessionIp,
65
- "X-Real-IP": sessionIp,
66
- Origin: DOMAIN,
67
- Referer: DOMAIN + "/",
68
- },
69
- body: JSON.stringify({ challenge, nonce: nonceStr }),
70
- signal: AbortSignal.timeout(7000),
71
- });
72
-
73
- if (!res.ok) throw new Error();
74
- const data = await res.json();
75
- if (!data.token) throw new Error();
76
-
77
- cachedSession = data.token;
78
- sessionExpiresAt = now + (data.expiresIn || 600) * 1000 - 30000;
79
- return cachedSession;
80
- } catch (e) {
81
- throw new Error();
82
- }
83
- }
84
-
85
- async function getProviders() {
86
- const now = Date.now();
87
- if (cachedProviders && now < providersExpiresAt) return cachedProviders;
88
- try {
89
- const res = await fetch(`${DOMAIN}/api/providers`, {
90
- headers: { "User-Agent": UA },
91
- signal: AbortSignal.timeout(5000)
92
- });
93
- if (!res.ok) return [];
94
- const data = await res.json();
95
- cachedProviders = data.providers || [];
96
- providersExpiresAt = now + 3600 * 1000;
97
- return cachedProviders;
98
- } catch (e) {
99
- return [];
100
- }
101
- }
102
-
103
- async function deriveKey(tokenString) {
104
- const rawKey = await crypto.subtle.importKey("raw", enc.encode(tokenString), "HKDF", false, ["deriveKey"]);
105
- return crypto.subtle.deriveKey(
106
- { name: "HKDF", hash: "SHA-256", salt, info },
107
- rawKey,
108
- { name: "AES-GCM", length: 256 },
109
- false,
110
- ["encrypt", "decrypt"],
111
- );
112
- }
113
-
114
- async function encryptParams(payloadObj, key) {
115
- const iv = crypto.getRandomValues(new Uint8Array(12));
116
- const data = enc.encode(JSON.stringify(payloadObj));
117
- const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
118
- const combined = new Uint8Array(iv.length + ct.byteLength);
119
- combined.set(iv);
120
- combined.set(new Uint8Array(ct), iv.length);
121
- return Buffer.from(combined).toString("base64");
122
- }
123
-
124
- async function decryptUrl(b64Url, key) {
125
- const raw = Buffer.from(b64Url, "base64");
126
- const iv = raw.slice(0, 12);
127
- const ct = raw.slice(12);
128
- try {
129
- const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
130
- return Buffer.from(plain).toString("utf8");
131
- } catch {
132
- return null;
133
- }
134
- }
135
-
136
- async function decryptDeep(obj, key) {
137
- if (!obj || typeof obj !== "object") return obj;
138
- if (Array.isArray(obj)) return Promise.all(obj.map(x => decryptDeep(x, key)));
139
-
140
- const out = {};
141
- for (const [k, v] of Object.entries(obj)) {
142
- if ((k === "url" || k === "file") && typeof v === "string" && v.length > 40) {
143
- try {
144
- const dec = await decryptUrl(v, key);
145
- out[k] = dec ? dec : v;
146
- } catch {
147
- out[k] = v;
148
- }
149
- } else if (k === "links" && Array.isArray(v)) {
150
- out[k] = await Promise.all(v.map(async x => {
151
- if (typeof x === "string" && x.length > 40) {
152
- try {
153
- const dec = await decryptUrl(x, key);
154
- return dec ? dec : x;
155
- } catch {
156
- return x;
157
- }
158
- }
159
- return x;
160
- }));
161
- } else {
162
- out[k] = await decryptDeep(v, key);
163
- }
164
- }
165
- return out;
166
- }
167
-
168
- export async function getStream({ id, s, e, clientIP }) {
169
- try {
170
- const sessionIp = generateRandomIP();
171
- const sessionJwt = await getValidSession(sessionIp);
172
- const actualKey = await deriveKey(sessionJwt);
173
-
174
- const isTv = s && e && s !== "null" && e !== "null";
175
- const contentType = isTv ? "tv" : "movies";
176
- const refererPath = isTv ? `tv/${id}-${s}-${e}/play` : `movie/${id}/play`;
177
-
178
- let providers = await getProviders();
179
- let activeProviders = providers.filter(p => p.active && (p.endpoints || []).includes(contentType));
180
- if (activeProviders.length === 0) activeProviders = [{ name: null }];
181
-
182
- const fetchProviderStream = async (provider) => {
183
- try {
184
- const payload = isTv
185
- ? { type: "tv", id: id.toString(), season: s.toString(), episode: e.toString() }
186
- : { type: "movie", id: id.toString() };
187
-
188
- if (provider.name) payload.provider = provider.name;
189
-
190
- const dParam = await encryptParams(payload, actualKey);
191
-
192
- const streamRes = await fetch(`${DOMAIN}/api/stream?d=${encodeURIComponent(dParam)}`, {
193
- headers: {
194
- Authorization: `Bearer ${sessionJwt}`,
195
- "User-Agent": UA,
196
- "X-Forwarded-For": sessionIp,
197
- "X-Real-IP": sessionIp,
198
- Origin: DOMAIN,
199
- Referer: `${DOMAIN}/${refererPath}`,
200
- },
201
- signal: AbortSignal.timeout(10000)
202
- });
203
-
204
- if (!streamRes.ok) return [];
205
- let rawData = await streamRes.json();
206
-
207
- if (rawData.enc) {
208
- const raw = Buffer.from(rawData.enc, "base64");
209
- const decIv = raw.slice(0, 12);
210
- const decCt = raw.slice(12);
211
- const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: decIv }, actualKey, decCt);
212
- rawData = JSON.parse(Buffer.from(decrypted).toString("utf-8"));
213
- }
214
-
215
- const streamData = await decryptDeep(rawData, actualKey);
216
-
217
- if (!streamData.ok || !streamData.streams || streamData.streams.length === 0) return [];
218
-
219
- const validStreams = [];
220
- for (const stream of streamData.streams) {
221
- if (!stream.url) continue;
222
- if (stream.quality && stream.quality.toUpperCase() !== "AUTO" && streamData.streams.some(st => st.quality && st.quality.toUpperCase() === "AUTO")) continue;
223
-
224
- let finalUrl = stream.url;
225
- if (finalUrl.startsWith("/api/")) finalUrl = `${DOMAIN}${finalUrl}`;
226
-
227
- validStreams.push({
228
- url: finalUrl,
229
- headers: {
230
- "User-Agent": UA,
231
- Referer: DOMAIN + "/",
232
- Origin: DOMAIN,
233
- },
234
- server: stream.source || stream.server || provider.nickname || provider.name || "Goated",
235
- quality: stream.quality || "Auto",
236
- type: finalUrl.includes(".mp4") ? "mp4" : "hls",
237
- skipVerify: true,
238
- skipProxy: true,
239
- });
240
- }
241
- return validStreams;
242
- } catch (err) {
243
- return [];
244
- }
245
- };
246
-
247
- const promises = activeProviders.map(p => fetchProviderStream(p));
248
- const results = await Promise.allSettled(promises);
249
-
250
- const allUrls = [];
251
- for (const res of results) {
252
- if (res.status === "fulfilled" && res.value.length > 0) {
253
- allUrls.push(...res.value);
254
- }
255
- }
256
-
257
- if (allUrls.length > 0) {
258
- return { ...allUrls[0], allUrls };
259
- }
260
- } catch (err) { }
261
- return null;
262
- }
@@ -1,62 +0,0 @@
1
- import { fetchJson } from '../utils/helpers.js';
2
-
3
- const BASE_URL = 'https://nebula.work.gd/private/7f6b9d179ba7ecd77a39520e';
4
-
5
- async function getImdbId(tmdbApiKey, tmdbId, s, e) {
6
- const key = tmdbApiKey;
7
- if (!key) return null;
8
- try {
9
- const type = s != null && e != null ? 'tv' : 'movie';
10
- const url = `https://api.themoviedb.org/3/${type}/${tmdbId}/external_ids?api_key=${key}`;
11
- const res = await fetchJson(url);
12
- return res?.imdb_id || null;
13
- } catch(err) {
14
- return null;
15
- }
16
- }
17
-
18
- export async function getStream({ id, s, e, tmdbApiKey }) {
19
- try {
20
- const imdbId = await getImdbId(tmdbApiKey, id, s, e);
21
- if (!imdbId) return null;
22
-
23
- const path = s != null && e != null
24
- ? `/stream/series/${imdbId}:${s}:${e}.json`
25
- : `/stream/movie/${imdbId}.json`;
26
-
27
- const response = await fetch(`${BASE_URL}${path}`, {
28
- headers: {
29
- 'Accept': 'application/json'
30
- },
31
- signal: AbortSignal.timeout(15000)
32
- });
33
-
34
- if (!response.ok) return null;
35
-
36
- const data = await response.json();
37
- if (!data?.streams?.length) return null;
38
-
39
- const allUrls = data.streams
40
- .filter(stream => stream.url && stream.url.trim() !== '' && !stream.url.includes('hubcloud'))
41
- .map(stream => {
42
- const headers = stream.behaviorHints?.proxyHeaders?.request || {};
43
-
44
- let serverName = stream.name || 'Unknown';
45
- if (serverName.includes('\n')) serverName = serverName.split('\n')[0];
46
-
47
- const filename = stream.filename || stream.behaviorHints?.filename;
48
- if (filename) serverName += ` - ${filename}`;
49
-
50
- return {
51
- url: stream.url,
52
- server: `Nebula - ${serverName}`,
53
- type: stream.url.includes('.m3u8') ? 'hls' : 'mp4',
54
- headers: Object.keys(headers).length > 0 ? headers : undefined,
55
- skipProxy: stream.behaviorHints?.notWebReady === false
56
- };
57
- }).filter(Boolean);
58
- return allUrls.length ? { allUrls } : null;
59
- } catch (err) {
60
- return null;
61
- }
62
- }