|
|
|
|
|
import { serve } from "https://deno.land/std/http/server.ts"; |
|
|
|
|
|
const PORT = Deno.env.get("PORT") || 3000; |
|
|
|
|
|
|
|
|
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"; |
|
|
|
|
|
|
|
|
interface MediaFile { |
|
|
file: string; |
|
|
type: string; |
|
|
quality?: string; |
|
|
lang?: string; |
|
|
season?: string | number; |
|
|
episode?: string | number; |
|
|
} |
|
|
|
|
|
interface Subtitle { |
|
|
url: string; |
|
|
lang: string; |
|
|
type?: string; |
|
|
} |
|
|
|
|
|
interface SourceInfo { |
|
|
provider: string; |
|
|
files: MediaFile[]; |
|
|
subtitles: Subtitle[]; |
|
|
headers: Record<string, string>; |
|
|
} |
|
|
|
|
|
interface ProviderSuccessResult { |
|
|
source: SourceInfo; |
|
|
} |
|
|
|
|
|
interface ErrorDetail { |
|
|
error: string; |
|
|
what_happened: string; |
|
|
report_issue: string; |
|
|
} |
|
|
|
|
|
interface ProviderErrorResult { |
|
|
provider: string; |
|
|
ERROR: ErrorDetail[]; |
|
|
} |
|
|
|
|
|
type ProviderFunctionReturn = ProviderSuccessResult | ProviderErrorResult; |
|
|
|
|
|
interface ProviderConfig { |
|
|
id: string; |
|
|
displayName: string; |
|
|
domain: string; |
|
|
fetchFunction: (tmdbId: string, s?: string, e?: string) => Promise<ProviderFunctionReturn>; |
|
|
} |
|
|
|
|
|
|
|
|
function createApiErrorObject(errorMessage: string, statusProviderName: string = "API"): ProviderErrorResult { |
|
|
return { |
|
|
provider: statusProviderName, |
|
|
ERROR: [{ |
|
|
error: `ERROR`, |
|
|
what_happened: errorMessage, |
|
|
report_issue: 'https://github.com/Inside4ndroid/TMDB-Embed-API/issues' |
|
|
}] |
|
|
}; |
|
|
} |
|
|
function createProviderErrorObject(providerName: string, errorMessage: string): ProviderErrorResult { |
|
|
return createApiErrorObject(errorMessage, providerName); |
|
|
} |
|
|
|
|
|
function stringAtob(input: string): string { |
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; |
|
|
let str = input.replace(/=+$/, ''); |
|
|
let output = ''; |
|
|
if (str.length % 4 === 1) throw new Error("'atob' failed: The string to be decoded is not correctly encoded."); |
|
|
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) { |
|
|
buffer = chars.indexOf(buffer); |
|
|
} |
|
|
return output; |
|
|
} |
|
|
|
|
|
async function requestGet(url: string, headers: Record<string, string> = {}) { |
|
|
try { |
|
|
const response = await fetch(url, { method: 'GET', headers }); |
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); |
|
|
return await response.json(); |
|
|
} catch (error) { |
|
|
console.error(`Request failed for ${url}:`, error); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
function getSizeQuality(url: string): number { |
|
|
try { |
|
|
const parts = url.split('/'); |
|
|
const base64Part = parts[parts.length - 2]; |
|
|
const decodedPart = stringAtob(base64Part); |
|
|
return Number(decodedPart) || 1080; |
|
|
} catch (e) { |
|
|
console.warn(`Failed to get size quality for URL ${url}:`, e); |
|
|
return 720; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const EMBED_SU_DOMAIN = "https://embed.su"; |
|
|
const EMBED_SU_PROXY = "https://iqslgbok.deploy.cx/param/User-Agent=" + encodeURIComponent(USER_AGENT) + "/param/Origin=" + encodeURIComponent(EMBED_SU_DOMAIN) + "/param/Referer=" + encodeURIComponent(EMBED_SU_DOMAIN) + "/"; |
|
|
|
|
|
const EMBED_SU_HEADERS = { |
|
|
'User-Agent': USER_AGENT, |
|
|
'Referer': EMBED_SU_DOMAIN, |
|
|
'Origin': EMBED_SU_DOMAIN, |
|
|
}; |
|
|
|
|
|
async function getEmbedSu(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> { |
|
|
const providerName = PROVIDERS.embedsu.displayName; |
|
|
try { |
|
|
const urlSearch = s && e ? `${EMBED_SU_PROXY}${EMBED_SU_DOMAIN}/embed/tv/${tmdb_id}/${s}/${e}` : `${EMBED_SU_PROXY}${EMBED_SU_DOMAIN}/embed/movie/${tmdb_id}`; |
|
|
const htmlSearchResponse = await fetch(urlSearch, { method: 'GET', headers: EMBED_SU_HEADERS }); |
|
|
if (!htmlSearchResponse.ok) return createProviderErrorObject(providerName, `Failed to fetch initial page: HTTP ${htmlSearchResponse.status}`); |
|
|
|
|
|
const textSearch = await htmlSearchResponse.text(); |
|
|
const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i); |
|
|
const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : ""; |
|
|
if (!hashEncode) return createProviderErrorObject(providerName, "No encoded hash found in initial page"); |
|
|
|
|
|
let hashDecode; |
|
|
try { hashDecode = JSON.parse(stringAtob(hashEncode)); } |
|
|
catch (err) { return createProviderErrorObject(providerName, `Failed to decode initial hash: ${err.message}`); } |
|
|
|
|
|
const mEncrypt = hashDecode.hash; |
|
|
if (!mEncrypt) return createProviderErrorObject(providerName, "No encrypted hash found in decoded data"); |
|
|
|
|
|
let firstDecode; |
|
|
try { firstDecode = (stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join("")); } |
|
|
catch (err) { return createProviderErrorObject(providerName, `Failed to decode first layer: ${err.message}`); } |
|
|
|
|
|
let secondDecode; |
|
|
try { secondDecode = JSON.parse(stringAtob(firstDecode.join("").split("").reverse().join(""))); } |
|
|
catch (err) { return createProviderErrorObject(providerName, `Failed to decode second layer: ${err.message}`); } |
|
|
|
|
|
if (!secondDecode || !Array.isArray(secondDecode) || secondDecode.length === 0) { |
|
|
return createProviderErrorObject(providerName, "No valid sources found after decoding"); |
|
|
} |
|
|
|
|
|
for (const item of secondDecode) { |
|
|
try { |
|
|
if (!item || !item.hash) continue; |
|
|
const urlDirect = `${EMBED_SU_PROXY}${EMBED_SU_DOMAIN}/api/e/${item.hash}`; |
|
|
const dataDirect = await requestGet(urlDirect, { "Referer": EMBED_SU_DOMAIN, "User-Agent": USER_AGENT, "Origin": EMBED_SU_DOMAIN }); |
|
|
if (!dataDirect || !dataDirect.source) { console.warn(`${providerName}: No source found for hash ${item.hash}`); continue; } |
|
|
|
|
|
const tracks: Subtitle[] = (dataDirect.subtitles || []).map((sub: any) => ({ |
|
|
url: sub.file, lang: sub.label ? sub.label.split('-')[0].trim().toLowerCase() : 'en' |
|
|
})).filter((track: Subtitle) => track.url); |
|
|
|
|
|
const requestDirectSize = await fetch(EMBED_SU_PROXY + dataDirect.source, { headers: EMBED_SU_HEADERS, method: "GET" }); |
|
|
if (!requestDirectSize.ok) { console.warn(`${providerName}: Failed to fetch source ${dataDirect.source}: HTTP ${requestDirectSize.status}`); continue; } |
|
|
|
|
|
const parseRequest = await requestDirectSize.text(); |
|
|
const patternSize = parseRequest.split('\n').filter(line => line.includes('/proxy/')); |
|
|
|
|
|
const directQuality: MediaFile[] = patternSize.map(patternItem => { |
|
|
try { |
|
|
const sizeQuality = getSizeQuality(patternItem); |
|
|
let dURL = `${EMBED_SU_DOMAIN}${patternItem}`; |
|
|
dURL = dURL.replace(".png", ".m3u8"); |
|
|
const fileObj: MediaFile = { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' }; |
|
|
if (s && e) { |
|
|
fileObj.season = s; |
|
|
fileObj.episode = e; |
|
|
} |
|
|
return fileObj; |
|
|
} catch (err) { console.warn(`${providerName}: Failed to process quality for pattern: ${patternItem}`, err); return null; } |
|
|
}).filter((item): item is MediaFile => item !== null); |
|
|
|
|
|
if (!directQuality.length) { console.warn(`${providerName}: No valid qualities found for source ${dataDirect.source}`); continue; } |
|
|
|
|
|
return { source: { provider: providerName, files: directQuality, subtitles: tracks, headers: { "Referer": EMBED_SU_DOMAIN, "User-Agent": USER_AGENT, "Origin": EMBED_SU_DOMAIN } } }; |
|
|
} catch (error) { console.error(`${providerName}: Error processing item ${item.hash}:`, error); } |
|
|
} |
|
|
return createProviderErrorObject(providerName, "No valid sources found after processing all available items"); |
|
|
} catch (error) { |
|
|
console.error(`${providerName}: Unexpected error:`, error); |
|
|
return createProviderErrorObject(providerName, `Unexpected error: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const VIDSRC_SU_DOMAIN = "https://vidsrc.su/"; |
|
|
const VIDSRC_SU_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': VIDSRC_SU_DOMAIN, 'Origin': VIDSRC_SU_DOMAIN }; |
|
|
|
|
|
async function getVidSrcSu(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> { |
|
|
const providerName = PROVIDERS.vidsrcsu.displayName; |
|
|
const embedUrl = s && e ? `${VIDSRC_SU_DOMAIN}embed/tv/${tmdb_id}/${s}/${e}` : `${VIDSRC_SU_DOMAIN}embed/movie/${tmdb_id}`; |
|
|
|
|
|
try { |
|
|
const response = await fetch(embedUrl, { headers: VIDSRC_SU_HEADERS }); |
|
|
if (!response.ok) return createProviderErrorObject(providerName, `Failed to fetch embed page: HTTP ${response.status}`); |
|
|
|
|
|
const html = await response.text(); |
|
|
let subtitles: Subtitle[] = []; |
|
|
|
|
|
const servers: MediaFile[] = [...html.matchAll(/label: 'Server (?:[^']*)', url: '(https?:\/\/[^']+\.m3u8[^']*)'/gi)].map(match => { |
|
|
const fileObj: MediaFile = { file: match[1], type: "hls", lang: "en" }; |
|
|
if (s && e) { |
|
|
fileObj.season = s; |
|
|
fileObj.episode = e; |
|
|
} |
|
|
return fileObj; |
|
|
}); |
|
|
|
|
|
const subtitlesMatch = html.match(/const subtitles = \[(.*?)\];/s); |
|
|
if (subtitlesMatch && subtitlesMatch[1]) { |
|
|
try { |
|
|
const subRaw = `[${subtitlesMatch[1]}]`; |
|
|
let parsedSubs = JSON.parse(subRaw); |
|
|
subtitles = parsedSubs.filter((sub: any) => sub && sub.url && sub.language) |
|
|
.map((sub: any) => ({ |
|
|
url: sub.url, lang: sub.language.toLowerCase(), |
|
|
type: sub.format || (sub.url.includes('.vtt') ? 'vtt' : (sub.url.includes('.srt') ? 'srt' : undefined)) |
|
|
})); |
|
|
} catch (parseError) { console.error(`${providerName}: Error parsing subtitles:`, parseError); subtitles = []; } |
|
|
} |
|
|
|
|
|
if (servers.length === 0) return createProviderErrorObject(providerName, "No valid video streams found in embed page"); |
|
|
return { source: { provider: providerName, files: servers, subtitles: subtitles, headers: { "Referer": VIDSRC_SU_DOMAIN, "User-Agent": USER_AGENT, "Origin": VIDSRC_SU_DOMAIN } } }; |
|
|
} catch (error) { |
|
|
console.error(`${providerName}: Unexpected error:`, error); |
|
|
return createProviderErrorObject(providerName, `Unexpected error: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const AUTOEMBED_DOMAIN = "https://autoembed.cc/"; |
|
|
const AUTOEMBED_API_URL_BASE = "https://tom.autoembed.cc/api/getVideoSource"; |
|
|
|
|
|
|
|
|
function parseAutoEmbedM3U8(m3u8Content: string, s?: string, e?: string, m3u8Url?: string): MediaFile[] { |
|
|
try { |
|
|
const lines = m3u8Content.split('\n'); |
|
|
const sources: MediaFile[] = []; |
|
|
let currentQuality: string | undefined = undefined; |
|
|
|
|
|
let baseUrl = ''; |
|
|
let domain = ''; |
|
|
if (m3u8Url) { |
|
|
try { |
|
|
const urlObj = new URL(m3u8Url); |
|
|
domain = `${urlObj.protocol}//${urlObj.hostname}`; |
|
|
|
|
|
|
|
|
const urlPath = m3u8Url.split('/'); |
|
|
urlPath.pop(); |
|
|
baseUrl = urlPath.join('/') + '/'; |
|
|
} catch (error) { |
|
|
console.error('AutoEmbed: Error extracting URL information:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
for (let i = 0; i < lines.length; i++) { |
|
|
const trimmedLine = lines[i].trim(); |
|
|
if (trimmedLine.startsWith('#EXT-X-STREAM-INF:')) { |
|
|
const resolutionMatch = trimmedLine.match(/RESOLUTION=\d+x(\d+)/); |
|
|
currentQuality = resolutionMatch && resolutionMatch[1] ? `${resolutionMatch[1]}p` : undefined; |
|
|
for (let j = i + 1; j < lines.length; j++) { |
|
|
const nextLineTrimmed = lines[j].trim(); |
|
|
if (nextLineTrimmed && !nextLineTrimmed.startsWith('#')) { |
|
|
|
|
|
let filePath = nextLineTrimmed; |
|
|
|
|
|
|
|
|
if (!nextLineTrimmed.match(/^https?:\/\//)) { |
|
|
if (nextLineTrimmed.startsWith('/')) { |
|
|
|
|
|
if (domain) { |
|
|
filePath = `${domain}${nextLineTrimmed}`; |
|
|
} |
|
|
} else { |
|
|
|
|
|
if (baseUrl) { |
|
|
filePath = `${baseUrl}${nextLineTrimmed}`; |
|
|
} |
|
|
} |
|
|
} |
|
|
const fileObj: MediaFile = { |
|
|
file: filePath, type: "hls", |
|
|
quality: currentQuality || 'unknown', lang: "en" |
|
|
}; |
|
|
if (s && e) { |
|
|
fileObj.season = s; |
|
|
fileObj.episode = e; |
|
|
} |
|
|
sources.push(fileObj); |
|
|
i = j; break; |
|
|
} |
|
|
if (nextLineTrimmed.startsWith('#EXT')) break; |
|
|
} |
|
|
currentQuality = undefined; |
|
|
} |
|
|
} |
|
|
return sources; |
|
|
} catch (error) { console.error('AutoEmbed: Error parsing m3u8:', error); return []; } |
|
|
} |
|
|
|
|
|
function mapAutoEmbedSubtitles(apiSubtitles: any[]): Subtitle[] { |
|
|
if (!apiSubtitles || !Array.isArray(apiSubtitles)) return []; |
|
|
try { |
|
|
return apiSubtitles.map(subtitle => { |
|
|
const lang = (subtitle.label || 'unknown').split(' ')[0].toLowerCase(); |
|
|
const fileUrl = subtitle.file || ''; |
|
|
if (!fileUrl) return null; |
|
|
const fileExtension = fileUrl.split('.').pop()?.toLowerCase(); |
|
|
const type = fileExtension === 'vtt' ? 'vtt' : (fileExtension === 'srt' ? 'srt' : undefined); |
|
|
return { url: fileUrl, lang: lang, type: type }; |
|
|
}).filter((sub): sub is Subtitle => sub !== null && !!sub.url); |
|
|
} catch (error) { console.error('AutoEmbed: Error mapping subtitles:', error); return []; } |
|
|
} |
|
|
|
|
|
async function getAutoEmbed(tmdb_id: string, s?: string, e?: string): Promise<ProviderFunctionReturn> { |
|
|
const providerName = PROVIDERS.autoembed.displayName; |
|
|
const params = new URLSearchParams(); |
|
|
if (s && e) { params.append("type", "tv"); params.append("id", `${tmdb_id}/${s}/${e}`); } |
|
|
else { params.append("type", "movie"); params.append("id", tmdb_id); } |
|
|
const apiUrl = `${AUTOEMBED_API_URL_BASE}?${params.toString()}`; |
|
|
|
|
|
try { |
|
|
const response = await fetch(apiUrl, { headers: { 'Referer': AUTOEMBED_DOMAIN, 'User-Agent': USER_AGENT } }); |
|
|
if (!response.ok) { |
|
|
let errorBody = ""; try { errorBody = await response.text(); } catch (_) { } |
|
|
return createProviderErrorObject(providerName, `API request failed: HTTP ${response.status}. ${errorBody}`); |
|
|
} |
|
|
const data = await response.json(); |
|
|
if (data.error || !data.videoSource) return createProviderErrorObject(providerName, data.error || "No videoSource found in API response"); |
|
|
|
|
|
const m3u8Url = data.videoSource; |
|
|
const m3u8Response = await fetch(m3u8Url, { headers: { 'Referer': AUTOEMBED_DOMAIN, 'User-Agent': USER_AGENT } }); |
|
|
if (!m3u8Response.ok) return createProviderErrorObject(providerName, `Failed to fetch m3u8 from ${m3u8Url}: HTTP ${m3u8Response.status}`); |
|
|
|
|
|
const m3u8Content = await m3u8Response.text(); |
|
|
|
|
|
const files = parseAutoEmbedM3U8(m3u8Content, s, e, m3u8Url); |
|
|
if (files.length === 0) return createProviderErrorObject(providerName, "No valid streams found after parsing m3u8"); |
|
|
|
|
|
const subtitles = data.subtitles ? mapAutoEmbedSubtitles(data.subtitles) : []; |
|
|
return { source: { provider: providerName, files: files, subtitles: subtitles, headers: { "Referer": AUTOEMBED_DOMAIN, "User-Agent": USER_AGENT, "Origin": AUTOEMBED_DOMAIN } } }; |
|
|
} catch (error) { |
|
|
console.error(`${providerName}: Unexpected error - `, error); |
|
|
return createProviderErrorObject(providerName, `Network or processing error: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const PROVIDERS: Record<string, ProviderConfig> = { |
|
|
embedsu: { id: "embedsu", displayName: "EmbedSu", domain: EMBED_SU_DOMAIN, fetchFunction: getEmbedSu }, |
|
|
vidsrcsu: { id: "vidsrcsu", displayName: "VidsrcSU", domain: VIDSRC_SU_DOMAIN, fetchFunction: getVidSrcSu }, |
|
|
autoembed: { id: "autoembed", displayName: "AutoEmbed", domain: AUTOEMBED_DOMAIN, fetchFunction: getAutoEmbed }, |
|
|
}; |
|
|
|
|
|
|
|
|
async function getAllProviders(tmdb_id: string, mediaType: "movie" | "tv", s?: string, e?: string): Promise<ProviderSuccessResult[] | [ProviderErrorResult]> { |
|
|
const providerFetchPromises: Promise<ProviderFunctionReturn>[] = []; |
|
|
for (const providerId in PROVIDERS) { |
|
|
const config = PROVIDERS[providerId]; |
|
|
providerFetchPromises.push( |
|
|
config.fetchFunction(tmdb_id, s, e) |
|
|
.catch(err => { |
|
|
console.error(`Critical error during ${config.displayName} fetch: ${err}`); |
|
|
return createProviderErrorObject(config.displayName, `Internal unhandled error: ${err.message || String(err)}`); |
|
|
}) |
|
|
); |
|
|
} |
|
|
if (providerFetchPromises.length === 0) return [createApiErrorObject("No providers are configured.")]; |
|
|
|
|
|
const allResults = await Promise.all(providerFetchPromises); |
|
|
const successfulResults = allResults.filter((r): r is ProviderSuccessResult => r !== null && typeof r === 'object' && 'source' in r); |
|
|
|
|
|
if (successfulResults.length > 0) return successfulResults; |
|
|
else return [createApiErrorObject("No valid sources found from any provider.")]; |
|
|
} |
|
|
|
|
|
|
|
|
async function handleRequest(request: Request): Promise<Response> { |
|
|
const url = new URL(request.url); |
|
|
const path = url.pathname; |
|
|
const searchParams = url.searchParams; |
|
|
const pathParts = path.split('/').filter(part => part !== ''); |
|
|
const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Content-Type": "application/json" }; |
|
|
|
|
|
if (request.method === "OPTIONS") return new Response(null, { headers: corsHeaders, status: 204 }); |
|
|
|
|
|
try { |
|
|
const mediaType = pathParts[0]?.toLowerCase(); |
|
|
let resultData: ProviderFunctionReturn | ProviderSuccessResult[] | [ProviderErrorResult]; |
|
|
|
|
|
if (mediaType === "movie" || mediaType === "tv") { |
|
|
const tmdbIdInput = pathParts.length > 2 ? pathParts[2] : pathParts[1]; |
|
|
const providerIdInput = pathParts.length > 2 ? pathParts[1].toLowerCase() : null; |
|
|
|
|
|
if (!tmdbIdInput || !/^\d+$/.test(tmdbIdInput)) { |
|
|
resultData = createApiErrorObject("Valid TMDB ID is required."); |
|
|
return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 }); |
|
|
} |
|
|
const tmdbId = tmdbIdInput; |
|
|
let season: string | undefined = undefined; |
|
|
let episode: string | undefined = undefined; |
|
|
|
|
|
if (mediaType === "tv") { |
|
|
const sParam = searchParams.get("s"); |
|
|
const eParam = searchParams.get("e"); |
|
|
if (!sParam || !eParam) { |
|
|
resultData = createApiErrorObject("Season (s) and episode (e) parameters are required for TV shows."); |
|
|
return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 }); |
|
|
} |
|
|
season = sParam; |
|
|
episode = eParam; |
|
|
} |
|
|
|
|
|
if (providerIdInput) { |
|
|
const providerConfig = PROVIDERS[providerIdInput]; |
|
|
if (providerConfig) resultData = await providerConfig.fetchFunction(tmdbId, season, episode); |
|
|
else { |
|
|
resultData = createApiErrorObject(`Invalid provider: ${providerIdInput}. Available: ${Object.keys(PROVIDERS).join(', ')}`); |
|
|
return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 400 }); |
|
|
} |
|
|
} else resultData = await getAllProviders(tmdbId, mediaType, season, episode); |
|
|
} else { |
|
|
resultData = createApiErrorObject("Invalid route. Use /movie/tmdb_id or /tv/tmdb_id?s=S&e=E. For specific provider: /movie/provider_name/tmdb_id"); |
|
|
return new Response(JSON.stringify(resultData), { headers: corsHeaders, status: 404 }); |
|
|
} |
|
|
return new Response(JSON.stringify(resultData), { headers: corsHeaders }); |
|
|
} catch (error) { |
|
|
console.error("Server error:", error); |
|
|
const errorResponse = createApiErrorObject(`Server error: ${error.message || String(error)}`); |
|
|
return new Response(JSON.stringify(errorResponse), { headers: corsHeaders, status: 500 }); |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(`TMDB Embed API server starting on port ${PORT}...`); |
|
|
console.log(`Available providers: ${Object.keys(PROVIDERS).join(', ')}`); |
|
|
console.log("Routes:"); |
|
|
console.log(" GET /movie/{tmdb_id}"); |
|
|
console.log(" GET /movie/{provider_id}/{tmdb_id}"); |
|
|
console.log(" GET /tv/{tmdb_id}?s={season_number}&e={episode_number}"); |
|
|
console.log(" GET /tv/{provider_id}/{tmdb_id}?s={season_number}&e={episode_number}"); |
|
|
|
|
|
serve(handleRequest, { port: Number(PORT) }); |