@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.
- package/.env.example +1 -0
- package/README.md +2 -32
- package/package.json +1 -1
- package/src/config.js +45 -46
- package/src/downloads/downloads.js +8 -4
- package/src/sdk.d.ts +1 -1
- package/src/sources/1embed.js +34 -11
- package/src/sources/anihq.js +14 -2
- package/src/sources/anineko.js +122 -15
- package/src/sources/cinejoy.js +210 -0
- package/src/sources/flaxmovies.js +21 -9
- package/src/sources/fsonic.js +2 -2
- package/src/sources/fsonline.js +2 -2
- package/src/sources/lmscript.js +43 -0
- package/src/sources/videasy.js +6 -12
- package/src/sources/vidvault.js +114 -0
- package/src/sources/vidzee.js +73 -87
- package/src/sources/vixsrc.js +28 -14
- package/src/utils/helpers.js +56 -20
- package/src/sources/goated.js +0 -262
- package/src/sources/nebula.js +0 -62
package/src/sources/vixsrc.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
78
|
-
|
|
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
|
-
|
|
100
|
+
server: 'VixSrc',
|
|
101
|
+
skipProxy: true,
|
|
88
102
|
headers: {
|
|
89
103
|
...HEADERS,
|
|
90
104
|
'Referer': `${BASE_URL}/`
|
package/src/utils/helpers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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/${
|
|
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
|
-
|
|
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
|
-
|
|
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/${
|
|
119
|
-
|
|
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
|
|
154
|
+
const titleText = mainData?.title || mainData?.name || '';
|
|
129
155
|
const ot = mainData?.original_title || mainData?.original_name || '';
|
|
130
|
-
if (
|
|
131
|
-
if (ot && 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
|
-
|
|
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
|
-
|
|
185
|
-
if (!k) return null;
|
|
221
|
+
if (!apiKey) return null;
|
|
186
222
|
try {
|
|
187
|
-
const data = await fetchJson(`https://api.themoviedb.org/3/find/${
|
|
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 (
|
|
190
|
-
else if (
|
|
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; }
|
package/src/sources/goated.js
DELETED
|
@@ -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
|
-
}
|
package/src/sources/nebula.js
DELETED
|
@@ -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
|
-
}
|