@vyla-entertainment/sdk 1.14.0 → 1.16.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 +4 -1
- package/src/config.js +96 -7
- package/src/sources/3eyedraven.js +136 -0
- package/src/sources/cinemaarmy.js +181 -0
- package/src/sources/movenerds.js +125 -0
- package/src/sources/rivestream.js +0 -2
- package/src/sources/thugtv.js +62 -0
- package/src/sources/vidy.js +187 -0
- package/src/sources/vixsrc.js +37 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vyla-entertainment/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"test": "node test/index.js"
|
|
6
6
|
},
|
|
@@ -29,5 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"node-tls-client": "^2.1.0"
|
|
32
35
|
}
|
|
33
36
|
}
|
package/src/config.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
// Currently active sources reported on 6:18:22 PM PST 9/13/2026
|
|
2
|
-
|
|
3
|
-
// None Anime Tested: 936075
|
|
4
|
-
// Anime Tested: 37854
|
|
5
|
-
|
|
6
|
-
// Fshare/Fsonic Tested: 155 ( they don't have Michael )
|
|
7
|
-
|
|
8
1
|
export const SOURCES = [
|
|
2
|
+
{
|
|
3
|
+
key: '3eyedraven',
|
|
4
|
+
label: '3eyedraven',
|
|
5
|
+
sourceFile: '3eyedraven',
|
|
6
|
+
proxyParam: '3er',
|
|
7
|
+
timeout: 25000,
|
|
8
|
+
jitter: 500,
|
|
9
|
+
retries: 2,
|
|
10
|
+
multiUrl: false,
|
|
11
|
+
skipProxy: true,
|
|
12
|
+
cdnHeaders: [{
|
|
13
|
+
pattern: /3eyedraven\.watch/i,
|
|
14
|
+
headers: {
|
|
15
|
+
Referer: 'https://3eyedraven.watch/',
|
|
16
|
+
Origin: 'https://3eyedraven.watch',
|
|
17
|
+
},
|
|
18
|
+
}],
|
|
19
|
+
},
|
|
20
|
+
|
|
9
21
|
{
|
|
10
22
|
key: '123anime',
|
|
11
23
|
label: '123Anime',
|
|
@@ -106,6 +118,25 @@ export const SOURCES = [
|
|
|
106
118
|
}],
|
|
107
119
|
},
|
|
108
120
|
|
|
121
|
+
{
|
|
122
|
+
key: 'cinemaarmy',
|
|
123
|
+
label: 'CinemaArmy',
|
|
124
|
+
sourceFile: 'cinemaarmy',
|
|
125
|
+
proxyParam: 'ca',
|
|
126
|
+
timeout: 30000,
|
|
127
|
+
jitter: 500,
|
|
128
|
+
retries: 2,
|
|
129
|
+
multiUrl: true,
|
|
130
|
+
skipProxy: true,
|
|
131
|
+
cdnHeaders: [{
|
|
132
|
+
pattern: /cinema\.army/i,
|
|
133
|
+
headers: {
|
|
134
|
+
Referer: 'https://cinema.army/',
|
|
135
|
+
Origin: 'https://cinema.army',
|
|
136
|
+
},
|
|
137
|
+
}],
|
|
138
|
+
},
|
|
139
|
+
|
|
109
140
|
{
|
|
110
141
|
key: 'dulo',
|
|
111
142
|
label: 'Dulo',
|
|
@@ -191,6 +222,25 @@ export const SOURCES = [
|
|
|
191
222
|
},
|
|
192
223
|
},
|
|
193
224
|
|
|
225
|
+
{
|
|
226
|
+
key: 'movenerds',
|
|
227
|
+
label: 'MoverNerds',
|
|
228
|
+
sourceFile: 'movenerds',
|
|
229
|
+
proxyParam: 'mnr',
|
|
230
|
+
timeout: 30000,
|
|
231
|
+
jitter: 500,
|
|
232
|
+
retries: 2,
|
|
233
|
+
multiUrl: true,
|
|
234
|
+
skipProxy: true,
|
|
235
|
+
cdnHeaders: [{
|
|
236
|
+
pattern: /movenerds\.space/i,
|
|
237
|
+
headers: {
|
|
238
|
+
Referer: 'https://movenerds.space/',
|
|
239
|
+
Origin: 'https://movenerds.space',
|
|
240
|
+
},
|
|
241
|
+
}],
|
|
242
|
+
},
|
|
243
|
+
|
|
194
244
|
{
|
|
195
245
|
key: 'movienight',
|
|
196
246
|
label: 'MovieNight',
|
|
@@ -293,6 +343,26 @@ export const SOURCES = [
|
|
|
293
343
|
},
|
|
294
344
|
}],
|
|
295
345
|
},
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
key: 'thugtv',
|
|
349
|
+
label: 'ThugTV',
|
|
350
|
+
sourceFile: 'thugtv',
|
|
351
|
+
proxyParam: 'tv',
|
|
352
|
+
timeout: 30000,
|
|
353
|
+
jitter: 500,
|
|
354
|
+
retries: 2,
|
|
355
|
+
multiUrl: true,
|
|
356
|
+
skipProxy: true,
|
|
357
|
+
cdnHeaders: [{
|
|
358
|
+
pattern: /thugtv\.online|thuktv\.onrender\.com/i,
|
|
359
|
+
headers: {
|
|
360
|
+
Referer: 'https://thugtv.online/',
|
|
361
|
+
Origin: 'https://thugtv.online',
|
|
362
|
+
},
|
|
363
|
+
}],
|
|
364
|
+
},
|
|
365
|
+
|
|
296
366
|
{
|
|
297
367
|
key: 'vidapi',
|
|
298
368
|
label: 'VidAPI',
|
|
@@ -426,6 +496,25 @@ export const SOURCES = [
|
|
|
426
496
|
multiUrl: true,
|
|
427
497
|
},
|
|
428
498
|
|
|
499
|
+
{
|
|
500
|
+
key: 'vidy',
|
|
501
|
+
label: 'Vidy',
|
|
502
|
+
sourceFile: 'vidy',
|
|
503
|
+
proxyParam: 'vy',
|
|
504
|
+
timeout: 25000,
|
|
505
|
+
jitter: 500,
|
|
506
|
+
retries: 2,
|
|
507
|
+
multiUrl: true,
|
|
508
|
+
skipProxy: true,
|
|
509
|
+
cdnHeaders: [{
|
|
510
|
+
pattern: /wecollege\.net|vidy\.st/i,
|
|
511
|
+
headers: {
|
|
512
|
+
Referer: 'https://www.vidy.st/',
|
|
513
|
+
Origin: 'https://www.vidy.st',
|
|
514
|
+
},
|
|
515
|
+
}],
|
|
516
|
+
},
|
|
517
|
+
|
|
429
518
|
{
|
|
430
519
|
key: 'vidzee',
|
|
431
520
|
label: 'VidZee',
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getUA } from "../utils/helpers.js";
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://3eyedraven.watch";
|
|
4
|
+
const API_BASE = "https://3eyedraven.watch/api";
|
|
5
|
+
|
|
6
|
+
function getHeaders() {
|
|
7
|
+
return {
|
|
8
|
+
"User-Agent": getUA(),
|
|
9
|
+
Accept: "application/json",
|
|
10
|
+
Referer: `${BASE_URL}/`,
|
|
11
|
+
Origin: BASE_URL,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveApiKey(args) {
|
|
16
|
+
return (
|
|
17
|
+
args?.tmdbApiKey ||
|
|
18
|
+
args?.sdk?.tmdbApiKey ||
|
|
19
|
+
process.env.TMDB_API_KEY ||
|
|
20
|
+
null
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getTitle(id, isTv, apiKey) {
|
|
25
|
+
if (!apiKey) return null;
|
|
26
|
+
const type = isTv ? "tv" : "movie";
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(
|
|
29
|
+
`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
|
|
30
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
31
|
+
);
|
|
32
|
+
if (!res.ok) return null;
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
return isTv ? data.name : data.title;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function slugify(text) {
|
|
41
|
+
return text
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.normalize("NFKD")
|
|
44
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
45
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
46
|
+
.replace(/^-|-$/g, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function absolute(url) {
|
|
50
|
+
if (!url) return "";
|
|
51
|
+
if (/^https?:\/\//.test(url)) return url;
|
|
52
|
+
return `${BASE_URL}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getStream(args) {
|
|
56
|
+
const { id, s = null, e = null } = args;
|
|
57
|
+
const isTv = s != null && e != null;
|
|
58
|
+
|
|
59
|
+
const apiKey = resolveApiKey(args);
|
|
60
|
+
const title = await getTitle(id, isTv, apiKey);
|
|
61
|
+
|
|
62
|
+
if (!title) {
|
|
63
|
+
if (process.env.DEBUG_CAUSE) {
|
|
64
|
+
console.error(
|
|
65
|
+
`[3eyedraven] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
|
|
66
|
+
`(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const identifier = `${slugify(title)}-${id}`;
|
|
73
|
+
|
|
74
|
+
const url = isTv
|
|
75
|
+
? `${API_BASE}/stream/${identifier}?season=${s}&episode=${e}`
|
|
76
|
+
: `${API_BASE}/stream/${identifier}`;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(url, {
|
|
80
|
+
headers: getHeaders(),
|
|
81
|
+
signal: AbortSignal.timeout(15000),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
if (process.env.DEBUG_CAUSE) {
|
|
86
|
+
console.error(`[3eyedraven] HTTP ${res.status} for ${url}`);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
const stream = data?.stream;
|
|
93
|
+
if (!stream?.url) {
|
|
94
|
+
if (process.env.DEBUG_CAUSE) {
|
|
95
|
+
console.error(`[3eyedraven] No stream.url in response for ${url}`);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const streamUrl = absolute(stream.url);
|
|
101
|
+
if (!/^https?:\/\//.test(streamUrl)) return null;
|
|
102
|
+
|
|
103
|
+
const subtitles = (stream.subtitles || [])
|
|
104
|
+
.map(sub => {
|
|
105
|
+
const subUrl = absolute(sub.url);
|
|
106
|
+
if (!subUrl) return null;
|
|
107
|
+
return {
|
|
108
|
+
label: sub.label || sub.language || "Subtitle",
|
|
109
|
+
file: subUrl,
|
|
110
|
+
type: "vtt",
|
|
111
|
+
source: "3eyedraven",
|
|
112
|
+
};
|
|
113
|
+
})
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
url: streamUrl,
|
|
118
|
+
headers: {
|
|
119
|
+
"User-Agent": getUA(),
|
|
120
|
+
Referer: `${BASE_URL}/`,
|
|
121
|
+
Origin: BASE_URL,
|
|
122
|
+
},
|
|
123
|
+
server: "3eyedraven",
|
|
124
|
+
quality: "1080p",
|
|
125
|
+
type: stream.format === "hls" ? "hls" : "mp4",
|
|
126
|
+
subtitles,
|
|
127
|
+
skipProxy: true,
|
|
128
|
+
};
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (process.env.DEBUG_CAUSE) {
|
|
131
|
+
console.error(`[3eyedraven] fetch error for ${url}:`, err.message);
|
|
132
|
+
if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client";
|
|
3
|
+
|
|
4
|
+
const BASE_URL = "https://cinema.army";
|
|
5
|
+
const API_BASE = "https://cinema.army/api";
|
|
6
|
+
|
|
7
|
+
const XOR_KEY = "f16a6b92fcd6eed3e9476297f893f2505c32f79bc6efef9b6c95496aa5914548";
|
|
8
|
+
const HMAC_KEY = "a2cfc9eaa2a3690b5d4f6b5f958dbdeae52a097283d375bd253ee7c7062524f5";
|
|
9
|
+
|
|
10
|
+
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
11
|
+
|
|
12
|
+
const SERVERS = [
|
|
13
|
+
{ key: "cookiebakers", method: "POST", label: "CookieBakers" },
|
|
14
|
+
{ key: "tcloud", method: "GET", label: "TCloud" },
|
|
15
|
+
{ key: "ipcloud", method: "GET", label: "IPcloud" },
|
|
16
|
+
{ key: "dcloud", method: "GET", label: "Dcloud" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
let tlsInitialized = false;
|
|
20
|
+
let session = null;
|
|
21
|
+
|
|
22
|
+
async function ensureSession() {
|
|
23
|
+
if (!tlsInitialized) {
|
|
24
|
+
await initTLS();
|
|
25
|
+
tlsInitialized = true;
|
|
26
|
+
}
|
|
27
|
+
if (!session) {
|
|
28
|
+
session = new Session({
|
|
29
|
+
clientIdentifier: ClientIdentifier.chrome_131,
|
|
30
|
+
timeout: 15000,
|
|
31
|
+
insecureSkipVerify: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return session;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getHeaders(token, id, isTv, s, e) {
|
|
38
|
+
const referer = isTv
|
|
39
|
+
? `${BASE_URL}/watch?id=${id}&type=tv&season=${s}&episode=${e}`
|
|
40
|
+
: `${BASE_URL}/watch?id=${id}&type=movie`;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"User-Agent": UA,
|
|
44
|
+
"Accept": "application/json",
|
|
45
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
46
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
47
|
+
"Cache-Control": "no-cache",
|
|
48
|
+
"Pragma": "no-cache",
|
|
49
|
+
"Referer": referer,
|
|
50
|
+
"Origin": BASE_URL,
|
|
51
|
+
"Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
|
|
52
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
53
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
54
|
+
"Sec-Fetch-Dest": "empty",
|
|
55
|
+
"Sec-Fetch-Mode": "cors",
|
|
56
|
+
"Sec-Fetch-Site": "same-origin",
|
|
57
|
+
"Priority": "u=1, i",
|
|
58
|
+
"X-Stream-Token": token,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function generateToken(id) {
|
|
63
|
+
try {
|
|
64
|
+
const minute = Math.floor(Date.now() / 1000 / 60);
|
|
65
|
+
return crypto
|
|
66
|
+
.createHmac("sha256", HMAC_KEY)
|
|
67
|
+
.update(`${id}:${minute}`)
|
|
68
|
+
.digest("hex");
|
|
69
|
+
} catch {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function decryptStreamUrl(encoded) {
|
|
75
|
+
if (!encoded) return "";
|
|
76
|
+
if (encoded.startsWith("http://") || encoded.startsWith("https://")) return encoded;
|
|
77
|
+
try {
|
|
78
|
+
const decoded = Buffer.from(encoded, "base64").toString("binary");
|
|
79
|
+
let out = "";
|
|
80
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
81
|
+
out += String.fromCharCode(
|
|
82
|
+
decoded.charCodeAt(i) ^ XOR_KEY.charCodeAt(i % XOR_KEY.length)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
} catch {
|
|
87
|
+
return encoded;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function getStream(args) {
|
|
92
|
+
const { id, s = null, e = null } = args;
|
|
93
|
+
const isTv = s != null && e != null;
|
|
94
|
+
|
|
95
|
+
const token = generateToken(id);
|
|
96
|
+
const headers = getHeaders(token, id, isTv, s, e);
|
|
97
|
+
|
|
98
|
+
const sess = await ensureSession();
|
|
99
|
+
|
|
100
|
+
const requests = SERVERS.map(server => {
|
|
101
|
+
const path = isTv
|
|
102
|
+
? `/${server.key}/tv/${id}/${s}/${e}`
|
|
103
|
+
: `/${server.key}/movie/${id}`;
|
|
104
|
+
return { url: `${API_BASE}${path}`, server };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const results = await Promise.allSettled(
|
|
108
|
+
requests.map(async ({ url, server }) => {
|
|
109
|
+
try {
|
|
110
|
+
const res = server.method === "POST"
|
|
111
|
+
? await sess.post(url, { headers })
|
|
112
|
+
: await sess.get(url, { headers });
|
|
113
|
+
|
|
114
|
+
if (res.status !== 200) {
|
|
115
|
+
if (process.env.DEBUG_CAUSE) {
|
|
116
|
+
console.error(`[CinemaArmy ${server.key}] HTTP ${res.status}`);
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
let data;
|
|
123
|
+
try {
|
|
124
|
+
data = JSON.parse(text);
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const encrypted = data?.stream?.url;
|
|
130
|
+
if (!encrypted) return null;
|
|
131
|
+
|
|
132
|
+
const streamUrl = decryptStreamUrl(encrypted);
|
|
133
|
+
if (!streamUrl || !/^https?:\/\//.test(streamUrl)) return null;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
url: streamUrl,
|
|
137
|
+
headers: {
|
|
138
|
+
"User-Agent": UA,
|
|
139
|
+
Referer: `${BASE_URL}/`,
|
|
140
|
+
Origin: BASE_URL,
|
|
141
|
+
},
|
|
142
|
+
server: `CinemaArmy (${server.label})`,
|
|
143
|
+
quality: "1080p",
|
|
144
|
+
type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
|
|
145
|
+
};
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (process.env.DEBUG_CAUSE) {
|
|
148
|
+
console.error(`[CinemaArmy ${server.key}] ${err.message}`);
|
|
149
|
+
if (err.cause) {
|
|
150
|
+
console.error(` cause.code: ${err.cause.code || "n/a"}`);
|
|
151
|
+
console.error(` cause.message: ${err.cause.message || err.cause}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const allUrls = results
|
|
160
|
+
.filter(r => r.status === "fulfilled" && r.value)
|
|
161
|
+
.map(r => r.value);
|
|
162
|
+
|
|
163
|
+
if (!allUrls.length) return null;
|
|
164
|
+
|
|
165
|
+
return { allUrls };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function cleanup() {
|
|
169
|
+
if (session) {
|
|
170
|
+
try {
|
|
171
|
+
await session.close();
|
|
172
|
+
} catch { }
|
|
173
|
+
session = null;
|
|
174
|
+
}
|
|
175
|
+
if (tlsInitialized) {
|
|
176
|
+
try {
|
|
177
|
+
await destroyTLS();
|
|
178
|
+
} catch { }
|
|
179
|
+
tlsInitialized = false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getUA } from "../utils/helpers.js";
|
|
2
|
+
|
|
3
|
+
const API_BASE = "https://api.movenerds.space/api";
|
|
4
|
+
|
|
5
|
+
function getHeaders() {
|
|
6
|
+
return {
|
|
7
|
+
"User-Agent": getUA(),
|
|
8
|
+
Accept: "application/json",
|
|
9
|
+
Referer: "https://movenerds.space/",
|
|
10
|
+
Origin: "https://movenerds.space",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveApiKey(args) {
|
|
15
|
+
return (
|
|
16
|
+
args?.tmdbApiKey ||
|
|
17
|
+
args?.sdk?.tmdbApiKey ||
|
|
18
|
+
process.env.TMDB_API_KEY ||
|
|
19
|
+
null
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getMeta(id, isTv, apiKey) {
|
|
24
|
+
if (!apiKey) return null;
|
|
25
|
+
const type = isTv ? "tv" : "movie";
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(
|
|
28
|
+
`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
|
|
29
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
30
|
+
);
|
|
31
|
+
if (!res.ok) return null;
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const title = isTv ? data.name : data.title;
|
|
34
|
+
const date = isTv ? data.first_air_date : data.release_date;
|
|
35
|
+
const year = date ? parseInt(date.slice(0, 4), 10) : null;
|
|
36
|
+
return { title, year };
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function mapSource(source) {
|
|
43
|
+
if (!source || !source.url) return null;
|
|
44
|
+
if (source.type !== "hls" && source.type !== "mp4") return null;
|
|
45
|
+
if (!/^https?:\/\//.test(source.url)) return null;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
url: source.url,
|
|
49
|
+
headers: {
|
|
50
|
+
"User-Agent": source.headers?.["User-Agent"] || getUA(),
|
|
51
|
+
Referer: source.headers?.Referer || "https://movenerds.space/",
|
|
52
|
+
Origin: source.headers?.Origin || "https://movenerds.space",
|
|
53
|
+
},
|
|
54
|
+
server: source.name || source.providerName || "MoverNerds",
|
|
55
|
+
quality: source.quality || "1080p",
|
|
56
|
+
type: source.type,
|
|
57
|
+
skipProxy: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function getStream(args) {
|
|
62
|
+
const { id, s = null, e = null } = args;
|
|
63
|
+
const isTv = s != null && e != null;
|
|
64
|
+
|
|
65
|
+
const apiKey = resolveApiKey(args);
|
|
66
|
+
const meta = await getMeta(id, isTv, apiKey);
|
|
67
|
+
|
|
68
|
+
if (!meta?.title) {
|
|
69
|
+
if (process.env.DEBUG_CAUSE) {
|
|
70
|
+
console.error(
|
|
71
|
+
`[movenerds] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
|
|
72
|
+
`(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const params = new URLSearchParams({
|
|
79
|
+
tmdbId: String(id),
|
|
80
|
+
type: isTv ? "tv" : "movie",
|
|
81
|
+
title: meta.title,
|
|
82
|
+
});
|
|
83
|
+
if (meta.year) params.set("year", String(meta.year));
|
|
84
|
+
if (isTv) {
|
|
85
|
+
params.set("season", String(s));
|
|
86
|
+
params.set("episode", String(e));
|
|
87
|
+
params.set("language", "en");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const url = `${API_BASE}/stream?${params.toString()}`;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(url, {
|
|
94
|
+
headers: getHeaders(),
|
|
95
|
+
signal: AbortSignal.timeout(20000),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
if (process.env.DEBUG_CAUSE) {
|
|
100
|
+
console.error(`[movenerds] HTTP ${res.status} for ${url}`);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = await res.json();
|
|
106
|
+
const primary = Array.isArray(data?.primarySources) ? data.primarySources : [];
|
|
107
|
+
if (!primary.length) {
|
|
108
|
+
if (process.env.DEBUG_CAUSE) {
|
|
109
|
+
console.error(`[movenerds] No primarySources in response for ${url}`);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const allUrls = primary.map(mapSource).filter(Boolean);
|
|
115
|
+
if (!allUrls.length) return null;
|
|
116
|
+
|
|
117
|
+
return { allUrls };
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (process.env.DEBUG_CAUSE) {
|
|
120
|
+
console.error(`[movenerds] fetch error for ${url}:`, err.message);
|
|
121
|
+
if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getUA } from "../utils/helpers.js";
|
|
2
|
+
|
|
3
|
+
const API_BASE = "https://thuktv.onrender.com/api";
|
|
4
|
+
|
|
5
|
+
function getHeaders() {
|
|
6
|
+
return {
|
|
7
|
+
"User-Agent": getUA(),
|
|
8
|
+
Accept: "application/json",
|
|
9
|
+
Referer: "https://thugtv.online/",
|
|
10
|
+
Origin: "https://thugtv.online",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mapSource(source) {
|
|
15
|
+
if (!source || !source.url) return null;
|
|
16
|
+
if (source.type !== "hls" && source.type !== "mp4") return null;
|
|
17
|
+
if (!/^https?:\/\//.test(source.url)) return null;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
url: source.url,
|
|
21
|
+
headers: {
|
|
22
|
+
"User-Agent": getUA(),
|
|
23
|
+
Referer: "https://thugtv.online/",
|
|
24
|
+
Origin: "https://thugtv.online",
|
|
25
|
+
},
|
|
26
|
+
server: source.name || source.label || "ThugTV",
|
|
27
|
+
quality: source.quality || "1080p",
|
|
28
|
+
type: source.type,
|
|
29
|
+
skipProxy: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getStream(args) {
|
|
34
|
+
const { id, s = null, e = null } = args;
|
|
35
|
+
const isTv = s != null && e != null;
|
|
36
|
+
|
|
37
|
+
const url = isTv
|
|
38
|
+
? `${API_BASE}/stream/tv/${id}/${s}/${e}`
|
|
39
|
+
: `${API_BASE}/stream/movie/${id}`;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
headers: getHeaders(),
|
|
44
|
+
signal: AbortSignal.timeout(15000),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) return null;
|
|
47
|
+
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
const sources = Array.isArray(data?.sources) ? data.sources : [];
|
|
50
|
+
if (!sources.length) return null;
|
|
51
|
+
|
|
52
|
+
const allUrls = sources
|
|
53
|
+
.map(mapSource)
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
|
|
56
|
+
if (!allUrls.length) return null;
|
|
57
|
+
|
|
58
|
+
return { allUrls };
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { getUA } from "../utils/helpers.js";
|
|
2
|
+
|
|
3
|
+
const API_URL = "https://api.wecollege.net";
|
|
4
|
+
const SERVERS = ["miami", "atlanta", "seattle", "denver", "dallas", "tampa"];
|
|
5
|
+
const MAGIC = [109, 118, 109, 49];
|
|
6
|
+
|
|
7
|
+
function imul(a, b) {
|
|
8
|
+
return Math.imul(a | 0, b | 0) >>> 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function pFn(e) {
|
|
12
|
+
e = e >>> 0;
|
|
13
|
+
e = (e ^ (e >>> 16)) >>> 0;
|
|
14
|
+
e = imul(e, 2246822507);
|
|
15
|
+
e = (e ^ (e >>> 13)) >>> 0;
|
|
16
|
+
e = imul(e, 3266489909);
|
|
17
|
+
e = (e ^ (e >>> 16)) >>> 0;
|
|
18
|
+
return e;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function gFn(e, t) {
|
|
22
|
+
e = e >>> 0;
|
|
23
|
+
t = t & 31;
|
|
24
|
+
if (t === 0) return e;
|
|
25
|
+
return ((e << t) | (e >>> (32 - t))) >>> 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function decryptVidy(ciphertext, seedStr, mediaId) {
|
|
29
|
+
let padded = ciphertext.replace(/-/g, "+").replace(/_/g, "/");
|
|
30
|
+
while (padded.length % 4) padded += "=";
|
|
31
|
+
|
|
32
|
+
const raw = new Uint8Array(Buffer.from(padded, "base64"));
|
|
33
|
+
const length = raw.length;
|
|
34
|
+
|
|
35
|
+
let tFnv = 2166136261;
|
|
36
|
+
for (let i = 0; i < seedStr.length; i++) {
|
|
37
|
+
tFnv = imul((tFnv ^ seedStr.charCodeAt(i)) >>> 0, 16777619);
|
|
38
|
+
}
|
|
39
|
+
const fnvRes = pFn(tFnv);
|
|
40
|
+
|
|
41
|
+
const innerArg = (((mediaId >>> 0) ^ 2654435769) >>> 0);
|
|
42
|
+
const inner = pFn(innerArg);
|
|
43
|
+
let n = pFn(((fnvRes ^ inner) >>> 0));
|
|
44
|
+
|
|
45
|
+
const S = {};
|
|
46
|
+
for (let e = 0; e < 8; e++) {
|
|
47
|
+
const t = n % 61;
|
|
48
|
+
n = gFn((n + 2654435769) >>> 0, 7 + (7 & e));
|
|
49
|
+
S[t] = ((n ^ pFn(n)) >>> 0);
|
|
50
|
+
n = pFn((n + t) >>> 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let acc = pFn(((2779096485 ^ n) >>> 0));
|
|
54
|
+
const keystream = new Uint8Array(length);
|
|
55
|
+
let sCnt = 0;
|
|
56
|
+
let idx = 0;
|
|
57
|
+
|
|
58
|
+
while (idx < length) {
|
|
59
|
+
const sVal = acc;
|
|
60
|
+
const o = sVal % 61;
|
|
61
|
+
const hasO = Object.prototype.hasOwnProperty.call(S, o);
|
|
62
|
+
const iVal = hasO ? -1 : 0;
|
|
63
|
+
const d = hasO ? S[o] : 0;
|
|
64
|
+
const nTerm = ((d ^ imul(2654435769, sCnt + 1)) >>> 0);
|
|
65
|
+
sCnt += 1;
|
|
66
|
+
const aTerm = sVal;
|
|
67
|
+
let lVal = (((aTerm ^ nTerm) | (aTerm & nTerm & iVal)) >>> 0);
|
|
68
|
+
const term1 = gFn((lVal + sVal) >>> 0, 31 & o);
|
|
69
|
+
const term2 = gFn(sVal, (31 & imul(o, 7)) >>> 0);
|
|
70
|
+
lVal = ((term1 ^ term2) >>> 0);
|
|
71
|
+
const newS = pFn((lVal + 2654435769) >>> 0);
|
|
72
|
+
S[o] = newS;
|
|
73
|
+
acc = newS;
|
|
74
|
+
const tVal = newS;
|
|
75
|
+
|
|
76
|
+
keystream[idx++] = tVal & 0xFF;
|
|
77
|
+
if (idx < length) keystream[idx++] = (tVal >>> 8) & 0xFF;
|
|
78
|
+
if (idx < length) keystream[idx++] = (tVal >>> 16) & 0xFF;
|
|
79
|
+
if (idx < length) keystream[idx++] = (tVal >>> 24) & 0xFF;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < length; i++) {
|
|
83
|
+
raw[i] ^= keystream[i];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < MAGIC.length; i++) {
|
|
87
|
+
if (raw[i] !== MAGIC[i]) {
|
|
88
|
+
throw new Error("Vidy decrypt failed: magic header mismatch");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return new TextDecoder().decode(raw.slice(MAGIC.length));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function getStream(args) {
|
|
96
|
+
const { id, s = null, e = null } = args;
|
|
97
|
+
const isTv = s != null && e != null;
|
|
98
|
+
const mediaId = Number(id);
|
|
99
|
+
|
|
100
|
+
const headers = {
|
|
101
|
+
"User-Agent": getUA(),
|
|
102
|
+
Referer: "https://www.vidy.st/",
|
|
103
|
+
Origin: "https://www.vidy.st",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const seedUrl = new URL(`${API_URL}/seed`);
|
|
108
|
+
seedUrl.searchParams.set("mediaId", String(id));
|
|
109
|
+
|
|
110
|
+
const seedRes = await fetch(seedUrl.toString(), {
|
|
111
|
+
headers,
|
|
112
|
+
signal: AbortSignal.timeout(6000),
|
|
113
|
+
});
|
|
114
|
+
if (!seedRes.ok) return null;
|
|
115
|
+
|
|
116
|
+
const seedData = await seedRes.json();
|
|
117
|
+
const seed = seedData?.seed;
|
|
118
|
+
if (!seed) return null;
|
|
119
|
+
|
|
120
|
+
for (const server of SERVERS) {
|
|
121
|
+
try {
|
|
122
|
+
const srcUrl = new URL(`${API_URL}/${server}/sources`);
|
|
123
|
+
srcUrl.searchParams.set("mediaType", isTv ? "tv" : "movie");
|
|
124
|
+
srcUrl.searchParams.set("tmdbId", String(id));
|
|
125
|
+
srcUrl.searchParams.set("enc", "2");
|
|
126
|
+
srcUrl.searchParams.set("seed", seed);
|
|
127
|
+
if (isTv) {
|
|
128
|
+
srcUrl.searchParams.set("seasonId", String(s));
|
|
129
|
+
srcUrl.searchParams.set("episodeId", String(e));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const res = await fetch(srcUrl.toString(), {
|
|
133
|
+
headers,
|
|
134
|
+
signal: AbortSignal.timeout(6000),
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok) continue;
|
|
137
|
+
|
|
138
|
+
const text = await res.text();
|
|
139
|
+
|
|
140
|
+
let data;
|
|
141
|
+
try {
|
|
142
|
+
data = JSON.parse(decryptVidy(text, seed, mediaId));
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sources = data?.sources || [];
|
|
148
|
+
const playlist = data?.playlist || "";
|
|
149
|
+
if (!sources.length && !playlist) continue;
|
|
150
|
+
|
|
151
|
+
let streamUrl = playlist;
|
|
152
|
+
let quality = "1080p";
|
|
153
|
+
|
|
154
|
+
for (const src of sources) {
|
|
155
|
+
if (src?.quality === "1080p" && src?.url) {
|
|
156
|
+
streamUrl = src.url;
|
|
157
|
+
quality = "1080p";
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!streamUrl && sources.length) {
|
|
162
|
+
streamUrl = sources[0]?.url || "";
|
|
163
|
+
quality = sources[0]?.quality || "";
|
|
164
|
+
}
|
|
165
|
+
if (!streamUrl) continue;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
url: streamUrl,
|
|
169
|
+
headers: {
|
|
170
|
+
"User-Agent": getUA(),
|
|
171
|
+
Referer: "https://www.vidy.st/",
|
|
172
|
+
Origin: "https://www.vidy.st",
|
|
173
|
+
},
|
|
174
|
+
server: "Vidy",
|
|
175
|
+
quality,
|
|
176
|
+
type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
|
|
177
|
+
};
|
|
178
|
+
} catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/sources/vixsrc.js
CHANGED
|
@@ -52,8 +52,8 @@ export async function getStream({ id, s, e, absoluteBase }) {
|
|
|
52
52
|
playlist = `${BASE_URL}${playlist}`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const
|
|
56
|
-
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${
|
|
55
|
+
const defaultLang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
|
|
56
|
+
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${defaultLang}`;
|
|
57
57
|
|
|
58
58
|
const masterProxy = absoluteBase && absoluteBase !== '/'
|
|
59
59
|
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}`
|
|
@@ -62,50 +62,52 @@ export async function getStream({ id, s, e, absoluteBase }) {
|
|
|
62
62
|
const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null);
|
|
63
63
|
if (!playlistText?.includes('#EXTM3U')) return null;
|
|
64
64
|
|
|
65
|
-
let finalUrl = null;
|
|
66
|
-
let bestHeight = 0;
|
|
67
65
|
const lines = playlistText.split('\n');
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
const audioTracks = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.startsWith('#EXT-X-MEDIA') && line.includes('TYPE=AUDIO')) {
|
|
71
|
+
const name = line.match(/NAME=["']([^"']+)["']/)?.[1] || 'Default';
|
|
72
|
+
const rendition = line.match(/rendition=([a-zA-Z0-9_-]+)/)?.[1]
|
|
73
|
+
|| line.match(/LANGUAGE=["']([^"']+)["']/)?.[1]
|
|
74
|
+
|| name;
|
|
75
|
+
const key = rendition.toLowerCase();
|
|
76
|
+
if (!seen.has(key)) {
|
|
77
|
+
seen.add(key);
|
|
78
|
+
audioTracks.push({ name, lang: rendition });
|
|
79
|
+
}
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!finalUrl.startsWith('http')) {
|
|
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
|
-
}
|
|
94
|
-
}
|
|
83
|
+
const allUrls = [];
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
if (audioTracks.length > 0) {
|
|
86
|
+
for (const track of audioTracks) {
|
|
87
|
+
const streamUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${track.lang}`;
|
|
88
|
+
allUrls.push({
|
|
89
|
+
url: streamUrl,
|
|
90
|
+
server: `VixSrc (${track.name})`,
|
|
101
91
|
skipProxy: true,
|
|
102
92
|
headers: {
|
|
103
93
|
...HEADERS,
|
|
104
94
|
'Referer': `${BASE_URL}/`
|
|
105
95
|
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
allUrls.push({
|
|
100
|
+
url: rawMasterUrl,
|
|
101
|
+
server: 'VixSrc',
|
|
102
|
+
skipProxy: true,
|
|
103
|
+
headers: {
|
|
104
|
+
...HEADERS,
|
|
105
|
+
'Referer': `${BASE_URL}/`
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { allUrls };
|
|
109
111
|
} catch (err) {
|
|
110
112
|
return null;
|
|
111
113
|
}
|