mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
183 lines
5.8 KiB
JavaScript
183 lines
5.8 KiB
JavaScript
import fetch from 'node-fetch';
|
|
|
|
function createErrorObject(errorMessage) {
|
|
return {
|
|
provider: "EmbedSU",
|
|
ERROR: [{
|
|
error: `ERROR`,
|
|
what_happened: errorMessage,
|
|
report_issue: 'https://github.com/Inside4ndroid/TMDB-Embed-API/issues'
|
|
}]
|
|
};
|
|
}
|
|
|
|
export async function getEmbedSu(tmdb_id, s, e) {
|
|
const DOMAIN = "https://embed.su";
|
|
const headers = {
|
|
'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",
|
|
'Referer': DOMAIN,
|
|
'Origin': DOMAIN,
|
|
};
|
|
|
|
try {
|
|
const urlSearch = s && e ? `${DOMAIN}/embed/tv/${tmdb_id}/${s}/${e}` : `${DOMAIN}/embed/movie/${tmdb_id}`;
|
|
const htmlSearch = await fetch(urlSearch, { method: 'GET', headers });
|
|
|
|
if (!htmlSearch.ok) {
|
|
return { sources: [createErrorObject(`Failed to fetch initial page: HTTP ${htmlSearch.status}`)] };
|
|
}
|
|
|
|
const textSearch = await htmlSearch.text();
|
|
const hashEncodeMatch = textSearch.match(/JSON\.parse\(atob\(\`([^\`]+)/i);
|
|
const hashEncode = hashEncodeMatch ? hashEncodeMatch[1] : "";
|
|
|
|
if (!hashEncode) {
|
|
return { sources: [createErrorObject("No encoded hash found in initial page")] };
|
|
}
|
|
|
|
let hashDecode;
|
|
try {
|
|
hashDecode = JSON.parse(await stringAtob(hashEncode));
|
|
} catch (e) {
|
|
return { sources: [createErrorObject(`Failed to decode initial hash: ${e.message}`)] };
|
|
}
|
|
|
|
const mEncrypt = hashDecode.hash;
|
|
if (!mEncrypt) {
|
|
return { sources: [createErrorObject("No encrypted hash found in decoded data")] };
|
|
}
|
|
|
|
let firstDecode;
|
|
try {
|
|
firstDecode = (await stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join(""));
|
|
} catch (e) {
|
|
return { sources: [createErrorObject(`Failed to decode first layer: ${e.message}`)] };
|
|
}
|
|
|
|
let secondDecode;
|
|
try {
|
|
secondDecode = JSON.parse(await stringAtob(firstDecode.join("").split("").reverse().join("")));
|
|
} catch (e) {
|
|
return { sources: [createErrorObject(`Failed to decode second layer: ${e.message}`)] };
|
|
}
|
|
|
|
if (!secondDecode || secondDecode.length === 0) {
|
|
return { sources: [createErrorObject("No valid sources found after decoding")] };
|
|
}
|
|
|
|
const sources = [];
|
|
const subtitles = [];
|
|
|
|
for (const item of secondDecode) {
|
|
try {
|
|
const urlDirect = `${DOMAIN}/api/e/${item.hash}`;
|
|
const dataDirect = await requestGet(urlDirect, {
|
|
"Referer": DOMAIN,
|
|
"User-Agent": headers['User-Agent'],
|
|
"Origin": DOMAIN
|
|
});
|
|
|
|
if (!dataDirect || !dataDirect.source) {
|
|
console.warn(`No source found for hash ${item.hash}`);
|
|
continue;
|
|
}
|
|
|
|
const tracks = (dataDirect.subtitles || []).map(sub => ({
|
|
url: sub.file,
|
|
lang: sub.label ? sub.label.split('-')[0].trim() : 'en'
|
|
})).filter(track => track.url);
|
|
|
|
const requestDirectSize = await fetch(dataDirect.source, { headers, method: "GET" });
|
|
if (!requestDirectSize.ok) {
|
|
console.warn(`Failed to fetch source ${dataDirect.source}: HTTP ${requestDirectSize.status}`);
|
|
continue;
|
|
}
|
|
|
|
const parseRequest = await requestDirectSize.text();
|
|
const patternSize = parseRequest.split('\n').filter(item => item.includes('/proxy/'));
|
|
|
|
const directQuality = patternSize.map(patternItem => {
|
|
try {
|
|
const sizeQuality = getSizeQuality(patternItem);
|
|
let dURL = `${DOMAIN}${patternItem}`;
|
|
dURL = dURL.replace(".png", ".m3u8");
|
|
return { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' };
|
|
} catch (e) {
|
|
console.warn(`Failed to process quality for pattern: ${patternItem}`, e);
|
|
return null;
|
|
}
|
|
}).filter(item => item !== null);
|
|
|
|
if (!directQuality.length) {
|
|
console.warn(`No valid qualities found for source ${dataDirect.source}`);
|
|
continue;
|
|
}
|
|
|
|
sources.push({
|
|
provider: "EmbedSu",
|
|
files: directQuality,
|
|
subtitles: tracks,
|
|
headers: {
|
|
"Referer": DOMAIN,
|
|
"User-Agent": headers['User-Agent'],
|
|
"Origin": DOMAIN
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error(`Error processing item ${item.hash}:`, e);
|
|
// Continue to next item instead of failing entire request
|
|
}
|
|
}
|
|
|
|
if (sources.length === 0) {
|
|
return { sources: [createErrorObject("No valid sources found after processing all items")] };
|
|
}
|
|
|
|
return { sources };
|
|
} catch (e) {
|
|
return { sources: [createErrorObject(`Unexpected error: ${e.message}`)] };
|
|
}
|
|
}
|
|
|
|
function getSizeQuality(url) {
|
|
try {
|
|
const parts = url.split('/');
|
|
const base64Part = parts[parts.length - 2];
|
|
const decodedPart = atob(base64Part);
|
|
return Number(decodedPart) || 1080;
|
|
} catch (e) {
|
|
console.warn(`Failed to get size quality for URL ${url}:`, e);
|
|
return 720; // Default quality if parsing fails
|
|
}
|
|
}
|
|
|
|
async function stringAtob(input) {
|
|
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, headers = {}) {
|
|
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;
|
|
}
|
|
} |