@vyla-entertainment/sdk 1.11.0 → 1.13.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.
@@ -0,0 +1,150 @@
1
+ import { getUA } from '../utils/helpers.js';
2
+
3
+ const BASE_URL = 'https://dulo.mov';
4
+
5
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6
+
7
+ async function resolveDetails(sdk, id, isTv) {
8
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
9
+ if (!apiKey) return null;
10
+ const type = isTv ? 'tv' : 'movie';
11
+ try {
12
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, {
13
+ signal: AbortSignal.timeout(6000),
14
+ });
15
+ if (!res.ok) return null;
16
+ const data = await res.json();
17
+ const title = isTv ? data.name : data.title;
18
+ const releaseDate = isTv ? data.first_air_date : data.release_date;
19
+ const year = releaseDate ? releaseDate.split('-')[0] : '';
20
+ const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90);
21
+ return { title, year, runtime };
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ async function fetchSource(url, headers) {
28
+ try {
29
+ const res = await fetch(url, {
30
+ headers,
31
+ signal: AbortSignal.timeout(8000),
32
+ });
33
+ if (!res.ok) return null;
34
+ return await res.json();
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export async function getStream({ id, s, e, sdk }) {
41
+ const isTv = Boolean(s && e);
42
+ const mediaType = isTv ? 'tv' : 'movie';
43
+ const ua = getUA();
44
+
45
+ const details = await resolveDetails(sdk, id, isTv);
46
+ const title = details?.title || '';
47
+ const year = details?.year || '';
48
+ const runtime = details?.runtime || '';
49
+
50
+ const referer = isTv
51
+ ? `${BASE_URL}/watch/tv/${id}/${s}/${e}`
52
+ : `${BASE_URL}/watch/movie/${id}`;
53
+
54
+ const defaultHeaders = {
55
+ 'User-Agent': ua,
56
+ Referer: referer,
57
+ Origin: BASE_URL,
58
+ Accept: '*/*',
59
+ 'Sec-Fetch-Dest': 'empty',
60
+ 'Sec-Fetch-Mode': 'cors',
61
+ 'Sec-Fetch-Site': 'same-origin',
62
+ };
63
+
64
+ const baseParams = new URLSearchParams({
65
+ tmdbId: String(id),
66
+ type: mediaType,
67
+ });
68
+
69
+ const additionalParams = new URLSearchParams({
70
+ tmdbId: String(id),
71
+ type: mediaType,
72
+ progressive: 'true',
73
+ });
74
+
75
+ if (title) additionalParams.set('title', title);
76
+ if (year) additionalParams.set('year', String(year));
77
+ if (runtime) additionalParams.set('runtime', String(runtime));
78
+
79
+ if (isTv) {
80
+ baseParams.set('season', String(s));
81
+ baseParams.set('episode', String(e));
82
+ additionalParams.set('season', String(s));
83
+ additionalParams.set('episode', String(e));
84
+ }
85
+
86
+ const endpoints = [
87
+ `${BASE_URL}/api/sources?${baseParams.toString()}`,
88
+ `${BASE_URL}/api/sources/willow?${baseParams.toString()}`,
89
+ `${BASE_URL}/api/sources/additional?${additionalParams.toString()}`,
90
+ ];
91
+
92
+ const seenUrls = new Set();
93
+ const allUrls = [];
94
+
95
+ let pendingUrls = [...endpoints];
96
+ const maxPollTime = Date.now() + 16000;
97
+
98
+ while (pendingUrls.length > 0 && Date.now() < maxPollTime) {
99
+ const results = await Promise.all(
100
+ pendingUrls.map((url) => fetchSource(url, defaultHeaders))
101
+ );
102
+
103
+ const nextPending = [];
104
+
105
+ results.forEach((data, index) => {
106
+ if (!data) return;
107
+
108
+ if (Array.isArray(data.sources) && data.sources.length > 0) {
109
+ for (const src of data.sources) {
110
+ let streamUrl = String(src?.url || '').trim();
111
+ if (!streamUrl) continue;
112
+
113
+ if (streamUrl.startsWith('/')) {
114
+ streamUrl = `${BASE_URL}${streamUrl}`;
115
+ }
116
+
117
+ if (!streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
118
+ seenUrls.add(streamUrl);
119
+
120
+ allUrls.push({
121
+ url: streamUrl,
122
+ label: src.label || 'Dulo',
123
+ headers: {
124
+ 'User-Agent': ua,
125
+ Referer: referer,
126
+ },
127
+ captions: Array.isArray(src.captions) ? src.captions : [],
128
+ });
129
+ }
130
+ }
131
+
132
+ if (data.pending) {
133
+ nextPending.push(pendingUrls[index]);
134
+ }
135
+ });
136
+
137
+ pendingUrls = nextPending;
138
+ if (pendingUrls.length > 0) {
139
+ await sleep(1500);
140
+ }
141
+ }
142
+
143
+ if (allUrls.length === 0) return null;
144
+
145
+ return {
146
+ url: allUrls[0].url,
147
+ allUrls,
148
+ headers: allUrls[0].headers,
149
+ };
150
+ }
@@ -0,0 +1,117 @@
1
+ import { getUA } from '../utils/helpers.js';
2
+
3
+ const BASE_URL = 'https://movienig.ht';
4
+
5
+ const PRIORITY_SERVERS = [
6
+ { id: 'dallas', label: 'Dallas 4K' },
7
+ { id: 'austin', label: 'Austin' },
8
+ { id: 'helena', label: 'Helena' },
9
+ { id: 'seattle', label: 'Seattle 4K' },
10
+ { id: 'vixsrc-1', label: 'Newport Beach' },
11
+ { id: 'tucson', label: 'Tucson' },
12
+ { id: 'salem', label: 'Salem' },
13
+ ];
14
+
15
+ async function resolveDetails(sdk, id, isTv) {
16
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
17
+ if (!apiKey) return null;
18
+ const type = isTv ? 'tv' : 'movie';
19
+ try {
20
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
21
+ signal: AbortSignal.timeout(6000),
22
+ });
23
+ if (!res.ok) return null;
24
+ const data = await res.json();
25
+ const title = isTv ? data.name : data.title;
26
+ const releaseDate = isTv ? data.first_air_date : data.release_date;
27
+ const year = releaseDate ? parseInt(releaseDate.split('-')[0], 10) : null;
28
+ const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
29
+ return { title, year, imdbId };
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export async function getStream({ id, s, e, sdk }) {
36
+ const isTv = Boolean(s && e);
37
+ const ua = getUA();
38
+
39
+ const details = await resolveDetails(sdk, id, isTv);
40
+ const title = details?.title || String(id);
41
+ const year = details?.year;
42
+ const imdbId = details?.imdbId;
43
+
44
+ const encTitle = encodeURIComponent(title);
45
+ const yearQuery = year ? `&year=${year}` : '';
46
+ const imdbQuery = imdbId ? `&imdbId=${imdbId}` : '';
47
+
48
+ const seenUrls = new Set();
49
+ const allUrls = [];
50
+
51
+ const defaultHeaders = {
52
+ 'User-Agent': ua,
53
+ Referer: `${BASE_URL}/`,
54
+ Origin: BASE_URL,
55
+ Accept: 'text/event-stream',
56
+ };
57
+
58
+ const tasks = PRIORITY_SERVERS.map(async (server) => {
59
+ try {
60
+ const url = isTv
61
+ ? `${BASE_URL}/api/stream/v1/tv/${id}/${s}/${e}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`
62
+ : `${BASE_URL}/api/stream/v1/movie/${id}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`;
63
+
64
+ const res = await fetch(url, {
65
+ headers: defaultHeaders,
66
+ signal: AbortSignal.timeout(6000),
67
+ });
68
+
69
+ if (!res.ok) return;
70
+ const bodyStr = await res.text();
71
+
72
+ if (bodyStr.includes('event: done')) {
73
+ const doneIdx = bodyStr.indexOf('event: done');
74
+ const dataIdx = bodyStr.indexOf('data: ', doneIdx);
75
+
76
+ if (dataIdx !== -1) {
77
+ const jsonStart = dataIdx + 6;
78
+ const jsonEnd = bodyStr.indexOf('\n', jsonStart);
79
+ const jsonText = (jsonEnd !== -1 ? bodyStr.substring(jsonStart, jsonEnd) : bodyStr.substring(jsonStart)).trim();
80
+
81
+ const data = JSON.parse(jsonText);
82
+ if (Array.isArray(data?.sources)) {
83
+ for (const src of data.sources) {
84
+ if (!src || typeof src !== 'object') continue;
85
+ const rawUrl = String(src.url || '').trim();
86
+ if (!rawUrl || seenUrls.has(rawUrl)) continue;
87
+
88
+ seenUrls.add(rawUrl);
89
+
90
+ const quality = src.quality ? String(src.quality) : 'Auto';
91
+ const titleQuality = quality !== 'Auto' ? ` (${quality})` : '';
92
+
93
+ allUrls.push({
94
+ url: rawUrl,
95
+ label: `MovieNight ${server.label}${titleQuality}`,
96
+ headers: {
97
+ 'User-Agent': ua,
98
+ Referer: `${BASE_URL}/`,
99
+ },
100
+ });
101
+ }
102
+ }
103
+ }
104
+ }
105
+ } catch { }
106
+ });
107
+
108
+ await Promise.all(tasks);
109
+
110
+ if (allUrls.length === 0) return null;
111
+
112
+ return {
113
+ url: allUrls[0].url,
114
+ allUrls,
115
+ headers: allUrls[0].headers,
116
+ };
117
+ }
@@ -0,0 +1,282 @@
1
+ import { getUA } from '../utils/helpers.js';
2
+
3
+ const API_BASE = 'https://api.wecollege.net';
4
+ const REFERER = 'https://www.movy.bz/';
5
+ const ORIGIN = 'https://www.movy.bz';
6
+ const MAGIC = [109, 118, 109, 49];
7
+
8
+ const SERVERS = [
9
+ { endpoint: 'miami', name: 'Miami', note: 'Original audio (Up to 4K)' },
10
+ { endpoint: 'seattle', name: 'Seattle', note: 'Original audio' },
11
+ { endpoint: 'denver', name: 'Denver', note: 'Original audio' },
12
+ { endpoint: 'chicago', name: 'Chicago', note: 'Original audio' },
13
+ { endpoint: 'dallas', name: 'Dallas', note: 'Original audio' },
14
+ { endpoint: 'atlanta', name: 'Atlanta', note: 'Original audio' },
15
+ { endpoint: 'houston', name: 'Houston', note: 'Original audio' },
16
+ { endpoint: 'austin', name: 'Austin', note: 'Original audio' },
17
+ { endpoint: 'boston', name: 'Boston', note: 'Original audio' },
18
+ { endpoint: 'munich', name: 'Munich', note: 'German audio', extra: 'language=german' },
19
+ { endpoint: 'berlin', name: 'Berlin', note: 'German audio' },
20
+ { endpoint: 'paris', name: 'Paris', note: 'French audio' },
21
+ { endpoint: 'delhi', name: 'Delhi', note: 'Hindi audio' },
22
+ { endpoint: 'cancun', name: 'Cancun', note: 'Spanish audio' },
23
+ ];
24
+
25
+ const seedCache = new Map();
26
+
27
+ function _l(e) {
28
+ let v = e >>> 0;
29
+ v = (v ^ (v >>> 16)) >>> 0;
30
+ v = Math.imul(v, 0x85ebca6b) >>> 0;
31
+ v = (v ^ (v >>> 13)) >>> 0;
32
+ v = Math.imul(v, 0xc2b2ae35) >>> 0;
33
+ return (v ^ (v >>> 16)) >>> 0;
34
+ }
35
+
36
+ function _u(e, t) {
37
+ const shift = t & 31;
38
+ if (shift === 0) return e >>> 0;
39
+ return (((e << shift) >>> 0) | (e >>> (32 - shift))) >>> 0;
40
+ }
41
+
42
+ function _fnv1a(str) {
43
+ let t = 0x811c9dc5 >>> 0;
44
+ for (let i = 0; i < str.length; i++) {
45
+ const code = str.charCodeAt(i);
46
+ t = Math.imul((t ^ code) >>> 0, 0x1000193) >>> 0;
47
+ }
48
+ return _l(t);
49
+ }
50
+
51
+ function _initKeyState(seed, tmdbId) {
52
+ const s = new Array(61).fill(0);
53
+ const isSet = new Array(61).fill(false);
54
+ const idVal = ((tmdbId >>> 0) ^ 0x9e3779b9) >>> 0;
55
+ let r = _l((_fnv1a(seed) ^ _l(idVal)) >>> 0);
56
+
57
+ for (let e = 0; e < 8; e++) {
58
+ const t = r % 61;
59
+ r = _u((r + 0x9e3779b9) >>> 0, 7 + (7 & e));
60
+ s[t] = (r ^ _l(r)) >>> 0;
61
+ isSet[t] = true;
62
+ r = _l((r + t) >>> 0);
63
+ }
64
+
65
+ const acc = _l((0xa5a5a5a5 ^ r) >>> 0);
66
+ return { s, isSet, acc };
67
+ }
68
+
69
+ function _nextKeystreamWord(state, t) {
70
+ const r = state.s;
71
+ let nState = state.acc;
72
+ const i = nState % 61;
73
+ const d = state.isSet[i] ? r[i] : 0;
74
+ const c = Math.imul((t + 1) >>> 0, 0x9e3779b9) >>> 0;
75
+ const a = nState;
76
+ const sVal = (d ^ c) >>> 0;
77
+ const h = state.isSet[i] ? ((a | sVal) >>> 0) : ((a ^ sVal) >>> 0);
78
+ const term1 = _u((h + nState) >>> 0, 31 & i);
79
+ const term2 = _u(nState, 31 & (i * 7));
80
+ nState = _l((((term1 ^ term2) >>> 0) + 0x9e3779b9) >>> 0);
81
+ r[i] = nState;
82
+ state.isSet[i] = true;
83
+ state.acc = nState;
84
+ return nState >>> 0;
85
+ }
86
+
87
+ function _generateKeyStream(seed, tmdbId, len) {
88
+ const state = _initKeyState(seed, tmdbId);
89
+ const out = new Uint8Array(len);
90
+ let wordIdx = 0;
91
+ let byteIdx = 0;
92
+
93
+ while (byteIdx < len) {
94
+ const word = _nextKeystreamWord(state, wordIdx++);
95
+ out[byteIdx++] = word & 0xFF;
96
+ if (byteIdx < len) out[byteIdx++] = (word >>> 8) & 0xFF;
97
+ if (byteIdx < len) out[byteIdx++] = (word >>> 16) & 0xFF;
98
+ if (byteIdx < len) out[byteIdx++] = (word >>> 24) & 0xFF;
99
+ }
100
+ return out;
101
+ }
102
+
103
+ function decrypt(cipherB64, seed, tmdbId) {
104
+ try {
105
+ let normalized = cipherB64.replace(/-/g, '+').replace(/_/g, '/');
106
+ while (normalized.length % 4 !== 0) normalized += '=';
107
+ const cipherBytes = Buffer.from(normalized, 'base64');
108
+ if (cipherBytes.length <= MAGIC.length) return null;
109
+
110
+ const ks = _generateKeyStream(seed, tmdbId, cipherBytes.length);
111
+ const decrypted = new Uint8Array(cipherBytes.length);
112
+ for (let i = 0; i < cipherBytes.length; i++) {
113
+ decrypted[i] = cipherBytes[i] ^ ks[i];
114
+ }
115
+
116
+ for (let k = 0; k < MAGIC.length; k++) {
117
+ if (decrypted[k] !== MAGIC[k]) return null;
118
+ }
119
+
120
+ return Buffer.from(decrypted.subarray(MAGIC.length)).toString('utf8');
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ async function getSeed(tmdbId, ua) {
127
+ const now = Date.now();
128
+ const cached = seedCache.get(tmdbId);
129
+ if (cached && cached.expiresAt > now + 5000) {
130
+ return cached.seed;
131
+ }
132
+
133
+ try {
134
+ const res = await fetch(`${API_BASE}/seed?mediaId=${tmdbId}`, {
135
+ headers: {
136
+ 'User-Agent': ua,
137
+ Referer: REFERER,
138
+ Origin: ORIGIN,
139
+ },
140
+ signal: AbortSignal.timeout(8000),
141
+ });
142
+
143
+ if (res.ok) {
144
+ const data = await res.json();
145
+ const seed = data?.seed ? String(data.seed) : '';
146
+ const ttlMs = typeof data?.ttlMs === 'number' ? data.ttlMs : 30000;
147
+ if (seed) {
148
+ seedCache.set(tmdbId, { seed, expiresAt: now + ttlMs });
149
+ return seed;
150
+ }
151
+ }
152
+ } catch { }
153
+
154
+ return null;
155
+ }
156
+
157
+ async function resolveDetails(sdk, id, isTv) {
158
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
159
+ if (!apiKey) return null;
160
+ const type = isTv ? 'tv' : 'movie';
161
+ try {
162
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
163
+ signal: AbortSignal.timeout(6000),
164
+ });
165
+ if (!res.ok) return null;
166
+ const data = await res.json();
167
+ const title = isTv ? data.name : data.title;
168
+ const releaseDate = isTv ? data.first_air_date : data.release_date;
169
+ const year = releaseDate ? parseInt(releaseDate.split('-')[0], 10) : null;
170
+ const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
171
+ return { title, year, imdbId };
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ function formatQuality(raw) {
178
+ const lower = String(raw).toLowerCase();
179
+ if (lower.includes('2160') || lower.includes('4k')) return '4K';
180
+ if (lower.includes('1080')) return '1080p';
181
+ if (lower.includes('720')) return '720p';
182
+ if (lower.includes('480')) return '480p';
183
+ if (lower.includes('360')) return '360p';
184
+ if (raw) return String(raw);
185
+ return 'Auto';
186
+ }
187
+
188
+ export async function getStream({ id, s, e, sdk }) {
189
+ const isTv = Boolean(s && e);
190
+ const mediaType = isTv ? 'tv' : 'movie';
191
+ const tmdbId = parseInt(id, 10);
192
+ const ua = getUA();
193
+
194
+ const details = await resolveDetails(sdk, id, isTv);
195
+ const title = details?.title || String(id);
196
+ const year = details?.year;
197
+ const imdbId = details?.imdbId;
198
+
199
+ const seed = await getSeed(tmdbId, ua);
200
+ if (!seed) return null;
201
+
202
+ const params = new URLSearchParams({
203
+ title,
204
+ mediaType,
205
+ tmdbId: String(tmdbId),
206
+ enc: '2',
207
+ seed,
208
+ });
209
+
210
+ if (year) params.set('year', String(year));
211
+ if (isTv) {
212
+ if (s) params.set('seasonId', String(s));
213
+ if (e) params.set('episodeId', String(e));
214
+ }
215
+ if (imdbId) params.set('imdbId', imdbId);
216
+
217
+ const baseQuery = params.toString();
218
+ const seenUrls = new Set();
219
+ const allUrls = [];
220
+
221
+ const defaultHeaders = {
222
+ 'User-Agent': ua,
223
+ Referer: REFERER,
224
+ Origin: ORIGIN,
225
+ };
226
+
227
+ const tasks = SERVERS.map(async (server) => {
228
+ let fullUrl = `${API_BASE}/${server.endpoint}/sources?${baseQuery}`;
229
+ if (server.extra) fullUrl += `&${server.extra}`;
230
+
231
+ try {
232
+ const res = await fetch(fullUrl, {
233
+ headers: defaultHeaders,
234
+ signal: AbortSignal.timeout(8000),
235
+ });
236
+
237
+ if (!res.ok) return;
238
+ const encText = (await res.text()).trim();
239
+ if (!encText || encText.startsWith('<')) return;
240
+
241
+ const decJsonStr = decrypt(encText, seed, tmdbId);
242
+ if (!decJsonStr) return;
243
+
244
+ const parsed = JSON.parse(decJsonStr);
245
+ if (!Array.isArray(parsed?.sources)) return;
246
+
247
+ for (const src of parsed.sources) {
248
+ if (!src || typeof src !== 'object') continue;
249
+ const streamUrl = String(src.url || '').trim();
250
+ if (!streamUrl || seenUrls.has(streamUrl)) continue;
251
+
252
+ seenUrls.add(streamUrl);
253
+
254
+ const cleanQuality = formatQuality(src.quality || 'Auto');
255
+ const isSpecificAudio = server.note.toLowerCase().includes('audio') && !server.note.toLowerCase().includes('original');
256
+ const langLabel = isSpecificAudio ? server.note.split(' ')[0] : '';
257
+ const label = isSpecificAudio
258
+ ? `${server.name} (${langLabel}) · ${cleanQuality}`
259
+ : `${server.name} · ${cleanQuality}`;
260
+
261
+ allUrls.push({
262
+ url: streamUrl,
263
+ label,
264
+ headers: {
265
+ 'User-Agent': ua,
266
+ Referer: REFERER,
267
+ },
268
+ });
269
+ }
270
+ } catch { }
271
+ });
272
+
273
+ await Promise.all(tasks);
274
+
275
+ if (allUrls.length === 0) return null;
276
+
277
+ return {
278
+ url: allUrls[0].url,
279
+ allUrls,
280
+ headers: allUrls[0].headers,
281
+ };
282
+ }
@@ -1,7 +1,7 @@
1
1
  import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
2
2
 
3
- const DOMAIN = 'https://purstream.club';
4
- const API_BASE = 'https://api.purstream.club/api/v1';
3
+ const DOMAIN = 'https://purstream.ad';
4
+ const API_BASE = 'https://api.purstream.ad/api/v1';
5
5
 
6
6
  const HEADERS = {
7
7
  'User-Agent': USER_AGENT,
@@ -0,0 +1,129 @@
1
+ import { getUA } from '../utils/helpers.js';
2
+
3
+ const BASE_URL = 'https://streamaggregator.in';
4
+
5
+ async function resolveDetails(sdk, id, isTv) {
6
+ const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
7
+ if (!apiKey) return null;
8
+ const type = isTv ? 'tv' : 'movie';
9
+ try {
10
+ const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, {
11
+ signal: AbortSignal.timeout(6000),
12
+ });
13
+ if (!res.ok) return null;
14
+ const data = await res.json();
15
+ const title = isTv ? data.name : data.title;
16
+ const releaseDate = isTv ? data.first_air_date : data.release_date;
17
+ const year = releaseDate ? releaseDate.split('-')[0] : null;
18
+ return { title, year };
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export async function getStream({ id, s, e, sdk }) {
25
+ const isTv = Boolean(s && e);
26
+ const mediaType = isTv ? 'tv' : 'movie';
27
+ const ua = getUA();
28
+
29
+ const details = await resolveDetails(sdk, id, isTv);
30
+ const title = details?.title || String(id);
31
+ const year = details?.year || '';
32
+
33
+ const formattedTitle = encodeURIComponent(title).replaceAll('%20', '+');
34
+ const params = new URLSearchParams({
35
+ type: mediaType,
36
+ stream: 'true',
37
+ });
38
+
39
+ if (year) params.set('year', String(year));
40
+ if (isTv) {
41
+ params.set('season', String(s));
42
+ params.set('episode', String(e));
43
+ }
44
+
45
+ const scrapeUrl = `${BASE_URL}/api/scrape?title=${formattedTitle}&${params.toString()}`;
46
+
47
+ const res = await fetch(scrapeUrl, {
48
+ headers: {
49
+ 'User-Agent': ua,
50
+ Referer: `${BASE_URL}/`,
51
+ Origin: BASE_URL,
52
+ Accept: 'text/event-stream, application/x-ndjson, text/plain, */*',
53
+ },
54
+ signal: AbortSignal.timeout(90000),
55
+ });
56
+
57
+ if (!res.ok) return null;
58
+
59
+ const lines = [];
60
+ if (res.body?.getReader) {
61
+ const reader = res.body.getReader();
62
+ const decoder = new TextDecoder('utf-8');
63
+ let buffer = '';
64
+
65
+ try {
66
+ while (true) {
67
+ const { done, value } = await reader.read();
68
+ if (done) break;
69
+ buffer += decoder.decode(value, { stream: true });
70
+ const parts = buffer.split('\n');
71
+ buffer = parts.pop() || '';
72
+ for (const part of parts) {
73
+ const trimmed = part.trim();
74
+ if (trimmed) lines.push(trimmed);
75
+ }
76
+ }
77
+ } catch { }
78
+ if (buffer.trim()) lines.push(buffer.trim());
79
+ } else {
80
+ const text = await res.text();
81
+ for (const line of text.split('\n')) {
82
+ const trimmed = line.trim();
83
+ if (trimmed) lines.push(trimmed);
84
+ }
85
+ }
86
+
87
+ const seenUrls = new Set();
88
+ const allUrls = [];
89
+
90
+ for (const line of lines) {
91
+ try {
92
+ const raw = line.startsWith('data:') ? line.slice(5).trim() : line;
93
+ if (!raw || raw === '[DONE]') continue;
94
+ const item = JSON.parse(raw);
95
+ const streamUrl = String(item?.url || '').trim();
96
+ if (!streamUrl || !streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
97
+
98
+ seenUrls.add(streamUrl);
99
+
100
+ const serverName = item.server || item.providerKey || 'Stream';
101
+ const quality = item.quality ? ` · ${item.quality}` : '';
102
+ const size = item.size ? ` (${item.size})` : '';
103
+ const label = `${serverName}${quality}${size}`;
104
+
105
+ const headers = {
106
+ 'User-Agent': ua,
107
+ ...(item.headers && typeof item.headers === 'object' ? item.headers : {}),
108
+ };
109
+
110
+ if (item.referer) {
111
+ headers.Referer = item.referer;
112
+ }
113
+
114
+ allUrls.push({
115
+ url: streamUrl,
116
+ label,
117
+ headers,
118
+ });
119
+ } catch { }
120
+ }
121
+
122
+ if (allUrls.length === 0) return null;
123
+
124
+ return {
125
+ url: allUrls[0].url,
126
+ allUrls,
127
+ headers: allUrls[0].headers,
128
+ };
129
+ }