Major Update : Added 2Embed, Update api routes, update json response format.

This commit is contained in:
Inside4ndroid
2025-04-04 23:36:41 +01:00
parent 179c8c148f
commit ce1288490d
7 changed files with 1039 additions and 62 deletions
+340
View File
@@ -0,0 +1,340 @@
import fetch from "node-fetch";
import JsUnpacker from '../utils/jsunpack.js';
import * as cheerio from 'cheerio';
const DOMAIN = "https://www.2embed.cc";
const PLAYER_URL = "https://uqloads.xyz";
let subtitles = [];
let sources = [];
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,
};
function parseQuality(qualityString) {
if (!qualityString) return 0;
const q = qualityString.toUpperCase();
if (q === '4K') return 4000;
if (q.includes('1080P')) return 1080;
if (q.includes('720P')) return 720;
const numMatch = q.match(/(\d+)/);
return numMatch ? parseInt(numMatch[1], 10) : 0;
}
function createErrorObject(errorMessage) {
return {
provider: "2Embed",
ERROR: [{
error: `ERROR`,
what_happened: errorMessage,
report_issue: 'https://github.com/Inside4ndroid/vidsrc-api-js/issues'
}]
};
}
export async function getTwoEmbed(tmdb_id, s, e) {
subtitles = [];
sources = [];
const tmdbId = tmdb_id;
const initialReferer = s && e ? `${DOMAIN}/embedtv/${tmdbId}&s=${s}&e=${e}` : `${DOMAIN}/embed/${tmdbId}`;
const url = initialReferer;
try {
const response = await fetch(url, {
method: "POST",
headers: {
Referer: url,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": headers['User-Agent'],
},
body: "pls=pls"
});
if (!response.ok) {
return { sources: [createErrorObject(`HTTP error fetching initial URL! Status: ${response.status}`)] };
}
const data = await response.text();
let extractedValue = null;
let isSwishId = false;
let match = data.match(/swish\?id=(?<id>[\w\d]+)/);
if (match && match.groups && match.groups.id) {
extractedValue = match.groups.id;
isSwishId = true;
} else {
match = data.match(/'(.*?player4u.*?)'/);
if (match && match[1]) {
extractedValue = match[1];
} else {
return { sources: [createErrorObject("No relevant swish ID or player4u URL found in initial data.")] };
}
}
if (extractedValue) {
if (isSwishId) {
const streamUrl = await resolve(`${PLAYER_URL}/e/${extractedValue}`, DOMAIN);
if (!streamUrl) {
return { sources: [createErrorObject(`Could not resolve stream URL for swish ID: ${extractedValue}`)] };
}
const parsedSources = await parseM3U8(streamUrl, PLAYER_URL);
if (parsedSources.length > 0) {
sources.push({
provider: "2Embed/Swish",
files: parsedSources,
subtitles: subtitles,
headers: {
"Referer": PLAYER_URL,
"User-Agent": headers['User-Agent'],
"Origin": PLAYER_URL
}
});
} else {
return { sources: [createErrorObject("No valid sources found in M3U8 for swish ID")] };
}
} else {
const listPageResponse = await fetch(extractedValue, {
headers: {
Referer: url,
"User-Agent": headers['User-Agent']
}
});
if (!listPageResponse.ok) {
return { sources: [createErrorObject(`Failed to fetch player4u list page (${extractedValue}): ${listPageResponse.status}`)] };
}
const listPageHtml = await listPageResponse.text();
const $ = cheerio.load(listPageHtml);
let highestQuality = -1;
let bestPartialUrl = null;
$('li.slide-toggle a.playbtnx').each((index, element) => {
const linkText = $(element).text();
const onclickAttr = $(element).attr('onclick');
if (!linkText || !onclickAttr) return;
const qualityMatch = linkText.match(/\s*(\d+p|4K)\s*/i);
const qualityString = qualityMatch ? qualityMatch[1].toUpperCase() : null;
const urlMatch = onclickAttr.match(/go\('([^']+)'\)/);
const partialUrl = urlMatch ? urlMatch[1] : null;
if (!qualityString || !partialUrl) return;
const qualityValue = parseQuality(qualityString);
if (qualityValue > highestQuality) {
highestQuality = qualityValue;
bestPartialUrl = partialUrl;
}
});
if (bestPartialUrl) {
const idMatch = bestPartialUrl.match(/\?id=([\w\d]+)/);
if (idMatch && idMatch[1]) {
const player4uId = idMatch[1];
const resolveUrl = `${PLAYER_URL}/e/${player4uId}`;
const streamUrl = await resolve(resolveUrl, extractedValue);
if (!streamUrl) {
return { sources: [createErrorObject(`Could not resolve stream URL for player4u ID: ${player4uId}`)] };
}
const parsedSources = await parseM3U8(streamUrl, PLAYER_URL);
if (parsedSources.length > 0) {
sources.push({
provider: "2Embed/Player4u",
files: parsedSources,
subtitles: subtitles,
headers: {
"Referer": PLAYER_URL,
"User-Agent": headers['User-Agent'],
"Origin": PLAYER_URL
}
});
} else {
return { sources: [createErrorObject("No valid sources found in M3U8 for player4u ID")] };
}
} else {
return { sources: [createErrorObject("Could not extract player4u ID from best quality URL")] };
}
} else {
return { sources: [createErrorObject("No valid quality options found on player4u page")] };
}
}
}
return { sources: sources.length > 0 ? { sources } : [createErrorObject("No sources found after processing")] };
} catch (error) {
return { sources: [createErrorObject(`Unexpected error: ${error.message}`)] };
}
}
async function resolve(url, referer) {
try {
const response = await fetch(url, {
headers: {
Referer: referer,
"User-Agent": headers['User-Agent']
}
});
if (!response.ok) {
console.error(`Resolve failed for ${url}: Status ${response.status}`);
return null;
}
const data = await response.text();
const packedDataRegex = /eval\(function(.*?)split.*\)\)\)/s;
const packedDataMatch = data.match(packedDataRegex);
if (packedDataMatch) {
const packedJS = packedDataMatch[0];
const unpacker = new JsUnpacker(packedJS);
if (unpacker.detect()) {
const unpackedJS = unpacker.unpack();
if (!unpackedJS) {
console.error("JsUnpacker failed to unpack");
return null;
}
await parseSubs(unpackedJS);
const fileRegex = /sources\s*:\s*\[\s*\{\s*file\s*:\s*"([^"]+)"/;
const matchUri = unpackedJS.match(fileRegex);
if (matchUri && matchUri[1]) {
return matchUri[1];
} else {
console.error("Could not find file URL in unpacked JS");
return null;
}
} else {
console.error("JsUnpacker could not detect packed data in resolve response");
return null;
}
} else {
console.error("No packed JS data found in resolve response for:", url);
if (url.includes('.m3u8')) return url;
return null;
}
} catch (error) {
console.error(`Error during resolve for ${url}:`, error);
return null;
}
}
async function parseM3U8(m3u8Url, referer) {
const parsedSources = [];
try {
const response = await fetch(m3u8Url, {
headers: {
"User-Agent": headers['User-Agent'],
"Referer": referer
}
});
if (!response.ok) {
console.error(`Failed to fetch M3U8 ${m3u8Url}: Status ${response.status}`);
return [];
}
const m3u8Content = await response.text();
const lines = m3u8Content.split('\n');
let currentQuality = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#EXT-X-STREAM-INF:')) {
currentQuality = null;
const resolutionMatch = line.match(/RESOLUTION=\d+x(\d+)/);
if (resolutionMatch && resolutionMatch[1]) {
currentQuality = resolutionMatch[1] + "p";
} else {
const bwMatch = line.match(/BANDWIDTH=(\d+)/);
if (bwMatch) {
currentQuality = `bw${bwMatch[1]}`;
}
}
} else if (currentQuality && (line.startsWith('http') || (line.includes('.m3u8') && !line.startsWith('#')))) {
let resolvedUrl = line;
if (!line.startsWith('http')) {
const baseUrl = new URL(m3u8Url);
resolvedUrl = new URL(line, baseUrl.origin + baseUrl.pathname.substring(0, baseUrl.pathname.lastIndexOf('/') + 1)).href;
}
parsedSources.push({
file: resolvedUrl,
type: 'hls',
quality: currentQuality,
lang: 'en'
});
currentQuality = null;
}
}
parsedSources.sort((a, b) => parseQuality(b.quality) - parseQuality(a.quality));
return parsedSources;
} catch (error) {
console.error(`Error parsing M3U8 ${m3u8Url}:`, error);
return [];
}
}
async function parseSubs(scriptstring) {
if (typeof subtitles === 'undefined') {
console.error("Subtitles array is not accessible in parseSubs");
return;
}
try {
const setupMatch = scriptstring.match(/jwplayer\([\s\S]*?\)\.setup\((\{[\s\S]*?\})\)/);
if (!setupMatch || !setupMatch[1]) {
return;
}
let setupStr = setupMatch[1];
setupStr = setupStr.replace(/\\'/g, "'");
setupStr = setupStr.replace(/'/g, '"');
setupStr = setupStr.replace(/([{,]\s*)([a-zA-Z0-9_$]+)(\s*:)/g, '$1"$2"$3');
setupStr = setupStr.replace(/,\s*([}\]])/g, '$1');
let setupConfig;
try {
setupConfig = JSON.parse(setupStr);
} catch (err) {
return;
}
const subtitleTracks = (setupConfig.tracks || []).filter(track =>
track && (track.kind === "captions" || track.kind === "subtitles") && track.file
).map(track => ({
url: track.file,
lang: track.label || track.kind
}));
subtitleTracks.forEach(newSub => {
if (!subtitles.some(existingSub => existingSub.url === newSub.url && existingSub.lang === newSub.lang)) {
subtitles.push(newSub);
}
});
} catch (error) {
console.error("Error during subtitle parsing:", error);
}
}
+116 -53
View File
@@ -1,5 +1,16 @@
import fetch from 'node-fetch';
function createErrorObject(errorMessage) {
return {
provider: "EmbedSU",
ERROR: [{
error: `ERROR`,
what_happened: errorMessage,
report_issue: 'https://github.com/Inside4ndroid/vidsrc-api-js/issues'
}]
};
}
export async function getEmbedSu(tmdb_id, s, e) {
const DOMAIN = "https://embed.su";
const headers = {
@@ -11,82 +22,133 @@ export async function getEmbedSu(tmdb_id, s, e) {
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 });
const textSearch = await htmlSearch.text();
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;
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 hashDecode = JSON.parse(await stringAtob(hashEncode));
const mEncrypt = hashDecode.hash;
if (!mEncrypt) return;
if (!mEncrypt) {
return { sources: [createErrorObject("No encrypted hash found in decoded data")] };
}
const firstDecode = (await stringAtob(mEncrypt)).split(".").map(item => item.split("").reverse().join(""));
const secondDecode = JSON.parse(await stringAtob(firstDecode.join("").split("").reverse().join("")));
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}`)] };
}
if (!secondDecode || secondDecode.length === 0) return;
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) {
const urlDirect = `${DOMAIN}/api/e/${item.hash}`;
const dataDirect = await requestGet(urlDirect, {
"Referer": DOMAIN,
"User-Agent": headers['User-Agent'],
"Origin": DOMAIN
});
if (!dataDirect.source) continue;
const tracks = dataDirect.subtitles.map(sub => ({
url: sub.file,
lang: sub.label.split('-')[0].trim()
})).filter(track => track.lang);
const requestDirectSize = await fetch(dataDirect.source, { headers, method: "GET" });
const parseRequest = await requestDirectSize.text();
const patternSize = parseRequest.split('\n').filter(item => item.includes('/proxy/'));
const directQuality = patternSize.map(patternItem => {
const sizeQuality = getSizeQuality(patternItem);
let dURL = `${DOMAIN}${patternItem}`;
dURL = dURL.replace(".png", ".m3u8");
return { file: dURL, type: 'hls', quality: `${sizeQuality}p`, lang: 'en' };
});
if (!directQuality.length) continue;
sources.push({
provider: "EmbedSu",
files: directQuality,
headers: {
try {
const urlDirect = `${DOMAIN}/api/e/${item.hash}`;
const dataDirect = await requestGet(urlDirect, {
"Referer": DOMAIN,
"User-Agent": headers['User-Agent'],
"Origin": DOMAIN
}
});
});
subtitles.push(...tracks);
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
}
}
return {
sources,
subtitles
};
if (sources.length === 0) {
return { sources: [createErrorObject("No valid sources found after processing all items")] };
}
return { sources };
} catch (e) {
return { provider: "EmbedSu", sources: [], subtitles: [] };
return { sources: [createErrorObject(`Unexpected error: ${e.message}`)] };
}
}
function getSizeQuality(url) {
const parts = url.split('/');
const base64Part = parts[parts.length - 2];
const decodedPart = atob(base64Part);
const sizeQuality = Number(decodedPart) || 1080;
return sizeQuality;
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) {
@@ -115,6 +177,7 @@ async function requestGet(url, headers = {}) {
return await response.json();
} catch (error) {
return "";
console.error(`Request failed for ${url}:`, error);
return null;
}
}
+106
View File
@@ -0,0 +1,106 @@
export default class JsUnpacker {
static Unbase = class {
constructor(radix) {
this.ALPHABET_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
this.ALPHABET_95 = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
this.alphabet = null;
this.dictionary = {};
this.radix = radix;
if (radix > 36) {
if (radix < 62) {
this.alphabet = this.ALPHABET_62.substring(0, radix);
} else if (radix > 62 && radix < 95) {
this.alphabet = this.ALPHABET_95.substring(0, radix);
} else if (radix === 62) {
this.alphabet = this.ALPHABET_62;
} else if (radix === 95) {
this.alphabet = this.ALPHABET_95;
}
for (let i = 0; i < this.alphabet.length; i++) {
this.dictionary[this.alphabet[i]] = i;
}
}
}
unbase(str) {
let ret = 0;
if (this.alphabet === null) {
ret = parseInt(str, this.radix);
} else {
const tmp = str.split('').reverse().join('');
for (let i = 0; i < tmp.length; i++) {
ret += Math.pow(this.radix, i) * this.dictionary[tmp[i]];
}
}
return ret;
}
};
constructor(packedJS) {
this.packedJS = packedJS;
}
detect() {
const js = this.packedJS.replace(/\s/g, '');
const regex = /eval\(function\(p,a,c,k,e,(?:r|d)/;
return regex.test(js);
}
unpack() {
let js = this.packedJS;
try {
const regex = /\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/s;
const match = js.match(regex);
if (match && match.length === 5) {
let payload = match[1].replace(/\\'/g, "'");
const radixStr = match[2];
const countStr = match[3];
const symtab = match[4].split('|');
let radix = 36;
let count = 0;
try {
radix = parseInt(radixStr);
} catch (e) {
}
try {
count = parseInt(countStr);
} catch (e) {
}
if (symtab.length !== count) {
throw new Error("Unknown p.a.c.k.e.r. encoding");
}
const unbase = new this.constructor.Unbase(radix);
const wordRegex = /\b\w+\b/g;
let decoded = payload;
let replaceOffset = 0;
let wordMatch;
while ((wordMatch = wordRegex.exec(payload)) !== null) {
const word = wordMatch[0];
const x = unbase.unbase(word);
let value = null;
if (x < symtab.length) {
value = symtab[x];
}
if (value && value.length > 0) {
const start = wordMatch.index + replaceOffset;
const end = start + word.length;
decoded = decoded.substring(0, start) + value + decoded.substring(end);
replaceOffset += (value.length - word.length);
}
}
return decoded;
}
} catch (e) {
return null;
}
return null;
}
}