@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 +1 -1
- package/src/config.js +107 -0
- package/src/health.js +60 -20
- package/src/sources/4movies.js +77 -0
- package/src/sources/aether.js +99 -0
- package/src/sources/castle.js +415 -0
- package/src/sources/framextv.js +126 -0
- package/src/sources/meowtv.js +140 -0
- package/src/sources/vidora.js +94 -0
package/package.json
CHANGED
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
|
|
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(
|
|
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) ?
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
+
}
|