@vyla-entertainment/sdk 1.12.0 → 1.14.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.
@@ -1,53 +1,123 @@
1
- import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
1
+ import { getUA } from '../utils/helpers.js';
2
2
 
3
- const API_BASE = 'https://enc-dec.app/api';
4
- const DOMAIN = 'https://vidfast.vc';
5
- const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
3
+ const BASE_URL = 'https://vidfast.vc';
4
+ const API = 'https://enc-dec.app/api';
5
+
6
+ export async function getStream({ id, s, e }) {
7
+ const isTv = Boolean(s && e);
8
+ const targetUrl = isTv
9
+ ? `${BASE_URL}/tv/${id}/${s}/${e}/`
10
+ : `${BASE_URL}/movie/${id}`;
11
+
12
+ const ua = getUA();
13
+ const baseHeaders = {
14
+ 'User-Agent': ua,
15
+ 'Referer': `${BASE_URL}/`,
16
+ 'X-Requested-With': 'XMLHttpRequest',
17
+ };
6
18
 
7
- async function getVidfastMeta(id, s, e) {
8
19
  try {
9
- const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
10
- const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
11
-
12
- const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
13
- if (!match?.[1]) return null;
14
-
15
- const encData = await fetchJson(`${API_BASE}/enc-vidfast?text=${encodeURIComponent(match[1])}`);
16
- if (encData.status !== 200 || !encData.result) return null;
17
-
18
- const { servers: serversUrl, stream: streamUrl, token } = encData.result;
19
- const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
20
-
21
- const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
22
- const decServersData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: serversEncrypted }) });
23
-
24
- if (decServersData.status !== 200 || !decServersData.result) return null;
25
- return { servers: decServersData.result, streamUrl, reqHeaders };
26
- } catch { return null; }
27
- }
28
-
29
- export async function getStream({ id, s, e, server }) {
30
- const meta = await getVidfastMeta(id, s, e);
31
- if (!meta?.servers?.length) return null;
32
- const { servers, streamUrl, reqHeaders } = meta;
33
- let targets = servers;
34
- if (server && server !== 'all') {
35
- const cleanName = server.replace('VidFast - ', '');
36
- targets = servers.filter(srv => srv.name === cleanName);
37
- if (!targets.length) targets = servers;
20
+ const pageRes = await fetch(targetUrl, {
21
+ headers: { 'User-Agent': ua },
22
+ signal: AbortSignal.timeout(10000),
23
+ });
24
+ if (!pageRes.ok) return null;
25
+ const html = await pageRes.text();
26
+
27
+ const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
28
+ if (!match || !match[1]) return null;
29
+ const enToken = match[1];
30
+
31
+ const s1Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(enToken)}&stage=1`, {
32
+ signal: AbortSignal.timeout(8000),
33
+ });
34
+ const s1Data = await s1Res.json();
35
+ if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
36
+
37
+ const token1 = s1Data.result.token;
38
+ const stage1Url = s1Data.result.stage1;
39
+
40
+ const post1Res = await fetch(stage1Url, {
41
+ method: 'POST',
42
+ headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
43
+ signal: AbortSignal.timeout(10000),
44
+ });
45
+ if (!post1Res.ok) return null;
46
+ const stage1Text = await post1Res.text();
47
+
48
+ const s2Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(stage1Text)}&stage=2`, {
49
+ signal: AbortSignal.timeout(8000),
50
+ });
51
+ const s2Data = await s2Res.json();
52
+ if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
53
+
54
+ const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
55
+ const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
56
+
57
+ const serversRes = await fetch(serversUrl, {
58
+ method: 'POST',
59
+ headers: currentHeaders,
60
+ signal: AbortSignal.timeout(10000),
61
+ });
62
+ if (!serversRes.ok) return null;
63
+ const serversEncrypted = await serversRes.text();
64
+
65
+ const decServersRes = await fetch(`${API}/dec-vidfast`, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({ text: serversEncrypted }),
69
+ signal: AbortSignal.timeout(8000),
70
+ });
71
+ const decServersData = await decServersRes.json();
72
+ if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
73
+
74
+ const allUrls = [];
75
+
76
+ for (const server of decServersData.result) {
77
+ if (!server?.data) continue;
78
+ try {
79
+ const streamUrl = `${streamBaseUrl}/${server.data}`;
80
+ const streamEncRes = await fetch(streamUrl, {
81
+ method: 'POST',
82
+ headers: currentHeaders,
83
+ signal: AbortSignal.timeout(8000),
84
+ });
85
+ if (!streamEncRes.ok) continue;
86
+ const streamEncText = await streamEncRes.text();
87
+
88
+ const decStreamRes = await fetch(`${API}/dec-vidfast`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ text: streamEncText }),
92
+ signal: AbortSignal.timeout(8000),
93
+ });
94
+ const decStreamData = await decStreamRes.json();
95
+ if (decStreamData?.status !== 200 || !decStreamData.result) continue;
96
+
97
+ const streamResult = decStreamData.result;
98
+ const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
99
+
100
+ if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
101
+ allUrls.push({
102
+ url: resultUrl,
103
+ label: server.name || server.title || 'VidFast',
104
+ headers: {
105
+ 'User-Agent': ua,
106
+ 'Referer': `${BASE_URL}/`,
107
+ },
108
+ });
109
+ }
110
+ } catch { }
111
+ }
112
+
113
+ if (allUrls.length === 0) return null;
114
+
115
+ return {
116
+ url: allUrls[0].url,
117
+ allUrls,
118
+ headers: allUrls[0].headers,
119
+ };
120
+ } catch {
121
+ return null;
38
122
  }
