@vyla-entertainment/sdk 1.21.0 → 1.23.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/README.md +2 -1
- package/package.json +1 -1
- package/src/config.js +145 -0
- package/src/health.js +60 -20
- package/src/sources/4movies.js +77 -0
- package/src/sources/aether.js +99 -0
- package/src/sources/braflix.js +80 -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/src/sources/zenox.js +108 -0
|
@@ -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
|
+
}
|