stnh70 commited on
Commit
11367e1
·
verified ·
1 Parent(s): dfd3452

Create web.ts

Browse files
Files changed (1) hide show
  1. web.ts +1424 -0
web.ts ADDED
@@ -0,0 +1,1424 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // // TMDB Embed API for Deno
3
+ // import { serve } from "https://deno.land/std/http/server.ts";
4
+
5
+ // const PORT = Deno.env.get("PORT") || 3000;
6
+
7
+ // // Constants
8
+ // const EMBED_SU_DOMAIN = "https://embed.su";
9
+ // const VIDSRC_SU_DOMAIN = "https://vidsrc.su/";
10
+
11
+ // // Headers
12
+ // const EMBED_SU_HEADERS = {
13
+ // 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
14
+ // 'Referer': EMBED_SU_DOMAIN,
15
+ // 'Origin': EMBED_SU_DOMAIN,
16
+ // };
17
+
18
+ // const VIDSRC_SU_HEADERS = {
19
+ // 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
20
+ // 'Referer': VIDSRC_SU_DOMAIN,
21
+ // 'Origin': VIDSRC_SU_DOMAIN,
22
+ // };
23
+
24
+ // // Helper functions
25
+ // function createErrorObject(provider: string, errorMessage: string) {
26
+ // return {
27
+ // provider,
28
+ // ERROR: [{
29
+ // error: `ERROR`,
30
+ // what_happened: errorMessage,
31
+ // report_issue: 'https://github.com/Inside4ndroid/TMDB-Embed-API/issues'
32
+ // }]
33
+ // };
34
+ // }
35
+
36
+ // function stringAtob(input: string): string {
37
+ // const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
38
+ // let str = input.replace(/=+$/, '');
39
+ // let output = '';
40
+
41
+ // if (str.length % 4 === 1) {
42
+ // throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
43
+ // }
44
+
45
+ // for (let bc = 0, bs = 0, buffer, i = 0; buffer = str.charAt(i++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0) {
46
+ // buffer = chars.indexOf(buffer);
47
+ // }
48
+
49
+ // return output;
50
+ // }
51
+
52
+ // async function requestGet(url: string, headers: Record<string, string> = {}) {
53
+ // try {
54
+ // const response = await fetch(url, { method: 'GET', headers });
55
+
56
+ // if (!response.ok) {
57
+ // throw new Error(`HTTP error! status: ${response.status}`);
58
+ // }
59
+
60
+ // return await response.json();
61
+ // } catch (error) {
62
+ // console.error(`Request failed for ${url}:`, error);
63
+ // return null;
64
+ // }
65
+ // }
66
+
67
+ // function getSizeQuality(url: string): number {
68
+ // try {
69
+ // const parts = url.split('/');
70
+ // const base64Part = parts[parts.length - 2];
71
+ // const decodedPart = atob(base64Part);
72
+ // return Number(decodedPart) || 1080;
73
+ // } catch (e) {
74
+ // console.warn(`Failed to get size quality for URL ${url}:`, e);
75
+ // return 720; // Default quality if parsing fails
76
+ // }
77
+ // }
78
+
79
+ // // Provider functions
80
+ // async function getEmbedSu(tmdb_id: string, s?: string, e?: string) {
81
+ // try {
82
+ // const urlSearch = s && e ? `${EMBED_SU_DOMAIN}/embed/tv/${tmdb_id}/${s}/${e}` : `${EMBED_SU_DOMAIN}/embed/movie/${tmdb_id}`;
83
+ // const htmlSearch = await fetch(urlSearch, { method: 'GET', headers: EMBED_SU_HEADERS });
84
+
85
+ // if (!htmlSearch.ok) {
86
+ // return { sources: [createErrorObject("EmbedSu", `Failed to fetch initial page: HTTP ${htmlSearch.status}`)] };
87
+ // }
88
+
89
+ // const textSearch = await htmlSearch.text();
90
+ // const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i);
91
+ // const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : "";
92
+
93
+ // if (!hashEncode) {
94
+ // return { sources: [createErrorObject("EmbedSu", "No encoded hash found in initial page")] };
95
+ // }
96
+
97
+ // let hashDecode;
98
+ // try {
99
+ // hashDecode = JSON.parse(stringAtob(hashEncode));
100
+ // } catch (e) {
101
+ // return { sources: [createErrorObject("EmbedSu", `Failed to decode initial hash: ${e}`)] };
102
+ // }
103
+
104
+ // const mEncrypt = hashDecode.hash;
105
+ // if (!mEncrypt) {
106
+ // return { sources: [createErrorObject("EmbedSu", "No encrypted hash found in decoded data")] };
107
+ // }
108
+
109
+ // let firstDecode;
110
+ // try {
111
+ // firstDecode = (stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join(""));
112
+ // } catch (e) {
113
+ // return { sources: [createErrorObject("EmbedSu", `Failed to decode first layer: ${e}`)] };
114
+ // }
115
+
116
+ // let secondDecode;
117
+ // try {
118
+ // secondDecode = JSON.parse(stringAtob(firstDecode.join("").split("").reverse().join("")));
119
+ // } catch (e) {
120
+ // return { sources: [createErrorObject("EmbedSu", `Failed to decode second layer: ${e}`)] };
121
+ // }
122
+
123
+ // if (!secondDecode || secondDecode.length === 0) {
124
+ // return { sources: [createErrorObject("EmbedSu", "No valid sources found after decoding")] };
125
+ // }
126
+
127
+ // const sources = [];
128
+ // const subtitles = [];
129
+
130
+ // for (const item of secondDecode) {
131
+ // try {
132
+ // const urlDirect = `${EMBED_SU_DOMAIN}/api/e/${item.hash}`;
133
+ // const dataDirect = await requestGet(urlDirect, {
134
+ // "Referer": EMBED_SU_DOMAIN,
135
+ // "User-Agent": EMBED_SU_HEADERS['User-Agent'],
136
+ // "Origin": EMBED_SU_DOMAIN
137
+ // });
138
+
139
+ // if (!dataDirect || !dataDirect.source) {
140
+ // console.warn(`No source found for hash ${item.hash}`);
141
+ // continue;
142
+ // }
143
+
144
+ // const tracks = (dataDirect.subtitles || []).map((sub: any) => ({
145
+ // url: sub.file,
146
+ // lang: sub.label ? sub.label.split('-')[0].trim() : 'en'
147
+ // })).filter((track: any) => track.url);
148
+
149
+ // const requestDirectSize = await fetch(dataDirect.source, { headers: EMBED_SU_HEADERS, method: "GET" });
150
+ // if (!requestDirectSize.ok) {
151
+ // console.warn(`Failed to fetch source ${dataDirect.source}: HTTP ${requestDirectSize.status}`);
152
+ // continue;
153
+ // }
154
+
155
+ // const parseRequest = await requestDirectSize.text();
156
+ // const patternSize = parseRequest.split('\n').filter(item => item.includes('/proxy/'));
157
+
158
+ // const directQuality = patternSize.map(patternItem => {
159
+ // try {
160
+ // const sizeQuality = getSizeQuality(patternItem);
161
+ // let dURL = `${EMBED_SU_DOMAIN}${patternItem}`;
162
+ // dURL = dURL.replace(".png", ".m3u8");
163
+ // return { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' };
164
+ // } catch (e) {
165
+ // console.warn(`Failed to process quality for pattern: ${patternItem}`, e);
166
+ // return null;
167
+ // }
168
+ // }).filter(item => item !== null);
169
+
170
+ // if (!directQuality.length) {
171
+ // console.warn(`No valid qualities found for source ${dataDirect.source}`);
172
+ // continue;
173
+ // }
174
+
175
+ // return {
176
+ // source: {
177
+ // provider: "EmbedSu",
178
+ // files: directQuality,
179
+ // subtitles: tracks,
180
+ // headers: {
181
+ // "Referer": EMBED_SU_DOMAIN,
182
+ // "User-Agent": EMBED_SU_HEADERS['User-Agent'],
183
+ // "Origin": EMBED_SU_DOMAIN
184
+ // }
185
+ // }
186
+ // };
187
+ // } catch (e) {
188
+ // console.error(`Error processing item ${item.hash}:`, e);
189
+ // // Continue to next item instead of failing entire request
190
+ // }
191
+ // }
192
+
193
+ // if (sources.length === 0) {
194
+ // return { sources: [createErrorObject("EmbedSu", "No valid sources found after processing all items")] };
195
+ // }
196
+
197
+ // return { sources };
198
+ // } catch (e) {
199
+ // return { sources: [createErrorObject("EmbedSu", `Unexpected error: ${e}`)] };
200
+ // }
201
+ // }
202
+
203
+ // async function getVidSrcSu(tmdb_id: string, s?: string, e?: string) {
204
+ // const embedUrl = s && e ? `${VIDSRC_SU_DOMAIN}embed/tv/${tmdb_id}/${s}/${e}` : `${VIDSRC_SU_DOMAIN}embed/movie/${tmdb_id}`;
205
+
206
+ // try {
207
+ // const response = await fetch(embedUrl, { headers: VIDSRC_SU_HEADERS });
208
+ // if (!response.ok) {
209
+ // throw new Error(`HTTP error! status: ${response.status}`);
210
+ // }
211
+
212
+ // const html = await response.text();
213
+ // let subtitles = [];
214
+
215
+ // const servers = [...html.matchAll(/label: 'Server (3|4|5|7|8|9|10|12|13|15|17|18|19)', url: '(https.*)'/g)].map(match => ({
216
+ // file: match[2],
217
+ // type: "hls",
218
+ // lang: "en"
219
+ // }));
220
+
221
+ // const subtitlesMatch = html.match(/const subtitles = \[(.*)\];/g);
222
+ // if (subtitlesMatch && subtitlesMatch[0]) {
223
+ // try {
224
+ // subtitles = JSON.parse(subtitlesMatch[0].replace('const subtitles = ', '').replaceAll(';', ''));
225
+ // subtitles.shift();
226
+ // subtitles = subtitles.map((subtitle: any) => ({
227
+ // url: subtitle.url,
228
+ // lang: subtitle.language,
229
+ // type: subtitle.format
230
+ // }));
231
+ // } catch (parseError) {
232
+ // console.error("Error parsing subtitles:", parseError);
233
+ // subtitles = [];
234
+ // }
235
+ // }
236
+
237
+ // if (!servers || servers.length === 0) {
238
+ // return createErrorObject("VidsrcSU", "No valid video streams found");
239
+ // }
240
+
241
+ // return {
242
+ // source: {
243
+ // provider: "VidsrcSU",
244
+ // files: servers,
245
+ // subtitles: subtitles,
246
+ // headers: {
247
+ // "Referer": VIDSRC_SU_DOMAIN,
248
+ // "User-Agent": VIDSRC_SU_HEADERS['User-Agent'],
249
+ // "Origin": VIDSRC_SU_DOMAIN
250
+ // }
251
+ // }
252
+ // };
253
+ // } catch (error) {
254
+ // return createErrorObject("VidsrcSU", `Unexpected error: ${error}`);
255
+ // }
256
+ // }
257
+
258
+ // // Get all providers for a movie or TV show
259
+ // async function getAllProviders(tmdb_id: string, mediaType: "movie" | "tv", s?: string, e?: string) {
260
+ // const providers = ["embedsu", "vidsrcsu"];
261
+ // const results = [];
262
+
263
+ // for (const provider of providers) {
264
+ // try {
265
+ // let result;
266
+ // if (provider === "embedsu") {
267
+ // result = await getEmbedSu(tmdb_id, s, e);
268
+ // } else if (provider === "vidsrcsu") {
269
+ // result = await getVidSrcSu(tmdb_id, s, e);
270
+ // }
271
+
272
+ // if (result && (!result.sources || !result.sources[0]?.ERROR)) {
273
+ // results.push(result);
274
+ // }
275
+ // } catch (error) {
276
+ // console.error(`Error fetching provider ${provider}:`, error);
277
+ // }
278
+ // }
279
+
280
+ // return results.length > 0 ? results : [createErrorObject("AllProviders", "No valid sources found from any provider")];
281
+ // }
282
+
283
+ // // API Routes
284
+ // async function handleRequest(request: Request): Promise<Response> {
285
+ // const url = new URL(request.url);
286
+ // const path = url.pathname;
287
+ // const searchParams = url.searchParams;
288
+
289
+ // // Parse URL
290
+ // const pathParts = path.split('/').filter(part => part !== '');
291
+
292
+ // // Set CORS headers
293
+ // const headers = {
294
+ // "Access-Control-Allow-Origin": "*",
295
+ // "Access-Control-Allow-Methods": "GET, OPTIONS",
296
+ // "Access-Control-Allow-Headers": "Content-Type",
297
+ // "Content-Type": "application/json"
298
+ // };
299
+
300
+ // // Handle preflight requests
301
+ // if (request.method === "OPTIONS") {
302
+ // return new Response(null, { headers, status: 204 });
303
+ // }
304
+
305
+ // try {
306
+ // // Handle movie routes
307
+ // if (pathParts[0]?.toLowerCase() === "movie") {
308
+ // // Single provider for movie
309
+ // if (pathParts.length === 3) {
310
+ // const provider = pathParts[1].toLowerCase();
311
+ // const tmdbId = pathParts[2];
312
+
313
+ // let result;
314
+ // if (provider === "embedsu") {
315
+ // result = await getEmbedSu(tmdbId);
316
+ // } else if (provider === "vidsrcsu") {
317
+ // result = await getVidSrcSu(tmdbId);
318
+ // } else {
319
+ // return new Response(JSON.stringify(createErrorObject("API", "Invalid provider")), { headers, status: 400 });
320
+ // }
321
+
322
+ // return new Response(JSON.stringify(result), { headers });
323
+ // }
324
+ // // All providers for movie
325
+ // else if (pathParts.length === 2) {
326
+ // const tmdbId = pathParts[1];
327
+ // const results = await getAllProviders(tmdbId, "movie");
328
+ // return new Response(JSON.stringify(results), { headers });
329
+ // }
330
+ // }
331
+ // // Handle TV routes
332
+ // else if (pathParts[0]?.toLowerCase() === "tv") {
333
+ // const season = searchParams.get("s");
334
+ // const episode = searchParams.get("e");
335
+
336
+ // if (!season || !episode) {
337
+ // return new Response(JSON.stringify(createErrorObject("API", "Season and episode parameters are required for TV shows")), { headers, status: 400 });
338
+ // }
339
+
340
+ // // Single provider for TV
341
+ // if (pathParts.length === 3) {
342
+ // const provider = pathParts[1].toLowerCase();
343
+ // const tmdbId = pathParts[2];
344
+
345
+ // let result;
346
+ // if (provider === "embedsu") {
347
+ // result = await getEmbedSu(tmdbId, season, episode);
348
+ // } else if (provider === "vidsrcsu") {
349
+ // result = await getVidSrcSu(tmdbId, season, episode);
350
+ // } else {
351
+ // return new Response(JSON.stringify(createErrorObject("API", "Invalid provider")), { headers, status: 400 });
352
+ // }
353
+
354
+ // return new Response(JSON.stringify(result), { headers });
355
+ // }
356
+ // // All providers for TV
357
+ // else if (pathParts.length === 2) {
358
+ // const tmdbId = pathParts[1];
359
+ // const results = await getAllProviders(tmdbId, "tv", season, episode);
360
+ // return new Response(JSON.stringify(results), { headers });
361
+ // }
362
+ // }
363
+
364
+ // // If no route matches
365
+ // return new Response(JSON.stringify(createErrorObject("API", "Invalid route")), { headers, status: 404 });
366
+ // } catch (error) {
367
+ // console.error("Server error:", error);
368
+ // return new Response(JSON.stringify(createErrorObject("API", `Server error: ${error}`)), { headers, status: 500 });
369
+ // }
370
+ // }
371
+
372
+ // // Start the server
373
+ // console.log(`Starting server on port ${PORT}...`);
374
+ // serve(handleRequest, { port: Number(PORT) });
375
+
376
+ // // TMDB Embed API for Deno
377
+ // import { serve } from "https://deno.land/std/http/server.ts";
378
+
379
+ // const PORT = Deno.env.get("PORT") || 3000;
380
+
381
+ // // --- Constants & Global Config ---
382
+ // const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
383
+
384
+ // // --- Types ---
385
+ // interface MediaFile {
386
+ // file: string;
387
+ // type: string;
388
+ // quality?: string;
389
+ // lang?: string; // Typically for audio tracks, but your EmbedSu example has it for video
390
+ // }
391
+
392
+ // interface Subtitle {
393
+ // url: string;
394
+ // lang: string;
395
+ // type?: string;
396
+ // }
397
+
398
+ // // Structure for the "source" object within a successful provider response
399
+ // interface SourceInfo {
400
+ // provider: string;
401
+ // files: MediaFile[];
402
+ // subtitles: Subtitle[];
403
+ // headers: Record<string, string>;
404
+ // }
405
+
406
+ // // Wrapper for a successful provider's data
407
+ // interface ProviderSuccessResult {
408
+ // source: SourceInfo;
409
+ // }
410
+
411
+ // interface ErrorDetail {
412
+ // error: string;
413
+ // what_happened: string;
414
+ // report_issue: string;
415
+ // }
416
+
417
+ // // Structure for a provider's error response
418
+ // interface ProviderErrorResult {
419
+ // provider: string;
420
+ // ERROR: ErrorDetail[];
421
+ // }
422
+
423
+ // // A provider function can return either a success or an error
424
+ // type ProviderFunctionReturn = ProviderSuccessResult | ProviderErrorResult;
425
+
426
+ // interface ProviderConfig {
427
+ // id: string;
428
+ // displayName: string;
429
+ // domain: string;
430
+ // fetchFunction: (tmdbId: string, s?: string, e?: string) => Promise<ProviderFunctionReturn>;
431
+ // }
432
+
433
+ // // --- Helper functions ---
434
+ // // Changed providerName to "API" for general API errors to avoid confusion with specific provider errors
435
+ // function createApiErrorObject(errorMessage: string, statusProviderName: string = "API"): ProviderErrorResult {
436
+ // return {
437
+ // provider: statusProviderName, // This will be "API" for route errors, or a specific provider for its errors
438
+ // ERROR: [{
439
+ // error: `ERROR`,
440
+ // what_happened: errorMessage,
441
+ // report_issue: 'https://github.com/Inside4ndroid/TMDB-Embed-API/issues'
442
+ // }]
443
+ // };
444
+ // }
445
+ // // Specific helper for provider functions to use their own name
446
+ // function createProviderErrorObject(providerName: string, errorMessage: string): ProviderErrorResult {
447
+ // return createApiErrorObject(errorMessage, providerName);
448
+ // }
449
+
450
+
451
+ // function stringAtob(input: string): string {
452
+ // const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
453
+ // let str = input.replace(/=+$/, '');
454
+ // let output = '';
455
+
456
+ // if (str.length % 4 === 1) {
457
+ // throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
458
+ // }
459
+
460
+ // for (let bc = 0, bs = 0, buffer, i = 0; buffer = str.charAt(i++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0) {
461
+ // buffer = chars.indexOf(buffer);
462
+ // }
463
+ // return output;
464
+ // }
465
+
466
+ // async function requestGet(url: string, headers: Record<string, string> = {}) {
467
+ // try {
468
+ // const response = await fetch(url, { method: 'GET', headers });
469
+ // if (!response.ok) {
470
+ // throw new Error(`HTTP error! status: ${response.status}`);
471
+ // }
472
+ // return await response.json();
473
+ // } catch (error) {
474
+ // console.error(`Request failed for ${url}:`, error);
475
+ // return null;
476
+ // }
477
+ // }
478
+
479
+ // function getSizeQuality(url: string): number {
480
+ // try {
481
+ // const parts = url.split('/');
482
+ // const base64Part = parts[parts.length - 2];
483
+ // const decodedPart = stringAtob(base64Part);
484
+ // return Number(decodedPart) || 1080;
485
+ // } catch (e) {
486
+ // console.warn(`Failed to get size quality for URL ${url}:`, e);
487
+ // return 720;
488
+ // }
489
+ // }
490
+
491
+
492
+ // // --- Provider Specific Logic ---
493
+
494
+ // // == EmbedSu Provider ==
495
+ // const EMBED_SU_DOMAIN = "https://embed.su";
496
+ // const EMBED_SU_HEADERS = {
497
+ // 'User-Agent': USER_AGENT,
498
+ // 'Referer': EMBED_SU_DOMAIN,
499
+ // 'Origin': EMBED_SU_DOMAIN,
500
+ // };
501
+
502
+ // async function getEmbedSu(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> {
503
+ // const providerName = PROVIDERS.embedsu.displayName;
504
+ // try {
505
+ // const urlSearch = s && e ? `${EMBED_SU_DOMAIN}/embed/tv/${tmdb_id}/${s}/${e}` : `${EMBED_SU_DOMAIN}/embed/movie/${tmdb_id}`;
506
+ // const htmlSearchResponse = await fetch(urlSearch, { method: 'GET', headers: EMBED_SU_HEADERS });
507
+
508
+ // if (!htmlSearchResponse.ok) {
509
+ // return createProviderErrorObject(providerName, `Failed to fetch initial page: HTTP ${htmlSearchResponse.status}`);
510
+ // }
511
+
512
+ // const textSearch = await htmlSearchResponse.text();
513
+ // const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i);
514
+ // const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : "";
515
+
516
+ // if (!hashEncode) {
517
+ // return createProviderErrorObject(providerName, "No encoded hash found in initial page");
518
+ // }
519
+
520
+ // let hashDecode;
521
+ // try {
522
+ // hashDecode = JSON.parse(stringAtob(hashEncode));
523
+ // } catch (err) {
524
+ // return createProviderErrorObject(providerName, `Failed to decode initial hash: ${err.message}`);
525
+ // }
526
+
527
+ // const mEncrypt = hashDecode.hash;
528
+ // if (!mEncrypt) {
529
+ // return createProviderErrorObject(providerName, "No encrypted hash found in decoded data");
530
+ // }
531
+
532
+ // let firstDecode;
533
+ // try {
534
+ // firstDecode = (stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join(""));
535
+ // } catch (err) {
536
+ // return createProviderErrorObject(providerName, `Failed to decode first layer: ${err.message}`);
537
+ // }
538
+
539
+ // let secondDecode;
540
+ // try {
541
+ // secondDecode = JSON.parse(stringAtob(firstDecode.join("").split("").reverse().join("")));
542
+ // } catch (err) {
543
+ // return createProviderErrorObject(providerName, `Failed to decode second layer: ${err.message}`);
544
+ // }
545
+
546
+ // if (!secondDecode || !Array.isArray(secondDecode) || secondDecode.length === 0) {
547
+ // return createProviderErrorObject(providerName, "No valid sources found after decoding");
548
+ // }
549
+
550
+ // for (const item of secondDecode) {
551
+ // try {
552
+ // if (!item || !item.hash) continue;
553
+
554
+ // const urlDirect = `${EMBED_SU_DOMAIN}/api/e/${item.hash}`;
555
+ // const dataDirect = await requestGet(urlDirect, {
556
+ // "Referer": EMBED_SU_DOMAIN,
557
+ // "User-Agent": USER_AGENT,
558
+ // "Origin": EMBED_SU_DOMAIN
559
+ // });
560
+
561
+ // if (!dataDirect || !dataDirect.source) {
562
+ // console.warn(`${providerName}: No source found for hash ${item.hash}`);
563
+ // continue;
564
+ // }
565
+
566
+ // const tracks: Subtitle[] = (dataDirect.subtitles || []).map((sub: any) => ({
567
+ // url: sub.file,
568
+ // lang: sub.label ? sub.label.split('-')[0].trim().toLowerCase() : 'en'
569
+ // })).filter((track: Subtitle) => track.url);
570
+
571
+ // const requestDirectSize = await fetch(dataDirect.source, { headers: EMBED_SU_HEADERS, method: "GET" });
572
+ // if (!requestDirectSize.ok) {
573
+ // console.warn(`${providerName}: Failed to fetch source ${dataDirect.source}: HTTP ${requestDirectSize.status}`);
574
+ // continue;
575
+ // }
576
+
577
+ // const parseRequest = await requestDirectSize.text();
578
+ // const patternSize = parseRequest.split('\n').filter(line => line.includes('/proxy/'));
579
+
580
+ // const directQuality: MediaFile[] = patternSize.map(patternItem => {
581
+ // try {
582
+ // const sizeQuality = getSizeQuality(patternItem);
583
+ // let dURL = `${EMBED_SU_DOMAIN}${patternItem}`;
584
+ // dURL = dURL.replace(".png", ".m3u8");
585
+ // // As per your example, EmbedSu files also have 'lang'
586
+ // return { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' };
587
+ // } catch (err) {
588
+ // console.warn(`${providerName}: Failed to process quality for pattern: ${patternItem}`, err);
589
+ // return null;
590
+ // }
591
+ // }).filter((item): item is MediaFile => item !== null);
592
+
593
+ // if (!directQuality.length) {
594
+ // console.warn(`${providerName}: No valid qualities found for source ${dataDirect.source}`);
595
+ // continue;
596
+ // }
597
+
598
+ // return { // Corrected output structure
599
+ // source: {
600
+ // provider: providerName,
601
+ // files: directQuality,
602
+ // subtitles: tracks,
603
+ // headers: {
604
+ // "Referer": EMBED_SU_DOMAIN,
605
+ // "User-Agent": USER_AGENT,
606
+ // "Origin": EMBED_SU_DOMAIN
607
+ // }
608
+ // }
609
+ // };
610
+ // } catch (e) {
611
+ // console.error(`${providerName}: Error processing item ${item.hash}:`, e);
612
+ // }
613
+ // }
614
+ // return createProviderErrorObject(providerName, "No valid sources found after processing all available items");
615
+
616
+ // } catch (e) {
617
+ // console.error(`${providerName}: Unexpected error:`, e);
618
+ // return createProviderErrorObject(providerName, `Unexpected error: ${e.message}`);
619
+ // }
620
+ // }
621
+
622
+ // // == VidSrc.SU Provider ==
623
+ // const VIDSRC_SU_DOMAIN = "https://vidsrc.su/";
624
+ // const VIDSRC_SU_HEADERS = {
625
+ // 'User-Agent': USER_AGENT,
626
+ // 'Referer': VIDSRC_SU_DOMAIN,
627
+ // 'Origin': VIDSRC_SU_DOMAIN,
628
+ // };
629
+
630
+ // async function getVidSrcSu(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> {
631
+ // const providerName = PROVIDERS.vidsrcsu.displayName;
632
+ // const embedUrl = s && e
633
+ // ? `${VIDSRC_SU_DOMAIN}embed/tv/${tmdb_id}/${s}/${e}`
634
+ // : `${VIDSRC_SU_DOMAIN}embed/movie/${tmdb_id}`;
635
+
636
+ // try {
637
+ // const response = await fetch(embedUrl, { headers: VIDSRC_SU_HEADERS });
638
+ // if (!response.ok) {
639
+ // return createProviderErrorObject(providerName, `Failed to fetch embed page: HTTP ${response.status}`);
640
+ // }
641
+
642
+ // const html = await response.text();
643
+ // let subtitles: Subtitle[] = [];
644
+
645
+ // // As per your example, VidSrcSu files also have 'lang'
646
+ // const servers: MediaFile[] = [...html.matchAll(/label: 'Server (?:[^']*)', url: '(https?:\/\/[^']+\.m3u8[^']*)'/gi)].map(match => ({
647
+ // file: match[1],
648
+ // type: "hls",
649
+ // lang: "en" // Defaulting to 'en' as per your example, quality not available here
650
+ // }));
651
+
652
+ // const subtitlesMatch = html.match(/const subtitles = \[(.*?)\];/s);
653
+ // if (subtitlesMatch && subtitlesMatch[1]) {
654
+ // try {
655
+ // const subRaw = `[${subtitlesMatch[1]}]`;
656
+ // let parsedSubs = JSON.parse(subRaw);
657
+ // // Your original code shifted, check if this is intended from your test data
658
+ // // parsedSubs.shift(); // If the first item is a placeholder
659
+
660
+ // subtitles = parsedSubs.filter((sub: any) => sub && sub.url && sub.language)
661
+ // .map((sub: any) => ({
662
+ // url: sub.url,
663
+ // lang: sub.language.toLowerCase(),
664
+ // type: sub.format || (sub.url.includes('.vtt') ? 'vtt' : (sub.url.includes('.srt') ? 'srt' : undefined))
665
+ // }));
666
+ // } catch (parseError) {
667
+ // console.error(`${providerName}: Error parsing subtitles:`, parseError, "Subtitle string:", subtitlesMatch[1]);
668
+ // subtitles = [];
669
+ // }
670
+ // }
671
+
672
+ // if (servers.length === 0) {
673
+ // return createProviderErrorObject(providerName, "No valid video streams found in embed page");
674
+ // }
675
+
676
+ // return { // Corrected output structure
677
+ // source: {
678
+ // provider: providerName,
679
+ // files: servers,
680
+ // subtitles: subtitles,
681
+ // headers: {
682
+ // "Referer": VIDSRC_SU_DOMAIN,
683
+ // "User-Agent": USER_AGENT,
684
+ // "Origin": VIDSRC_SU_DOMAIN
685
+ // }
686
+ // }
687
+ // };
688
+ // } catch (error) {
689
+ // console.error(`${providerName}: Unexpected error:`, error);
690
+ // return createProviderErrorObject(providerName, `Unexpected error: ${error.message}`);
691
+ // }
692
+ // }
693
+
694
+ // // == AutoEmbed Provider ==
695
+ // const AUTOEMBED_DOMAIN = "https://autoembed.cc/";
696
+ // const AUTOEMBED_API_URL_BASE = "https://tom.autoembed.cc/api/getVideoSource";
697
+
698
+ // function parseAutoEmbedM3U8(m3u8Content: string): MediaFile[] {
699
+ // try {
700
+ // const lines = m3u8Content.split('\n');
701
+ // const sources: MediaFile[] = [];
702
+ // let currentQuality: string | undefined = undefined;
703
+
704
+ // for (let i = 0; i < lines.length; i++) {
705
+ // const trimmedLine = lines[i].trim();
706
+
707
+ // if (trimmedLine.startsWith('#EXT-X-STREAM-INF:')) {
708
+ // const resolutionMatch = trimmedLine.match(/RESOLUTION=\d+x(\d+)/);
709
+ // currentQuality = resolutionMatch && resolutionMatch[1] ? `${resolutionMatch[1]}p` : undefined;
710
+
711
+ // for (let j = i + 1; j < lines.length; j++) {
712
+ // const nextLineTrimmed = lines[j].trim();
713
+ // if (nextLineTrimmed && !nextLineTrimmed.startsWith('#')) {
714
+ // // AutoEmbed, like others, might need lang: 'en' if it's consistent
715
+ // sources.push({
716
+ // file: nextLineTrimmed,
717
+ // type: "hls",
718
+ // quality: currentQuality || 'unknown',
719
+ // lang: "en" // Assuming 'en' if not specified, to match other providers
720
+ // });
721
+ // i = j;
722
+ // break;
723
+ // }
724
+ // if (nextLineTrimmed.startsWith('#EXT')) { // Stop if another #EXT tag is found before URL
725
+ // break;
726
+ // }
727
+ // }
728
+ // currentQuality = undefined; // Reset for next stream info
729
+ // }
730
+ // }
731
+ // return sources;
732
+ // } catch (error) {
733
+ // console.error('AutoEmbed: Error parsing m3u8:', error);
734
+ // return [];
735
+ // }
736
+ // }
737
+
738
+ // function mapAutoEmbedSubtitles(apiSubtitles: any[]): Subtitle[] {
739
+ // if (!apiSubtitles || !Array.isArray(apiSubtitles)) return [];
740
+ // try {
741
+ // return apiSubtitles.map(subtitle => {
742
+ // const lang = (subtitle.label || 'unknown').split(' ')[0].toLowerCase();
743
+ // const fileUrl = subtitle.file || '';
744
+ // if (!fileUrl) return null;
745
+
746
+ // const fileExtension = fileUrl.split('.').pop()?.toLowerCase();
747
+ // const type = fileExtension === 'vtt' ? 'vtt' : (fileExtension === 'srt' ? 'srt' : undefined);
748
+
749
+ // return { url: fileUrl, lang: lang, type: type };
750
+ // }).filter((sub): sub is Subtitle => sub !== null && !!sub.url);
751
+ // } catch (error) {
752
+ // console.error('AutoEmbed: Error mapping subtitles:', error);
753
+ // return [];
754
+ // }
755
+ // }
756
+
757
+ // async function getAutoEmbed(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> {
758
+ // const providerName = PROVIDERS.autoembed.displayName;
759
+
760
+ // const params = new URLSearchParams();
761
+ // if (s && e) {
762
+ // params.append("type", "tv");
763
+ // params.append("id", `${tmdb_id}/${s}/${e}`); // Format per your original code
764
+ // } else {
765
+ // params.append("type", "movie");
766
+ // params.append("id", tmdb_id);
767
+ // }
768
+ // const apiUrl = `${AUTOEMBED_API_URL_BASE}?${params.toString()}`;
769
+
770
+ // try {
771
+ // const response = await fetch(apiUrl, {
772
+ // headers: {
773
+ // 'Referer': AUTOEMBED_DOMAIN, // Referer for the API call itself
774
+ // 'User-Agent': USER_AGENT,
775
+ // }
776
+ // });
777
+
778
+ // if (!response.ok) {
779
+ // let errorBody = "";
780
+ // try { errorBody = await response.text(); } catch (_) {}
781
+ // return createProviderErrorObject(providerName, `API request failed: HTTP ${response.status}. ${errorBody}`);
782
+ // }
783
+
784
+ // const data = await response.json();
785
+
786
+ // if (data.error || !data.videoSource) {
787
+ // return createProviderErrorObject(providerName, data.error || "No videoSource found in API response");
788
+ // }
789
+
790
+ // const m3u8Url = data.videoSource;
791
+ // const m3u8Response = await fetch(m3u8Url, { // Fetching the M3U8 may also need specific headers
792
+ // headers: {
793
+ // 'Referer': AUTOEMBED_DOMAIN, // Often M3U8s require referer of the page that linked them
794
+ // 'User-Agent': USER_AGENT
795
+ // }
796
+ // });
797
+
798
+ // if (!m3u8Response.ok) {
799
+ // return createProviderErrorObject(providerName, `Failed to fetch m3u8 from ${m3u8Url}: HTTP ${m3u8Response.status}`);
800
+ // }
801
+
802
+ // const m3u8Content = await m3u8Response.text();
803
+ // const files = parseAutoEmbedM3U8(m3u8Content);
804
+
805
+ // if (files.length === 0) {
806
+ // return createProviderErrorObject(providerName, "No valid streams found after parsing m3u8");
807
+ // }
808
+
809
+ // const subtitles = data.subtitles ? mapAutoEmbedSubtitles(data.subtitles) : [];
810
+
811
+ // return { // Corrected output structure
812
+ // source: {
813
+ // provider: providerName,
814
+ // files: files,
815
+ // subtitles: subtitles,
816
+ // headers: { // Headers client might need for playback
817
+ // "Referer": AUTOEMBED_DOMAIN, // This is the referer for the final stream
818
+ // "User-Agent": USER_AGENT,
819
+ // "Origin": AUTOEMBED_DOMAIN
820
+ // }
821
+ // }
822
+ // };
823
+ // } catch (error) {
824
+ // console.error(`${providerName}: Unexpected error - `, error);
825
+ // return createProviderErrorObject(providerName, `Network or processing error: ${error.message}`);
826
+ // }
827
+ // }
828
+
829
+
830
+ // // --- Provider Configuration ---
831
+ // const PROVIDERS: Record<string, ProviderConfig> = {
832
+ // embedsu: {
833
+ // id: "embedsu",
834
+ // displayName: "EmbedSu",
835
+ // domain: EMBED_SU_DOMAIN,
836
+ // fetchFunction: getEmbedSu,
837
+ // },
838
+ // vidsrcsu: {
839
+ // id: "vidsrcsu",
840
+ // displayName: "VidsrcSU",
841
+ // domain: VIDSRC_SU_DOMAIN,
842
+ // fetchFunction: getVidSrcSu,
843
+ // },
844
+ // autoembed: {
845
+ // id: "autoembed",
846
+ // displayName: "AutoEmbed",
847
+ // domain: AUTOEMBED_DOMAIN,
848
+ // fetchFunction: getAutoEmbed,
849
+ // },
850
+ // };
851
+
852
+
853
+ // // --- Core Logic ---
854
+ // // getAllProviders now returns Array<ProviderSuccessResult> or Array<ProviderErrorResult> (with a single API error if all fail)
855
+ // async function getAllProviders(tmdb_id: string, mediaType: "movie" | "tv", s?: string, e?: string): Promise<ProviderSuccessResult[] | [ProviderErrorResult]> {
856
+ // const providerFetchPromises: Promise<ProviderFunctionReturn>[] = [];
857
+
858
+ // for (const providerId in PROVIDERS) {
859
+ // const config = PROVIDERS[providerId];
860
+ // providerFetchPromises.push(
861
+ // config.fetchFunction(tmdb_id, s, e)
862
+ // .catch(err => {
863
+ // console.error(`Critical error during ${config.displayName} fetch: ${err}`);
864
+ // // Return a ProviderErrorResult if the fetchFunction itself throws an unhandled error
865
+ // return createProviderErrorObject(config.displayName, `Internal unhandled error: ${err.message || String(err)}`);
866
+ // })
867
+ // );
868
+ // }
869
+
870
+ // if (providerFetchPromises.length === 0) {
871
+ // // This case should ideally not be hit if PROVIDERS is populated
872
+ // return [createApiErrorObject("No providers are configured.")];
873
+ // }
874
+
875
+ // const allResults = await Promise.all(providerFetchPromises);
876
+
877
+ // // Filter for successful results (those that have a 'source' property)
878
+ // const successfulResults = allResults.filter(
879
+ // (r): r is ProviderSuccessResult => r !== null && typeof r === 'object' && 'source' in r
880
+ // );
881
+
882
+ // if (successfulResults.length > 0) {
883
+ // return successfulResults; // Returns Array<{ source: SourceInfo }>
884
+ // } else {
885
+ // // All providers failed or returned error objects.
886
+ // // Return a single error object in an array, as per your original API behavior.
887
+ // return [createApiErrorObject("No valid sources found from any provider.")];
888
+ // }
889
+ // }
890
+
891
+
892
+ // // --- API Routes & Server ---
893
+ // async function handleRequest(request: Request): Promise<Response> {
894
+ // const url = new URL(request.url);
895
+ // const path = url.pathname;
896
+ // const searchParams = url.searchParams;
897
+
898
+ // const pathParts = path.split('/').filter(part => part !== '');
899
+
900
+ // const corsHeaders = {
901
+ // "Access-Control-Allow-Origin": "*",
902
+ // "Access-Control-Allow-Methods": "GET, OPTIONS",
903
+ // "Access-Control-Allow-Headers": "Content-Type",
904
+ // "Content-Type": "application/json"
905
+ // };
906
+
907
+ // if (request.method === "OPTIONS") {
908
+ // return new Response(null, { headers: corsHeaders, status: 204 });
909
+ // }
910
+
911
+ // try {
912
+ // const mediaType = pathParts[0]?.toLowerCase();
913
+ // // resultData can be a single success/error obj, or an array of success/error objs
914
+ // let resultData: ProviderFunctionReturn | ProviderSuccessResult[] | [ProviderErrorResult];
915
+
916
+ // if (mediaType === "movie" || mediaType === "tv") {
917
+ // const tmdbIdInput = pathParts.length > 2 ? pathParts[2] : pathParts[1];
918
+ // const providerIdInput = pathParts.length > 2 ? pathParts[1].toLowerCase() : null;
919
+
920
+ // if (!tmdbIdInput || !/^\d+$/.test(tmdbIdInput)) {
921
+ // resultData = createApiErrorObject("Valid TMDB ID is required.");
922
+ // return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 });
923
+ // }
924
+ // const tmdbId = tmdbIdInput;
925
+
926
+ // let season: string | undefined = undefined;
927
+ // let episode: string | undefined = undefined;
928
+
929
+ // if (mediaType === "tv") {
930
+ // const sParam = searchParams.get("s");
931
+ // const eParam = searchParams.get("e");
932
+ // if (!sParam || !eParam) {
933
+ // resultData = createApiErrorObject("Season (s) and episode (e) parameters are required for TV shows.");
934
+ // return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 });
935
+ // }
936
+ // season = sParam;
937
+ // episode = eParam;
938
+ // }
939
+
940
+ // if (providerIdInput) { // Single provider request
941
+ // const providerConfig = PROVIDERS[providerIdInput];
942
+ // if (providerConfig) {
943
+ // resultData = await providerConfig.fetchFunction(tmdbId, season, episode);
944
+ // // If resultData is an error, it will be ProviderErrorResult {provider: "...", ERROR: ...}
945
+ // // If success, it will be ProviderSuccessResult {source: {...}}
946
+ // } else {
947
+ // resultData = createApiErrorObject(`Invalid provider: ${providerIdInput}. Available: ${Object.keys(PROVIDERS).join(', ')}`);
948
+ // return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 });
949
+ // }
950
+ // } else { // All providers request
951
+ // resultData = await getAllProviders(tmdbId, mediaType, season, episode);
952
+ // // resultData will be ProviderSuccessResult[] or [ProviderErrorResult]
953
+ // }
954
+ // } else {
955
+ // resultData = createApiErrorObject("Invalid route. Use /movie/tmdb_id or /tv/tmdb_id?s=S&e=E. For specific provider: /movie/provider_name/tmdb_id");
956
+ // return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 404 });
957
+ // }
958
+
959
+ // // Now resultData is either a single object (success or error) or an array (of successes or a single API error)
960
+ // return new Response(JSON.stringify(resultData), { headers: corsHeaders });
961
+
962
+ // } catch (error) {
963
+ // console.error("Server error:", error);
964
+ // const errorResponse = createApiErrorObject(`Server error: ${error.message || String(error)}`);
965
+ // return new Response(JSON.stringify(errorResponse), { headers: corsHeaders, status: 500 });
966
+ // }
967
+ // }
968
+
969
+ // console.log(`TMDB Embed API server starting on port ${PORT}...`);
970
+ // console.log(`Available providers: ${Object.keys(PROVIDERS).join(', ')}`);
971
+ // console.log("Routes:");
972
+ // console.log(" GET /movie/{tmdb_id} -> Returns Array or [ErrorObject]");
973
+ // console.log(" GET /movie/{provider_id}/{tmdb_id} -> Returns Object (Success or Error)");
974
+ // console.log(" GET /tv/{tmdb_id}?s={season_number}&e={episode_number} -> Returns Array or [ErrorObject]");
975
+ // console.log(" GET /tv/{provider_id}/{tmdb_id}?s={season_number}&e={episode_number} -> Returns Object (Success or Error)");
976
+
977
+ // serve(handleRequest, { port: Number(PORT) });
978
+ // TMDB Embed API for Deno
979
+ import { serve } from "https://deno.land/std/http/server.ts";
980
+
981
+ const PORT = Deno.env.get("PORT") || 3000;
982
+
983
+ // --- Constants & Global Config ---
984
+ const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
985
+
986
+ // --- Types ---
987
+ interface MediaFile {
988
+ file: string;
989
+ type: string;
990
+ quality?: string;
991
+ lang?: string;
992
+ season?: string | number; // Added for TV shows
993
+ episode?: string | number; // Added for TV shows
994
+ }
995
+
996
+ interface Subtitle {
997
+ url: string;
998
+ lang: string;
999
+ type?: string;
1000
+ }
1001
+
1002
+ interface SourceInfo {
1003
+ provider: string;
1004
+ files: MediaFile[];
1005
+ subtitles: Subtitle[];
1006
+ headers: Record<string, string>;
1007
+ }
1008
+
1009
+ interface ProviderSuccessResult {
1010
+ source: SourceInfo;
1011
+ }
1012
+
1013
+ interface ErrorDetail {
1014
+ error: string;
1015
+ what_happened: string;
1016
+ report_issue: string;
1017
+ }
1018
+
1019
+ interface ProviderErrorResult {
1020
+ provider: string;
1021
+ ERROR: ErrorDetail[];
1022
+ }
1023
+
1024
+ type ProviderFunctionReturn = ProviderSuccessResult | ProviderErrorResult;
1025
+
1026
+ interface ProviderConfig {
1027
+ id: string;
1028
+ displayName: string;
1029
+ domain: string;
1030
+ fetchFunction: (tmdbId: string, s?: string, e?: string) => Promise<ProviderFunctionReturn>;
1031
+ }
1032
+
1033
+ // --- Helper functions ---
1034
+ function createApiErrorObject(errorMessage: string, statusProviderName: string = "API"): ProviderErrorResult {
1035
+ return {
1036
+ provider: statusProviderName,
1037
+ ERROR: [{
1038
+ error: `ERROR`,
1039
+ what_happened: errorMessage,
1040
+ report_issue: 'https://github.com/Inside4ndroid/TMDB-Embed-API/issues'
1041
+ }]
1042
+ };
1043
+ }
1044
+ function createProviderErrorObject(providerName: string, errorMessage: string): ProviderErrorResult {
1045
+ return createApiErrorObject(errorMessage, providerName);
1046
+ }
1047
+
1048
+ function stringAtob(input: string): string {
1049
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
1050
+ let str = input.replace(/=+$/, '');
1051
+ let output = '';
1052
+ if (str.length % 4 === 1) throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
1053
+ for (let bc = 0, bs = 0, buffer, i = 0; buffer = str.charAt(i++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0) {
1054
+ buffer = chars.indexOf(buffer);
1055
+ }
1056
+ return output;
1057
+ }
1058
+
1059
+ async function requestGet(url: string, headers: Record<string, string> = {}) {
1060
+ try {
1061
+ const response = await fetch(url, { method: 'GET', headers });
1062
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
1063
+ return await response.json();
1064
+ } catch (error) {
1065
+ console.error(`Request failed for ${url}:`, error);
1066
+ return null;
1067
+ }
1068
+ }
1069
+
1070
+ function getSizeQuality(url: string): number {
1071
+ try {
1072
+ const parts = url.split('/');
1073
+ const base64Part = parts[parts.length - 2];
1074
+ const decodedPart = stringAtob(base64Part);
1075
+ return Number(decodedPart) || 1080;
1076
+ } catch (e) {
1077
+ console.warn(`Failed to get size quality for URL ${url}:`, e);
1078
+ return 720;
1079
+ }
1080
+ }
1081
+
1082
+ // --- Provider Specific Logic ---
1083
+
1084
+ // == EmbedSu Provider ==
1085
+ const EMBED_SU_DOMAIN = "https://embed.su";
1086
+ const EMBED_SU_HEADERS = {
1087
+ 'User-Agent': USER_AGENT,
1088
+ 'Referer': EMBED_SU_DOMAIN,
1089
+ 'Origin': EMBED_SU_DOMAIN,
1090
+ };
1091
+
1092
+ async function getEmbedSu(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> {
1093
+ const providerName = PROVIDERS.embedsu.displayName;
1094
+ try {
1095
+ const urlSearch = s && e ? `${EMBED_SU_DOMAIN}/embed/tv/${tmdb_id}/${s}/${e}` : `${EMBED_SU_DOMAIN}/embed/movie/${tmdb_id}`;
1096
+ const htmlSearchResponse = await fetch(urlSearch, { method: 'GET', headers: EMBED_SU_HEADERS });
1097
+ if (!htmlSearchResponse.ok) return createProviderErrorObject(providerName, `Failed to fetch initial page: HTTP ${htmlSearchResponse.status}`);
1098
+
1099
+ const textSearch = await htmlSearchResponse.text();
1100
+ const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i);
1101
+ const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : "";
1102
+ if (!hashEncode) return createProviderErrorObject(providerName, "No encoded hash found in initial page");
1103
+
1104
+ let hashDecode;
1105
+ try { hashDecode = JSON.parse(stringAtob(hashEncode)); }
1106
+ catch (err) { return createProviderErrorObject(providerName, `Failed to decode initial hash: ${err.message}`); }
1107
+
1108
+ const mEncrypt = hashDecode.hash;
1109
+ if (!mEncrypt) return createProviderErrorObject(providerName, "No encrypted hash found in decoded data");
1110
+
1111
+ let firstDecode;
1112
+ try { firstDecode = (stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join("")); }
1113
+ catch (err) { return createProviderErrorObject(providerName, `Failed to decode first layer: ${err.message}`); }
1114
+
1115
+ let secondDecode;
1116
+ try { secondDecode = JSON.parse(stringAtob(firstDecode.join("").split("").reverse().join(""))); }
1117
+ catch (err) { return createProviderErrorObject(providerName, `Failed to decode second layer: ${err.message}`); }
1118
+
1119
+ if (!secondDecode || !Array.isArray(secondDecode) || secondDecode.length === 0) {
1120
+ return createProviderErrorObject(providerName, "No valid sources found after decoding");
1121
+ }
1122
+
1123
+ for (const item of secondDecode) {
1124
+ try {
1125
+ if (!item || !item.hash) continue;
1126
+ const urlDirect = `${EMBED_SU_DOMAIN}/api/e/${item.hash}`;
1127
+ const dataDirect = await requestGet(urlDirect, { "Referer": EMBED_SU_DOMAIN, "User-Agent": USER_AGENT, "Origin": EMBED_SU_DOMAIN });
1128
+ if (!dataDirect || !dataDirect.source) { console.warn(`${providerName}: No source found for hash ${item.hash}`); continue; }
1129
+
1130
+ const tracks: Subtitle[] = (dataDirect.subtitles || []).map((sub: any) => ({
1131
+ url: sub.file, lang: sub.label ? sub.label.split('-')[0].trim().toLowerCase() : 'en'
1132
+ })).filter((track: Subtitle) => track.url);
1133
+
1134
+ const requestDirectSize = await fetch(dataDirect.source, { headers: EMBED_SU_HEADERS, method: "GET" });
1135
+ if (!requestDirectSize.ok) { console.warn(`${providerName}: Failed to fetch source ${dataDirect.source}: HTTP ${requestDirectSize.status}`); continue; }
1136
+
1137
+ const parseRequest = await requestDirectSize.text();
1138
+ const patternSize = parseRequest.split('\n').filter(line => line.includes('/proxy/'));
1139
+
1140
+ const directQuality: MediaFile[] = patternSize.map(patternItem => {
1141
+ try {
1142
+ const sizeQuality = getSizeQuality(patternItem);
1143
+ let dURL = `${EMBED_SU_DOMAIN}${patternItem}`;
1144
+ dURL = dURL.replace(".png", ".m3u8");
1145
+ const fileObj: MediaFile = { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' };
1146
+ if (s && e) { // Add season and episode if they exist (TV show)
1147
+ fileObj.season = s;
1148
+ fileObj.episode = e;
1149
+ }
1150
+ return fileObj;
1151
+ } catch (err) { console.warn(`${providerName}: Failed to process quality for pattern: ${patternItem}`, err); return null; }
1152
+ }).filter((item): item is MediaFile => item !== null);
1153
+
1154
+ if (!directQuality.length) { console.warn(`${providerName}: No valid qualities found for source ${dataDirect.source}`); continue; }
1155
+
1156
+ return { source: { provider: providerName, files: directQuality, subtitles: tracks, headers: { "Referer": EMBED_SU_DOMAIN, "User-Agent": USER_AGENT, "Origin": EMBED_SU_DOMAIN } } };
1157
+ } catch (error) { console.error(`${providerName}: Error processing item ${item.hash}:`, error); }
1158
+ }
1159
+ return createProviderErrorObject(providerName, "No valid sources found after processing all available items");
1160
+ } catch (error) {
1161
+ console.error(`${providerName}: Unexpected error:`, error);
1162
+ return createProviderErrorObject(providerName, `Unexpected error: ${error.message}`);
1163
+ }
1164
+ }
1165
+
1166
+ // == VidSrc.SU Provider ==
1167
+ const VIDSRC_SU_DOMAIN = "https://vidsrc.su/";
1168
+ const VIDSRC_SU_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': VIDSRC_SU_DOMAIN, 'Origin': VIDSRC_SU_DOMAIN };
1169
+
1170
+ async function getVidSrcSu(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> {
1171
+ const providerName = PROVIDERS.vidsrcsu.displayName;
1172
+ const embedUrl = s && e ? `${VIDSRC_SU_DOMAIN}embed/tv/${tmdb_id}/${s}/${e}` : `${VIDSRC_SU_DOMAIN}embed/movie/${tmdb_id}`;
1173
+
1174
+ try {
1175
+ const response = await fetch(embedUrl, { headers: VIDSRC_SU_HEADERS });
1176
+ if (!response.ok) return createProviderErrorObject(providerName, `Failed to fetch embed page: HTTP ${response.status}`);
1177
+
1178
+ const html = await response.text();
1179
+ let subtitles: Subtitle[] = [];
1180
+
1181
+ const servers: MediaFile[] = [...html.matchAll(/label: 'Server (?:[^']*)', url: '(https?:\/\/[^']+\.m3u8[^']*)'/gi)].map(match => {
1182
+ const fileObj: MediaFile = { file: match[1], type: "hls", lang: "en" };
1183
+ if (s && e) { // Add season and episode if they exist
1184
+ fileObj.season = s;
1185
+ fileObj.episode = e;
1186
+ }
1187
+ return fileObj;
1188
+ });
1189
+
1190
+ const subtitlesMatch = html.match(/const subtitles = \[(.*?)\];/s);
1191
+ if (subtitlesMatch && subtitlesMatch[1]) {
1192
+ try {
1193
+ const subRaw = `[${subtitlesMatch[1]}]`;
1194
+ let parsedSubs = JSON.parse(subRaw);
1195
+ subtitles = parsedSubs.filter((sub: any) => sub && sub.url && sub.language)
1196
+ .map((sub: any) => ({
1197
+ url: sub.url, lang: sub.language.toLowerCase(),
1198
+ type: sub.format || (sub.url.includes('.vtt') ? 'vtt' : (sub.url.includes('.srt') ? 'srt' : undefined))
1199
+ }));
1200
+ } catch (parseError) { console.error(`${providerName}: Error parsing subtitles:`, parseError); subtitles = []; }
1201
+ }
1202
+
1203
+ if (servers.length === 0) return createProviderErrorObject(providerName, "No valid video streams found in embed page");
1204
+ return { source: { provider: providerName, files: servers, subtitles: subtitles, headers: { "Referer": VIDSRC_SU_DOMAIN, "User-Agent": USER_AGENT, "Origin": VIDSRC_SU_DOMAIN } } };
1205
+ } catch (error) {
1206
+ console.error(`${providerName}: Unexpected error:`, error);
1207
+ return createProviderErrorObject(providerName, `Unexpected error: ${error.message}`);
1208
+ }
1209
+ }
1210
+
1211
+ // == AutoEmbed Provider ==
1212
+ const AUTOEMBED_DOMAIN = "https://autoembed.cc/";
1213
+ const AUTOEMBED_API_URL_BASE = "https://tom.autoembed.cc/api/getVideoSource";
1214
+
1215
+ // Modified parseAutoEmbedM3U8 to accept s and e
1216
+ function parseAutoEmbedM3U8(m3u8Content: string, s?: string, e?: string, m3u8Url?: string): MediaFile[] {
1217
+ try {
1218
+ const lines = m3u8Content.split('\n');
1219
+ const sources: MediaFile[] = [];
1220
+ let currentQuality: string | undefined = undefined;
1221
+
1222
+ let baseUrl = '';
1223
+ let domain = '';
1224
+ if (m3u8Url) {
1225
+ try {
1226
+ const urlObj = new URL(m3u8Url);
1227
+ domain = `${urlObj.protocol}//${urlObj.hostname}`;
1228
+
1229
+ // Get base URL by removing the filename from the full URL
1230
+ const urlPath = m3u8Url.split('/');
1231
+ urlPath.pop(); // Remove the filename
1232
+ baseUrl = urlPath.join('/') + '/';
1233
+ } catch (error) {
1234
+ console.error('AutoEmbed: Error extracting URL information:', error);
1235
+ }
1236
+ }
1237
+
1238
+ for (let i = 0; i < lines.length; i++) {
1239
+ const trimmedLine = lines[i].trim();
1240
+ if (trimmedLine.startsWith('#EXT-X-STREAM-INF:')) {
1241
+ const resolutionMatch = trimmedLine.match(/RESOLUTION=\d+x(\d+)/);
1242
+ currentQuality = resolutionMatch && resolutionMatch[1] ? `${resolutionMatch[1]}p` : undefined;
1243
+ for (let j = i + 1; j < lines.length; j++) {
1244
+ const nextLineTrimmed = lines[j].trim();
1245
+ if (nextLineTrimmed && !nextLineTrimmed.startsWith('#')) {
1246
+ // Handle different types of paths
1247
+ let filePath = nextLineTrimmed;
1248
+
1249
+ // Skip URLs that already have protocol
1250
+ if (!nextLineTrimmed.match(/^https?:\/\//)) {
1251
+ if (nextLineTrimmed.startsWith('/')) {
1252
+ // Absolute path starting with '/'
1253
+ if (domain) {
1254
+ filePath = `${domain}${nextLineTrimmed}`;
1255
+ }
1256
+ } else {
1257
+ // Relative path
1258
+ if (baseUrl) {
1259
+ filePath = `${baseUrl}${nextLineTrimmed}`;
1260
+ }
1261
+ }
1262
+ }
1263
+ const fileObj: MediaFile = {
1264
+ file: filePath, type: "hls",
1265
+ quality: currentQuality || 'unknown', lang: "en"
1266
+ };
1267
+ if (s && e) { // Add season and episode if they exist
1268
+ fileObj.season = s;
1269
+ fileObj.episode = e;
1270
+ }
1271
+ sources.push(fileObj);
1272
+ i = j; break;
1273
+ }
1274
+ if (nextLineTrimmed.startsWith('#EXT')) break;
1275
+ }
1276
+ currentQuality = undefined;
1277
+ }
1278
+ }
1279
+ return sources;
1280
+ } catch (error) { console.error('AutoEmbed: Error parsing m3u8:', error); return []; }
1281
+ }
1282
+
1283
+ function mapAutoEmbedSubtitles(apiSubtitles: any[]): Subtitle[] {
1284
+ if (!apiSubtitles || !Array.isArray(apiSubtitles)) return [];
1285
+ try {
1286
+ return apiSubtitles.map(subtitle => {
1287
+ const lang = (subtitle.label || 'unknown').split(' ')[0].toLowerCase();
1288
+ const fileUrl = subtitle.file || '';
1289
+ if (!fileUrl) return null;
1290
+ const fileExtension = fileUrl.split('.').pop()?.toLowerCase();
1291
+ const type = fileExtension === 'vtt' ? 'vtt' : (fileExtension === 'srt' ? 'srt' : undefined);
1292
+ return { url: fileUrl, lang: lang, type: type };
1293
+ }).filter((sub): sub is Subtitle => sub !== null && !!sub.url);
1294
+ } catch (error) { console.error('AutoEmbed: Error mapping subtitles:', error); return []; }
1295
+ }
1296
+
1297
+ async function getAutoEmbed(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> {
1298
+ const providerName = PROVIDERS.autoembed.displayName;
1299
+ const params = new URLSearchParams();
1300
+ if (s && e) { params.append("type", "tv"); params.append("id", `${tmdb_id}/${s}/${e}`); }
1301
+ else { params.append("type", "movie"); params.append("id", tmdb_id); }
1302
+ const apiUrl = `${AUTOEMBED_API_URL_BASE}?${params.toString()}`;
1303
+
1304
+ try {
1305
+ const response = await fetch(apiUrl, { headers: { 'Referer': AUTOEMBED_DOMAIN, 'User-Agent': USER_AGENT } });
1306
+ if (!response.ok) {
1307
+ let errorBody = ""; try { errorBody = await response.text(); } catch (_) {}
1308
+ return createProviderErrorObject(providerName, `API request failed: HTTP ${response.status}. ${errorBody}`);
1309
+ }
1310
+ const data = await response.json();
1311
+ if (data.error || !data.videoSource) return createProviderErrorObject(providerName, data.error || "No videoSource found in API response");
1312
+
1313
+ const m3u8Url = data.videoSource;
1314
+ const m3u8Response = await fetch(m3u8Url, { headers: { 'Referer': AUTOEMBED_DOMAIN, 'User-Agent': USER_AGENT } });
1315
+ if (!m3u8Response.ok) return createProviderErrorObject(providerName, `Failed to fetch m3u8 from ${m3u8Url}: HTTP ${m3u8Response.status}`);
1316
+
1317
+ const m3u8Content = await m3u8Response.text();
1318
+ // Pass s and e to the M3U8 parser
1319
+ const files = parseAutoEmbedM3U8(m3u8Content, s, e, m3u8Url);
1320
+ if (files.length === 0) return createProviderErrorObject(providerName, "No valid streams found after parsing m3u8");
1321
+
1322
+ const subtitles = data.subtitles ? mapAutoEmbedSubtitles(data.subtitles) : [];
1323
+ return { source: { provider: providerName, files: files, subtitles: subtitles, headers: { "Referer": AUTOEMBED_DOMAIN, "User-Agent": USER_AGENT, "Origin": AUTOEMBED_DOMAIN } } };
1324
+ } catch (error) {
1325
+ console.error(`${providerName}: Unexpected error - `, error);
1326
+ return createProviderErrorObject(providerName, `Network or processing error: ${error.message}`);
1327
+ }
1328
+ }
1329
+
1330
+ // --- Provider Configuration ---
1331
+ const PROVIDERS: Record<string, ProviderConfig> = {
1332
+ embedsu: { id: "embedsu", displayName: "EmbedSu", domain: EMBED_SU_DOMAIN, fetchFunction: getEmbedSu },
1333
+ vidsrcsu: { id: "vidsrcsu", displayName: "VidsrcSU", domain: VIDSRC_SU_DOMAIN, fetchFunction: getVidSrcSu },
1334
+ autoembed: { id: "autoembed", displayName: "AutoEmbed", domain: AUTOEMBED_DOMAIN, fetchFunction: getAutoEmbed },
1335
+ };
1336
+
1337
+ // --- Core Logic ---
1338
+ async function getAllProviders(tmdb_id: string, mediaType: "movie" | "tv", s?: string, e?: string): Promise<ProviderSuccessResult[] | [ProviderErrorResult]> {
1339
+ const providerFetchPromises: Promise<ProviderFunctionReturn>[] = [];
1340
+ for (const providerId in PROVIDERS) {
1341
+ const config = PROVIDERS[providerId];
1342
+ providerFetchPromises.push(
1343
+ config.fetchFunction(tmdb_id, s, e)
1344
+ .catch(err => {
1345
+ console.error(`Critical error during ${config.displayName} fetch: ${err}`);
1346
+ return createProviderErrorObject(config.displayName, `Internal unhandled error: ${err.message || String(err)}`);
1347
+ })
1348
+ );
1349
+ }
1350
+ if (providerFetchPromises.length === 0) return [createApiErrorObject("No providers are configured.")];
1351
+
1352
+ const allResults = await Promise.all(providerFetchPromises);
1353
+ const successfulResults = allResults.filter((r): r is ProviderSuccessResult => r !== null && typeof r === 'object' && 'source' in r);
1354
+
1355
+ if (successfulResults.length > 0) return successfulResults;
1356
+ else return [createApiErrorObject("No valid sources found from any provider.")];
1357
+ }
1358
+
1359
+ // --- API Routes & Server ---
1360
+ async function handleRequest(request: Request): Promise<Response> {
1361
+ const url = new URL(request.url);
1362
+ const path = url.pathname;
1363
+ const searchParams = url.searchParams;
1364
+ const pathParts = path.split('/').filter(part => part !== '');
1365
+ const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Content-Type": "application/json" };
1366
+
1367
+ if (request.method === "OPTIONS") return new Response(null, { headers: corsHeaders, status: 204 });
1368
+
1369
+ try {
1370
+ const mediaType = pathParts[0]?.toLowerCase();
1371
+ let resultData: ProviderFunctionReturn | ProviderSuccessResult[] | [ProviderErrorResult];
1372
+
1373
+ if (mediaType === "movie" || mediaType === "tv") {
1374
+ const tmdbIdInput = pathParts.length > 2 ? pathParts[2] : pathParts[1];
1375
+ const providerIdInput = pathParts.length > 2 ? pathParts[1].toLowerCase() : null;
1376
+
1377
+ if (!tmdbIdInput || !/^\d+$/.test(tmdbIdInput)) {
1378
+ resultData = createApiErrorObject("Valid TMDB ID is required.");
1379
+ return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 });
1380
+ }
1381
+ const tmdbId = tmdbIdInput;
1382
+ let season: string | undefined = undefined;
1383
+ let episode: string | undefined = undefined;
1384
+
1385
+ if (mediaType === "tv") {
1386
+ const sParam = searchParams.get("s");
1387
+ const eParam = searchParams.get("e");
1388
+ if (!sParam || !eParam) {
1389
+ resultData = createApiErrorObject("Season (s) and episode (e) parameters are required for TV shows.");
1390
+ return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 });
1391
+ }
1392
+ season = sParam;
1393
+ episode = eParam;
1394
+ }
1395
+
1396
+ if (providerIdInput) {
1397
+ const providerConfig = PROVIDERS[providerIdInput];
1398
+ if (providerConfig) resultData = await providerConfig.fetchFunction(tmdbId, season, episode);
1399
+ else {
1400
+ resultData = createApiErrorObject(`Invalid provider: ${providerIdInput}. Available: ${Object.keys(PROVIDERS).join(', ')}`);
1401
+ return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 });
1402
+ }
1403
+ } else resultData = await getAllProviders(tmdbId, mediaType, season, episode);
1404
+ } else {
1405
+ resultData = createApiErrorObject("Invalid route. Use /movie/tmdb_id or /tv/tmdb_id?s=S&e=E. For specific provider: /movie/provider_name/tmdb_id");
1406
+ return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 404 });
1407
+ }
1408
+ return new Response(JSON.stringify(resultData), { headers: corsHeaders });
1409
+ } catch (error) {
1410
+ console.error("Server error:", error);
1411
+ const errorResponse = createApiErrorObject(`Server error: ${error.message || String(error)}`);
1412
+ return new Response(JSON.stringify(errorResponse), { headers: corsHeaders, status: 500 });
1413
+ }
1414
+ }
1415
+
1416
+ console.log(`TMDB Embed API server starting on port ${PORT}...`);
1417
+ console.log(`Available providers: ${Object.keys(PROVIDERS).join(', ')}`);
1418
+ console.log("Routes:");
1419
+ console.log(" GET /movie/{tmdb_id}");
1420
+ console.log(" GET /movie/{provider_id}/{tmdb_id}");
1421
+ console.log(" GET /tv/{tmdb_id}?s={season_number}&e={episode_number}");
1422
+ console.log(" GET /tv/{provider_id}/{tmdb_id}?s={season_number}&e={episode_number}");
1423
+
1424
+ serve(handleRequest, { port: Number(PORT) });