39
- const results = await Promise.allSettled(targets.map(async srv => {
40
- const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
41
- const decStreamData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: streamEncrypted }) });
42
- if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
43
- return { url: decStreamData.result.url, server: `VidFast - ${srv.name || 'Server'}`, type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4', subtitles: (decStreamData.result.captions || []).map(c => ({ url: c.file, lang: c.label || 'Unknown' })), headers: { ...HEADERS, 'Origin': DOMAIN } };
44
- }));
45
- const allUrls = [];
46
- for (const r of results) if (r.status === 'fulfilled') allUrls.push(r.value);
47
- return allUrls.length ? { allUrls } : null;
48
- }
49
-
50
- export async function getSources(args) {
51
- const meta = await getVidfastMeta(args.id, args.s, args.e);
52
- return meta?.servers ? meta.servers.map(srv => `VidFast - ${srv.name}`) : [];
53
123
  }
@@ -1,79 +1,123 @@
1
- import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
1
+ import { getUA } from '../utils/helpers.js';
2
2
 
3
- const API_BASE = 'https://enc-dec.app/api';
4
- const DOMAIN = 'https://vidup.to';
5
- const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
3
+ const BASE_URL = 'https://vidup.to';
4
+ const API = 'https://enc-dec.app/api';
5
+
6
+ export async function getStream({ id, s, e }) {
7
+ const isTv = Boolean(s && e);
8
+ const targetUrl = isTv
9
+ ? `${BASE_URL}/tv/${id}/${s}/${e}/`
10
+ : `${BASE_URL}/movie/${id}`;
11
+
12
+ const ua = getUA();
13
+ const baseHeaders = {
14
+ 'User-Agent': ua,
15
+ 'Referer': `${BASE_URL}/`,
16
+ 'X-Requested-With': 'XMLHttpRequest',
17
+ };
6
18
 
7
- async function getVidupMeta(id, s, e) {
8
19
  try {
9
- const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
10
- const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
20
+ const pageRes = await fetch(targetUrl, {
21
+ headers: { 'User-Agent': ua },
22
+ signal: AbortSignal.timeout(10000),
23
+ });
24
+ if (!pageRes.ok) return null;
25
+ const html = await pageRes.text();
11
26
 
12
- const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
13
- if (!match?.[1]) return null;
27
+ const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
28
+ if (!match || !match[1]) return null;
29
+ const enToken = match[1];
14
30
 
15
- const encData = await fetchJson(`${API_BASE}/enc-vidup?text=${encodeURIComponent(match[1])}`);
16
- if (encData.status !== 200 || !encData.result) return null;
31
+ const s1Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(enToken)}&stage=1`, {
32
+ signal: AbortSignal.timeout(8000),
33
+ });
34
+ const s1Data = await s1Res.json();
35
+ if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
17
36
 
18
- const { servers: serversUrl, stream: streamUrl, token } = encData.result;
19
- const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
37
+ const token1 = s1Data.result.token;
38
+ const stage1Url = s1Data.result.stage1;
20
39
 
21
- const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
22
- const decServersData = await fetchJson(`${API_BASE}/dec-vidup`, {
40
+ const post1Res = await fetch(stage1Url, {
23
41
  method: 'POST',
24
- headers: { 'Content-Type': 'application/json' },
25
- body: JSON.stringify({ text: serversEncrypted })
42
+ headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
43
+ signal: AbortSignal.timeout(10000),
26
44
  });
45
+ if (!post1Res.ok) return null;
46
+ const stage1Text = await post1Res.text();
27
47
 
28
- if (decServersData.status !== 200 || !decServersData.result) return null;
29
- return { servers: decServersData.result, streamUrl, reqHeaders };
30
- } catch { return null; }
31
- }
32
-
33
- export async function getStream({ id, s, e, server }) {
34
- const meta = await getVidupMeta(id, s, e);
35
- if (!meta?.servers?.length) return null;
48
+ const s2Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(stage1Text)}&stage=2`, {
49
+ signal: AbortSignal.timeout(8000),
50
+ });
51
+ const s2Data = await s2Res.json();
52
+ if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
36
53
 
37
- const { servers, streamUrl, reqHeaders } = meta;
38
- let targets = servers;
54
+ const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
55
+ const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
39
56
 
40
- if (server && server !== 'all') {
41
- const cleanName = server.replace('VidUp - ', '');
42
- targets = servers.filter(srv => srv.name === cleanName);
43
- if (!targets.length) targets = servers;
44
- }
57
+ const serversRes = await fetch(serversUrl, {
58
+ method: 'POST',
59
+ headers: currentHeaders,
60
+ signal: AbortSignal.timeout(10000),
61
+ });
62
+ if (!serversRes.ok) return null;
63
+ const serversEncrypted = await serversRes.text();
45
64
 
46
- const results = await Promise.allSettled(targets.map(async srv => {
47
- const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
48
- const decStreamData = await fetchJson(`${API_BASE}/dec-vidup`, {
65
+ const decServersRes = await fetch(`${API}/dec-vidup`, {
49
66
  method: 'POST',
50
67
  headers: { 'Content-Type': 'application/json' },
51
- body: JSON.stringify({ text: streamEncrypted })
68
+ body: JSON.stringify({ text: serversEncrypted }),
69
+ signal: AbortSignal.timeout(8000),
52
70
  });
53
-
54
- if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
71
+ const decServersData = await decServersRes.json();
72
+ if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
73
+
74
+ const allUrls = [];
75
+
76
+ for (const server of decServersData.result) {
77
+ if (!server?.data) continue;
78
+ try {
79
+ const streamUrl = `${streamBaseUrl}/${server.data}`;
80
+ const streamEncRes = await fetch(streamUrl, {
81
+ method: 'POST',
82
+ headers: currentHeaders,
83
+ signal: AbortSignal.timeout(8000),
84
+ });
85
+ if (!streamEncRes.ok) continue;
86
+ const streamEncText = await streamEncRes.text();
87
+
88
+ const decStreamRes = await fetch(`${API}/dec-vidup`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ text: streamEncText }),
92
+ signal: AbortSignal.timeout(8000),
93
+ });
94
+ const decStreamData = await decStreamRes.json();
95
+ if (decStreamData?.status !== 200 || !decStreamData.result) continue;
96
+
97
+ const streamResult = decStreamData.result;
98
+ const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
99
+
100
+ if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
101
+ allUrls.push({
102
+ url: resultUrl,
103
+ label: server.name || server.title || 'VidUp',
104
+ headers: {
105
+ 'User-Agent': ua,
106
+ 'Referer': `${BASE_URL}/`,
107
+ },
108
+ });
109
+ }
110
+ } catch { }
111
+ }
112
+
113
+ if (allUrls.length === 0) return null;
55
114
 
56
115
  return {
57
- url: decStreamData.result.url,
58
- server: `VidUp - ${srv.name || 'Server'}`,
59
- type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4',
60
- subtitles: (decStreamData.result.captions || []).map(c => ({
61
- url: c.file,
62
- lang: c.label || 'Unknown'
63
- })),
64
- headers: { ...HEADERS, 'Origin': DOMAIN }
116
+ url: allUrls[0].url,
117
+ allUrls,
118
+ headers: allUrls[0].headers,
65
119
  };
66
- }));
67
-
68
- const allUrls = [];
69
- for (const r of results) {
70
- if (r.status === 'fulfilled') allUrls.push(r.value);
120
+ } catch {
121
+ return null;
71
122
  }
