@vyla-entertainment/sdk 1.0.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/LICENSE +407 -0
- package/README.md +44 -0
- package/package.json +31 -0
- package/src/config.js +519 -0
- package/src/downloads/downloads.js +8 -0
- package/src/downloads/sources/trendimovies.js +63 -0
- package/src/health.js +38 -0
- package/src/sdk.js +65 -0
- package/src/sources/123anime.js +57 -0
- package/src/sources/1embed.js +92 -0
- package/src/sources/anihq.js +112 -0
- package/src/sources/anineko.js +81 -0
- package/src/sources/anipm.js +70 -0
- package/src/sources/flaxmovies.js +14 -0
- package/src/sources/fsharetv.js +44 -0
- package/src/sources/fsonic.js +47 -0
- package/src/sources/fsonline.js +46 -0
- package/src/sources/goated.js +262 -0
- package/src/sources/hexa.js +82 -0
- package/src/sources/kisskh.js +60 -0
- package/src/sources/lookmovie.js +78 -0
- package/src/sources/mapple.js +115 -0
- package/src/sources/meowtv.js +111 -0
- package/src/sources/nebula.js +62 -0
- package/src/sources/nekowatch.js +57 -0
- package/src/sources/opstream.js +123 -0
- package/src/sources/peachify.js +78 -0
- package/src/sources/pengu.js +45 -0
- package/src/sources/purstream.js +104 -0
- package/src/sources/vidapi.js +115 -0
- package/src/sources/vidcore.js +43 -0
- package/src/sources/videasy.js +98 -0
- package/src/sources/vidfast.js +53 -0
- package/src/sources/vidlink.js +28 -0
- package/src/sources/vidnest.js +77 -0
- package/src/sources/vidrift.js +95 -0
- package/src/sources/vidrock.js +145 -0
- package/src/sources/vidsync.js +38 -0
- package/src/sources/vidup.js +79 -0
- package/src/sources/vidzee.js +120 -0
- package/src/sources/vixsrc.js +98 -0
- package/src/sources/xpass.js +40 -0
- package/src/subtitles.js +127 -0
- package/src/utils/helpers.js +194 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const API_BASE = 'https://api.meowtv.ru';
|
|
4
|
+
const REFERER = 'https://meowtv.ru/';
|
|
5
|
+
const ENC_DEC_API = 'https://enc-dec.app/api/dec-meowtv';
|
|
6
|
+
|
|
7
|
+
const HEADERS = {
|
|
8
|
+
'User-Agent': USER_AGENT,
|
|
9
|
+
'Accept': 'application/json',
|
|
10
|
+
'Referer': REFERER,
|
|
11
|
+
'Origin': 'https://meowtv.ru',
|
|
12
|
+
'Accept-Language': 'en-US,en;q=0.9'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const SERVERS = [
|
|
16
|
+
'pseudo',
|
|
17
|
+
'lynx',
|
|
18
|
+
'tik',
|
|
19
|
+
'ipcloud',
|
|
20
|
+
'v4:English',
|
|
21
|
+
'turkce',
|
|
22
|
+
'v5:Hindi',
|
|
23
|
+
'v4:Hindi',
|
|
24
|
+
'v6:Hindi'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const TIMEOUT_MS = 15000;
|
|
28
|
+
|
|
29
|
+
export const VERIFY_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': REFERER, 'Origin': 'https://meowtv.ru' };
|
|
30
|
+
|
|
31
|
+
async function decryptPayload(payload) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetchJson(ENC_DEC_API, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ data: payload })
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (response?.status !== 200 || !response?.result) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return typeof response.result === 'string' ? JSON.parse(response.result) : response.result;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function getStream({ id, s, e, server }) {
|
|
50
|
+
try {
|
|
51
|
+
const isTv = !!s;
|
|
52
|
+
let targets = SERVERS;
|
|
53
|
+
if (server && server !== 'all') {
|
|
54
|
+
const cleanName = server.replace('MeowTV - ', '');
|
|
55
|
+
targets = SERVERS.includes(cleanName) ? [cleanName] : [targets[0]];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results = await Promise.all(targets.map(async srv => {
|
|
59
|
+
try {
|
|
60
|
+
return await Promise.race([
|
|
61
|
+
(async () => {
|
|
62
|
+
const path = isTv
|
|
63
|
+
? `/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(srv)}`
|
|
64
|
+
: `/streams/movie/${id}?s=${encodeURIComponent(srv)}`;
|
|
65
|
+
|
|
66
|
+
const payload = await fetchJson(`${API_BASE}${path}`, {
|
|
67
|
+
headers: HEADERS
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!payload) return null;
|
|
71
|
+
|
|
72
|
+
const data = await decryptPayload(payload);
|
|
73
|
+
if (!data) return null;
|
|
74
|
+
|
|
75
|
+
const urls = [];
|
|
76
|
+
if (data?.url?.startsWith('http')) {
|
|
77
|
+
urls.push({
|
|
78
|
+
url: data.url,
|
|
79
|
+
server: `MeowTV - ${srv}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(data?.streams)) {
|
|
84
|
+
for (const stream of data.streams) {
|
|
85
|
+
if (stream?.url?.startsWith('http')) {
|
|
86
|
+
urls.push({
|
|
87
|
+
url: stream.url,
|
|
88
|
+
server: `MeowTV - ${srv} (${stream.language || 'Unknown'})`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return urls;
|
|
94
|
+
})(),
|
|
95
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout exceeded')), TIMEOUT_MS))
|
|
96
|
+
]);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
const allUrls = results.filter(Boolean).flat();
|
|
103
|
+
return allUrls.length ? { allUrls } : null;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getSources() {
|
|
110
|
+
return SERVERS.map(s => `MeowTV - ${s}`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fetchJson, fetchText, getTmdbInfo, tmdbToAnilist } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://nekowatch.xyz";
|
|
4
|
+
const ANIME_JS_URL = `${BASE_URL}/js/anime.js?v=v263_fast_player_start`;
|
|
5
|
+
const FALLBACK_ORDER = ['kiwi', 'ally', 'bee', 'arc', 'anikoto', 'jet', 'bonk', 'moo', 'hop', 'pulsar', 'pahe'];
|
|
6
|
+
|
|
7
|
+
let providerOrderCache = null, providerOrderTs = 0;
|
|
8
|
+
|
|
9
|
+
async function getProviderOrder() {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
if (providerOrderCache && now - providerOrderTs < 1800000) return providerOrderCache;
|
|
12
|
+
try {
|
|
13
|
+
const js = await fetchText(ANIME_JS_URL, { signal: AbortSignal.timeout(6000) });
|
|
14
|
+
const match = js.match(/PROVIDER_ORDER\s*=\s*\[([\s\S]*?)\]/);
|
|
15
|
+
if (match) {
|
|
16
|
+
const order = [...match[1].matchAll(/['"]([a-z0-9_]+)['"]/gi)].map(m => m[1]);
|
|
17
|
+
if (order.length) { providerOrderCache = order; providerOrderTs = now; return order; }
|
|
18
|
+
}
|
|
19
|
+
} catch { }
|
|
20
|
+
return providerOrderCache || FALLBACK_ORDER;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getStream({ id, s, e, audio }) {
|
|
24
|
+
try {
|
|
25
|
+
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
|
26
|
+
if (!info || !info.isAnime) return null;
|
|
27
|
+
const anilistId = await tmdbToAnilist(id, s ? 'tv' : 'movie', s, info.titles, info.year);
|
|
28
|
+
if (!anilistId) return null;
|
|
29
|
+
const ep = e || 1;
|
|
30
|
+
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
|
31
|
+
const providerOrder = await getProviderOrder();
|
|
32
|
+
for (const aud of audiosToTry) {
|
|
33
|
+
const results = await Promise.allSettled(providerOrder.map(p => fetchJson(`${BASE_URL}/api/anime/watch/${p}/${anilistId}/${aud}/${p}-${ep}`, { signal: AbortSignal.timeout(8000) })));
|
|
34
|
+
const allUrls = [], seenActual = new Set();
|
|
35
|
+
for (const r of results) {
|
|
36
|
+
if (r.status !== 'fulfilled' || !r.value) continue;
|
|
37
|
+
const data = r.value;
|
|
38
|
+
let streams = [], actual = data.attempted?.actualProvider || data.provider;
|
|
39
|
+
if (Array.isArray(data.streams)) streams = data.streams;
|
|
40
|
+
else {
|
|
41
|
+
for (const key of Object.keys(data)) if (data[key] && Array.isArray(data[key].streams)) { streams.push(...data[key].streams); if (!actual) actual = data[key].provider || key; }
|
|
42
|
+
}
|
|
43
|
+
if (!streams.length) continue;
|
|
44
|
+
if (actual) { if (seenActual.has(actual)) continue; seenActual.add(actual); }
|
|
45
|
+
const valid = streams.filter(st => st.type === "hls" && st.url).sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
46
|
+
for (const st of valid) allUrls.push({ url: st.url, type: "hls", audio: aud, server: st.server ? `NekoWatch-${st.server}` : "NekoWatch", headers: st.referer ? { Referer: st.referer } : undefined, skipProxy: /\.workers\.dev(\/|$)/i.test(st.url) });
|
|
47
|
+
}
|
|
48
|
+
if (allUrls.length) return { allUrls };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function getSources(args) {
|
|
55
|
+
const stream = await getStream(args);
|
|
56
|
+
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
|
57
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = 'https://opstream.fun';
|
|
4
|
+
const API_BASE = `${BASE_URL}/api/resolve`;
|
|
5
|
+
|
|
6
|
+
const API_HEADERS = {
|
|
7
|
+
'User-Agent': USER_AGENT,
|
|
8
|
+
'Accept': 'application/x-ndjson; charset=utf-8',
|
|
9
|
+
'Referer': `${BASE_URL}/`,
|
|
10
|
+
'Origin': BASE_URL,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const STREAM_HEADERS = {
|
|
14
|
+
'User-Agent': USER_AGENT,
|
|
15
|
+
'Referer': `${BASE_URL}/`,
|
|
16
|
+
'Origin': BASE_URL,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function getStream({ id, s, e }) {
|
|
20
|
+
try {
|
|
21
|
+
const isTv = s != null && e != null;
|
|
22
|
+
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
|
23
|
+
|
|
24
|
+
if (!info?.titles?.length || !info.imdbId) return null;
|
|
25
|
+
|
|
26
|
+
const params = new URLSearchParams({
|
|
27
|
+
type: isTv ? 'tv' : 'movie',
|
|
28
|
+
tmdbId: id,
|
|
29
|
+
title: info.titles[0],
|
|
30
|
+
year: info.year || '',
|
|
31
|
+
imdbId: info.imdbId,
|
|
32
|
+
progress: '1'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (isTv) {
|
|
36
|
+
params.append('season', s);
|
|
37
|
+
params.append('episode', e);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const res = await fetch(`${API_BASE}?${params}`, {
|
|
41
|
+
headers: API_HEADERS,
|
|
42
|
+
signal: AbortSignal.timeout(15000)
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!res.ok) return null;
|
|
46
|
+
|
|
47
|
+
const text = await res.text();
|
|
48
|
+
const lines = text.split('\n');
|
|
49
|
+
|
|
50
|
+
let streamData = null;
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed) continue;
|
|
54
|
+
try {
|
|
55
|
+
const json = JSON.parse(trimmed);
|
|
56
|
+
if (json.t === 'done' && json.data) {
|
|
57
|
+
streamData = json.data;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
} catch { continue; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!streamData) return null;
|
|
64
|
+
|
|
65
|
+
const subtitles = (streamData.captions || []).map(c => ({
|
|
66
|
+
url: c.url.startsWith('http') ? c.url : `${BASE_URL}${c.url}`,
|
|
67
|
+
lang: c.label || 'Unknown'
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
const allUrls = [];
|
|
71
|
+
|
|
72
|
+
if (streamData.qualities && streamData.qualities.length > 0) {
|
|
73
|
+
for (const q of streamData.qualities) {
|
|
74
|
+
let finalUrl = q.url;
|
|
75
|
+
if (!finalUrl.startsWith('http')) {
|
|
76
|
+
finalUrl = `${BASE_URL}${finalUrl}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isDash = finalUrl.includes('.mpd');
|
|
80
|
+
const isHls = finalUrl.includes('.m3u8');
|
|
81
|
+
|
|
82
|
+
allUrls.push({
|
|
83
|
+
url: finalUrl,
|
|
84
|
+
server: `OpStream - ${q.label}`,
|
|
85
|
+
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
|
|
86
|
+
headers: STREAM_HEADERS,
|
|
87
|
+
subtitles,
|
|
88
|
+
skipProxy: true
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} else if (streamData.url) {
|
|
92
|
+
let finalUrl = streamData.url;
|
|
93
|
+
if (finalUrl.includes('?u=')) {
|
|
94
|
+
try {
|
|
95
|
+
const uParam = new URL(finalUrl, BASE_URL).searchParams.get('u');
|
|
96
|
+
if (uParam) finalUrl = Buffer.from(uParam, 'base64').toString('utf8');
|
|
97
|
+
} catch {}
|
|
98
|
+
} else if (!finalUrl.startsWith('http')) {
|
|
99
|
+
finalUrl = `${BASE_URL}${finalUrl}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isDash = streamData.kind === 'dash' || finalUrl.includes('.mpd');
|
|
103
|
+
const isHls = streamData.kind === 'hls' || finalUrl.includes('.m3u8');
|
|
104
|
+
|
|
105
|
+
allUrls.push({
|
|
106
|
+
url: finalUrl,
|
|
107
|
+
server: 'OpStream',
|
|
108
|
+
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
|
|
109
|
+
headers: STREAM_HEADERS,
|
|
110
|
+
subtitles,
|
|
111
|
+
skipProxy: true
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (allUrls.length === 0) return null;
|
|
116
|
+
|
|
117
|
+
return { allUrls };
|
|
118
|
+
} catch { return null; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function getSources() {
|
|
122
|
+
return ['OpStream'];
|
|
123
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const DEC_API = "https://enc-dec.app/api/dec-peachify";
|
|
4
|
+
const ORIGIN = "https://peachify.top";
|
|
5
|
+
const HEADERS = { "User-Agent": USER_AGENT, "Origin": ORIGIN, "Referer": `${ORIGIN}/` };
|
|
6
|
+
|
|
7
|
+
const SERVERS = [
|
|
8
|
+
{ label: "Wolf", path: "air", api: "https://usa.eat-peach.sbs" },
|
|
9
|
+
{ label: "Spider", path: "holly", api: "https://usa.eat-peach.sbs" },
|
|
10
|
+
{ label: "Iron", path: "moviebox", api: "https://uwu.eat-peach.sbs" },
|
|
11
|
+
{ label: "Multi", path: "multi", api: "https://usa.eat-peach.sbs" },
|
|
12
|
+
{ label: "Dark", path: "net", api: "https://uwu.eat-peach.sbs" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export async function getStream({ id, s, e, server }) {
|
|
16
|
+
try {
|
|
17
|
+
const isTv = s != null && e != null;
|
|
18
|
+
const segment = isTv ? `tv/${id}/${s}/${e}` : `movie/${id}`;
|
|
19
|
+
|
|
20
|
+
let targets = SERVERS;
|
|
21
|
+
if (server && server !== 'all') {
|
|
22
|
+
const cleanName = server.replace('Peachify - ', '');
|
|
23
|
+
targets = SERVERS.filter(srv => srv.label === cleanName);
|
|
24
|
+
if (!targets.length) targets = SERVERS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const settled = await Promise.allSettled(targets.map(async srv => {
|
|
28
|
+
const url = `${srv.api}/${srv.path}/${segment}`;
|
|
29
|
+
const rawData = await fetchJson(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
|
30
|
+
|
|
31
|
+
if (!rawData?.data) throw new Error();
|
|
32
|
+
|
|
33
|
+
const decJson = await fetchJson(DEC_API, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ text: rawData.data }),
|
|
37
|
+
signal: AbortSignal.timeout(10000)
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
|
41
|
+
const res = decJson.result;
|
|
42
|
+
|
|
43
|
+
const sources = Array.isArray(res) ? res : (res.sources || [res]);
|
|
44
|
+
const subs = res.subtitles || [];
|
|
45
|
+
|
|
46
|
+
return sources.map(source => {
|
|
47
|
+
const streamUrl = source.url || source.file || source.link;
|
|
48
|
+
if (!streamUrl) return null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
url: streamUrl,
|
|
52
|
+
server: `Peachify - ${srv.label}`,
|
|
53
|
+
quality: source.quality || "Auto",
|
|
54
|
+
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
|
55
|
+
headers: { ...HEADERS, "Origin": ORIGIN },
|
|
56
|
+
subtitles: subs.map(sub => ({
|
|
57
|
+
url: sub.url || sub.file,
|
|
58
|
+
lang: sub.language || sub.lang || sub.label || 'Unknown'
|
|
59
|
+
})),
|
|
60
|
+
skipProxy: false,
|
|
61
|
+
skipVerify: true,
|
|
62
|
+
skipHlsCheck: true
|
|
63
|
+
};
|
|
64
|
+
}).filter(Boolean);
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const allUrls = [];
|
|
68
|
+
for (const r of settled) {
|
|
69
|
+
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return allUrls.length ? { allUrls } : null;
|
|
73
|
+
} catch { return null; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function getSources() {
|
|
77
|
+
return SERVERS.map(s => `Peachify - ${s.label}`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fetchJson } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
|
|
4
|
+
const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
|
|
5
|
+
|
|
6
|
+
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
|
|
7
|
+
const key = tmdbApiKey;
|
|
8
|
+
if (!key) return null;
|
|
9
|
+
try {
|
|
10
|
+
const url = s != null && e != null
|
|
11
|
+
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
|
|
12
|
+
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
|
|
13
|
+
const res = await fetchJson(url);
|
|
14
|
+
return res?.imdb_id || null;
|
|
15
|
+
} catch { return null; }
|
|
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 res = await fetchJson(`${BASE_URL}${path}`);
|
|
28
|
+
if (!res?.streams?.length) return null;
|
|
29
|
+
|
|
30
|
+
const allUrls = res.streams.map(stream => {
|
|
31
|
+
if (!stream.url) return null;
|
|
32
|
+
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
|
|
33
|
+
return {
|
|
34
|
+
url: stream.url,
|
|
35
|
+
server: `Pengu - ${stream.name || 'Server'}`,
|
|
36
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
37
|
+
skipProxy: stream.behaviorHints?.notWebReady === false
|
|
38
|
+
};
|
|
39
|
+
}).filter(Boolean);
|
|
40
|
+
|
|
41
|
+
return allUrls.length ? { allUrls } : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const DOMAIN = 'https://purstream.club';
|
|
4
|
+
const API_BASE = 'https://api.purstream.club/api/v1';
|
|
5
|
+
|
|
6
|
+
const HEADERS = {
|
|
7
|
+
'User-Agent': USER_AGENT,
|
|
8
|
+
'Accept': 'application/json, text/plain, */*',
|
|
9
|
+
'Referer': `${DOMAIN}/`,
|
|
10
|
+
'Origin': DOMAIN,
|
|
11
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
12
|
+
'Sec-Fetch-Dest': 'empty',
|
|
13
|
+
'Sec-Fetch-Mode': 'cors',
|
|
14
|
+
'Sec-Fetch-Site': 'same-site'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function getStream({ id, s, e }) {
|
|
18
|
+
try {
|
|
19
|
+
const isTv = s != null;
|
|
20
|
+
|
|
21
|
+
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if (!info?.titles?.length) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const title = info.titles[0];
|
|
29
|
+
|
|
30
|
+
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
|
|
31
|
+
|
|
32
|
+
const searchData = await fetchJson(searchUrl, {
|
|
33
|
+
headers: HEADERS,
|
|
34
|
+
signal: AbortSignal.timeout(10000)
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const items = searchData?.data?.items?.movies?.items || [];
|
|
38
|
+
|
|
39
|
+
const type = isTv ? 'tv' : 'movie';
|
|
40
|
+
const lowerTitle = title.toLowerCase();
|
|
41
|
+
|
|
42
|
+
let match = items.find(item =>
|
|
43
|
+
item.type === type &&
|
|
44
|
+
item.title?.toLowerCase() === lowerTitle &&
|
|
45
|
+
(!info.year || item.release_date?.startsWith(String(info.year)))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (!match) {
|
|
49
|
+
match = items.find(item => item.type === type);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!match?.id) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const streamUrl = isTv
|
|
57
|
+
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
|
|
58
|
+
: `${API_BASE}/stream/${match.id}`;
|
|
59
|
+
|
|
60
|
+
const json = await fetchJson(streamUrl, {
|
|
61
|
+
headers: HEADERS,
|
|
62
|
+
signal: AbortSignal.timeout(10000)
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const sources = json?.data?.items?.sources;
|
|
66
|
+
|
|
67
|
+
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const chosen = sources.find(src => src.stream_url);
|
|
72
|
+
|
|
73
|
+
if (!chosen?.stream_url) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = {
|
|
78
|
+
url: chosen.stream_url,
|
|
79
|
+
headers: {
|
|
80
|
+
'User-Agent': USER_AGENT,
|
|
81
|
+
'Referer': `${DOMAIN}/`
|
|
82
|
+
},
|
|
83
|
+
server: 'purstream',
|
|
84
|
+
quality: chosen.source_name || 'Auto',
|
|
85
|
+
type: chosen.format || 'hls',
|
|
86
|
+
skipProxy: true
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function getSources(args) {
|
|
97
|
+
const stream = await getStream(args);
|
|
98
|
+
|
|
99
|
+
return stream ? [stream] : [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const VERIFY_HEADERS = {
|
|
103
|
+
...HEADERS
|
|
104
|
+
};
|