72
-
73
- return allUrls.length ? { allUrls } : null;
74
- }
75
-
76
- export async function getSources(args) {
77
- const meta = await getVidupMeta(args.id, args.s, args.e);
78
- return meta?.servers ? meta.servers.map(srv => `VidUp - ${srv.name}`) : [];
79
123
  }
@@ -1,324 +0,0 @@
1
- import { getUA } from '../utils/helpers.js';
2
-
3
- const INITIAL_BASE_URL = 'https://4khdhub.one';
4
- let cachedBaseUrl = null;
5
- let cachedBaseUrlExpiry = 0;
6
-
7
- function rot13(str) {
8
- return str.replace(/[a-zA-Z]/g, (c) => {
9
- const code = c.charCodeAt(0);
10
- if (code >= 65 && code <= 90) {
11
- return String.fromCharCode(((code - 65 + 13) % 26) + 65);
12
- }
13
- return String.fromCharCode(((code - 97 + 13) % 26) + 97);
14
- });
15
- }
16
-
17
- function levenshtein(a, b) {
18
- if (a === b) return 0;
19
- if (!a.length) return b.length;
20
- if (!b.length) return a.length;
21
- const la = a.length;
22
- const lb = b.length;
23
- const d = Array.from({ length: la + 1 }, () => new Array(lb + 1).fill(0));
24
- for (let i = 0; i <= la; i++) d[i][0] = i;
25
- for (let j = 0; j <= lb; j++) d[0][j] = j;
26
- for (let i = 1; i <= la; i++) {
27
- for (let j = 1; j <= lb; j++) {
28
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
29
- d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
30
- }
31
- }
32
- return d[la][lb];
33
- }
34
-
35
- async function getBaseUrl(ua) {
36
- if (cachedBaseUrl && Date.now() < cachedBaseUrlExpiry) {
37
- return cachedBaseUrl;
38
- }
39
-
40
- let currentUrl = INITIAL_BASE_URL;
41
- for (let i = 0; i < 10; i++) {
42
- try {
43
- const res = await fetch(currentUrl, {
44
- method: 'HEAD',
45
- headers: { 'User-Agent': ua },
46
- redirect: 'manual',
47
- signal: AbortSignal.timeout(10000),
48
- });
49
-
50
- if (res.status >= 300 && res.status < 400) {
51
- const location = res.headers.get('location');
52
- if (location) {
53
- currentUrl = new URL(location, currentUrl).toString();
54
- continue;
55
- }
56
- }
57
- break;
58
- } catch {
59
- break;
60
- }
61
- }
62
-
63
- const parsed = new URL(currentUrl);
64
- cachedBaseUrl = `${parsed.protocol}//${parsed.host}`;
65
- cachedBaseUrlExpiry = Date.now() + 3600000;
66
- return cachedBaseUrl;
67
- }
68
-
69
- async function resolveMetadata(sdk, id, isTv) {
70
- const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
71
- if (!apiKey) return null;
72
- const type = isTv ? 'tv' : 'movie';
73
- try {
74
- const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, {
75
- signal: AbortSignal.timeout(8000),
76
- });
77
- if (!res.ok) return null;
78
- const data = await res.json();
79
- const title = isTv ? data.name : data.title;
80
- const releaseDate = isTv ? data.first_air_date : data.release_date;
81
- const year = releaseDate ? parseInt(releaseDate.split('-')[0], 10) : null;
82
- return { title, year };
83
- } catch {
84
- return null;
85
- }
86
- }
87
-
88
- function sanitizeStreamUrl(rawUrl) {
89
- try {
90
- const parsed = new URL(rawUrl);
91
- parsed.pathname = parsed.pathname.replaceAll('::', '%3A%3A');
92
- return parsed.toString();
93
- } catch {
94
- return rawUrl.replaceAll('::', '%3A%3A');
95
- }
96
- }
97
-
98
- async function validateStreamUrl(url, ua) {
99
- try {
100
- const res = await fetch(url, {
101
- headers: {
102
- 'User-Agent': ua,
103
- Range: 'bytes=0-1024',
104
- },
105
- signal: AbortSignal.timeout(4000),
106
- });
107
-
108
- const contentType = (res.headers.get('content-type') || '').toLowerCase();
109
- const isSuccess = (res.status >= 200 && res.status < 300) || res.status === 206;
110
- const isHtmlOrJsonError = contentType.includes('text/html') || contentType.includes('application/json');
111
-
112
- if (!isSuccess || isHtmlOrJsonError) return false;
113
- return true;
114
- } catch {
115
- return true;
116
- }
117
- }
118
-
119
- async function resolveRedirectUrl(redirectUrl, ua) {
120
- try {
121
- const res = await fetch(redirectUrl, {
122
- headers: { 'User-Agent': ua },
123
- signal: AbortSignal.timeout(15000),
124
- });
125
- if (!res.ok) return null;
126
- const html = await res.text();
127
-
128
- let nextUrl = '';
129
- const downloadMatch = html.match(/<a id="download" href="(.*?)"/);
130
- if (downloadMatch) {
131
- nextUrl = downloadMatch[1];
132
- } else {
133
- const varMatch = html.match(/var url = '(.*?)';/);
134
- if (varMatch) {
135
- nextUrl = varMatch[1];
136
- }
137
- }
138
-
139
- if (!nextUrl) {
140
- const match = html.match(/'o','(.*?)'/);
141
- if (!match) return null;
142
- const rawData = match[1];
143
- const step1 = Buffer.from(rawData, 'base64').toString('utf8');
144
- const step2 = Buffer.from(step1, 'base64').toString('utf8');
145
- const step3 = rot13(step2);
146
- const step4 = Buffer.from(step3, 'base64').toString('utf8');
147
- const data = JSON.parse(step4);
148
- return Buffer.from(data.o, 'base64').toString('utf8');
149
- }
150
-
151
- const interRes = await fetch(nextUrl, {
152
- headers: { 'User-Agent': ua },
153
- signal: AbortSignal.timeout(15000),
154
- });
155
- if (!interRes.ok) return null;
156
- const interHtml = await interRes.text();
157
-
158
- const finalMatch = interHtml.match(/<a href="([^"]+)"[^>]*class="[^"]*btn-success/);
159
- if (finalMatch) {
160
- return finalMatch[1];
161
- }
162
-
163
- return null;
164
- } catch {
165
- return null;
166
- }
167
- }
168
-
169
- async function extractSourceResults(itemHtml, ua) {
170
- const sizeMatch = itemHtml.match(/([\d.]+ ?[GM]B)/);
171
- const sizeStr = sizeMatch ? sizeMatch[1] : null;
172
-
173
- const heightMatch = itemHtml.match(/\d{3,}p/);
174
- const heightStr = heightMatch ? heightMatch[0] : null;
175
-
176
- const fileTitleMatch = itemHtml.match(/class="(?:file-title|episode-file-title)"[^>]*>([^<]+)/);
177
- const fileTitle = fileTitleMatch ? fileTitleMatch[1].trim() : 'Unknown';
178
-
179
- const displayParts = [fileTitle];
180
- if (heightStr) displayParts.push(heightStr);
181
- if (sizeStr) displayParts.push(sizeStr);
182
-
183
- let hubCloudUrl = null;
184
- let hubDriveUrl = null;
185
-
186
- const linkRegex = /<a\s+[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
187
- let match;
188
- while ((match = linkRegex.exec(itemHtml)) !== null) {
189
- const href = match[1];
190
- const text = match[2];
191
- if (text.includes('HubCloud') && !hubCloudUrl) {
192
- hubCloudUrl = href;
193
- } else if (text.includes('HubDrive') && !hubDriveUrl) {
194
- hubDriveUrl = href;
195
- }
196
- }
197
-
198
- const redirectUrl = hubCloudUrl || hubDriveUrl;
199
- if (!redirectUrl) return null;
200
-
201
- const resolvedUrl = await resolveRedirectUrl(redirectUrl, ua);
202
- if (!resolvedUrl) return null;
203
-
204
- const safeUrl = sanitizeStreamUrl(resolvedUrl);
205
- const isValid = await validateStreamUrl(safeUrl, ua);
206
- if (!isValid) return null;
207
-
208
- return {
209
- url: safeUrl,
210
- label: displayParts.join(' · '),
211
- headers: {
212
- 'User-Agent': ua,
213
- },
214
- };
215
- }
216
-
217
- export async function getStream({ id, s, e, sdk }) {
218
- const isTv = Boolean(s && e);
219
- const ua = getUA();
220
-
221
- const meta = await resolveMetadata(sdk, id, isTv);
222
- if (!meta?.title) return null;
223
-
224
- const baseUrl = await getBaseUrl(ua);
225
- const searchUrl = `${baseUrl}/?s=${encodeURIComponent(meta.title)}`;
226
-
227
- const searchRes = await fetch(searchUrl, {
228
- headers: { 'User-Agent': ua },
229
- signal: AbortSignal.timeout(15000),
230
- });
231
- if (!searchRes.ok) return null;
232
- const searchHtml = await searchRes.text();
233
-
234
- const cardRegex = /<a\s+([^>]*\bclass="[^"]*movie-card[^"]*"[^>]*)>([\s\S]*?)<\/a>/gi;
235
- const formatFilter = isTv ? 'Series' : 'Movies';
236
- let targetPageUrl = null;
237
-
238
- let cardMatch;
239
- while ((cardMatch = cardRegex.exec(searchHtml)) !== null) {
240
- const attributes = cardMatch[1];
241
- const inner = cardMatch[2];
242
-
243
- const hrefMatch = attributes.match(/href="([^"]+)"/);
244
- if (!hrefMatch) continue;
245
- const href = hrefMatch[1];
246
-
247
- const formatMatches = [...inner.matchAll(/class="movie-card-format"[^>]*>([\s\S]*?)<\//gi)];
248
- const formatTexts = formatMatches.map(m => m[1].trim());
249
- if (!formatTexts.some(t => t.includes(formatFilter))) continue;
250
-
251
- if (meta.year != null) {
252
- const metaMatch = inner.match(/class="movie-card-meta"[^>]*>([\s\S]*?)<\//i);
253
- const yearMatch = metaMatch ? metaMatch[1].match(/\d{4}/) : null;
254
- if (yearMatch) {
255
- const cardYear = parseInt(yearMatch[0], 10);
256
- if (Math.abs(cardYear - meta.year) > 1) continue;
257
- }
258
- }
259
-
260
- const titleMatch = inner.match(/class="movie-card-title"[^>]*>([\s\S]*?)<\//i);
261
- let cardTitle = titleMatch ? titleMatch[1] : '';
262
- cardTitle = cardTitle.replace(/\[.*?\]/g, '').trim();
263
-
264
- const diff = levenshtein(cardTitle.toLowerCase(), meta.title.toLowerCase());
265
- const isMatch = diff < 5 || (cardTitle.toLowerCase().includes(meta.title.toLowerCase()) && diff < 16);
266
- if (!isMatch) continue;
267
-
268
- targetPageUrl = new URL(href, baseUrl).toString();
269
- break;
270
- }
271
-
272
- if (!targetPageUrl) return null;
273
-
274
- const pageRes = await fetch(targetPageUrl, {
275
- headers: { 'User-Agent': ua },
276
- signal: AbortSignal.timeout(15000),
277
- });
278
- if (!pageRes.ok) return null;
279
- const pageHtml = await pageRes.text();
280
-
281
- const matchedHtmlBlocks = [];
282
-
283
- if (isTv) {
284
- const seasonStr = String(s).padStart(2, '0');
285
- const episodeStr = String(e).padStart(2, '0');
286
-
287
- const epItemRegex = /class="[^"]*episode-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-item"|<\/main>|$))/gi;
288
- let epMatch;
289
- while ((epMatch = epItemRegex.exec(pageHtml)) !== null) {
290
- const epBlock = epMatch[1];
291
- const titleMatch = epBlock.match(/class="episode-title"[^>]*>([\s\S]*?)<\//i);
292
- const titleText = titleMatch ? titleMatch[1] : '';
293
- if (!titleText.includes(`S${seasonStr}`)) continue;
294
-
295
- const dlItemRegex = /class="[^"]*episode-download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-download-item"|<\/div>\s*<\/div>|$))/gi;
296
- let dlMatch;
297
- while ((dlMatch = dlItemRegex.exec(epBlock)) !== null) {
298
- const dlBlock = dlMatch[1];
299
- if (dlBlock.includes(`Episode-${episodeStr}`)) {
300
- matchedHtmlBlocks.push(dlBlock);
301
- }
302
- }
303
- }
304
- } else {
305
- const dlItemRegex = /class="[^"]*download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*download-item"|<\/main>|$))/gi;
306
- let dlMatch;
307
- while ((dlMatch = dlItemRegex.exec(pageHtml)) !== null) {
308
- matchedHtmlBlocks.push(dlMatch[1]);
309
- }
310
- }
311
-
312
- if (matchedHtmlBlocks.length === 0) return null;
313
-
314
- const results = await Promise.all(matchedHtmlBlocks.map((block) => extractSourceResults(block, ua)));
315
- const allUrls = results.filter(Boolean);
316
-
317
- if (allUrls.length === 0) return null;
318
-
319
- return {
320
- url: allUrls[0].url,
321
- allUrls,
322
- headers: allUrls[0].headers,
323
- };
324
- }