forked from 50n50/sources
Merge ; commit '2fbfa76208b5a32145c3c9753d6069ccefca99ef'
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
|||||||
|
*.dev.json
|
||||||
|
*.tmp.json
|
||||||
|
*.temp.json
|
||||||
|
*.test.json
|
||||||
|
*.log
|
||||||
|
*.bak
|
||||||
|
*.old
|
||||||
|
*.temp
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
update_global_extractor.py
|
||||||
+14
-24
@@ -98,15 +98,12 @@ async function extractEpisodes(movieUrl) {
|
|||||||
error: "MovieID not found"
|
error: "MovieID not found"
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
const movieData = [{ name: "MovieID", data: movieIDMatch }];
|
|
||||||
const tokenResponse = await fetchv2(
|
const movieIdApiUrl = `https://enc-dec.app/api/enc-movies-flix?text=${movieIDMatch}`;
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs",
|
const movieIdTokenResponse = await fetchv2(movieIdApiUrl);
|
||||||
{},
|
const movieIdTokenData = await movieIdTokenResponse.json();
|
||||||
"POST",
|
const token = movieIdTokenData.result;
|
||||||
JSON.stringify(movieData)
|
|
||||||
);
|
|
||||||
const temp = await tokenResponse.json();
|
|
||||||
const token = temp[0]?.data;
|
|
||||||
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
|
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
|
||||||
const episodeListResponse = await fetchv2(episodeListUrl);
|
const episodeListResponse = await fetchv2(episodeListUrl);
|
||||||
const episodeListData = await episodeListResponse.json();
|
const episodeListData = await episodeListResponse.json();
|
||||||
@@ -115,23 +112,16 @@ async function extractEpisodes(movieUrl) {
|
|||||||
const episodeRegex = /<a[^>]+eid="([^"]+)"[^>]+num="([^"]+)"[^>]*>/g;
|
const episodeRegex = /<a[^>]+eid="([^"]+)"[^>]+num="([^"]+)"[^>]*>/g;
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||||
|
|
||||||
const episodeData = episodeMatches.map(([_, episodeToken, episodeNum]) => ({
|
const episodeTokenPromises = episodeMatches.map(([_, episodeToken]) => {
|
||||||
name: `Episode ${episodeNum}`,
|
const apiUrl = `https://enc-dec.app/api/enc-movies-flix?text=${episodeToken}`;
|
||||||
data: episodeToken
|
return fetchv2(apiUrl).then(response => response.json());
|
||||||
}));
|
});
|
||||||
|
|
||||||
console.log(JSON.stringify(episodeData));
|
const episodeTokenResults = await Promise.all(episodeTokenPromises);
|
||||||
const batchResponse = await fetchv2(
|
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs",
|
|
||||||
{},
|
|
||||||
"POST",
|
|
||||||
JSON.stringify(episodeData)
|
|
||||||
);
|
|
||||||
const batchResults = await batchResponse.json();
|
|
||||||
|
|
||||||
const episodes = batchResults.map((result, index) => ({
|
const episodes = episodeMatches.map(([_, episodeToken, episodeNum], index) => ({
|
||||||
number: parseInt(episodeMatches[index][2], 10),
|
number: parseInt(episodeNum, 10),
|
||||||
href: `https://1movies.bz/ajax/links/list?eid=${episodeMatches[index][1]}&_=${result.data}`
|
href: `https://1movies.bz/ajax/links/list?eid=${episodeToken}&_=${episodeTokenResults[index].result}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
return JSON.stringify(episodes);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"language": "English",
|
"language": "English",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
@@ -16,5 +16,5 @@
|
|||||||
"streamAsyncJS": false,
|
"streamAsyncJS": false,
|
||||||
"softsub": false,
|
"softsub": false,
|
||||||
"type": "anime",
|
"type": "anime",
|
||||||
"downloadSupport": true
|
"downloadSupport": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ async function searchResults(keyword) {
|
|||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
"referer": "https://anime-sama.fr/"
|
"referer": "https://anime-sama.org/"
|
||||||
};
|
};
|
||||||
|
|
||||||
const regex = /<a[^>]+href="([^"]+)"[\s\S]*?<img[^>]+src="([^"]+)"[\s\S]*?<h3[^>]*>(.*?)<\/h3>/gi;
|
const regex = /<a[^>]+href="([^"]+)"[\s\S]*?<img[^>]+src="([^"]+)"[\s\S]*?<h3[^>]*>(.*?)<\/h3>/gi;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchv2(
|
const response = await fetchv2(
|
||||||
"https://anime-sama.fr/template-php/defaut/fetch.php",
|
"https://anime-sama.org/template-php/defaut/fetch.php",
|
||||||
headers,
|
headers,
|
||||||
"POST",
|
"POST",
|
||||||
`query=${encodeURIComponent(keyword)}`
|
`query=${encodeURIComponent(keyword)}`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"language": "French",
|
"language": "French",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ async function extractEpisodes(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function extractStreamUrl(id) {
|
async function extractStreamUrl(id) {
|
||||||
if (!_0xCheck()) return 'https://files.catbox.moe/avolvc.mp4';
|
|
||||||
|
|
||||||
const cookieHeader = `key=${id}`;
|
const cookieHeader = `key=${id}`;
|
||||||
const headers = {
|
const headers = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"language": "English (SUB)",
|
"language": "English (SUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "720p",
|
"quality": "720p",
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://animeindo.skin/search/" + encodeURIComponent(keyword));
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const itemContainerRegex = /<div class="relative group overflow-hidden">([\s\S]*?)<\/div>\s*<!--\[if BLOCK\]><!\[endif\]-->/g;
|
||||||
|
let containerMatch;
|
||||||
|
while ((containerMatch = itemContainerRegex.exec(html)) !== null) {
|
||||||
|
const itemHtml = containerMatch[1];
|
||||||
|
|
||||||
|
const hrefMatch = itemHtml.match(/<a href="([^"]+)"/);
|
||||||
|
const href = hrefMatch ? hrefMatch[1].trim() : null;
|
||||||
|
|
||||||
|
const imgMatch = itemHtml.match(/<img[^>]+src="([^"]+)"/);
|
||||||
|
const image = imgMatch ? imgMatch[1].trim() : null;
|
||||||
|
|
||||||
|
const titleMatch = itemHtml.match(/<h3[^>]*>([^<]+)<\/h3>/);
|
||||||
|
const title = titleMatch ? titleMatch[1].trim() : null;
|
||||||
|
|
||||||
|
if (href && image && title) {
|
||||||
|
results.push({
|
||||||
|
href: href,
|
||||||
|
image: image,
|
||||||
|
title: title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<p class="text-gray-400 mt-3">(.*?)<\/p>/s;
|
||||||
|
const match = html.match(regex);
|
||||||
|
const description = match ? match[1].trim() : "N/A";
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const itemContainerRegex = /<div class="relative group">[\s\S]*?<a href="([^"]+)"[\s\S]*?<span>Season\s+(\d+)<\/span>[\s\S]*?<span>Episode\s+(\d+)<\/span>[\s\S]*?<\/div>/g;
|
||||||
|
let containerMatch;
|
||||||
|
while ((containerMatch = itemContainerRegex.exec(html)) !== null) {
|
||||||
|
const href = containerMatch[1].trim();
|
||||||
|
const season = parseInt(containerMatch[2], 10);
|
||||||
|
const episode = parseInt(containerMatch[3], 10);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
href: href,
|
||||||
|
season: season,
|
||||||
|
number: episode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
results.push({
|
||||||
|
href: url,
|
||||||
|
season: 1,
|
||||||
|
number: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
season: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const snapshotMatch = html.match(/wire:snapshot="(.+?)" wire:effects/);
|
||||||
|
if (!snapshotMatch) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedSnapshot = snapshotMatch[1]
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/\\\//g, '/')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&');
|
||||||
|
|
||||||
|
const data = JSON.parse(decodedSnapshot);
|
||||||
|
|
||||||
|
const videosArray = data.data.videos[0];
|
||||||
|
let fmLink = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < videosArray.length; i++) {
|
||||||
|
const item = videosArray[i];
|
||||||
|
|
||||||
|
if (Array.isArray(item) && item.length > 0) {
|
||||||
|
const videoObj = item[0];
|
||||||
|
if (videoObj && videoObj.label === "FM") {
|
||||||
|
fmLink = videoObj.link;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item && item.label === "FM") {
|
||||||
|
fmLink = item.link;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("FM Stream Link: " + fmLink);
|
||||||
|
|
||||||
|
if (fmLink) {
|
||||||
|
try {
|
||||||
|
const embedResponse = await fetchv2(fmLink);
|
||||||
|
const embedHtml = await embedResponse.text();
|
||||||
|
|
||||||
|
const iframeSrcMatch = embedHtml.match(/<iframe[^>]*src="([^"]+)"/);
|
||||||
|
if (iframeSrcMatch) {
|
||||||
|
const embedUrl = iframeSrcMatch[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filemoonResponse = await fetchv2(embedUrl);
|
||||||
|
const filemoonHtml = await filemoonResponse.text();
|
||||||
|
|
||||||
|
const finalIframeMatch = filemoonHtml.match(/<iframe[^>]*src="([^"]+)"/);
|
||||||
|
if (finalIframeMatch) {
|
||||||
|
const finalEmbedUrl = finalIframeMatch[1];
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
|
"Referer": embedUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalResponse = await fetchv2(finalEmbedUrl, headers);
|
||||||
|
const finalHtml = await finalResponse.text();
|
||||||
|
|
||||||
|
const streamUrl = await filemoonExtractor(finalHtml, embedUrl);
|
||||||
|
return streamUrl || "https://error.org/";
|
||||||
|
} else {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
} catch (filemoonErr) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
} catch (embedErr) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* SCHEME START */
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name filemoonExtractor
|
||||||
|
* @author Cufiy - Inspired by Churly
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function filemoonExtractor(html, url = null) {
|
||||||
|
// check if contains iframe, if does, extract the src and get the url
|
||||||
|
const regex = /<iframe[^>]+src="([^"]+)"[^>]*><\/iframe>/;
|
||||||
|
const match = html.match(regex);
|
||||||
|
if (match) {
|
||||||
|
console.log("Iframe URL: " + match[1]);
|
||||||
|
const iframeUrl = match[1];
|
||||||
|
const iframeResponse = await soraFetch(iframeUrl, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
|
"Referer": url,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("Iframe Response: " + iframeResponse.status);
|
||||||
|
html = await iframeResponse.text();
|
||||||
|
}
|
||||||
|
// console.log("HTML: " + html);
|
||||||
|
// get /<script[^>]*>([\s\S]*?)<\/script>/gi
|
||||||
|
const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
||||||
|
const scripts = [];
|
||||||
|
let scriptMatch;
|
||||||
|
while ((scriptMatch = scriptRegex.exec(html)) !== null) {
|
||||||
|
scripts.push(scriptMatch[1]);
|
||||||
|
}
|
||||||
|
// get the script with eval and m3u8
|
||||||
|
const evalRegex = /eval\((.*?)\)/;
|
||||||
|
const m3u8Regex = /m3u8/;
|
||||||
|
// console.log("Scripts: " + scripts);
|
||||||
|
const evalScript = scripts.find(script => evalRegex.test(script) && m3u8Regex.test(script));
|
||||||
|
if (!evalScript) {
|
||||||
|
console.log("No eval script found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const unpackedScript = unpack(evalScript);
|
||||||
|
// get the m3u8 url
|
||||||
|
const m3u8Regex2 = /https?:\/\/[^\s]+master\.m3u8[^\s]*?(\?[^"]*)?/;
|
||||||
|
const m3u8Match = unpackedScript.match(m3u8Regex2);
|
||||||
|
if (m3u8Match) {
|
||||||
|
return m3u8Match[0];
|
||||||
|
} else {
|
||||||
|
console.log("No M3U8 URL found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* REMOVE_START */
|
||||||
|
|
||||||
|
|
||||||
|
class Unbaser {
|
||||||
|
constructor(base) {
|
||||||
|
this.ALPHABET = {
|
||||||
|
62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
95: "' !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'",
|
||||||
|
};
|
||||||
|
this.dictionary = {};
|
||||||
|
this.base = base;
|
||||||
|
if (36 < base && base < 62) {
|
||||||
|
this.ALPHABET[base] = this.ALPHABET[base] ||
|
||||||
|
this.ALPHABET[62].substr(0, base);
|
||||||
|
}
|
||||||
|
if (2 <= base && base <= 36) {
|
||||||
|
this.unbase = (value) => parseInt(value, base);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
[...this.ALPHABET[base]].forEach((cipher, index) => {
|
||||||
|
this.dictionary[cipher] = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (er) {
|
||||||
|
throw Error("Unsupported base encoding.");
|
||||||
|
}
|
||||||
|
this.unbase = this._dictunbaser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_dictunbaser(value) {
|
||||||
|
let ret = 0;
|
||||||
|
[...value].reverse().forEach((cipher, index) => {
|
||||||
|
ret = ret + ((Math.pow(this.base, index)) * this.dictionary[cipher]);
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function unpack(source) {
|
||||||
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
|
if (count != symtab.length) {
|
||||||
|
throw Error("Malformed p.a.c.k.e.r. symtab.");
|
||||||
|
}
|
||||||
|
let unbase;
|
||||||
|
try {
|
||||||
|
unbase = new Unbaser(radix);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
throw Error("Unknown p.a.c.k.e.r. encoding.");
|
||||||
|
}
|
||||||
|
function lookup(match) {
|
||||||
|
const word = match;
|
||||||
|
let word2;
|
||||||
|
if (radix == 1) {
|
||||||
|
word2 = symtab[parseInt(word)];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
word2 = symtab[unbase.unbase(word)];
|
||||||
|
}
|
||||||
|
return word2 || word;
|
||||||
|
}
|
||||||
|
source = payload.replace(/\b\w+\b/g, lookup);
|
||||||
|
return _replacestrings(source);
|
||||||
|
function _filterargs(source) {
|
||||||
|
const juicers = [
|
||||||
|
/}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\), *(\d+), *(.*)\)\)/,
|
||||||
|
/}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\)/,
|
||||||
|
];
|
||||||
|
for (const juicer of juicers) {
|
||||||
|
const args = juicer.exec(source);
|
||||||
|
if (args) {
|
||||||
|
let a = args;
|
||||||
|
if (a[2] == "[]") {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
payload: a[1],
|
||||||
|
symtab: a[4].split("|"),
|
||||||
|
radix: parseInt(a[2]),
|
||||||
|
count: parseInt(a[3]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (ValueError) {
|
||||||
|
throw Error("Corrupted p.a.c.k.e.r. data.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Error("Could not make sense of p.a.c.k.e.r data (unexpected code structure)");
|
||||||
|
}
|
||||||
|
function _replacestrings(source) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses Sora's fetchv2 on ipad, fallbacks to regular fetch on Windows
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*
|
||||||
|
* @param {string} url The URL to make the request to.
|
||||||
|
* @param {object} [options] The options to use for the request.
|
||||||
|
* @param {object} [options.headers] The headers to send with the request.
|
||||||
|
* @param {string} [options.method='GET'] The method to use for the request.
|
||||||
|
* @param {string} [options.body=null] The body of the request.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Response|null>} The response from the server, or null if the
|
||||||
|
* request failed.
|
||||||
|
*/
|
||||||
|
async function soraFetch(url, options = { headers: {}, method: 'GET', body: null }) {
|
||||||
|
try {
|
||||||
|
return await fetchv2(url, options.headers ?? {}, options.method ?? 'GET', options.body ?? null);
|
||||||
|
} catch(e) {
|
||||||
|
try {
|
||||||
|
return await fetch(url, options);
|
||||||
|
} catch(error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* REMOVE_END */
|
||||||
|
|
||||||
|
/* SCHEME END */
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "AnimeIndo",
|
||||||
|
"iconUrl": "https://animeindo.skin/favicon/favicon-32x32.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Indonesian",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://animeindo.skin/home",
|
||||||
|
"searchBaseUrl": "https://animeindo.skin/home",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animeindo/animeindo.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
+54
-55
@@ -80,50 +80,49 @@ async function extractDetails(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractEpisodes(animeUrl) {
|
async function extractEpisodes(url) {
|
||||||
|
const sendEpisodes = async (endpoint, episodeData) => {
|
||||||
|
const promises = episodeData.map(item =>
|
||||||
|
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => ({ name: item.name, data: json.result }))
|
||||||
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
|
);
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchv2(animeUrl);
|
const actualUrl = url.replace("Animekai:", "").trim();
|
||||||
const htmlText = await response.text();
|
const htmlText = await (await fetchv2(actualUrl)).text();
|
||||||
|
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
||||||
if (!animeIdMatch) {
|
|
||||||
return [{
|
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
||||||
error: "AniID not found"
|
const tokenData = await tokenResponse.json();
|
||||||
}];
|
const token = tokenData.result;
|
||||||
}
|
|
||||||
|
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||||
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`);
|
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||||
const token = await tokenResponse.text();
|
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
||||||
|
|
||||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||||
|
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||||
const episodeListResponse = await fetchv2(episodeListUrl);
|
|
||||||
const episodeListData = await episodeListResponse.json();
|
const recentEpisodeMatches = episodeMatches.slice(-50);
|
||||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
|
||||||
|
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
||||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
name: `Episode ${episodeNum}`,
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
data: episodeToken
|
||||||
|
}));
|
||||||
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
|
||||||
name: `Episode ${episodeNum}`,
|
const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
|
||||||
data: episodeToken
|
|
||||||
}));
|
const episodes = batchResults.map((result, index) => ({
|
||||||
|
number: parseInt(recentEpisodeMatches[index][1], 10),
|
||||||
const batchResponse = await fetchv2(
|
href: `https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
}));
|
||||||
{},
|
|
||||||
"POST",
|
return JSON.stringify(episodes);
|
||||||
JSON.stringify(episodeData)
|
|
||||||
);
|
|
||||||
|
|
||||||
const batchResults = await batchResponse.json();
|
|
||||||
|
|
||||||
const episodes = batchResults.map((result, index) => ({
|
|
||||||
number: parseInt(episodeMatches[index][1], 10),
|
|
||||||
href: `https://animekai.to/ajax/links/list?token=${episodeMatches[index][2]}&_=${result.data}`
|
|
||||||
}));
|
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching episodes:" + err);
|
console.error("Error fetching episodes:" + err);
|
||||||
return [{
|
return [{
|
||||||
@@ -159,13 +158,13 @@ async function extractStreamUrl(url) {
|
|||||||
{ name: "Sub", data: serverIdSub }
|
{ name: "Sub", data: serverIdSub }
|
||||||
].filter(item => item.data);
|
].filter(item => item.data);
|
||||||
|
|
||||||
const tokenBatchResponse = await fetchv2(
|
const tokenPromises = tokenRequestData.map(item =>
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
||||||
{},
|
.then(res => res.json())
|
||||||
"POST",
|
.then(json => ({ name: item.name, data: json.result }))
|
||||||
JSON.stringify(tokenRequestData)
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
);
|
);
|
||||||
const tokenResults = await tokenBatchResponse.json();
|
const tokenResults = await Promise.all(tokenPromises);
|
||||||
|
|
||||||
const streamUrls = tokenResults.map(result => {
|
const streamUrls = tokenResults.map(result => {
|
||||||
const serverIdMap = {
|
const serverIdMap = {
|
||||||
@@ -210,13 +209,13 @@ async function extractStreamUrl(url) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptBatchResponse = await fetchv2(
|
const decryptPromises = decryptRequestData.map(item =>
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits",
|
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
||||||
{},
|
.then(res => res.json())
|
||||||
"POST",
|
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
||||||
JSON.stringify(decryptRequestData)
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
);
|
);
|
||||||
const decryptResults = await decryptBatchResponse.json();
|
const decryptResults = await Promise.all(decryptPromises);
|
||||||
|
|
||||||
const finalResults = {};
|
const finalResults = {};
|
||||||
decryptResults.forEach(result => {
|
decryptResults.forEach(result => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"language": "English",
|
"language": "English",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
@@ -80,50 +80,49 @@ async function extractDetails(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractEpisodes(animeUrl) {
|
async function extractEpisodes(url) {
|
||||||
|
const sendEpisodes = async (endpoint, episodeData) => {
|
||||||
|
const promises = episodeData.map(item =>
|
||||||
|
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => ({ name: item.name, data: json.result }))
|
||||||
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
|
);
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchv2(animeUrl);
|
const actualUrl = url.replace("Animekai:", "").trim();
|
||||||
const htmlText = await response.text();
|
const htmlText = await (await fetchv2(actualUrl)).text();
|
||||||
|
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
||||||
if (!animeIdMatch) {
|
|
||||||
return [{
|
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
||||||
error: "AniID not found"
|
const tokenData = await tokenResponse.json();
|
||||||
}];
|
const token = tokenData.result;
|
||||||
}
|
|
||||||
|
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||||
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`);
|
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||||
const token = await tokenResponse.text();
|
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
||||||
|
|
||||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||||
|
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||||
const episodeListResponse = await fetchv2(episodeListUrl);
|
|
||||||
const episodeListData = await episodeListResponse.json();
|
const recentEpisodeMatches = episodeMatches.slice(-50);
|
||||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
|
||||||
|
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
||||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
name: `Episode ${episodeNum}`,
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
data: episodeToken
|
||||||
|
}));
|
||||||
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
|
||||||
name: `Episode ${episodeNum}`,
|
const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
|
||||||
data: episodeToken
|
|
||||||
}));
|
const episodes = batchResults.map((result, index) => ({
|
||||||
|
number: parseInt(recentEpisodeMatches[index][1], 10),
|
||||||
const batchResponse = await fetchv2(
|
href: `https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
}));
|
||||||
{},
|
|
||||||
"POST",
|
return JSON.stringify(episodes);
|
||||||
JSON.stringify(episodeData)
|
|
||||||
);
|
|
||||||
|
|
||||||
const batchResults = await batchResponse.json();
|
|
||||||
|
|
||||||
const episodes = batchResults.map((result, index) => ({
|
|
||||||
number: parseInt(episodeMatches[index][1], 10),
|
|
||||||
href: `https://animekai.to/ajax/links/list?token=${episodeMatches[index][2]}&_=${result.data}`
|
|
||||||
}));
|
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching episodes:" + err);
|
console.error("Error fetching episodes:" + err);
|
||||||
return [{
|
return [{
|
||||||
@@ -159,13 +158,13 @@ async function extractStreamUrl(url) {
|
|||||||
{ name: "Sub", data: serverIdSub }
|
{ name: "Sub", data: serverIdSub }
|
||||||
].filter(item => item.data);
|
].filter(item => item.data);
|
||||||
|
|
||||||
const tokenBatchResponse = await fetchv2(
|
const tokenPromises = tokenRequestData.map(item =>
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
||||||
{},
|
.then(res => res.json())
|
||||||
"POST",
|
.then(json => ({ name: item.name, data: json.result }))
|
||||||
JSON.stringify(tokenRequestData)
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
);
|
);
|
||||||
const tokenResults = await tokenBatchResponse.json();
|
const tokenResults = await Promise.all(tokenPromises);
|
||||||
|
|
||||||
const streamUrls = tokenResults.map(result => {
|
const streamUrls = tokenResults.map(result => {
|
||||||
const serverIdMap = {
|
const serverIdMap = {
|
||||||
@@ -210,13 +209,13 @@ async function extractStreamUrl(url) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptBatchResponse = await fetchv2(
|
const decryptPromises = decryptRequestData.map(item =>
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits",
|
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
||||||
{},
|
.then(res => res.json())
|
||||||
"POST",
|
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
||||||
JSON.stringify(decryptRequestData)
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
);
|
);
|
||||||
const decryptResults = await decryptBatchResponse.json();
|
const decryptResults = await Promise.all(decryptPromises);
|
||||||
|
|
||||||
const finalResults = {};
|
const finalResults = {};
|
||||||
decryptResults.forEach(result => {
|
decryptResults.forEach(result => {
|
||||||
@@ -234,16 +233,16 @@ async function extractStreamUrl(url) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const decryptedUrls = await processStreams(streamUrls);
|
const decryptedUrls = await processStreams(streamUrls);
|
||||||
const decryptedSub = decryptedUrls.Sub || decryptedUrls.Softsub || decryptedUrls.Dub;
|
const decryptedDub = decryptedUrls.Sub || decryptedUrls.Dub || decryptedUrls.Softsub;
|
||||||
|
|
||||||
console.log(decryptedSub);
|
console.log(decryptedDub);
|
||||||
const headers = {
|
const headers = {
|
||||||
"Referer": "https://animekai.to/",
|
"Referer": "https://animekai.to/",
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (decryptedSub) {
|
if (decryptedDub) {
|
||||||
const response = await fetchv2(decryptedSub.replace("/e/", "/media/"), headers);
|
const response = await fetchv2(decryptedDub.replace("/e/", "/media/"), headers);
|
||||||
const responseJson = await response.json();
|
const responseJson = await response.json();
|
||||||
|
|
||||||
const result = responseJson?.result;
|
const result = responseJson?.result;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"language": "English",
|
"language": "English",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://animeler.pw/wp-json/kiranime/v1/anime/search?query=" + encodeURIComponent(keyword) + "&lang=jp&_locale=user");
|
||||||
|
const data = await response.json();
|
||||||
|
const html = data.result;
|
||||||
|
|
||||||
|
const regex = /<a href="([^"]+)"[^>]*>[\s\S]*?<img[^>]+src='([^']+)'[^>]*>[\s\S]*?<h3[^>]*>([^<]+)<\/h3>/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
title: match[3].trim(),
|
||||||
|
image: match[2].trim(),
|
||||||
|
href: match[1].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<div\s+class="block\s+w-full[^"]*"[^>]*>(.*?)<\/div>/s;
|
||||||
|
const match = html.match(regex);
|
||||||
|
const description = match ? match[1].trim() : "N/A";
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<div\s+class="swiper-slide[^"]*"[^>]*>\s*<a\s+href="(https:\/\/animeler\.pw\/[^"]+)"[^>]*title="[^"]*"[^>]*class="w-full[^"]*"[^>]*>[\s\S]*?<span[^>]*>\s*Bölüm\s+(\d+)\s*<\/span>[\s\S]*?<\/a>/gi;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
href: match[1].trim(),
|
||||||
|
number: parseInt(match[2], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results.reverse());
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const match = html.match(/<iframe[^>]+src=['"](https:\/\/play\.animeler\.pw\/fireplayer\/video\/([a-f0-9]+))['"]/i);
|
||||||
|
if (match) {
|
||||||
|
const fireplayerUrl = match[1];
|
||||||
|
const hash = match[2];
|
||||||
|
|
||||||
|
const postData = "hash=" + hash + "&r=https%3A%2F%2Fanimeler.pw%2F&s=";
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
'Origin': 'https://play.animeler.pw',
|
||||||
|
'Referer': fireplayerUrl,
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
|
||||||
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
|
};
|
||||||
|
|
||||||
|
const streamResponse = await fetchv2(fireplayerUrl + "?do=getVideo", headers, "POST", postData);
|
||||||
|
const streamData = await streamResponse.json();
|
||||||
|
|
||||||
|
const sibnetUrl = streamData.videoSrc;
|
||||||
|
const streamHeaders = {
|
||||||
|
"Host": "dv32-1.sibnet.ru",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0",
|
||||||
|
"Accept": "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5",
|
||||||
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br, zstd, identity",
|
||||||
|
"Range": "bytes=0-",
|
||||||
|
"Referer": "https://video.sibnet.ru/",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Sec-Fetch-Dest": "video",
|
||||||
|
"Sec-Fetch-Mode": "no-cors",
|
||||||
|
"Sec-Fetch-Site": "same-site",
|
||||||
|
"Priority": "u=0"
|
||||||
|
};
|
||||||
|
|
||||||
|
const subtitleUrl = "https://none.com/subtitles.vtt";
|
||||||
|
|
||||||
|
const data = await sibnetExtractor(sibnetUrl);
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
title: "Server 1",
|
||||||
|
streamUrl: data ? data.url : 'deijdiw',
|
||||||
|
headers: streamHeaders
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtitle: subtitleUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sibnetExtractor(embedUrl) {
|
||||||
|
const headers = {
|
||||||
|
Referer: embedUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, {
|
||||||
|
headers,
|
||||||
|
method: 'GET',
|
||||||
|
encoding: 'windows-1251'
|
||||||
|
});
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: videoUrl,
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function soraFetch(url, options = { headers: {}, method: 'GET', body: null, encoding: 'utf-8' }) {
|
||||||
|
try {
|
||||||
|
return await fetchv2(
|
||||||
|
url,
|
||||||
|
options.headers ?? {},
|
||||||
|
options.method ?? 'GET',
|
||||||
|
options.body ?? null,
|
||||||
|
true,
|
||||||
|
options.encoding ?? 'utf-8'
|
||||||
|
);
|
||||||
|
} catch(e) {
|
||||||
|
try {
|
||||||
|
return await fetch(url, options);
|
||||||
|
} catch(error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeHTMLEntities(text) {
|
||||||
|
text = text.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec));
|
||||||
|
|
||||||
|
const entities = {
|
||||||
|
'"': '"',
|
||||||
|
'&': '&',
|
||||||
|
''': "'",
|
||||||
|
'<': '<',
|
||||||
|
'>': '>'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entity in entities) {
|
||||||
|
text = text.replace(new RegExp(entity, 'g'), entities[entity]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Animeler",
|
||||||
|
"iconUrl": "https://animeler.pw/wp-content/uploads/cropped-animelerpw-224x224.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.1",
|
||||||
|
"language": "Turkish",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://animeler.pw/",
|
||||||
|
"searchBaseUrl": "https://animeler.pw/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animeler/animeler.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://api.cdnlibs.org/api/anime?fields[]=rate_avg&fields[]=rate&fields[]=releaseDate&q=" + keyword);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json && Array.isArray(json.data)) {
|
||||||
|
for (const item of json.data) {
|
||||||
|
const image = item.cover?.default || item.cover?.thumbnail || "";
|
||||||
|
results.push({
|
||||||
|
title: item.eng_name || item.name || "Unknown",
|
||||||
|
image: "https://passthrough-worker.simplepostrequest.workers.dev/?simple=" + image + "&referer=https://animelib.org/",
|
||||||
|
href: item.slug_url || item.slug || item.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(slug) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(
|
||||||
|
"https://api.cdnlibs.org/api/anime/" + slug +
|
||||||
|
"?fields[]=background&fields[]=eng_name&fields[]=otherNames&fields[]=summary&fields[]=releaseDate&fields[]=type_id&fields[]=caution&fields[]=views&fields[]=close_view&fields[]=rate_avg&fields[]=rate&fields[]=genres&fields[]=tags&fields[]=teams&fields[]=user&fields[]=franchise&fields[]=authors&fields[]=publisher&fields[]=userRating&fields[]=moderated&fields[]=metadata&fields[]=metadata.count&fields[]=metadata.close_comments&fields[]=anime_status_id&fields[]=time&fields[]=episodes&fields[]=episodes_count&fields[]=episodesSchedule&fields[]=shiki_rate"
|
||||||
|
);
|
||||||
|
const json = await response.json();
|
||||||
|
const data = json.data || {};
|
||||||
|
const aliases = Array.isArray(data.otherNames) ? data.otherNames.join(", ") : "";
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: data.summary || "No summary available",
|
||||||
|
airdate: data.releaseDate || "Unknown",
|
||||||
|
aliases: aliases
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
airdate: "Error",
|
||||||
|
aliases: ""
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(slug) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://api.cdnlibs.org/api/episodes?anime_id=" + slug);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json && Array.isArray(json.data)) {
|
||||||
|
for (const episode of json.data) {
|
||||||
|
results.push({
|
||||||
|
href: episode.id ? String(episode.id) : "",
|
||||||
|
number: parseFloat(episode.number) || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(ID) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://api.cdnlibs.org/api/episodes/" + ID);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
const data = json.data || {};
|
||||||
|
const players = data.players || [];
|
||||||
|
|
||||||
|
const parserPromises = players
|
||||||
|
.filter(player => player.src && player.team && player.team.name)
|
||||||
|
.map(async (player) => {
|
||||||
|
try {
|
||||||
|
const kodikUrl = "https:" + player.src;
|
||||||
|
const qualitiesJson = await kodikParser(kodikUrl);
|
||||||
|
const qualities = JSON.parse(qualitiesJson);
|
||||||
|
|
||||||
|
let highestQuality = null;
|
||||||
|
let highestQualityNum = 0;
|
||||||
|
|
||||||
|
for (const quality in qualities) {
|
||||||
|
if (qualities[quality].src) {
|
||||||
|
const qualityNum = parseInt(quality.replace('p', '')) || 0;
|
||||||
|
if (qualityNum > highestQualityNum) {
|
||||||
|
highestQualityNum = qualityNum;
|
||||||
|
highestQuality = qualities[quality].src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highestQuality) {
|
||||||
|
|
||||||
|
if (highestQuality.startsWith('//')) {
|
||||||
|
highestQuality = 'https:' + highestQuality;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: player.team.name,
|
||||||
|
streamUrl: highestQuality,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||||
|
"Referer": "https://kodik.info/"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(parserPromises);
|
||||||
|
|
||||||
|
const streams = results.filter(stream => stream !== null);
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: streams,
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kodikParser(url) {
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
"Referer": "https://v3.animelib.org/",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
||||||
|
};
|
||||||
|
const response = await fetchv2(url, headers);
|
||||||
|
const htmlText = await response.text();
|
||||||
|
|
||||||
|
const urlParamsMatch = htmlText.match(/var\s+urlParams\s*=\s*'([^']+)'/);
|
||||||
|
const videoInfoTypeMatch = htmlText.match(/videoInfo\.type\s*=\s*'([^']+)'/);
|
||||||
|
const videoInfoHashMatch = htmlText.match(/videoInfo\.hash\s*=\s*'([^']+)'/);
|
||||||
|
const videoInfoIdMatch = htmlText.match(/videoInfo\.id\s*=\s*'([^']+)'/);
|
||||||
|
|
||||||
|
const urlParams = urlParamsMatch ? JSON.parse(urlParamsMatch[1]) : {};
|
||||||
|
const videoInfo_type = videoInfoTypeMatch ? videoInfoTypeMatch[1] : '';
|
||||||
|
const videoInfo_hash = videoInfoHashMatch ? videoInfoHashMatch[1] : '';
|
||||||
|
const videoInfo_id = videoInfoIdMatch ? videoInfoIdMatch[1] : '';
|
||||||
|
|
||||||
|
const finalData =
|
||||||
|
`d=${urlParams.d}` +
|
||||||
|
`&d_sign=${urlParams.d_sign}` +
|
||||||
|
`&pd=${urlParams.pd}` +
|
||||||
|
`&pd_sign=${urlParams.pd_sign}` +
|
||||||
|
`&ref=${urlParams.ref}` +
|
||||||
|
`&ref_sign=${urlParams.ref_sign}` +
|
||||||
|
`&bad_user=false&cdn_is_working=false` +
|
||||||
|
`&type=${videoInfo_type}&hash=${videoInfo_hash}&id=${videoInfo_id}&info=%7B%7D`;
|
||||||
|
|
||||||
|
|
||||||
|
const headers2 = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"Referer": "https://kodik.info",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||||
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
|
};
|
||||||
|
const apiResponse = await fetchv2("https://kodik.info/ftor", headers2, "POST", finalData);
|
||||||
|
const apiJson = await apiResponse.json();
|
||||||
|
|
||||||
|
const qualities = {};
|
||||||
|
|
||||||
|
if (apiJson?.links) {
|
||||||
|
for (const quality in apiJson.links) {
|
||||||
|
const qualityData = apiJson.links[quality];
|
||||||
|
|
||||||
|
if (qualityData && qualityData[0] && qualityData[0].src) {
|
||||||
|
const encodedSrc = qualityData[0].src;
|
||||||
|
const decodedUrl = decode(encodedSrc);
|
||||||
|
|
||||||
|
qualities[quality] = {
|
||||||
|
src: decodedUrl,
|
||||||
|
type: qualityData[0].type || 'application/x-mpegURL'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(qualities, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return JSON.stringify({ error: "error.org" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode(input) {
|
||||||
|
const _0x1a = ["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", "=", String.fromCharCode, ">="];
|
||||||
|
(function() {})();
|
||||||
|
const _map = _0x1a[0];
|
||||||
|
let _o = '',
|
||||||
|
_b = 0,
|
||||||
|
_c = 0;
|
||||||
|
const _r = [];
|
||||||
|
for (let _i = 0; _i < input.length; _i++) {
|
||||||
|
const _ch = input[_i];
|
||||||
|
if (/[a-zA-Z]/.test(_ch)) {
|
||||||
|
const _cc = _ch.charCodeAt(0);
|
||||||
|
const _max = _ch <= 'Z' ? 90 : 122;
|
||||||
|
let _sh = _cc + 18;
|
||||||
|
_r.push(String.fromCharCode(_sh <= _max ? _sh : _sh - 26));
|
||||||
|
} else _r.push(_ch);
|
||||||
|
}
|
||||||
|
const _rot = _r.join('');
|
||||||
|
for (let _j = 0; _j < _rot.length; _j++) {
|
||||||
|
const _ch = _rot[_j];
|
||||||
|
if (_ch === _0x1a[1]) break;
|
||||||
|
const _v = _map.indexOf(_ch);
|
||||||
|
if (_v === -1) continue;
|
||||||
|
_b = (_b << 6) | _v;
|
||||||
|
_c += 6;
|
||||||
|
if (_c >= 8) {
|
||||||
|
_c -= 8;
|
||||||
|
_o += _0x1a[2]((_b >> _c) & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _o;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "AnimeLib",
|
||||||
|
"iconUrl": "https://i.ibb.co/rfktbGyD/Untitled-design.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Russian",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://animelib.org/ru",
|
||||||
|
"searchBaseUrl": "https://animelib.org/ru",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animelib/animelib.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
+10
-3
@@ -1,3 +1,10 @@
|
|||||||
|
function cleanTitle(title) {
|
||||||
|
return title
|
||||||
|
.replace(/’/g, "'")
|
||||||
|
.replace(/–/g, "-")
|
||||||
|
.replace(/&#[0-9]+;/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
async function searchResults(keyword) {
|
async function searchResults(keyword) {
|
||||||
const results = [];
|
const results = [];
|
||||||
try {
|
try {
|
||||||
@@ -18,7 +25,7 @@ async function searchResults(keyword) {
|
|||||||
results.push({
|
results.push({
|
||||||
href: "https://animenana.com" + hrefMatch[1].trim(),
|
href: "https://animenana.com" + hrefMatch[1].trim(),
|
||||||
image: "https://animenana.com" + imgMatch[1].trim(),
|
image: "https://animenana.com" + imgMatch[1].trim(),
|
||||||
title: titleMatch[1].trim()
|
title: cleanTitle(titleMatch[1].trim())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +44,7 @@ async function searchResults(keyword) {
|
|||||||
results.push({
|
results.push({
|
||||||
href: "https://animenana.com" + hrefMatch[1].trim(),
|
href: "https://animenana.com" + hrefMatch[1].trim(),
|
||||||
image: "https://animenana.com" + imgMatch[1].trim(),
|
image: "https://animenana.com" + imgMatch[1].trim(),
|
||||||
title: titleMatch[1].trim()
|
title: cleanTitle(titleMatch[1].trim())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +58,7 @@ async function searchResults(keyword) {
|
|||||||
results.push({
|
results.push({
|
||||||
href: "https://animenana.com" + match[1].trim(),
|
href: "https://animenana.com" + match[1].trim(),
|
||||||
image: "https://animenana.com" + match[2].trim(),
|
image: "https://animenana.com" + match[2].trim(),
|
||||||
title: match[3].trim()
|
title: cleanTitle(match[3].trim())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "English (Hardsub)",
|
"language": "English (Hardsub)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
+16
-16
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "AnimeToast",
|
"sourceName": "AnimeToast",
|
||||||
"iconUrl": "https://www.animetoast.cc/wp-content/uploads/2018/03/toastfavi-300x300.png",
|
"iconUrl": "https://www.animetoast.cc/wp-content/uploads/2018/03/toastfavi-300x300.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "50/50 & Cufiy",
|
"name": "50/50 & Cufiy",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.2.12",
|
"version": "1.2.13",
|
||||||
"language": "German (DUB/SUB)",
|
"language": "German (DUB/SUB)",
|
||||||
"streamType": "MP4",
|
"streamType": "MP4",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"baseUrl": "https://www.animetoast.cc/",
|
"baseUrl": "https://www.animetoast.cc/",
|
||||||
"searchBaseUrl": "https://www.animetoast.cc/?s=the%s",
|
"searchBaseUrl": "https://www.animetoast.cc/?s=the%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animetoast/animetoast_v2.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animetoast/animetoast_v2.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
+665
-6
@@ -313,7 +313,7 @@ async function sendLog(message) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -321,8 +321,8 @@ async function sendLog(message) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -335,7 +335,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -398,8 +408,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -454,7 +470,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -513,6 +536,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -520,6 +550,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -527,6 +571,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -610,6 +710,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -664,6 +786,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -681,6 +1151,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -859,7 +1508,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -895,6 +1548,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -16,5 +16,5 @@
|
|||||||
"streamAsyncJS": false,
|
"streamAsyncJS": false,
|
||||||
"softsub": false,
|
"softsub": false,
|
||||||
"type": "anime",
|
"type": "anime",
|
||||||
"downloadSupport": true
|
"downloadSupport": false
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://aniweek.com/bbs/search.php?srows=240&gr_id=&sfl=wr_subject&stx=" + encodeURIComponent(keyword));
|
||||||
|
const html = await response.text();
|
||||||
|
const regex = /<div class="list-row">[\s\S]*?<a href="([^"]+)"[\s\S]*?<img src="([^"]+)" alt="([^"]+)"/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
title: match[3].trim(),
|
||||||
|
image: match[2].trim().replace("..","https://aniweek.com"),
|
||||||
|
href: "https://aniweek.com" + match[1].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
const regex = /<div class="view-stocon"><div class="c" id="animeContents">([\s\S]*?)<\/div><\/div>/;
|
||||||
|
|
||||||
|
let description = "N/A";
|
||||||
|
const match = regex.exec(html);
|
||||||
|
if (match) {
|
||||||
|
description = match[1].replace(/<br>/g, "\n").replace(/ /g, " ").replace(/<[^>]*>/g, "").replace(/\n\n+/g, "\n").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
const regex = /<li class="list-item">[\s\S]*?<div class="wr-num">(\d+)<\/div>[\s\S]*?<a href="([^"]+)"[\s\S]*?<\/li>/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
href: "https://aniweek.com" + match[2].trim(),
|
||||||
|
number: parseInt(match[1], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results.reverse());
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
const regex = /<input type="hidden" name="vurl" value="https:\/\/michealcdn\.com\/video\/([^"]+)"/;
|
||||||
|
|
||||||
|
const match = regex.exec(html);
|
||||||
|
if (match) {
|
||||||
|
try {
|
||||||
|
const videoId = match[1];
|
||||||
|
console.log("Video ID: " + videoId);
|
||||||
|
const postData = "hash=" + videoId + "&r=https%3A%2F%2Fmm.viaproducciones.net%2F"
|
||||||
|
const headers = {
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
};
|
||||||
|
const response = await fetchv2("https://michealcdn.com/player/index.php?data=" + videoId + "&do=getVideo", headers, "POST", postData);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Fetched video data: " + JSON.stringify(data));
|
||||||
|
|
||||||
|
const streamUrl = data.securedLink;
|
||||||
|
return JSON.stringify({
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"title": "Stream 1",
|
||||||
|
"streamUrl": streamUrl,
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Referer": "https://michealcdn.com/",
|
||||||
|
"Origin": "https://michealcdn.com",
|
||||||
|
"Sec-Fetch-Dest": "video",
|
||||||
|
"Sec-Fetch-Mode": "cors",
|
||||||
|
"Sec-Fetch-Site": "same-origin",
|
||||||
|
"Range": "bytes=0-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitle": ""
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error fetching video URL:"+ err);
|
||||||
|
}
|
||||||
|
return JSON.stringify({"streams": [{"title": "Error", "streamUrl": "https://error.org/", "headers": {}}], "subtitle": ""});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({"streams": [{"title": "Error", "streamUrl": "https://error.org/", "headers": {}}], "subtitle": ""});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({"streams": [{"title": "Error", "streamUrl": "https://error.org/", "headers": {}}], "subtitle": ""});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "AnimeWeek",
|
||||||
|
"iconUrl": "https://aniweek.com/apple-touch-icon.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.8",
|
||||||
|
"language": "Korean",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://aniweek.com/",
|
||||||
|
"searchBaseUrl": "https://aniweek.com/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animeweek/animeweek.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "AniWorld (ENG SUB)",
|
"sourceName": "AniWorld (ENG SUB)",
|
||||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Hamzo & Cufiy",
|
"name": "Hamzo & Cufiy",
|
||||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||||
},
|
},
|
||||||
"version": "0.2.7",
|
"version": "0.2.8",
|
||||||
"language": "English (SUB)",
|
"language": "English (SUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "720p",
|
"quality": "720p",
|
||||||
"baseUrl": "https://google.com",
|
"baseUrl": "https://google.com",
|
||||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldEngSub_v2.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldEngSub_v2.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"streamAsyncJS": false,
|
"streamAsyncJS": false,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "AniWorld (fixed)",
|
"sourceName": "AniWorld (Local Test)",
|
||||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Cufiy",
|
"name": "Cufiy",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"quality": "720p",
|
"quality": "720p",
|
||||||
"baseUrl": "https://vidmoly.to/",
|
"baseUrl": "https://vidmoly.to/",
|
||||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||||
"scriptUrl": "http://192.168.2.130/sora-sources2/aniworld/v2/AniWorldGerDub_v2.js",
|
"scriptUrl": "http://192.168.2.130/sora-module-repos/sources/aniworld/v2/AniWorldGerDub_v2.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "AniWorld (GER DUB)",
|
"sourceName": "AniWorld (GER DUB)",
|
||||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Hamzo & Cufiy",
|
"name": "Hamzo & Cufiy",
|
||||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||||
},
|
},
|
||||||
"version": "0.2.7",
|
"version": "0.2.8",
|
||||||
"language": "German (DUB)",
|
"language": "German (DUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "720p",
|
"quality": "720p",
|
||||||
"baseUrl": "https://google.com",
|
"baseUrl": "https://google.com",
|
||||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerDub_v2.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerDub_v2.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"streamAsyncJS": false,
|
"streamAsyncJS": false,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "AniWorld (GER SUB)",
|
"sourceName": "AniWorld (GER SUB)",
|
||||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Hamzo & Cufiy",
|
"name": "Hamzo & Cufiy",
|
||||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||||
},
|
},
|
||||||
"version": "0.2.7",
|
"version": "0.2.8",
|
||||||
"language": "German (SUB)",
|
"language": "German (SUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "720p",
|
"quality": "720p",
|
||||||
"baseUrl": "https://google.com",
|
"baseUrl": "https://google.com",
|
||||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerSub_v2.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerSub_v2.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"streamAsyncJS": false,
|
"streamAsyncJS": false,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
@@ -589,6 +589,8 @@ async function sendLog(message) {
|
|||||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||||
console.log(message);
|
console.log(message);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error sending log:', error);
|
console.error('Error sending log:', error);
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ async function sendLog(message) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -358,8 +358,8 @@ async function sendLog(message) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -372,7 +372,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -435,8 +445,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -491,7 +507,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -550,6 +573,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -557,6 +587,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -564,6 +608,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -647,6 +747,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -701,6 +823,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -718,6 +1188,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -896,7 +1545,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -932,6 +1585,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ function base64Decode(str) {
|
|||||||
async function sendLog(message) {
|
async function sendLog(message) {
|
||||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||||
console.log(message);
|
console.log(message);
|
||||||
// return;
|
return;
|
||||||
|
|
||||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -351,7 +351,7 @@ async function sendLog(message) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -359,8 +359,8 @@ async function sendLog(message) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -373,7 +373,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -436,8 +446,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -492,7 +508,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -551,6 +574,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -558,6 +588,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -565,6 +609,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -648,6 +748,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -702,6 +824,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -719,6 +1189,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -897,7 +1546,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -933,6 +1586,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ async function sendLog(message) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -358,8 +358,8 @@ async function sendLog(message) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -372,7 +372,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -435,8 +445,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -491,7 +507,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -550,6 +573,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -557,6 +587,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -564,6 +608,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -647,6 +747,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -701,6 +823,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -718,6 +1188,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -896,7 +1545,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -932,6 +1585,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://api-search.anroll.net/data?q=" + encodeURIComponent(keyword));
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
data.data.forEach(item => {
|
||||||
|
const baseUrl = item.generic_path.trim().startsWith('/f/')
|
||||||
|
? "https://www.anroll.net/_next/image?url=https://static.anroll.net/images/filmes/capas/"
|
||||||
|
: "https://www.anroll.net/_next/image?url=https://static.anroll.net/images/animes/capas/";
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
title: item.title.trim(),
|
||||||
|
image: baseUrl + item.slug.trim() + ".jpg&w=384&q=75",
|
||||||
|
href: item.generic_path.trim()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(slug) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://www.anroll.net/" + slug);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const match = html.match(/<div class="sinopse">(.*?)<\/div>/s);
|
||||||
|
const description = match ? match[1].trim() : "N/A";
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function extractEpisodes(slug) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
// If slug starts with /f/ (movie), extract ID and return as single episode
|
||||||
|
if (slug.startsWith('/f/')) {
|
||||||
|
results.push({
|
||||||
|
href: slug,
|
||||||
|
number: 1
|
||||||
|
});
|
||||||
|
return JSON.stringify(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process if slug starts with /a/ (anime)
|
||||||
|
if (!slug.startsWith('/a/')) {
|
||||||
|
return JSON.stringify(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchv2("https://www.anroll.net" + slug);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
// Extract series ID from __NEXT_DATA__
|
||||||
|
const scriptMatch = html.match(/<script id="__NEXT_DATA__" type="application\/json">({.*?})<\/script>/);
|
||||||
|
if (!scriptMatch) return JSON.stringify(results);
|
||||||
|
|
||||||
|
const jsonData = JSON.parse(scriptMatch[1]);
|
||||||
|
const seriesId = jsonData?.props?.pageProps?.data?.id_serie;
|
||||||
|
if (!seriesId) return JSON.stringify(results);
|
||||||
|
|
||||||
|
// Fetch all episodes from all pages
|
||||||
|
let page = 1;
|
||||||
|
let hasNextPage = true;
|
||||||
|
|
||||||
|
while (hasNextPage) {
|
||||||
|
const episodesResponse = await fetchv2(`https://apiv3-prd.anroll.net/animes/${seriesId}/episodes?page=${page}&order=desc`);
|
||||||
|
const episodesData = await episodesResponse.json();
|
||||||
|
|
||||||
|
if (episodesData.data && episodesData.data.length > 0) {
|
||||||
|
episodesData.data.forEach(episode => {
|
||||||
|
results.push({
|
||||||
|
href: episode.generate_id,
|
||||||
|
number: parseInt(episode.n_episodio, 10)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
hasNextPage = episodesData.meta?.hasNextPage || false;
|
||||||
|
page++;
|
||||||
|
} else {
|
||||||
|
hasNextPage = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results.reverse());
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(ID) {
|
||||||
|
try {
|
||||||
|
if (!ID.startsWith('/f/')) {
|
||||||
|
const response = await fetchv2("https://www.anroll.net/watch/e/" + ID);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const streamMatch = html.match(/streamUrl\\?":\\?"(https:\/\/[^"\\]+\.m3u8)/);
|
||||||
|
|
||||||
|
if (!streamMatch) return JSON.stringify({
|
||||||
|
streams: [{
|
||||||
|
title: "Server 1",
|
||||||
|
streamUrl: "https://error.org/no-stream-found",
|
||||||
|
headers: {}
|
||||||
|
}],
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const streamUrl = streamMatch[1];
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
title: "Server 1",
|
||||||
|
streamUrl: streamUrl,
|
||||||
|
headers: {
|
||||||
|
"Referer": "https://www.anroll.net/",
|
||||||
|
"Origin": "https://www.anroll.net"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchv2("https://www.anroll.net" + ID);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const scriptMatch = html.match(/<script id="__NEXT_DATA__" type="application\/json">({.*?})<\/script>/);
|
||||||
|
if (!scriptMatch) return "https://error.org/no-next-data";
|
||||||
|
|
||||||
|
const jsonData = JSON.parse(scriptMatch[1]);
|
||||||
|
const movieData = jsonData?.props?.pageProps?.data?.data_movie;
|
||||||
|
|
||||||
|
if (!movieData || !movieData.slug_filme) {
|
||||||
|
return "https://error.org/no-movie-data";
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = movieData.slug_filme;
|
||||||
|
const streamUrl = `https://cdn-zenitsu-2-gamabunta.b-cdn.net/cf/hls/movies/${slug}/movie.mp4/media-1/stream.m3u8`;
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
title: "Server 1",
|
||||||
|
streamUrl: streamUrl,
|
||||||
|
headers: {
|
||||||
|
"Referer": "https://www.anroll.net/",
|
||||||
|
"Origin": "https://www.anroll.net"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "AnimesRoll",
|
||||||
|
"iconUrl": "https://files.catbox.moe/9lx2bp.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Spanish",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://www.anroll.net/",
|
||||||
|
"searchBaseUrl": "https://www.anroll.net/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/anroll/anroll.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
+34
-37
@@ -260,8 +260,8 @@ async function searchResults(query) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const filteredResults = scoredResults
|
const filteredResults = scoredResults
|
||||||
.filter(r => r.score > 50) // Increased threshold to filter out weak matches
|
.filter(r => r.score > 50)
|
||||||
.sort((a, b) => b.score - a.score) // Sort by pre-calculated scores
|
.sort((a, b) => b.score - a.score)
|
||||||
.map(({ score, ...rest }) => rest);
|
.map(({ score, ...rest }) => rest);
|
||||||
|
|
||||||
return JSON.stringify(filteredResults.length > 0 ? filteredResults : [{
|
return JSON.stringify(filteredResults.length > 0 ? filteredResults : [{
|
||||||
@@ -330,18 +330,13 @@ async function extractDetails(url) {
|
|||||||
|
|
||||||
async function extractEpisodes(url) {
|
async function extractEpisodes(url) {
|
||||||
const sendEpisodes = async (endpoint, episodeData) => {
|
const sendEpisodes = async (endpoint, episodeData) => {
|
||||||
if (episodeData.length > 45) {
|
const promises = episodeData.map(item =>
|
||||||
const promises = episodeData.map(item =>
|
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
|
||||||
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
|
.then(res => res.json())
|
||||||
.then(res => res.text())
|
.then(json => ({ name: item.name, data: json.result }))
|
||||||
.then(data => ({ name: item.name, data }))
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
);
|
||||||
);
|
return Promise.all(promises);
|
||||||
return Promise.all(promises);
|
|
||||||
} else {
|
|
||||||
const resp = await fetchv2(endpoint, {}, "POST", JSON.stringify(episodeData));
|
|
||||||
return resp.json();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -351,8 +346,9 @@ async function extractEpisodes(url) {
|
|||||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||||
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
||||||
|
|
||||||
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`);
|
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
||||||
const token = await tokenResponse.text();
|
const tokenData = await tokenResponse.json();
|
||||||
|
const token = tokenData.result;
|
||||||
|
|
||||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||||
@@ -361,16 +357,18 @@ async function extractEpisodes(url) {
|
|||||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||||
|
|
||||||
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
const recentEpisodeMatches = episodeMatches.slice(-50);
|
||||||
|
|
||||||
|
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
||||||
name: `Episode ${episodeNum}`,
|
name: `Episode ${episodeNum}`,
|
||||||
data: episodeToken
|
data: episodeToken
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const batchResults = await sendEpisodes("https://ilovekai.simplepostrequest.workers.dev/?ilovefeet", episodeData);
|
const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
|
||||||
|
|
||||||
const episodes = batchResults.map((result, index) => ({
|
const episodes = batchResults.map((result, index) => ({
|
||||||
number: parseInt(episodeMatches[index][1], 10),
|
number: parseInt(recentEpisodeMatches[index][1], 10),
|
||||||
href: `Animekai:https://animekai.to/ajax/links/list?token=${episodeMatches[index][2]}&_=${result.data}`
|
href: `Animekai:https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
return JSON.stringify(episodes);
|
||||||
@@ -379,10 +377,9 @@ async function extractEpisodes(url) {
|
|||||||
const movieIDMatch = (htmlText.match(/<div class="detail-lower"[^>]*id="movie-rating"[^>]*data-id="([^"]+)"/) || [])[1];
|
const movieIDMatch = (htmlText.match(/<div class="detail-lower"[^>]*id="movie-rating"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||||
if (!movieIDMatch) return JSON.stringify([{ error: "MovieID not found" }]);
|
if (!movieIDMatch) return JSON.stringify([{ error: "MovieID not found" }]);
|
||||||
|
|
||||||
const movieData = [{ name: "MovieID", data: movieIDMatch }];
|
const tokenResponse = await fetchv2("https://enc-dec.app/api/enc-movies-flix?text=" + encodeURIComponent(movieIDMatch));
|
||||||
const tokenResponse = await fetchv2("https://ilovekai.simplepostrequest.workers.dev/ilovethighs", {}, "POST", JSON.stringify(movieData));
|
|
||||||
const temp = await tokenResponse.json();
|
const temp = await tokenResponse.json();
|
||||||
const token = temp[0]?.data;
|
const token = temp.result;
|
||||||
|
|
||||||
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
|
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
|
||||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||||
@@ -396,7 +393,7 @@ async function extractEpisodes(url) {
|
|||||||
data: episodeToken
|
data: episodeToken
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const batchResults = await sendEpisodes("https://ilovekai.simplepostrequest.workers.dev/?ilovethighs", episodeData);
|
const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-movies-flix?text", episodeData);
|
||||||
|
|
||||||
const episodes = batchResults.map((result, index) => ({
|
const episodes = batchResults.map((result, index) => ({
|
||||||
number: parseInt(episodeMatches[index][2], 10),
|
number: parseInt(episodeMatches[index][2], 10),
|
||||||
@@ -450,13 +447,13 @@ async function extractStreamUrl(url) {
|
|||||||
{ name: "Sub", data: serverIdSub }
|
{ name: "Sub", data: serverIdSub }
|
||||||
].filter(item => item.data);
|
].filter(item => item.data);
|
||||||
|
|
||||||
const tokenBatchResponse = await fetchv2(
|
const tokenPromises = tokenRequestData.map(item =>
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
||||||
{},
|
.then(res => res.json())
|
||||||
"POST",
|
.then(json => ({ name: item.name, data: json.result }))
|
||||||
JSON.stringify(tokenRequestData)
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
);
|
);
|
||||||
const tokenResults = await tokenBatchResponse.json();
|
const tokenResults = await Promise.all(tokenPromises);
|
||||||
|
|
||||||
const streamUrls = tokenResults.map(result => {
|
const streamUrls = tokenResults.map(result => {
|
||||||
const serverIdMap = {
|
const serverIdMap = {
|
||||||
@@ -501,13 +498,13 @@ async function extractStreamUrl(url) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptBatchResponse = await fetchv2(
|
const decryptPromises = decryptRequestData.map(item =>
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits",
|
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
||||||
{},
|
.then(res => res.json())
|
||||||
"POST",
|
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
||||||
JSON.stringify(decryptRequestData)
|
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||||
);
|
);
|
||||||
const decryptResults = await decryptBatchResponse.json();
|
const decryptResults = await Promise.all(decryptPromises);
|
||||||
|
|
||||||
const finalResults = {};
|
const finalResults = {};
|
||||||
decryptResults.forEach(result => {
|
decryptResults.forEach(result => {
|
||||||
@@ -569,7 +566,7 @@ async function extractStreamUrl(url) {
|
|||||||
if (dubStream) streams.push("Dubbed English", dubStream);
|
if (dubStream) streams.push("Dubbed English", dubStream);
|
||||||
|
|
||||||
const rawStream = decryptedRaw ? await getStream(decryptedRaw) : null;
|
const rawStream = decryptedRaw ? await getStream(decryptedRaw) : null;
|
||||||
if (rawStream) streams.push("Japanese", rawStream);
|
if (rawStream) streams.push("Original audio", rawStream);
|
||||||
|
|
||||||
const final = {
|
const final = {
|
||||||
streams,
|
streams,
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"language": "English",
|
"language": "English",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Tsumi (詰み) - Literally Everything 2.0",
|
||||||
|
"iconUrl": "https://files.catbox.moe/krovkt.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.2",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "4K",
|
||||||
|
"baseUrl": "https://google.com/",
|
||||||
|
"searchBaseUrl": "https://google.com/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/checkmate/checkmate.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
+13
-2
@@ -107,8 +107,14 @@ async function extractStreamUrl(url) {
|
|||||||
const iframeMatch = html.match(/<iframe[^>]+src="([^"]+)"/i);
|
const iframeMatch = html.match(/<iframe[^>]+src="([^"]+)"/i);
|
||||||
const iframeUrl = iframeMatch ? iframeMatch[1] : null;
|
const iframeUrl = iframeMatch ? iframeMatch[1] : null;
|
||||||
if (!iframeUrl) throw new Error("Iframe not found");
|
if (!iframeUrl) throw new Error("Iframe not found");
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
|
"Referer": url,
|
||||||
|
"origin": "https://coflix.cc"
|
||||||
|
};
|
||||||
|
|
||||||
const iframeResp = await fetchv2(iframeUrl);
|
const iframeResp = await fetchv2(iframeUrl, headers);
|
||||||
const iframeHtml = await iframeResp.text();
|
const iframeHtml = await iframeResp.text();
|
||||||
|
|
||||||
const uqloadMatch = iframeHtml.match(/<li[^>]*onclick="showVideo\('([^']+)',\s*'[^']+'\)">\s*<img[^>]*src="static\/server\/uqload[^"]*"[^>]*>\s*<span>Uqload/i);
|
const uqloadMatch = iframeHtml.match(/<li[^>]*onclick="showVideo\('([^']+)',\s*'[^']+'\)">\s*<img[^>]*src="static\/server\/uqload[^"]*"[^>]*>\s*<span>Uqload/i);
|
||||||
@@ -130,7 +136,12 @@ async function extractStreamUrl(url) {
|
|||||||
if (fileMonMatch) {
|
if (fileMonMatch) {
|
||||||
const filemoon = atob(fileMonMatch[1]);
|
const filemoon = atob(fileMonMatch[1]);
|
||||||
console.log("FileMon URL:" + filemoon);
|
console.log("FileMon URL:" + filemoon);
|
||||||
const fileResp = await fetchv2(filemoon);
|
const headers2 = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
|
"Referer": "https://lecteurvideo.com/"
|
||||||
|
};
|
||||||
|
const fileResp = await fetchv2(filemoon, headers2);
|
||||||
const fileHtml = await fileResp.text();
|
const fileHtml = await fileResp.text();
|
||||||
return filemoonExtractor(fileHtml, filemoon);
|
return filemoonExtractor(fileHtml, filemoon);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "French",
|
"language": "French",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
// Settings start
|
||||||
|
const preferedQualityOption = "Auto"; // ["Auto", "2160p", "1080p", "720p", "480p"]
|
||||||
|
const maxResultsPerResolution = 0;
|
||||||
|
const maxSize = 0;
|
||||||
|
const cachedOnly = false; // [true, false]
|
||||||
|
const removeTrash = true; // [true, false]
|
||||||
|
const resultFormat = "all"; // ["all"]
|
||||||
|
const debridService = "realdebrid"; // ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink"]
|
||||||
|
const debridApiKey = "";
|
||||||
|
const debridStreamProxyPassword = "";
|
||||||
|
const languagesExclude = "";
|
||||||
|
const languagesPreferred = "en";
|
||||||
|
const removeRanksUnder = -10000000000;
|
||||||
|
const allowEnglishInLanguages = false; // [true, false]
|
||||||
|
const removeUnknownLanguages = false; // [true, false]
|
||||||
|
// Settings end
|
||||||
|
|
||||||
|
async function searchResults(keyword) {
|
||||||
|
try {
|
||||||
|
const moviesresponse = await fetchv2(
|
||||||
|
"https://v3-cinemeta.strem.io/catalog/movie/top/search=" + encodeURIComponent(keyword) + ".json"
|
||||||
|
);
|
||||||
|
const moviesdata = await moviesresponse.json();
|
||||||
|
|
||||||
|
const results = moviesdata.metas.map(item => ({
|
||||||
|
title: item.name.trim(),
|
||||||
|
image: item.poster.trim(),
|
||||||
|
href: "Movie: " + (item.id.startsWith("tt") ? item.id : "")
|
||||||
|
}));
|
||||||
|
|
||||||
|
const showsresponse = await fetchv2(
|
||||||
|
"https://v3-cinemeta.strem.io/catalog/series/top/search=" + encodeURIComponent(keyword) + ".json"
|
||||||
|
);
|
||||||
|
const showsdata = await showsresponse.json();
|
||||||
|
|
||||||
|
const showResults = showsdata.metas.map(item => ({
|
||||||
|
title: item.name.trim(),
|
||||||
|
image: item.poster.trim(),
|
||||||
|
href: "TV: " + (item.id.startsWith("tt") ? item.id : "")
|
||||||
|
}));
|
||||||
|
|
||||||
|
results.push(...showResults);
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(ID) {
|
||||||
|
try {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = "https://v3-cinemeta.strem.io/meta/" + type + "/" + actualID + ".json";
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: data.meta.description || "N/A",
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: data.meta.released || "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(ID) {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (type === "series") {
|
||||||
|
const response = await fetchv2("https://v3-cinemeta.strem.io/meta/series/" + actualID + ".json");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const videos = data.meta.videos || [];
|
||||||
|
|
||||||
|
const shouldAdjust = videos.length > 0 && videos[0].season === 0;
|
||||||
|
|
||||||
|
let currentSeason = 0;
|
||||||
|
let episodeCounter = 0;
|
||||||
|
|
||||||
|
for (const video of videos) {
|
||||||
|
const adjustedSeason = shouldAdjust ? video.season + 1 : video.season;
|
||||||
|
if (adjustedSeason !== currentSeason) {
|
||||||
|
currentSeason = adjustedSeason;
|
||||||
|
episodeCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
episodeCounter++;
|
||||||
|
|
||||||
|
let adjustedId = video.id || "";
|
||||||
|
if (adjustedId && shouldAdjust) {
|
||||||
|
const idParts = adjustedId.split(':');
|
||||||
|
if (idParts.length === 3) {
|
||||||
|
idParts[1] = String(adjustedSeason);
|
||||||
|
adjustedId = idParts.join(':');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
href: "TV: " + adjustedId,
|
||||||
|
number: episodeCounter
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} else if (type === "movie") {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Movie: " + (actualID || ""),
|
||||||
|
number: 1
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(ID) {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
maxResultsPerResolution: maxResultsPerResolution,
|
||||||
|
maxSize: maxSize,
|
||||||
|
cachedOnly: cachedOnly,
|
||||||
|
removeTrash: removeTrash,
|
||||||
|
resultFormat: [resultFormat],
|
||||||
|
debridService: debridService,
|
||||||
|
debridApiKey: debridApiKey,
|
||||||
|
debridStreamProxyPassword: debridStreamProxyPassword,
|
||||||
|
languages: {
|
||||||
|
exclude: languagesExclude ? languagesExclude.split(",").map(s => s.trim()) : [],
|
||||||
|
preferred: languagesPreferred ? languagesPreferred.split(",").map(s => s.trim()) : ["en"]
|
||||||
|
},
|
||||||
|
resolutions: {},
|
||||||
|
options: {
|
||||||
|
remove_ranks_under: removeRanksUnder,
|
||||||
|
allow_english_in_languages: allowEnglishInLanguages,
|
||||||
|
remove_unknown_languages: removeUnknownLanguages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const encodedConfig = btoa(JSON.stringify(config));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = type === "movie"
|
||||||
|
? "https://comet.elfhosted.com/" + encodedConfig + "/stream/movie/" + actualID + ".json"
|
||||||
|
: "https://comet.elfhosted.com/" + encodedConfig + "/stream/series/" + actualID + ".json";
|
||||||
|
|
||||||
|
const response = await fetchv2(endpoint);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.streams || !Array.isArray(data.streams)) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamsByQuality = {
|
||||||
|
"2160p": [],
|
||||||
|
"1080p": [],
|
||||||
|
"720p": [],
|
||||||
|
"480p": []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const stream of data.streams) {
|
||||||
|
const name = stream.name || "";
|
||||||
|
let quality = null;
|
||||||
|
|
||||||
|
if (name.includes("2160p")) quality = "2160p";
|
||||||
|
else if (name.includes("1080p")) quality = "1080p";
|
||||||
|
else if (name.includes("720p")) quality = "720p";
|
||||||
|
else if (name.includes("480p")) quality = "480p";
|
||||||
|
|
||||||
|
if (quality && streamsByQuality[quality]) {
|
||||||
|
streamsByQuality[quality].push({
|
||||||
|
title: stream.name || "Unknown",
|
||||||
|
streamUrl: stream.url || "",
|
||||||
|
headers: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
if (preferedQualityOption === "Auto") {
|
||||||
|
results.push(...streamsByQuality["2160p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["1080p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["720p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["480p"].slice(0, 5));
|
||||||
|
} else {
|
||||||
|
if (streamsByQuality[preferedQualityOption]) {
|
||||||
|
results = streamsByQuality[preferedQualityOption].slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: results,
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Comet",
|
||||||
|
"iconUrl": "https://i.imgur.com/jmVoVMu.jpeg",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.1",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "MKV",
|
||||||
|
"quality": "4K",
|
||||||
|
"baseUrl": "https://www.google.com/",
|
||||||
|
"searchBaseUrl": "https://www.google.com/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/comet/comet.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true,
|
||||||
|
"settings": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
const response = await fetchv2(`https://desu-online.pl/?s=${keyword}`);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<article class="bs"[^>]*>.*?<a href="([^"]+)"[^>]*>.*?<img src="([^"]+)"[^>]*>.*?<h2[^>]*>(.*?)<\/h2>/gs;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
title: match[3].trim(),
|
||||||
|
image: match[2].trim(),
|
||||||
|
href: match[1].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
const results = [];
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const match = html.match(/<div class="entry-content"[^>]*>([\s\S]*?)<\/div>/);
|
||||||
|
|
||||||
|
let description = "N/A";
|
||||||
|
if (match) {
|
||||||
|
description = match[1]
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(code))
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
description: description,
|
||||||
|
aliases: 'N/A',
|
||||||
|
airdate: 'N/A'
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<a href="([^"]+)">\s*<div class="epl-num">([\d.]+)<\/div>/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
href: match[1].trim(),
|
||||||
|
number: parseInt(match[2], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
results.reverse();
|
||||||
|
return JSON.stringify(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
|
||||||
|
async function getBestHls(hlsUrl) {
|
||||||
|
try {
|
||||||
|
const res = await fetchv2(hlsUrl);
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
const regex = /#EXT-X-STREAM-INF:.*RESOLUTION=(\d+)x(\d+).*?\n(https?:\/\/[^\n]+)/g;
|
||||||
|
let match;
|
||||||
|
const streams = [];
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const width = parseInt(match[1]);
|
||||||
|
const height = parseInt(match[2]);
|
||||||
|
const url = match[3];
|
||||||
|
streams.push({ width, height, url });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streams.length === 0) return hlsUrl;
|
||||||
|
streams.sort((a, b) => b.height - a.height);
|
||||||
|
return streams[0].url;
|
||||||
|
} catch (err) {
|
||||||
|
return hlsUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractVideoIdFromIframely(iframelyUrl) {
|
||||||
|
try {
|
||||||
|
const iframeRes = await fetchv2(iframelyUrl);
|
||||||
|
const iframeHtml = await iframeRes.text();
|
||||||
|
const canonicalMatch = iframeHtml.match(/<meta name="canonical" content="https:\/\/www\.dailymotion\.com\/video\/([^"]+)"/);
|
||||||
|
return canonicalMatch ? canonicalMatch[1] : null;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const optionRegex = /<option value="([^"]+)"[^>]*>\s*DailyMotion\s*<\/option>/g;
|
||||||
|
const videoOptions = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = optionRegex.exec(html)) !== null) {
|
||||||
|
const base64Value = match[1];
|
||||||
|
try {
|
||||||
|
|
||||||
|
const decodedHtml = atob(base64Value);
|
||||||
|
|
||||||
|
const srcMatch = decodedHtml.match(/<iframe src="([^"]+)"/);
|
||||||
|
if (srcMatch) {
|
||||||
|
const iframeUrl = srcMatch[1];
|
||||||
|
const videoId = await extractVideoIdFromIframely("https:" + iframeUrl);
|
||||||
|
if (videoId) {
|
||||||
|
videoOptions.push({
|
||||||
|
videoId: videoId,
|
||||||
|
label: "Polish Hardsub"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const streams = [];
|
||||||
|
for (const option of videoOptions) {
|
||||||
|
try {
|
||||||
|
const metaRes = await fetchv2(`https://www.dailymotion.com/player/metadata/video/${option.videoId}`);
|
||||||
|
const metaJson = await metaRes.json();
|
||||||
|
const hlsLink = metaJson.qualities?.auto?.[0]?.url;
|
||||||
|
if (!hlsLink) continue;
|
||||||
|
|
||||||
|
const bestHls = await getBestHls(hlsLink);
|
||||||
|
streams.push(option.label);
|
||||||
|
streams.push(bestHls);
|
||||||
|
} catch (err) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: streams,
|
||||||
|
subtitles: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Desu-Online",
|
||||||
|
"iconUrl": "https://desu-online.pl/wp-content/uploads/2021/03/53454540000-300x300.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Polish",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://desu-online.pl/",
|
||||||
|
"searchBaseUrl": "https://desu-online.pl/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/desu-online/desu-online.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(`https://donghuanosekai.com/wp-json/site/search/?keyword=${encodeURIComponent(keyword)}&type=undefined&nonce=4c4380bfaa`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (data.hasOwnProperty(key)) {
|
||||||
|
const item = data[key];
|
||||||
|
results.push({
|
||||||
|
title: item.title,
|
||||||
|
image: item.img,
|
||||||
|
href: item.url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<div class="context">\s*<p>([\s\S]*?)<\/p>/i;
|
||||||
|
const match = html.match(regex);
|
||||||
|
const description = match ? match[1].trim() : "N/A";
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<a href="([^"]+)"[^>]*>\s*.*?<span class="episode">Episódio\s+(\d+)<\/span>/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
href: match[1].trim(),
|
||||||
|
number: parseInt(match[2], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results.reverse());
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const divRegex = /<div class="slideItem"[^>]*data-video-url="([^"]+)"[^>]*>\s*Player 2\s*<\/div>/i;
|
||||||
|
const divMatch = html.match(divRegex);
|
||||||
|
if (!divMatch) return "https://error.org/";
|
||||||
|
|
||||||
|
const playerUrl = divMatch[1].trim();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Referer": url,
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
||||||
|
};
|
||||||
|
const playerResponse = await fetchv2(playerUrl, headers);
|
||||||
|
const playerHtml = await playerResponse.text();
|
||||||
|
|
||||||
|
const iframeRegex = /<iframe[^>]+src="([^"]+)"/i;
|
||||||
|
const iframeMatch = playerHtml.match(iframeRegex);
|
||||||
|
|
||||||
|
if (!iframeMatch) return "https://error.org/";
|
||||||
|
|
||||||
|
const iframeSrc = iframeMatch[1];
|
||||||
|
|
||||||
|
const m3u8Regex = /v=(https:\/\/[^&"]+\.m3u8[^&"]*)/i;
|
||||||
|
const m3u8Match = iframeSrc.match(m3u8Regex);
|
||||||
|
|
||||||
|
return m3u8Match ? decodeURIComponent(m3u8Match[1]) : "https://error.org/";
|
||||||
|
} catch {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Donghanosekai",
|
||||||
|
"iconUrl": "https://files.catbox.moe/vgb4mq.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Spanish",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://donghuanosekai.com/",
|
||||||
|
"searchBaseUrl": "https://donghuanosekai.com/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/donghuanosekai/donghuanosekai.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true
|
||||||
|
}
|
||||||
+254
-30
@@ -147,7 +147,7 @@ function cleanHtmlSymbols(string) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.4} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -155,12 +155,13 @@ function cleanHtmlSymbols(string) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-08-13 03:44:07
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.4
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
function globalExtractor(providers) {
|
function globalExtractor(providers) {
|
||||||
for (const [url, provider] of Object.entries(providers)) {
|
for (const [url, provider] of Object.entries(providers)) {
|
||||||
try {
|
try {
|
||||||
@@ -307,6 +308,8 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
headers["encoding"] = "windows-1251"; // required
|
headers["encoding"] = "windows-1251"; // required
|
||||||
} else if (provider == 'sibnet') {
|
} else if (provider == 'sibnet') {
|
||||||
headers["encoding"] = "windows-1251"; // required
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
@@ -367,6 +370,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -374,6 +384,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "megacloud":
|
case "megacloud":
|
||||||
try {
|
try {
|
||||||
return await megacloudExtractor(html, url);
|
return await megacloudExtractor(html, url);
|
||||||
@@ -388,6 +405,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "sibnet":
|
case "sibnet":
|
||||||
try {
|
try {
|
||||||
return await sibnetExtractor(html, url);
|
return await sibnetExtractor(html, url);
|
||||||
@@ -395,6 +419,34 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from sibnet:", error);
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "uqload":
|
case "uqload":
|
||||||
try {
|
try {
|
||||||
return await uqloadExtractor(html, url);
|
return await uqloadExtractor(html, url);
|
||||||
@@ -402,6 +454,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from uqload:", error);
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -430,17 +489,11 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
// EXTRACTORS //
|
// EXTRACTORS //
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
// DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING //
|
// DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING //
|
||||||
|
|
||||||
|
|
||||||
/* --- bigwarp --- */
|
/* --- bigwarp --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,8 +511,6 @@ async function bigwarpExtractor(videoPage, url = null) {
|
|||||||
console.log("BigWarp HD Decoded:", bwDecoded);
|
console.log("BigWarp HD Decoded:", bwDecoded);
|
||||||
return bwDecoded;
|
return bwDecoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- doodstream --- */
|
/* --- doodstream --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -493,7 +544,27 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
@@ -549,8 +620,18 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
/* --- megacloud --- */
|
/* --- megacloud --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -560,16 +641,28 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
|
|
||||||
// Megacloud V3 specific
|
// Megacloud V3 specific
|
||||||
async function megacloudExtractor(html, embedUrl) {
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
const xraxParams = embedUrl.split('/').pop();
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
const nonce = await getNonce(embedUrl);
|
const nonce = await getNonce(embedUrl);
|
||||||
// return decrypt(secretKey, nonce, encryptedText);
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`);
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
const rawSourceData = await response.json();
|
const rawSourceData = await response.json();
|
||||||
const encrypted = rawSourceData?.sources;
|
const encrypted = rawSourceData?.sources;
|
||||||
let decryptedSources = null;
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
if (rawSourceData?.encrypted == false) {
|
if (rawSourceData?.encrypted == false) {
|
||||||
decryptedSources = rawSourceData.sources;
|
decryptedSources = rawSourceData.sources;
|
||||||
}
|
}
|
||||||
@@ -577,14 +670,14 @@ async function megacloudExtractor(html, embedUrl) {
|
|||||||
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
}
|
}
|
||||||
console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
// return the first source if it's an array
|
// return the first source if it's an array
|
||||||
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
try {
|
try {
|
||||||
return decryptedSources[0].file;
|
return decryptedSources[0].file;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error extracting MegaCloud stream URL:" + error);
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
return null;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// return {
|
// return {
|
||||||
@@ -774,7 +867,7 @@ async function megacloudExtractor(html, embedUrl) {
|
|||||||
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
*/
|
*/
|
||||||
async function getNonce(embedUrl) {
|
async function getNonce(embedUrl) {
|
||||||
const res = await fetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
if (match0?.[1]) {
|
if (match0?.[1]) {
|
||||||
@@ -857,10 +950,10 @@ async function megacloudExtractor(html, embedUrl) {
|
|||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
function fetchKey(name, url, timeout = 1000) {
|
function fetchKey(name, url) {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { method: 'get', timeout: timeout });
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
const key = await response.text();
|
const key = await response.text();
|
||||||
let trueKey = null;
|
let trueKey = null;
|
||||||
try {
|
try {
|
||||||
@@ -875,8 +968,6 @@ async function megacloudExtractor(html, embedUrl) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -894,8 +985,17 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
/* --- sibnet --- */
|
/* --- sibnet --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -920,8 +1020,119 @@ async function sibnetExtractor(html, embedUrl) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
/* --- uqload --- */
|
/* --- uqload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -938,7 +1149,20 @@ async function uqloadExtractor(html, embedUrl) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
@@ -978,8 +1202,6 @@ async function vidmolyExtractor(html, url = null) {
|
|||||||
return sourcesString;
|
return sourcesString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- vidoza --- */
|
/* --- vidoza --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -996,8 +1218,6 @@ async function vidozaExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- voe --- */
|
/* --- voe --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1093,10 +1313,6 @@ function voeShiftChars(str, shift) {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
// PLUGINS //
|
// PLUGINS //
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
@@ -1126,7 +1342,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -1163,6 +1383,10 @@ class Unbaser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
|
|||||||
+18
-18
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "DoraBash",
|
"sourceName": "DoraBash",
|
||||||
"iconUrl": "https://dorabash.com/wp-content/uploads/2023/06/cropped-Untitled_design-removebg-192x192.png",
|
"iconUrl": "https://dorabash.com/wp-content/uploads/2023/06/cropped-Untitled_design-removebg-192x192.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"language": "Hindi",
|
"language": "Hindi",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"baseUrl": "https://dorabash.com/",
|
"baseUrl": "https://dorabash.com/",
|
||||||
"searchBaseUrl": "https://dorabash.com/",
|
"searchBaseUrl": "https://dorabash.com/",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/dorabash/dorabash.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/dorabash/dorabash.js",
|
||||||
"type": "anime",
|
"type": "anime",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"softsub": false,
|
"softsub": false,
|
||||||
"downloadSupport": false
|
"downloadSupport": false
|
||||||
}
|
}
|
||||||
@@ -72,6 +72,7 @@ async function extractEpisodes(url) {
|
|||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractStreamUrl(url) {
|
async function extractStreamUrl(url) {
|
||||||
try {
|
try {
|
||||||
const response = await fetchv2(url);
|
const response = await fetchv2(url);
|
||||||
@@ -85,7 +86,7 @@ async function extractStreamUrl(url) {
|
|||||||
const serversJson = jsonMatch[1].replace(/\\/g, '');
|
const serversJson = jsonMatch[1].replace(/\\/g, '');
|
||||||
const servers = JSON.parse(serversJson);
|
const servers = JSON.parse(serversJson);
|
||||||
|
|
||||||
const fdewsdcServer = servers.find(server => server.name === "fdewsdc");
|
const fdewsdcServer = servers.find(server => server.name === "EarnVids");
|
||||||
if (fdewsdcServer) {
|
if (fdewsdcServer) {
|
||||||
console.log("Stream URL: " + fdewsdcServer.url);
|
console.log("Stream URL: " + fdewsdcServer.url);
|
||||||
return await extractEarnVids(fdewsdcServer.url);
|
return await extractEarnVids(fdewsdcServer.url);
|
||||||
@@ -97,8 +98,12 @@ async function extractStreamUrl(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function extractEarnVids(url) {
|
async function extractEarnVids(url) {
|
||||||
|
const headers = {
|
||||||
|
"Referer": "https://shahed4u.day/",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const response = await fetchv2(url);
|
const response = await fetchv2(url, headers);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
|
|
||||||
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "Arabic",
|
"language": "Arabic",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
+41
-60
@@ -80,72 +80,21 @@ async function extractEpisodes(url) {
|
|||||||
async function extractStreamUrl(url) {
|
async function extractStreamUrl(url) {
|
||||||
try {
|
try {
|
||||||
const header = {
|
const header = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/",
|
||||||
|
"Referer": url
|
||||||
};
|
};
|
||||||
const postData = "View=1";
|
const postData = "View=1";
|
||||||
const response = await fetchv2(url, header, "POST", postData);
|
const response = await fetchv2(url, header, "POST", postData);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const match = html.match(/<li data-link="(https:\/\/mivalyo\.com\/v\/[^"]+)"/);
|
|
||||||
const streamUrl = match ? match[1] : "https://files.catbox.moe/avolvc.mp4";
|
|
||||||
console.log("Stream URL:", streamUrl);
|
|
||||||
|
|
||||||
const responseTwo = await fetchv2(streamUrl);
|
|
||||||
const htmlTwo = await responseTwo.text();
|
|
||||||
const obfuscatedScript = htmlTwo.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
|
||||||
|
|
||||||
if (!obfuscatedScript) {
|
|
||||||
console.log("No obfuscated script found, using fallback");
|
|
||||||
return "https://files.catbox.moe/avolvc.mp4";
|
|
||||||
}
|
|
||||||
|
|
||||||
const unpackedScript = unpack(obfuscatedScript[1]);
|
|
||||||
console.log("Unpacked script:", unpackedScript.substring(0, 200) + "...");
|
|
||||||
|
|
||||||
let hlsLink = null;
|
|
||||||
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
|
||||||
|
|
||||||
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
|
||||||
if (streamMatch) {
|
|
||||||
hlsLink = streamMatch[1];
|
|
||||||
console.log("Found stream path:", hlsLink);
|
|
||||||
return baseUrl + hlsLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hlsObjectMatch = unpackedScript.match(/["']hls\d?["']\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/);
|
const earnvidsMatch = html.match(/<li[^>]*data-link=["']([^"']+)["'][^>]*>\s*<span>\s*<p>\s*EarnVids\s*<\/p>/i);
|
||||||
if (hlsObjectMatch) {
|
const embedResponse = await fetchv2(earnvidsMatch[1], header);
|
||||||
hlsLink = hlsObjectMatch[1];
|
const embedHtml = await embedResponse.text();
|
||||||
console.log("Found HLS object link:", hlsLink);
|
if (earnvidsMatch) {
|
||||||
return hlsLink;
|
const stream = await earnvidsExtractor(embedHtml, url);
|
||||||
|
return stream;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
const m3u8Match = unpackedScript.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/);
|
|
||||||
if (m3u8Match) {
|
|
||||||
hlsLink = m3u8Match[1];
|
|
||||||
console.log("Found m3u8 link:", hlsLink);
|
|
||||||
return hlsLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cdnMatch = unpackedScript.match(/["'](https?:\/\/[^"']*(?:cdn|stream)[^"']*\.m3u8[^"']*)["']/i);
|
|
||||||
if (cdnMatch) {
|
|
||||||
hlsLink = cdnMatch[1];
|
|
||||||
console.log("Found CDN link:", hlsLink);
|
|
||||||
return hlsLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linksMatch = unpackedScript.match(/links\s*[=:]\s*({[^}]+}|\[[^\]]+\])/);
|
|
||||||
if (linksMatch) {
|
|
||||||
const linksStr = linksMatch[1];
|
|
||||||
const urlInLinks = linksStr.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/);
|
|
||||||
if (urlInLinks) {
|
|
||||||
hlsLink = urlInLinks[1];
|
|
||||||
console.log("Found link in links object:", hlsLink);
|
|
||||||
return hlsLink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("No HLS link found in unpacked script, using fallback");
|
|
||||||
return "https://files.catbox.moe/avolvc.mp4";
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in extractStreamUrl:", err);
|
console.error("Error in extractStreamUrl:", err);
|
||||||
return "https://files.catbox.moe/avolvc.mp4";
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
@@ -153,6 +102,34 @@ async function extractStreamUrl(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* SCHEME START */
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* REMOVE_START */
|
||||||
|
|
||||||
/***********************************************************
|
/***********************************************************
|
||||||
* UNPACKER MODULE
|
* UNPACKER MODULE
|
||||||
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
@@ -271,3 +248,7 @@ function unpack(source) {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* REMOVE_END */
|
||||||
|
|
||||||
|
/* SCHEME END */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "Arabic",
|
"language": "Arabic",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
+16
-16
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "FireAnime SUB",
|
"sourceName": "FireAnime SUB",
|
||||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "50/50 & Cufiy",
|
"name": "50/50 & Cufiy",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"language": "German (SUB)",
|
"language": "German (SUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"baseUrl": "https://fireani.me/",
|
"baseUrl": "https://fireani.me/",
|
||||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerSub.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerSub.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "FireAnime DUB",
|
"sourceName": "FireAnime DUB",
|
||||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "50/50 & Cufiy",
|
"name": "50/50 & Cufiy",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"language": "German (DUB)",
|
"language": "German (DUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"baseUrl": "https://fireani.me/",
|
"baseUrl": "https://fireani.me/",
|
||||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerDub.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerDub.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
+16
-16
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "FireAnime English (SUB)",
|
"sourceName": "FireAnime English (SUB)",
|
||||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "50/50 & Cufiy",
|
"name": "50/50 & Cufiy",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"language": "English (SUB)",
|
"language": "English (SUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"baseUrl": "https://fireani.me/",
|
"baseUrl": "https://fireani.me/",
|
||||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeEngSub.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeEngSub.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"type": "anime"
|
"type": "anime"
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ async function sendLog(message) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -198,8 +198,8 @@ async function sendLog(message) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -212,7 +212,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -275,8 +285,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -331,7 +347,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -390,6 +413,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -397,6 +427,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -404,6 +448,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -487,6 +587,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -541,6 +663,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -558,6 +1028,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -736,7 +1385,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -772,6 +1425,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ async function sendLog(message) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -199,8 +199,8 @@ async function sendLog(message) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -213,7 +213,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -276,8 +286,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -332,7 +348,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -391,6 +414,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -398,6 +428,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -405,6 +449,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -488,6 +588,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -542,6 +664,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -559,6 +1029,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -737,7 +1386,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -773,6 +1426,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ async function sendLog(message) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -198,8 +198,8 @@ async function sendLog(message) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -212,7 +212,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -275,8 +285,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -331,7 +347,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -390,6 +413,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -397,6 +427,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -404,6 +448,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -487,6 +587,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -541,6 +663,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -558,6 +1028,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -736,7 +1385,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -772,6 +1425,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://hdfilme.blog/?story=" + keyword + "&do=search&subaction=search");
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<a class="block relative" href="([^"]+)"[^>]*>\s*<figure[^>]*>\s*<img[^>]+src="([^"]+)"[^>]*>\s*[\s\S]*?<h3[^>]*>([^<]+)<\/h3>/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
title: match[3].trim(),
|
||||||
|
image: match[2].trim().startsWith("http") ? match[2].trim() : "https://hdfilme.blog" + match[2].trim(),
|
||||||
|
href: match[1].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const match = html.match(/<div class="font-extralight prose max-w-none">([\s\S]*?)<\/div>/);
|
||||||
|
const description = match
|
||||||
|
? match[1].replace(/<[^>]+>/g, "").trim()
|
||||||
|
: "N/A";
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const seasonRegex = /Staffel\s+(\d+)([\s\S]*?)(?=<div class="su-spoiler su-spoiler-style-default su-spoiler-icon-arrow">|$)/g;
|
||||||
|
let seasonMatch;
|
||||||
|
|
||||||
|
while ((seasonMatch = seasonRegex.exec(html)) !== null) {
|
||||||
|
const seasonNumber = parseInt(seasonMatch[1], 10);
|
||||||
|
const seasonContent = seasonMatch[2];
|
||||||
|
|
||||||
|
const episodeRegex = /href="(https:\/\/supervideo\.tv\/[^"]+)"/g;
|
||||||
|
let episodeMatch;
|
||||||
|
let episodeNumber = 1;
|
||||||
|
|
||||||
|
while ((episodeMatch = episodeRegex.exec(seasonContent)) !== null) {
|
||||||
|
results.push({
|
||||||
|
number: episodeNumber,
|
||||||
|
href: episodeMatch[1].replace("https://supervideo.tv", "https://supervideo.cc").trim()
|
||||||
|
});
|
||||||
|
episodeNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
const playerMatch = html.match(/<div class="embed-responsive-item player-container-wrap guardahd-player"[^>]*><iframe[^>]+src="([^"]+)"/);
|
||||||
|
if (playerMatch) {
|
||||||
|
const playerUrl = playerMatch[1];
|
||||||
|
|
||||||
|
const playerResponse = await fetchv2(playerUrl);
|
||||||
|
const playerHtml = await playerResponse.text();
|
||||||
|
|
||||||
|
const mirrorRegex = /<li[^>]*data-link="([^"]+)"[^>]*>[\s\S]*?<\/li>/g;
|
||||||
|
let mirrorMatch;
|
||||||
|
|
||||||
|
while ((mirrorMatch = mirrorRegex.exec(playerHtml)) !== null) {
|
||||||
|
let link = mirrorMatch[1].trim();
|
||||||
|
|
||||||
|
if (link.includes("supervideo")) {
|
||||||
|
if (link.startsWith("//")) {
|
||||||
|
link = "https:" + link;
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
number: 1,
|
||||||
|
href: link
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{ href: "Error", number: "Error" }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
if (obfuscatedScript) {
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
console.log(unpackedScript);
|
||||||
|
const hlsMatch = unpackedScript.match(/file:"([^"]*\.m3u8[^"]*)"/);
|
||||||
|
if (hlsMatch) {
|
||||||
|
return hlsMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://error.org/";
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error extracting stream URL:", err);
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DEOBFUSCATOR CODE
|
||||||
|
*
|
||||||
|
* Copy the below code fully and paste it in your
|
||||||
|
* code. No need to modify anything.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Unbaser {
|
||||||
|
constructor(base) {
|
||||||
|
this.ALPHABET = {
|
||||||
|
62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
95: "' !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'",
|
||||||
|
};
|
||||||
|
this.dictionary = {};
|
||||||
|
this.base = base;
|
||||||
|
if (36 < base && base < 62) {
|
||||||
|
this.ALPHABET[base] = this.ALPHABET[base] ||
|
||||||
|
this.ALPHABET[62].substr(0, base);
|
||||||
|
}
|
||||||
|
if (2 <= base && base <= 36) {
|
||||||
|
this.unbase = (value) => parseInt(value, base);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
[...this.ALPHABET[base]].forEach((cipher, index) => {
|
||||||
|
this.dictionary[cipher] = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (er) {
|
||||||
|
throw Error("Unsupported base encoding.");
|
||||||
|
}
|
||||||
|
this.unbase = this._dictunbaser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_dictunbaser(value) {
|
||||||
|
let ret = 0;
|
||||||
|
[...value].reverse().forEach((cipher, index) => {
|
||||||
|
ret = ret + ((Math.pow(this.base, index)) * this.dictionary[cipher]);
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detect(source) {
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
|
function unpack(source) {
|
||||||
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
|
if (count != symtab.length) {
|
||||||
|
throw Error("Malformed p.a.c.k.e.r. symtab.");
|
||||||
|
}
|
||||||
|
let unbase;
|
||||||
|
try {
|
||||||
|
unbase = new Unbaser(radix);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
throw Error("Unknown p.a.c.k.e.r. encoding.");
|
||||||
|
}
|
||||||
|
function lookup(match) {
|
||||||
|
const word = match;
|
||||||
|
let word2;
|
||||||
|
if (radix == 1) {
|
||||||
|
word2 = symtab[parseInt(word)];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
word2 = symtab[unbase.unbase(word)];
|
||||||
|
}
|
||||||
|
return word2 || word;
|
||||||
|
}
|
||||||
|
source = payload.replace(/\b\w+\b/g, lookup);
|
||||||
|
return _replacestrings(source);
|
||||||
|
function _filterargs(source) {
|
||||||
|
const juicers = [
|
||||||
|
/}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\), *(\d+), *(.*)\)\)/,
|
||||||
|
/}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\)/,
|
||||||
|
];
|
||||||
|
for (const juicer of juicers) {
|
||||||
|
const args = juicer.exec(source);
|
||||||
|
if (args) {
|
||||||
|
let a = args;
|
||||||
|
if (a[2] == "[]") {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
payload: a[1],
|
||||||
|
symtab: a[4].split("|"),
|
||||||
|
radix: parseInt(a[2]),
|
||||||
|
count: parseInt(a[3]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (ValueError) {
|
||||||
|
throw Error("Corrupted p.a.c.k.e.r. data.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Error("Could not make sense of p.a.c.k.e.r data (unexpected code structure)");
|
||||||
|
}
|
||||||
|
function _replacestrings(source) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "HDFilme",
|
||||||
|
"iconUrl": "https://hdfilme.blog/templates/hdfilme/images/favicon-32x32.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "German",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://supervideo.cc/",
|
||||||
|
"searchBaseUrl": "https://supervideo.cc/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/hdfilme/hdfilme.js",
|
||||||
|
"type": "shows/movies",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": false,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://iptv-org.github.io/iptv/index.m3u");
|
||||||
|
const m3uContent = await response.text();
|
||||||
|
|
||||||
|
const lines = m3uContent.split('\n');
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
|
||||||
|
if (line.startsWith('#EXTINF:')) {
|
||||||
|
const logoMatch = line.match(/tvg-logo="([^"]*)"/);
|
||||||
|
const logo = logoMatch ? logoMatch[1] : '';
|
||||||
|
|
||||||
|
const lastCommaIndex = line.lastIndexOf(',');
|
||||||
|
const fullName = lastCommaIndex !== -1 ? line.substring(lastCommaIndex + 1).trim() : '';
|
||||||
|
|
||||||
|
const streamUrl = (i + 1 < lines.length) ? lines[i + 1].trim() : '';
|
||||||
|
|
||||||
|
const hlsEndings = [".m3u8", ".m3u", ".m3u?", ".m3u8?", ".m3u#", ".m3u8#"];
|
||||||
|
const urlLower = streamUrl.toLowerCase();
|
||||||
|
const isHls = hlsEndings.some(ending => urlLower.endsWith(ending));
|
||||||
|
if (fullName.toLowerCase().includes(keyword.toLowerCase()) && isHls) {
|
||||||
|
results.push({
|
||||||
|
title: fullName,
|
||||||
|
image: logo,
|
||||||
|
href: streamUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "N/A",
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
href: url,
|
||||||
|
number: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify({
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"title": "Server 1",
|
||||||
|
"streamUrl": url,
|
||||||
|
"headers": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitle": ""
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "IPTV-org",
|
||||||
|
"iconUrl": "https://files.catbox.moe/q9hxx0.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Multi",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://iptv-org.github.io/",
|
||||||
|
"searchBaseUrl": "https://iptv-org.github.io/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/iptv-org/iptv-org.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false,
|
||||||
|
"note": "USE DEFAULT PLAYER - USE DEFAULT PLAYER - USE DEFAULT PLAYER - USE DEFAULT PLAYER"
|
||||||
|
}
|
||||||
+29
-17
@@ -1,8 +1,8 @@
|
|||||||
async function searchResults(keyword) {
|
async function searchResults(keyword) {
|
||||||
const searchUrl = `https://kimcartoon.com.co/?s=${encodeURIComponent(keyword)}`;
|
const searchUrl = `https://kimcartoon.com.co/?s=${encodeURIComponent(keyword)}`;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(searchUrl);
|
const response = await fetchv2(searchUrl);
|
||||||
const html = await response;
|
const html = await response;text();
|
||||||
const results = [];
|
const results = [];
|
||||||
const articleRegex = /<article[^>]*class="bs styletwo"[\s\S]*?<\/article>/g;
|
const articleRegex = /<article[^>]*class="bs styletwo"[\s\S]*?<\/article>/g;
|
||||||
const items = html.match(articleRegex) || [];
|
const items = html.match(articleRegex) || [];
|
||||||
@@ -32,8 +32,8 @@ async function searchResults(keyword) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function extractDetails(url) {
|
async function extractDetails(url) {
|
||||||
const response = await fetch(url);
|
const response = await fetchv2(url);
|
||||||
const html = await response;
|
const html = await response.text();
|
||||||
const details = [];
|
const details = [];
|
||||||
const descriptionMatch = html.match(/<div class="entry-content" itemprop="description">\s*<p>(.*?)<\/p>/);
|
const descriptionMatch = html.match(/<div class="entry-content" itemprop="description">\s*<p>(.*?)<\/p>/);
|
||||||
const description = descriptionMatch ? descriptionMatch[1].trim() : `N/A`;
|
const description = descriptionMatch ? descriptionMatch[1].trim() : `N/A`;
|
||||||
@@ -48,8 +48,8 @@ async function extractDetails(url) {
|
|||||||
return JSON.stringify(details);
|
return JSON.stringify(details);
|
||||||
}
|
}
|
||||||
async function extractEpisodes(url) {
|
async function extractEpisodes(url) {
|
||||||
const response = await fetch(url);
|
const response = await fetchv2(url);
|
||||||
const html = await response;
|
const html = await response.text();
|
||||||
const episodes = [];
|
const episodes = [];
|
||||||
|
|
||||||
const allMatches = [...html.matchAll(/<li[^>]*>\s*<a href="([^"]+)">\s*<div class="epl-title">([^<]*) <\/div>/g)];
|
const allMatches = [...html.matchAll(/<li[^>]*>\s*<a href="([^"]+)">\s*<div class="epl-title">([^<]*) <\/div>/g)];
|
||||||
@@ -80,8 +80,8 @@ async function extractEpisodes(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function extractStreamUrl(url) {
|
async function extractStreamUrl(url) {
|
||||||
const embedResponse = await fetch(url);
|
const embedResponse = await fetchv2(url);
|
||||||
const data = await embedResponse;
|
const data = await embedResponse.text();
|
||||||
|
|
||||||
const embedMatch = data.match(/<div class="pembed" data-embed="(.*?)"/);
|
const embedMatch = data.match(/<div class="pembed" data-embed="(.*?)"/);
|
||||||
if (embedMatch && embedMatch[1]) {
|
if (embedMatch && embedMatch[1]) {
|
||||||
@@ -97,17 +97,29 @@ async function extractStreamUrl(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(fullEmbedUrl);
|
console.log(fullEmbedUrl);
|
||||||
const embedPageResponse = await fetch(fullEmbedUrl);
|
const embedPageResponse = await fetchv2(fullEmbedUrl);
|
||||||
const embedPageData = await embedPageResponse;
|
const embedPageData = await embedPageResponse.text();
|
||||||
|
|
||||||
console.log(embedPageData);
|
console.log(embedPageData);
|
||||||
const m3u8Match = embedPageData.match(/sources:\s*\[\{file:"(https:\/\/[^"]*\.m3u8)"/);
|
|
||||||
if (m3u8Match && m3u8Match[1]) {
|
const iframeMatch = embedPageData.match(/<iframe[^>]*src="([^"]+)"/);
|
||||||
const m3u8Url = m3u8Match[1];
|
if (iframeMatch && iframeMatch[1]) {
|
||||||
console.log(m3u8Url);
|
const iframeUrl = iframeMatch[1];
|
||||||
return m3u8Url;
|
console.log("Iframe URL:", iframeUrl);
|
||||||
|
|
||||||
|
const iframeResponse = await fetchv2(iframeUrl);
|
||||||
|
const iframeData = await iframeResponse.text();
|
||||||
|
|
||||||
|
const m3u8Match = iframeData.match(/sources:\s*\[\{"file":"(https:\/\/[^"]*\.m3u8)"/);
|
||||||
|
if (m3u8Match && m3u8Match[1]) {
|
||||||
|
const m3u8Url = m3u8Match[1];
|
||||||
|
console.log(m3u8Url);
|
||||||
|
return m3u8Url;
|
||||||
|
} else {
|
||||||
|
throw new Error("M3U8 URL not found in iframe content.");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("M3U8 URL not found.");
|
throw new Error("Iframe URL not found in embedPageData.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Embed URL not found.");
|
throw new Error("Embed URL not found.");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "English (DUB)",
|
"language": "English (DUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
@@ -13,5 +13,6 @@
|
|||||||
"searchBaseUrl": "https://kimcartoon.com.co/?s=%s",
|
"searchBaseUrl": "https://kimcartoon.com.co/?s=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/kimcartoon/kimcartoon.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/kimcartoon/kimcartoon.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"type": "anime/shows/movies"
|
"type": "anime/shows/movies",
|
||||||
|
"downloadSupport": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// Search, Details and Episodes function are from ibro's module.
|
||||||
|
|
||||||
|
async function searchResults(keyword) {
|
||||||
|
try {
|
||||||
|
const encodedKeyword = encodeURIComponent(keyword);
|
||||||
|
const responseText = await soraFetch(`https://kisskh.co/api/DramaList/Search?q=${encodedKeyword}&type=0`);
|
||||||
|
const data = await responseText.json();
|
||||||
|
|
||||||
|
const transformedResults = data.map(result => {
|
||||||
|
const editedTitle = result.title.replace(/[\s()']/g, '-');
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: result.title,
|
||||||
|
image: result.thumbnail,
|
||||||
|
href: `https://kisskh.co/Drama/${editedTitle}?id=${result.id}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(transformedResults);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Fetch error in searchResults:', error);
|
||||||
|
return JSON.stringify([{ title: 'Error', image: '', href: '' }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const match = url.match(/https:\/\/kisskh\.co\/Drama\/([^\/]+)\?id=([^\/]+)/);
|
||||||
|
if (!match) throw new Error("Invalid URL format");
|
||||||
|
|
||||||
|
const showId = match[2];
|
||||||
|
const responseText = await soraFetch(`https://kisskh.co/api/DramaList/Drama/${showId}?isq=false`);
|
||||||
|
const data = await responseText.json();
|
||||||
|
|
||||||
|
const transformedResults = [{
|
||||||
|
description: data.description || 'No description available',
|
||||||
|
aliases: ``,
|
||||||
|
airdate: `Released: ${data.releaseDate ? data.releaseDate : 'Unknown'}`
|
||||||
|
}];
|
||||||
|
|
||||||
|
return JSON.stringify(transformedResults);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Details error:', error);
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: 'Error loading description',
|
||||||
|
aliases: 'Duration: Unknown',
|
||||||
|
airdate: 'Aired/Released: Unknown'
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
try {
|
||||||
|
const match = url.match(/https:\/\/kisskh\.co\/Drama\/([^\/]+)\?id=([^\/]+)/);
|
||||||
|
if (!match) throw new Error("Invalid URL format");
|
||||||
|
const showTitle = match[1];
|
||||||
|
const showId = match[2];
|
||||||
|
|
||||||
|
const showResponseText = await soraFetch(`https://kisskh.co/api/DramaList/Drama/${showId}?isq=false`);
|
||||||
|
const showData = await showResponseText.json();
|
||||||
|
|
||||||
|
const episodes = showData.episodes?.map(episode => ({
|
||||||
|
href: `https://kisskh.co/Drama/${showTitle}/Episode-${episode.number}?id=${showId}&ep=${episode.id}`,
|
||||||
|
number: episode.number,
|
||||||
|
title: episode.name || `Episode ${episode.number}` || ""
|
||||||
|
}));
|
||||||
|
|
||||||
|
const reversedEpisodes = episodes.reverse();
|
||||||
|
|
||||||
|
console.log(reversedEpisodes);
|
||||||
|
|
||||||
|
return JSON.stringify(reversedEpisodes);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Fetch error in extractEpisodes:', error);
|
||||||
|
return JSON.stringify([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const episodeID = url.split('&ep=')[1];
|
||||||
|
|
||||||
|
const decryptedStreamResponse = await soraFetch(`https://enc-dec.app/api/enc-kisskh?text=${episodeID}&type=vid`);
|
||||||
|
const decryptedStreamData = await decryptedStreamResponse.json();
|
||||||
|
const streamkKey = decryptedStreamData.result;
|
||||||
|
|
||||||
|
const decryptedSubtitlesResponse = await soraFetch(`https://enc-dec.app/api/enc-kisskh?text=${episodeID}&type=sub`);
|
||||||
|
const decryptedSubtitlesData = await decryptedSubtitlesResponse.json();
|
||||||
|
const subtitlesKey = decryptedSubtitlesData.result;
|
||||||
|
|
||||||
|
const streamResponse = await soraFetch(`https://kisskh.co/api/DramaList/Episode/${episodeID}.png?err=false&ts=null&time=null&kkey=${streamkKey}`);
|
||||||
|
const streamData = await streamResponse.json();
|
||||||
|
const streamUrl = streamData.Video;
|
||||||
|
|
||||||
|
const subtitlesResponse = await soraFetch(`https://kisskh.co/api/Sub/${episodeID}?kkey=${subtitlesKey}`);
|
||||||
|
const subtitlesData = await subtitlesResponse.json();
|
||||||
|
const englishSubtitle = subtitlesData.find(sub => sub.land === "en");
|
||||||
|
const englishSubtitleUrl = englishSubtitle?.src || "none";
|
||||||
|
|
||||||
|
let formattedSubtitleUrl = englishSubtitleUrl;
|
||||||
|
if (/\?v=/.test(englishSubtitleUrl)) {
|
||||||
|
formattedSubtitleUrl = "https://kisskh-five.vercel.app/api/proxy?url=" + encodeURIComponent(englishSubtitleUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamUrl) {
|
||||||
|
const results = {
|
||||||
|
streams: [{
|
||||||
|
title: "Stream",
|
||||||
|
streamUrl,
|
||||||
|
headers: {
|
||||||
|
"Referer": "https://kisskh.co/",
|
||||||
|
"Origin": "https://kisskh.co"
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
subtitles: formattedSubtitleUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Fetch error in extractStreamUrl:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function soraFetch(url, options = { headers: {}, method: 'GET', body: null, encoding: 'utf-8' }) {
|
||||||
|
try {
|
||||||
|
return await fetchv2(
|
||||||
|
url,
|
||||||
|
options.headers ?? {},
|
||||||
|
options.method ?? 'GET',
|
||||||
|
options.body ?? null,
|
||||||
|
true,
|
||||||
|
options.encoding ?? 'utf-8'
|
||||||
|
);
|
||||||
|
} catch(e) {
|
||||||
|
try {
|
||||||
|
return await fetch(url, options);
|
||||||
|
} catch(error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Kisskh",
|
||||||
|
"iconUrl": "https://files.catbox.moe/wi6a1a.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://kisskh.co/",
|
||||||
|
"searchBaseUrl": "https://kisskh.co/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/kisskh/kisskh.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const response = await fetchv2(`https://v6.kuramanime.run/anime?search=${keyword}&order_by=oldest`);
|
||||||
|
const html = await response.text();
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
const animeEntryRegex = /<div class="product__item">[\s\S]*?<a href="([^"]+)"[\s\S]*?data-setbg="([^"]+)"[\s\S]*?<h5><a[^>]*>([^<]+)<\/a><\/h5>/g;
|
||||||
|
|
||||||
|
const entries = html.matchAll(animeEntryRegex);
|
||||||
|
for (const entry of entries) {
|
||||||
|
const href = entry[1].trim();
|
||||||
|
const imageUrl = entry[2].trim();
|
||||||
|
const title = entry[3].trim();
|
||||||
|
|
||||||
|
if (href && imageUrl && title) {
|
||||||
|
results.push({
|
||||||
|
title: title,
|
||||||
|
href: href,
|
||||||
|
image: imageUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(results));
|
||||||
|
return JSON.stringify(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const details = [];
|
||||||
|
const descriptionMatch = html.match(/<p id="synopsisField"[^>]*>([\s\S]*?)<\/p>/);
|
||||||
|
|
||||||
|
const description = descriptionMatch ?
|
||||||
|
descriptionMatch[1]
|
||||||
|
.replace(/<\/?[^>]+(>|$)/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim() :
|
||||||
|
'N/A';
|
||||||
|
|
||||||
|
details.push({
|
||||||
|
description: description,
|
||||||
|
aliases: 'N/A',
|
||||||
|
airdate: 'N/A'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(details);
|
||||||
|
return JSON.stringify(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
const episodes = [];
|
||||||
|
|
||||||
|
const oldestMatch = html.match(/<a\s+class=['"]btn btn-sm btn-secondary mb-1 mt-1['"]\s*href=['"]([^'"]+\/episode\/(\d+))['"][^>]*>\s*Ep\s*\d+\s*\(Terlama\)/i);
|
||||||
|
|
||||||
|
const newestMatch = html.match(/<a\s+class=['"]btn btn-sm btn-secondary mb-1 mt-1['"]\s*href=['"]([^'"]+\/episode\/(\d+))['"][^>]*>\s*Ep\s*\d+\s*\(Terbaru\)/i);
|
||||||
|
|
||||||
|
if (oldestMatch && newestMatch) {
|
||||||
|
const firstEpisode = parseInt(oldestMatch[2], 10);
|
||||||
|
const lastEpisode = parseInt(newestMatch[2], 10);
|
||||||
|
|
||||||
|
for (let i = firstEpisode; i <= lastEpisode; i++) {
|
||||||
|
episodes.push({
|
||||||
|
href: `${url}/episode/${i}`,
|
||||||
|
number: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const episodeLinks = [...html.matchAll(/<a\s+class=['"]btn btn-sm btn-danger mb-1 mt-1['"]\s*href=['"]([^'"]+\/episode\/(\d+))['"][^>]*>\s*Ep\s*\d+/gi)];
|
||||||
|
|
||||||
|
if (episodeLinks.length > 0) {
|
||||||
|
for (const match of episodeLinks) {
|
||||||
|
episodes.push({
|
||||||
|
href: match[1],
|
||||||
|
number: parseInt(match[2], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
episodes.push({
|
||||||
|
href: `${url}/episode/1`,
|
||||||
|
number: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Extracted episodes:" + JSON.stringify(episodes));
|
||||||
|
return JSON.stringify(episodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
const firstCodeMatch = /data-kk="([^"]+)"/.exec(html);
|
||||||
|
|
||||||
|
if (!firstCodeMatch) return null;
|
||||||
|
|
||||||
|
const firstCode = firstCodeMatch[1];
|
||||||
|
|
||||||
|
const responseTwo = await fetchv2(`https://v6.kuramanime.run/assets/js/${firstCode}.js`);
|
||||||
|
const htmlTwo = await responseTwo.text();
|
||||||
|
|
||||||
|
const authRouteMatch = /MIX_AUTH_ROUTE_PARAM:\s*'([^']+)'/.exec(htmlTwo);
|
||||||
|
const pageTokenMatch = /MIX_PAGE_TOKEN_KEY:\s*'([^']+)'/.exec(htmlTwo);
|
||||||
|
const streamServerMatch = /MIX_STREAM_SERVER_KEY:\s*'([^']+)'/.exec(htmlTwo);
|
||||||
|
|
||||||
|
const authRouteParam = authRouteMatch ? authRouteMatch[1] : null;
|
||||||
|
const pageTokenKey = pageTokenMatch ? pageTokenMatch[1] : null;
|
||||||
|
const streamServerKey = streamServerMatch ? streamServerMatch[1] : null;
|
||||||
|
|
||||||
|
const responseThree = await fetchv2(`https://v6.kuramanime.run/assets/${authRouteParam}`);
|
||||||
|
const thirdRandomAssCode = await responseThree.text();
|
||||||
|
const fullUrl = `${url}?${pageTokenMatch}=${thirdRandomAssCode}&${streamServerMatch}=kuramadrive&page=1`
|
||||||
|
|
||||||
|
const fullUrlClean = cleanUrl(fullUrl);
|
||||||
|
console.error(fullUrlClean);
|
||||||
|
|
||||||
|
const responseFour = await fetchv2(fullUrlClean);
|
||||||
|
const actualHtml = await responseFour.text();
|
||||||
|
|
||||||
|
const mp4SourceMatch = actualHtml.match(/<source[^>]+src="([^"]+)"[^>]+type="video\/mp4"/);
|
||||||
|
|
||||||
|
if (mp4SourceMatch) {
|
||||||
|
const mp4Url = mp4SourceMatch[1].replace(/&/g, '&');
|
||||||
|
console.error("Found MP4 URL:"+ mp4Url);
|
||||||
|
return mp4Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("No MP4 source found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanUrl(url) {
|
||||||
|
return url.replace(/MIX_PAGE_TOKEN_KEY:\s*'[^']+',/, '')
|
||||||
|
.replace(/MIX_STREAM_SERVER_KEY:\s*'[^']+',/, '')
|
||||||
|
.replace(/:\s*'[^']+'/, '');
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Kuramanime",
|
||||||
|
"iconUrl": "https://v8.kuramanime.tel/images/icons/icon-512x512.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Indonesian",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://v8.kuramanime.tel/",
|
||||||
|
"searchBaseUrl": "https://v8.kuramanime.tel/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/kuramanime/kuramanime.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
//Thanks ibro for the TMDB search!
|
||||||
|
|
||||||
|
async function searchResults(keyword) {
|
||||||
|
try {
|
||||||
|
let transformedResults = [];
|
||||||
|
|
||||||
|
const keywordGroups = {
|
||||||
|
trending: ["!trending", "!hot", "!tr", "!!"],
|
||||||
|
topRatedMovie: ["!top-rated-movie", "!topmovie", "!tm", "??"],
|
||||||
|
topRatedTV: ["!top-rated-tv", "!toptv", "!tt", "::"],
|
||||||
|
popularMovie: ["!popular-movie", "!popmovie", "!pm", ";;"],
|
||||||
|
popularTV: ["!popular-tv", "!poptv", "!pt", "++"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipTitleFilter = Object.values(keywordGroups).flat();
|
||||||
|
|
||||||
|
const shouldFilter = !matchesKeyword(keyword, skipTitleFilter);
|
||||||
|
|
||||||
|
// --- TMDB Section ---
|
||||||
|
const encodedKeyword = encodeURIComponent(keyword);
|
||||||
|
let baseUrl = null;
|
||||||
|
|
||||||
|
if (matchesKeyword(keyword, keywordGroups.trending)) {
|
||||||
|
baseUrl = `https://api.themoviedb.org/3/trending/all/week?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
|
||||||
|
} else if (matchesKeyword(keyword, keywordGroups.topRatedMovie)) {
|
||||||
|
baseUrl = `https://api.themoviedb.org/3/movie/top_rated?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
|
||||||
|
} else if (matchesKeyword(keyword, keywordGroups.topRatedTV)) {
|
||||||
|
baseUrl = `https://api.themoviedb.org/3/tv/top_rated?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
|
||||||
|
} else if (matchesKeyword(keyword, keywordGroups.popularMovie)) {
|
||||||
|
baseUrl = `https://api.themoviedb.org/3/movie/popular?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
|
||||||
|
} else if (matchesKeyword(keyword, keywordGroups.popularTV)) {
|
||||||
|
baseUrl = `https://api.themoviedb.org/3/tv/popular?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
|
||||||
|
} else {
|
||||||
|
baseUrl = `https://api.themoviedb.org/3/search/multi?api_key=9801b6b0548ad57581d111ea690c85c8&query=${encodedKeyword}&include_adult=false&page=`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataResults = [];
|
||||||
|
|
||||||
|
if (baseUrl) {
|
||||||
|
const pagePromises = Array.from({ length: 5 }, (_, i) =>
|
||||||
|
soraFetch(baseUrl + (i + 1)).then(r => r.json())
|
||||||
|
);
|
||||||
|
const pages = await Promise.all(pagePromises);
|
||||||
|
dataResults = pages.flatMap(p => p.results || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataResults.length > 0) {
|
||||||
|
function slugify(str) {
|
||||||
|
return (str || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-");
|
||||||
|
}
|
||||||
|
transformedResults = transformedResults.concat(
|
||||||
|
dataResults
|
||||||
|
.map(result => {
|
||||||
|
if (result.media_type === "movie" || result.title) {
|
||||||
|
const title = result.title || result.name || result.original_title || result.original_name || "Untitled";
|
||||||
|
const slug = slugify(title);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
image: result.poster_path ? `https://image.tmdb.org/t/p/w500${result.poster_path}` : "",
|
||||||
|
href: `movie/${result.id}-${slug}`,
|
||||||
|
};
|
||||||
|
} else if (result.media_type === "tv" || result.name) {
|
||||||
|
const title = result.name || result.title || result.original_name || result.original_title || "Untitled";
|
||||||
|
const slug = slugify(title);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
image: result.poster_path ? `https://image.tmdb.org/t/p/w500${result.poster_path}` : "",
|
||||||
|
href: `tv/${result.id}-${slug}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(result => result.title !== "Overflow")
|
||||||
|
.filter(result => result.title !== "My Marriage Partner Is My Student, a Cocky Troublemaker")
|
||||||
|
.filter(r => !shouldFilter || r.title.toLowerCase().includes(keyword.toLowerCase()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Transformed Results: " + JSON.stringify(transformedResults));
|
||||||
|
return JSON.stringify(transformedResults);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Fetch error in searchResults: " + error);
|
||||||
|
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesKeyword(keyword, commands) {
|
||||||
|
const lower = keyword.toLowerCase();
|
||||||
|
return commands.some(cmd => lower.startsWith(cmd.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
if(url.includes('movie')) {
|
||||||
|
const match = url.match(/movie\/([^\/]+)/);
|
||||||
|
if (!match) throw new Error("Invalid URL format");
|
||||||
|
|
||||||
|
const movieId = match[1];
|
||||||
|
const responseText = await soraFetch(`https://api.themoviedb.org/3/movie/${movieId}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
||||||
|
const data = await responseText.json();
|
||||||
|
|
||||||
|
const transformedResults = [{
|
||||||
|
description: data.overview || 'No description available',
|
||||||
|
aliases: `If stream fails try again!`,
|
||||||
|
airdate: `If stream fails try again!`
|
||||||
|
}];
|
||||||
|
|
||||||
|
return JSON.stringify(transformedResults);
|
||||||
|
} else if(url.includes('tv')) {
|
||||||
|
const match = url.match(/tv\/([^\/]+)/);
|
||||||
|
if (!match) throw new Error("Invalid URL format");
|
||||||
|
|
||||||
|
const showId = match[1];
|
||||||
|
const responseText = await soraFetch(`https://api.themoviedb.org/3/tv/${showId}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
||||||
|
const data = await responseText.json();
|
||||||
|
|
||||||
|
const transformedResults = [{
|
||||||
|
description: data.overview || 'No description available',
|
||||||
|
aliases: `If stream fails try again!`,
|
||||||
|
airdate: `If stream fails try again!`
|
||||||
|
}];
|
||||||
|
|
||||||
|
console.log(JSON.stringify(transformedResults));
|
||||||
|
return JSON.stringify(transformedResults);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid URL format");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Details error: ' + error);
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: 'Error loading description',
|
||||||
|
aliases: 'Duration: Unknown',
|
||||||
|
airdate: 'Aired/Released: Unknown'
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
try {
|
||||||
|
function slugify(str) {
|
||||||
|
return (str || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-");
|
||||||
|
}
|
||||||
|
if(url.startsWith('movie/')) {
|
||||||
|
const match = url.match(/movie\/(\d+)-(.+)/);
|
||||||
|
if (!match) throw new Error("Invalid movie URL format");
|
||||||
|
const movieId = match[1];
|
||||||
|
const titleSlug = match[2];
|
||||||
|
const movie = [
|
||||||
|
{ href: `movie/${movieId}-${titleSlug}`, number: 1, title: "Full Movie" }
|
||||||
|
];
|
||||||
|
return JSON.stringify(movie);
|
||||||
|
} else if(url.startsWith('tv/')) {
|
||||||
|
const match = url.match(/tv\/(\d+)-(.+)/);
|
||||||
|
if (!match) throw new Error("Invalid TV URL format");
|
||||||
|
const showId = match[1];
|
||||||
|
const titleSlug = match[2];
|
||||||
|
const showResponseText = await soraFetch(`https://api.themoviedb.org/3/tv/${showId}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
||||||
|
const showData = await showResponseText.json();
|
||||||
|
let allEpisodes = [];
|
||||||
|
for (const season of showData.seasons) {
|
||||||
|
const seasonNumber = season.season_number;
|
||||||
|
if(seasonNumber === 0) continue;
|
||||||
|
const seasonResponseText = await soraFetch(`https://api.themoviedb.org/3/tv/${showId}/season/${seasonNumber}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
||||||
|
const seasonData = await seasonResponseText.json();
|
||||||
|
if (seasonData.episodes && seasonData.episodes.length) {
|
||||||
|
const episodes = seasonData.episodes.map(episode => ({
|
||||||
|
href: `tv/${showId}-${seasonNumber}-${episode.episode_number}-${titleSlug}`,
|
||||||
|
number: episode.episode_number,
|
||||||
|
title: episode.name || ""
|
||||||
|
}));
|
||||||
|
allEpisodes = allEpisodes.concat(episodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(allEpisodes);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid URL format");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Fetch error in extractEpisodes: ' + error);
|
||||||
|
return JSON.stringify([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(ID) {
|
||||||
|
const htmlResponse = await soraFetch("https://mapple.uk/watch/" + ID);
|
||||||
|
const htmlText = await htmlResponse.text();
|
||||||
|
|
||||||
|
const jsUrlRegex = /(?:src|href)="([^"]*\.js(?:\?[^"]*)?)"/g;
|
||||||
|
const jsUrls = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = jsUrlRegex.exec(htmlText)) !== null) {
|
||||||
|
let jsUrl = match[1];
|
||||||
|
if (jsUrl.startsWith('/_next/') || jsUrl.startsWith('/static/')) {
|
||||||
|
jsUrl = 'https://mapple.uk' + jsUrl;
|
||||||
|
} else if (!jsUrl.startsWith('http')) {
|
||||||
|
jsUrl = 'https://mapple.uk/' + jsUrl;
|
||||||
|
}
|
||||||
|
jsUrls.push(jsUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueJsUrls = [...new Set(jsUrls)];
|
||||||
|
console.log("Found " + uniqueJsUrls.length + " unique JS files");
|
||||||
|
|
||||||
|
let actionIdentifier = null;
|
||||||
|
let foundPromiseResolver = null;
|
||||||
|
const foundPromise = new Promise((resolve) => {
|
||||||
|
foundPromiseResolver = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchPromises = uniqueJsUrls.map(async (url) => {
|
||||||
|
try {
|
||||||
|
if (actionIdentifier) return null;
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
if (actionIdentifier) return null;
|
||||||
|
const text = await response.text();
|
||||||
|
if (actionIdentifier) return null;
|
||||||
|
const actionMatch = text.match(/createServerReference\)\("([a-f0-9]{40,})"[^"]*"getStreamUrl/);
|
||||||
|
if (actionMatch && actionMatch[1] && !actionIdentifier) {
|
||||||
|
actionIdentifier = actionMatch[1];
|
||||||
|
console.log("Found actionIdentifier in: " + url);
|
||||||
|
foundPromiseResolver(actionIdentifier);
|
||||||
|
return actionIdentifier;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error fetching " + url + ": " + e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.race([
|
||||||
|
foundPromise,
|
||||||
|
Promise.all(searchPromises)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!actionIdentifier) {
|
||||||
|
throw new Error("Failed to extract action identifier from any chunk file");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Action Identifier: " + actionIdentifier);
|
||||||
|
|
||||||
|
const sessionResponse = await soraFetch("https://enc-dec.app/api/enc-mapple");
|
||||||
|
const sessionData = await sessionResponse.json();
|
||||||
|
console.log("Session Data: " + JSON.stringify(sessionData));
|
||||||
|
const sessionID = sessionData.result.sessionId || "N/A";
|
||||||
|
|
||||||
|
if (ID.startsWith('movie/')) {
|
||||||
|
const match = ID.match(/movie\/(\d+)-/);
|
||||||
|
const tmdbID = match ? match[1] : '';
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"next-action": actionIdentifier
|
||||||
|
};
|
||||||
|
const postData = `[{"mediaId":${tmdbID},"mediaType":"movie","tv_slug":"","source":"mapple","useFallbackVideo":false,"sessionId":"${sessionID}"}]`;
|
||||||
|
const response = await fetchv2("https://mapple.uk/watch/"+ ID, headers, "POST", postData);
|
||||||
|
const data = await response.text();
|
||||||
|
console.log("Stream data: " + data);
|
||||||
|
|
||||||
|
const lines = data.split('\n');
|
||||||
|
let streamData = null;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('"success":true')) {
|
||||||
|
streamData = JSON.parse(line.substring(line.indexOf('{')));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamData && streamData.data && streamData.data.stream_url) {
|
||||||
|
const m3u8Response = await soraFetch(streamData.data.stream_url, {
|
||||||
|
headers: {
|
||||||
|
"referer": "https://mapple.uk/",
|
||||||
|
"origin": "https://mapple.uk"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const m3u8Text = await m3u8Response.text();
|
||||||
|
console.log("M3U8 Playlist: " + m3u8Text);
|
||||||
|
|
||||||
|
const streams = [];
|
||||||
|
const lines = m3u8Text.split('\n');
|
||||||
|
const resolutionNames = {
|
||||||
|
"3840x2160": "4K",
|
||||||
|
"1920x1080": "1080p",
|
||||||
|
"1280x720": "720p",
|
||||||
|
"640x360": "360p"
|
||||||
|
};
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
|
const resolutionMatch = lines[i].match(/RESOLUTION=(\d+x\d+)/);
|
||||||
|
const streamUrl = lines[i + 1];
|
||||||
|
if (resolutionMatch && streamUrl && streamUrl.trim()) {
|
||||||
|
const resolution = resolutionMatch[1];
|
||||||
|
const friendlyName = resolutionNames[resolution] || resolution;
|
||||||
|
streams.push({
|
||||||
|
title: friendlyName,
|
||||||
|
streamUrl: streamUrl.trim(),
|
||||||
|
headers: {
|
||||||
|
"referer": "https://mapple.uk/",
|
||||||
|
"origin": "https://mapple.uk"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let englishSubtitleUrl = "";
|
||||||
|
try {
|
||||||
|
const subResponse = await soraFetch(`https://mapple.uk/api/subtitles?id=${tmdbID}&mediaType=movie`);
|
||||||
|
const subData = await subResponse.json();
|
||||||
|
const englishSub = subData.find(sub => sub.language === "en");
|
||||||
|
if (englishSub) {
|
||||||
|
englishSubtitleUrl = englishSub.url;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
englishSubtitleUrl = "";
|
||||||
|
}
|
||||||
|
console.log("English Subtitle URL: " + englishSubtitleUrl);
|
||||||
|
console.log("Extracted streams: " + JSON.stringify(streams));
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: streams.length > 0 ? streams : [
|
||||||
|
{
|
||||||
|
title: "Mapple Server",
|
||||||
|
streamUrl: streamData.data.stream_url,
|
||||||
|
headers: {
|
||||||
|
"referer": "https://mapple.uk/",
|
||||||
|
"origin": "https://mapple.uk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtitle: englishSubtitleUrl || ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to extract stream URL");
|
||||||
|
}
|
||||||
|
} else if (ID.startsWith('tv/')) {
|
||||||
|
const match = ID.match(/tv\/(\d+)-(\d+)-(\d+)/);
|
||||||
|
if (!match) throw new Error("Invalid TV URL format");
|
||||||
|
|
||||||
|
const tmdbID = match[1];
|
||||||
|
const seasonNumber = match[2];
|
||||||
|
const episodeNumber = match[3];
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"next-action": actionIdentifier
|
||||||
|
};
|
||||||
|
const postData = `[{"mediaId":${tmdbID},"mediaType":"tv","tv_slug":"${seasonNumber}-${episodeNumber}","source":"mapple","useFallbackVideo":false,"sessionId":"${sessionID}"}]`;
|
||||||
|
const response = await fetchv2("https://mapple.uk/watch/"+ ID, headers, "POST", postData);
|
||||||
|
const data = await response.text();
|
||||||
|
console.log("Stream data: " + data);
|
||||||
|
|
||||||
|
const lines = data.split('\n');
|
||||||
|
let streamData = null;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('"success":true')) {
|
||||||
|
streamData = JSON.parse(line.substring(line.indexOf('{')));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamData && streamData.data && streamData.data.stream_url) {
|
||||||
|
const m3u8Response = await soraFetch(streamData.data.stream_url, {
|
||||||
|
headers: {
|
||||||
|
"referer": "https://mapple.uk/",
|
||||||
|
"origin": "https://mapple.uk"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const m3u8Text = await m3u8Response.text();
|
||||||
|
console.log("M3U8 Playlist: " + m3u8Text);
|
||||||
|
|
||||||
|
const streams = [];
|
||||||
|
const lines = m3u8Text.split('\n');
|
||||||
|
const resolutionNames = {
|
||||||
|
"3840x2160": "4K",
|
||||||
|
"1920x1080": "1080p",
|
||||||
|
"1280x720": "720p",
|
||||||
|
"640x360": "360p"
|
||||||
|
};
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
|
const resolutionMatch = lines[i].match(/RESOLUTION=(\d+x\d+)/);
|
||||||
|
const streamUrl = lines[i + 1];
|
||||||
|
if (resolutionMatch && streamUrl && streamUrl.trim()) {
|
||||||
|
const resolution = resolutionMatch[1];
|
||||||
|
const friendlyName = resolutionNames[resolution] || resolution;
|
||||||
|
streams.push({
|
||||||
|
title: friendlyName,
|
||||||
|
streamUrl: streamUrl.trim(),
|
||||||
|
headers: {
|
||||||
|
"referer": "https://mapple.uk/",
|
||||||
|
"origin": "https://mapple.uk"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let englishSubtitleUrl = "";
|
||||||
|
try {
|
||||||
|
const subResponse = await soraFetch(`https://mapple.uk/api/subtitles?id=${tmdbID}&mediaType=tv&season=${seasonNumber}&episode=${episodeNumber}`);
|
||||||
|
const subData = await subResponse.json();
|
||||||
|
const englishSub = subData.find(sub => sub.language === "en");
|
||||||
|
if (englishSub) {
|
||||||
|
englishSubtitleUrl = englishSub.url;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
englishSubtitleUrl = "";
|
||||||
|
}
|
||||||
|
console.log("English Subtitle URL: " + englishSubtitleUrl);
|
||||||
|
console.log("Extracted streams: " + JSON.stringify(streams));
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: streams.length > 0 ? streams : [
|
||||||
|
{
|
||||||
|
title: "Mapple Server",
|
||||||
|
streamUrl: streamData.data.stream_url,
|
||||||
|
headers: {
|
||||||
|
"referer": "https://mapple.uk/",
|
||||||
|
"origin": "https://mapple.uk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtitle: englishSubtitleUrl || ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to extract stream URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function soraFetch(url, options = { headers: {}, method: 'GET', body: null, encoding: 'utf-8' }) {
|
||||||
|
try {
|
||||||
|
return await fetchv2(
|
||||||
|
url,
|
||||||
|
options.headers ?? {},
|
||||||
|
options.method ?? 'GET',
|
||||||
|
options.body ?? null,
|
||||||
|
true,
|
||||||
|
options.encoding ?? 'utf-8'
|
||||||
|
);
|
||||||
|
} catch(e) {
|
||||||
|
try {
|
||||||
|
return await fetch(url, options);
|
||||||
|
} catch(error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Mapple",
|
||||||
|
"iconUrl": "https://files.catbox.moe/evufrq.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.3",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "4K",
|
||||||
|
"baseUrl": "https://mapple.uk/",
|
||||||
|
"searchBaseUrl": "https://mapple.uk/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/mapple/mapple.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
// Settings start
|
||||||
|
const preferedQualityOption = "Auto"; // ["Auto", "2160p", "1080p", "720p", "480p"]
|
||||||
|
const debridService = "realdebrid"; // ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink"]
|
||||||
|
const debridApiKey = "";
|
||||||
|
const allowAdultContent = true; // [true, false]
|
||||||
|
// Settings end
|
||||||
|
|
||||||
|
async function searchResults(keyword) {
|
||||||
|
try {
|
||||||
|
const moviesresponse = await fetchv2(
|
||||||
|
"https://v3-cinemeta.strem.io/catalog/movie/top/search=" + encodeURIComponent(keyword) + ".json"
|
||||||
|
);
|
||||||
|
const moviesdata = await moviesresponse.json();
|
||||||
|
|
||||||
|
const results = moviesdata.metas.map(item => ({
|
||||||
|
title: item.name.trim(),
|
||||||
|
image: item.poster.trim(),
|
||||||
|
href: "Movie: " + (item.id.startsWith("tt") ? item.id : "")
|
||||||
|
}));
|
||||||
|
|
||||||
|
const showsresponse = await fetchv2(
|
||||||
|
"https://v3-cinemeta.strem.io/catalog/series/top/search=" + encodeURIComponent(keyword) + ".json"
|
||||||
|
);
|
||||||
|
const showsdata = await showsresponse.json();
|
||||||
|
|
||||||
|
const showResults = showsdata.metas.map(item => ({
|
||||||
|
title: item.name.trim(),
|
||||||
|
image: item.poster.trim(),
|
||||||
|
href: "TV: " + (item.id.startsWith("tt") ? item.id : "")
|
||||||
|
}));
|
||||||
|
|
||||||
|
results.push(...showResults);
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(ID) {
|
||||||
|
try {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = "https://v3-cinemeta.strem.io/meta/" + type + "/" + actualID + ".json";
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: data.meta.description || "N/A",
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: data.meta.released || "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(ID) {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (type === "series") {
|
||||||
|
const response = await fetchv2("https://v3-cinemeta.strem.io/meta/series/" + actualID + ".json");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const videos = data.meta.videos || [];
|
||||||
|
|
||||||
|
const shouldAdjust = videos.length > 0 && videos[0].season === 0;
|
||||||
|
|
||||||
|
let currentSeason = 0;
|
||||||
|
let episodeCounter = 0;
|
||||||
|
|
||||||
|
for (const video of videos) {
|
||||||
|
const adjustedSeason = shouldAdjust ? video.season + 1 : video.season;
|
||||||
|
if (adjustedSeason !== currentSeason) {
|
||||||
|
currentSeason = adjustedSeason;
|
||||||
|
episodeCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
episodeCounter++;
|
||||||
|
|
||||||
|
let adjustedId = video.id || "";
|
||||||
|
if (adjustedId && shouldAdjust) {
|
||||||
|
const idParts = adjustedId.split(':');
|
||||||
|
if (idParts.length === 3) {
|
||||||
|
idParts[1] = String(adjustedSeason);
|
||||||
|
adjustedId = idParts.join(':');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
href: "TV: " + adjustedId,
|
||||||
|
number: episodeCounter
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} else if (type === "movie") {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Movie: " + (actualID || ""),
|
||||||
|
number: 1
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(ID) {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userConfig = {
|
||||||
|
streaming_provider: {
|
||||||
|
service: debridService,
|
||||||
|
token: debridApiKey
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allowAdultContent) {
|
||||||
|
userConfig.nudity_filter = ["Disable"];
|
||||||
|
userConfig.certification_filter = ["Disable"];
|
||||||
|
} else {
|
||||||
|
userConfig.nudity_filter = ["Severe", "Moderate"];
|
||||||
|
userConfig.certification_filter = ["Adults+"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await encrypt({
|
||||||
|
customUserData: userConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
const match = url.match(/\/([^\/]+)\/manifest\.json$/);
|
||||||
|
const encryptedConfig = match ? match[1] : null;
|
||||||
|
|
||||||
|
if (!encryptedConfig) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = type === "movie"
|
||||||
|
? "https://mediafusion.elfhosted.com/" + encryptedConfig + "/stream/movie/" + actualID + ".json"
|
||||||
|
: "https://mediafusion.elfhosted.com/" + encryptedConfig + "/stream/series/" + actualID + ".json";
|
||||||
|
|
||||||
|
const response = await fetchv2(endpoint);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.streams || !Array.isArray(data.streams)) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamsByQuality = {
|
||||||
|
"2160p": [],
|
||||||
|
"1080p": [],
|
||||||
|
"720p": [],
|
||||||
|
"480p": []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const stream of data.streams) {
|
||||||
|
const name = stream.name || "";
|
||||||
|
let quality = null;
|
||||||
|
|
||||||
|
if (name.includes("2160P")) {
|
||||||
|
quality = "2160p";
|
||||||
|
} else if (name.includes("1080P")) {
|
||||||
|
quality = "1080p";
|
||||||
|
} else if (name.includes("720P")) {
|
||||||
|
quality = "720p";
|
||||||
|
} else if (name.includes("480P")) {
|
||||||
|
quality = "480p";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality && streamsByQuality[quality]) {
|
||||||
|
streamsByQuality[quality].push({
|
||||||
|
title: (stream.name || "Unknown").replace(/\n/g, " "),
|
||||||
|
streamUrl: stream.url || "",
|
||||||
|
headers: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
if (preferedQualityOption === "Auto") {
|
||||||
|
results.push(...streamsByQuality["2160p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["1080p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["720p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["480p"].slice(0, 5));
|
||||||
|
} else {
|
||||||
|
if (streamsByQuality[preferedQualityOption]) {
|
||||||
|
results = streamsByQuality[preferedQualityOption].slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: results,
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encrypt(config = {}) {
|
||||||
|
const baseUrl = config.baseUrl || 'https://mediafusion.elfhosted.com';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let userData;
|
||||||
|
|
||||||
|
if (config.customUserData) {
|
||||||
|
userData = config.customUserData;
|
||||||
|
} else {
|
||||||
|
userData = {};
|
||||||
|
|
||||||
|
if (config.provider && config.token) {
|
||||||
|
userData.streaming_provider = {
|
||||||
|
service: config.provider,
|
||||||
|
token: config.token
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.username) {
|
||||||
|
userData.streaming_provider.username = config.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.catalogs && config.catalogs.length > 0) {
|
||||||
|
userData.selected_catalogs = config.catalogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.enableCatalogs !== undefined) {
|
||||||
|
userData.enable_catalogs = config.enableCatalogs;
|
||||||
|
} else {
|
||||||
|
userData.enable_catalogs = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.maxSizeGB !== undefined && config.maxSizeGB > 0) {
|
||||||
|
userData.max_size = config.maxSizeGB * 1024 * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.maxStreamsPerResolution !== undefined) {
|
||||||
|
userData.max_streams_per_resolution = config.maxStreamsPerResolution;
|
||||||
|
} else {
|
||||||
|
userData.max_streams_per_resolution = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.sortingPriority) {
|
||||||
|
userData.torrent_sorting_priority = config.sortingPriority;
|
||||||
|
} else {
|
||||||
|
userData.torrent_sorting_priority = ["cached", "resolution", "size_mb"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.showFullTorrentName !== undefined) {
|
||||||
|
userData.show_full_torrent_name = config.showFullTorrentName;
|
||||||
|
} else {
|
||||||
|
userData.show_full_torrent_name = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.nudityFilter) {
|
||||||
|
userData.nudity_filter = config.nudityFilter;
|
||||||
|
} else {
|
||||||
|
userData.nudity_filter = ["Disable"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.certificationFilter) {
|
||||||
|
userData.certification_filter = config.certificationFilter;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
userData.certification_filter = ["Disable"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.qualityFilter) {
|
||||||
|
userData.quality_filter = config.qualityFilter;
|
||||||
|
} else {
|
||||||
|
userData.quality_filter = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.apiPassword) {
|
||||||
|
userData.api_password = config.apiPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.mediaflowConfig) {
|
||||||
|
userData.mediaflow_config = config.mediaflowConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.rpdbConfig) {
|
||||||
|
userData.rpdb_config = config.rpdbConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchv2(
|
||||||
|
`${baseUrl}/encrypt-user-data`,
|
||||||
|
{ 'Content-Type': 'application/json' },
|
||||||
|
"POST",
|
||||||
|
JSON.stringify(userData)
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
|
||||||
|
const installationUrl = `${baseUrl}/${data.encrypted_str}/manifest.json`;
|
||||||
|
return installationUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating MediaFusion URL:'+ error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "MediaFusion",
|
||||||
|
"iconUrl": "https://github.com/mhdzumair/MediaFusion/raw/main/resources/images/mediafusion_logo.png?raw=true",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.1",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "MKV",
|
||||||
|
"quality": "4K",
|
||||||
|
"baseUrl": "https://www.google.com/",
|
||||||
|
"searchBaseUrl": "https://www.google.com/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/mediafusion/mediafusion.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true,
|
||||||
|
"settings": true
|
||||||
|
}
|
||||||
@@ -77,10 +77,10 @@ async function extractEpisodes(url) {
|
|||||||
const formData = `acc=episodes&i=${i}&u=${u}&p=${page}`;
|
const formData = `acc=episodes&i=${i}&u=${u}&p=${page}`;
|
||||||
console.log("Form Data: " + formData);
|
console.log("Form Data: " + formData);
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
"Origin": "https://wwv.monoschinos2.net",
|
"Referer": "https://wvv.monoschinos2.net/",
|
||||||
"Referer": url
|
"Host": "wvv.monoschinos2.net",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||||
};
|
};
|
||||||
const response = await fetchv2("https://wwv.monoschinos2.net/ajax_pagination", headers, "POST", formData);
|
const response = await fetchv2("https://wwv.monoschinos2.net/ajax_pagination", headers, "POST", formData);
|
||||||
const pageHtml = await response.text();
|
const pageHtml = await response.text();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "Spanish (DUB/SUB)",
|
"language": "Spanish (DUB/SUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2("https://nimegami.id/?s=" + encodeURIComponent(keyword) + "&post_type=post");
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<article>[\s\S]*?<a href="([^"]+)"[^>]*>\s*<img[^>]+src="([^"]+)"[\s\S]*?<h2[^>]*>\s*<a[^>]+>([^<]+)<\/a>/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
title: match[3].trim(),
|
||||||
|
image: match[2].trim(),
|
||||||
|
href: match[1].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
let description = "N/A";
|
||||||
|
let aliases = "N/A";
|
||||||
|
let airdate = "N/A";
|
||||||
|
|
||||||
|
// Extract synopsis/description
|
||||||
|
const synopsisMatch = html.match(/<div class="content" itemprop="text"[^>]*>[\s\S]*?<\/div>/);
|
||||||
|
if (synopsisMatch) {
|
||||||
|
// Remove HTML tags and clean up the text
|
||||||
|
description = synopsisMatch[0]
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: aliases,
|
||||||
|
airdate: airdate
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
// First try to extract individual episodes (excluding batch)
|
||||||
|
const htmlWithoutBatch = html.replace(/<div class="batch-dlcuy">[\s\S]*?<\/div>/g, '');
|
||||||
|
const episodeRegex = /<h4>([^<]*Episode\s+(\d+)[^<]*)<\/h4>\s*<ul>([\s\S]*?)<\/ul>/gi;
|
||||||
|
|
||||||
|
let episodeMatch;
|
||||||
|
while ((episodeMatch = episodeRegex.exec(htmlWithoutBatch)) !== null) {
|
||||||
|
const episodeTitle = episodeMatch[1];
|
||||||
|
const episodeNumber = parseInt(episodeMatch[2], 10);
|
||||||
|
const linksHtml = episodeMatch[3];
|
||||||
|
|
||||||
|
const qualityRegex = /<strong>(\d+p)<\/strong>[\s\S]*?<a[^>]*href="([^"]+)"[^>]*>/g;
|
||||||
|
let qualityMatch;
|
||||||
|
let highestQualityLink = null;
|
||||||
|
let highestQuality = 0;
|
||||||
|
|
||||||
|
while ((qualityMatch = qualityRegex.exec(linksHtml)) !== null) {
|
||||||
|
const quality = parseInt(qualityMatch[1], 10);
|
||||||
|
const link = qualityMatch[2].trim();
|
||||||
|
|
||||||
|
if (quality > highestQuality) {
|
||||||
|
highestQuality = quality;
|
||||||
|
highestQualityLink = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highestQualityLink) {
|
||||||
|
results.push({
|
||||||
|
number: episodeNumber,
|
||||||
|
href: highestQualityLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
const movieRegex = /<strong>(\d+p)<\/strong>[\s\S]*?<a[^>]*href="([^"]+)"[^>]*>/g;
|
||||||
|
let movieMatch;
|
||||||
|
let highestQuality = 0;
|
||||||
|
let movieLink = null;
|
||||||
|
|
||||||
|
while ((movieMatch = movieRegex.exec(html)) !== null) {
|
||||||
|
const quality = parseInt(movieMatch[1], 10);
|
||||||
|
const link = movieMatch[2].trim();
|
||||||
|
|
||||||
|
if (quality > highestQuality) {
|
||||||
|
highestQuality = quality;
|
||||||
|
movieLink = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movieLink) {
|
||||||
|
results.push({
|
||||||
|
number: 1,
|
||||||
|
href: movieLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
number: "Error",
|
||||||
|
title: "Error",
|
||||||
|
href: "Error",
|
||||||
|
quality: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const streams = [];
|
||||||
|
|
||||||
|
const downloadDivMatch = html.match(/<div id="download_div"[\s\S]*?<\/div>/);
|
||||||
|
if (downloadDivMatch) {
|
||||||
|
const downloadDiv = downloadDivMatch[0];
|
||||||
|
|
||||||
|
const linkRegex = /<a[^>]*href="([^"]+)"[^>]*id="ini_download_[^"]*"[^>]*>/g;
|
||||||
|
let linkMatch;
|
||||||
|
let serverCount = 1;
|
||||||
|
|
||||||
|
while ((linkMatch = linkRegex.exec(downloadDiv)) !== null) {
|
||||||
|
const streamUrl = linkMatch[1].trim();
|
||||||
|
|
||||||
|
streams.push({
|
||||||
|
title: `Server ${serverCount}`,
|
||||||
|
streamUrl: streamUrl,
|
||||||
|
headers: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
serverCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: streams,
|
||||||
|
subtitle: "https://placeholder.com/subtitles.vtt"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://placeholder.com/subtitles.vtt"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Nimegami",
|
||||||
|
"iconUrl": "https://nimegami.id/wp-content/uploads/2018/07/cropped-logo-3v2-192x192.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Indonesian",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://nimegami.id/",
|
||||||
|
"searchBaseUrl": "https://nimegami.id/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/nimegami/nimegami.js",
|
||||||
|
"type": "anime",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true
|
||||||
|
}
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(`https://e.kortw.cc/vodsearch/-------------.html?keyword=${keyword}`);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<li class="qy-mod-li[^>]*>[\s\S]*?<a href="([^"]+)"[\s\S]*?background-image:\s*url\(([^)]+)\)[\s\S]*?<span class="text-score">[^<]*<\/span>[\s\S]*?<span>([^<]+)<\/span>[\s\S]*?<\/li>/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
title: match[3].trim(),
|
||||||
|
image: "https://www.nivod.cc" + match[2].trim(),
|
||||||
|
href: "https://www.nivod.cc" + match[1].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const match = html.match(/id="show-desc">([\s\S]*?)<\/div>/);
|
||||||
|
const description = match ? match[1].trim() : "N/A";
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<a\s+href="([^"]+)">\s*<div\s+class="item"\s*>第(\d+)集<\/div>\s*<\/a>/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
href: match[1].trim().startsWith('/') ? 'https://www.nivod.cc' + match[1].trim() : match[1].trim(),
|
||||||
|
number: parseInt(match[2], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results.reverse());
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const match = url.match(/\/vodplay\/(\d+)\/(ep\d+)/);
|
||||||
|
if (!match) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoId = match[1];
|
||||||
|
const episodeId = match[2];
|
||||||
|
const apiUrl = `https://www.nivod.cc/xhr_playinfo/${videoId}-${episodeId}`;
|
||||||
|
|
||||||
|
const response = await fetchv2(apiUrl);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(JSON.stringify(data));
|
||||||
|
const streams = [];
|
||||||
|
|
||||||
|
if (data.pdatas && Array.isArray(data.pdatas)) {
|
||||||
|
data.pdatas.forEach((item, index) => {
|
||||||
|
const streamUrl = item.playurl;
|
||||||
|
|
||||||
|
let hostname = '';
|
||||||
|
const match = streamUrl.match(/^https?:\/\/([^\/]+)/);
|
||||||
|
if (match) {
|
||||||
|
hostname = match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.push({
|
||||||
|
title: `Server ${index + 1}`,
|
||||||
|
streamUrl: streamUrl,
|
||||||
|
headers: {
|
||||||
|
"Host": hostname,
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||||
|
"Referer": "https://www.nivod.cc/",
|
||||||
|
"Origin": "https://www.nivod.cc",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Sec-Fetch-Dest": "empty",
|
||||||
|
"Sec-Fetch-Mode": "cors",
|
||||||
|
"Sec-Fetch-Site": "cross-site"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: streams,
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Nivod",
|
||||||
|
"iconUrl": "https://www.nivod.cc/static/nivod/image/logo/logo_512x512.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Chinese",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://www.nivod.cc/",
|
||||||
|
"searchBaseUrl": "https://www.nivod.cc/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/nivod/nivod.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": false
|
||||||
|
}
|
||||||
+17
-17
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "s.to (ENG DUB)",
|
"sourceName": "s.to (ENG DUB)",
|
||||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Cufiy",
|
"name": "Cufiy",
|
||||||
"icon": "https://files.catbox.moe/ttj4fc.gif"
|
"icon": "https://files.catbox.moe/ttj4fc.gif"
|
||||||
},
|
},
|
||||||
"version": "0.3.14",
|
"version": "0.3.15",
|
||||||
"language": "English (DUB)",
|
"language": "English (DUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "720p",
|
"quality": "720p",
|
||||||
"baseUrl": "https://google.com",
|
"baseUrl": "https://google.com",
|
||||||
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToEngDub_v2.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToEngDub_v2.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"streamAsyncJS": false,
|
"streamAsyncJS": false,
|
||||||
"type": "shows"
|
"type": "shows"
|
||||||
}
|
}
|
||||||
+665
-6
@@ -352,7 +352,7 @@ function base64Decode(str) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -360,8 +360,8 @@ function base64Decode(str) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -374,7 +374,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -437,8 +447,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -493,7 +509,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -552,6 +575,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -559,6 +589,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -566,6 +610,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -649,6 +749,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -703,6 +825,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -720,6 +1190,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -898,7 +1547,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -934,6 +1587,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
+17
-17
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"sourceName": "s.to (GER DUB)",
|
"sourceName": "s.to (GER DUB)",
|
||||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Hamzo & Cufiy",
|
"name": "Hamzo & Cufiy",
|
||||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||||
},
|
},
|
||||||
"version": "0.3.14",
|
"version": "0.3.15",
|
||||||
"language": "German (DUB)",
|
"language": "German (DUB)",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "720p",
|
"quality": "720p",
|
||||||
"baseUrl": "https://google.com",
|
"baseUrl": "https://google.com",
|
||||||
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToGerDub_v2.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToGerDub_v2.js",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"streamAsyncJS": false,
|
"streamAsyncJS": false,
|
||||||
"type": "shows"
|
"type": "shows"
|
||||||
}
|
}
|
||||||
+665
-6
@@ -352,7 +352,7 @@ function base64Decode(str) {
|
|||||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||||
|
|
||||||
/* {GE START} */
|
/* {GE START} */
|
||||||
/* {VERSION: 1.1.3} */
|
/* {VERSION: 1.1.8} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name global_extractor.js
|
* @name global_extractor.js
|
||||||
@@ -360,8 +360,8 @@ function base64Decode(str) {
|
|||||||
* @author Cufiy
|
* @author Cufiy
|
||||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||||
* @date 2025-07-23 17:47:48
|
* @date 2025-11-05 15:44:57
|
||||||
* @version 1.1.3
|
* @version 1.1.8
|
||||||
* @note This file was generated automatically.
|
* @note This file was generated automatically.
|
||||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||||
*/
|
*/
|
||||||
@@ -374,7 +374,17 @@ function globalExtractor(providers) {
|
|||||||
// check if streamUrl is not null, a string, and starts with http or https
|
// check if streamUrl is not null, a string, and starts with http or https
|
||||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
// if its an array, get the value that starts with http
|
||||||
|
} else if (Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
return httpStream;
|
||||||
|
}
|
||||||
|
} else if (streamUrl || typeof streamUrl !== "string") {
|
||||||
|
// check if it's a valid stream URL
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore the error and try the next provider
|
// Ignore the error and try the next provider
|
||||||
}
|
}
|
||||||
@@ -437,8 +447,14 @@ async function multiExtractor(providers) {
|
|||||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||||
// check if streamUrl is not null, a string, and starts with http or https
|
|
||||||
|
if (streamUrl && Array.isArray(streamUrl)) {
|
||||||
|
const httpStream = streamUrl.find(url => url.startsWith("http"));
|
||||||
|
if (httpStream) {
|
||||||
|
streamUrl = httpStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
// check if provider is already in streams, if it is, add a number to it
|
// check if provider is already in streams, if it is, add a number to it
|
||||||
if (
|
if (
|
||||||
!streamUrl ||
|
!streamUrl ||
|
||||||
@@ -493,7 +509,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
if(provider == 'bigwarp') {
|
if(provider == 'bigwarp') {
|
||||||
delete headers["User-Agent"];
|
delete headers["User-Agent"];
|
||||||
headers["x-requested-with"] = "XMLHttpRequest";
|
headers["x-requested-with"] = "XMLHttpRequest";
|
||||||
|
} else if (provider == 'vk') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'sibnet') {
|
||||||
|
headers["encoding"] = "windows-1251"; // required
|
||||||
|
} else if (provider == 'supervideo') {
|
||||||
|
delete headers["User-Agent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the url
|
// fetch the url
|
||||||
// and pass the response to the extractor function
|
// and pass the response to the extractor function
|
||||||
console.log("Fetching URL: " + url);
|
console.log("Fetching URL: " + url);
|
||||||
@@ -552,6 +575,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from doodstream:", error);
|
console.log("Error extracting stream URL from doodstream:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "earnvids":
|
||||||
|
try {
|
||||||
|
return await earnvidsExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from earnvids:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "filemoon":
|
case "filemoon":
|
||||||
try {
|
try {
|
||||||
return await filemoonExtractor(html, url);
|
return await filemoonExtractor(html, url);
|
||||||
@@ -559,6 +589,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from filemoon:", error);
|
console.log("Error extracting stream URL from filemoon:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "lulustream":
|
||||||
|
try {
|
||||||
|
return await lulustreamExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from lulustream:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "megacloud":
|
||||||
|
try {
|
||||||
|
return await megacloudExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from megacloud:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "mp4upload":
|
case "mp4upload":
|
||||||
try {
|
try {
|
||||||
return await mp4uploadExtractor(html, url);
|
return await mp4uploadExtractor(html, url);
|
||||||
@@ -566,6 +610,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
|||||||
console.log("Error extracting stream URL from mp4upload:", error);
|
console.log("Error extracting stream URL from mp4upload:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "sendvid":
|
||||||
|
try {
|
||||||
|
return await sendvidExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sendvid:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "sibnet":
|
||||||
|
try {
|
||||||
|
return await sibnetExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from sibnet:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamtape":
|
||||||
|
try {
|
||||||
|
return await streamtapeExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamtape:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "streamup":
|
||||||
|
try {
|
||||||
|
return await streamupExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from streamup:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "supervideo":
|
||||||
|
try {
|
||||||
|
return await supervideoExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from supervideo:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uploadcx":
|
||||||
|
try {
|
||||||
|
return await uploadcxExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uploadcx:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "uqload":
|
||||||
|
try {
|
||||||
|
return await uqloadExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from uqload:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "videospk":
|
||||||
|
try {
|
||||||
|
return await videospkExtractor(html, url);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting stream URL from videospk:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case "vidmoly":
|
case "vidmoly":
|
||||||
try {
|
try {
|
||||||
return await vidmolyExtractor(html, url);
|
return await vidmolyExtractor(html, url);
|
||||||
@@ -649,6 +749,28 @@ function randomStr(length) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* --- earnvids --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name earnvidsExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function earnvidsExtractor(html, url = null) {
|
||||||
|
try {
|
||||||
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
|
||||||
|
console.log("HLS Link:" + baseUrl + hlsLink);
|
||||||
|
return baseUrl + hlsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "https://files.catbox.moe/avolvc.mp4";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- filemoon --- */
|
/* --- filemoon --- */
|
||||||
|
|
||||||
/* {REQUIRED PLUGINS: unbaser} */
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
@@ -703,6 +825,354 @@ async function filemoonExtractor(html, url = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- lulustream --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LuluStream Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function lulustreamExtractor(data, url = null) {
|
||||||
|
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||||
|
const scriptMatch = scriptRegex.exec(data);
|
||||||
|
const decoded = scriptMatch ? scriptMatch[1] : false;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
/* --- megacloud --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name megacloudExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Megacloud V3 specific
|
||||||
|
async function megacloudExtractor(html, embedUrl) {
|
||||||
|
// TESTING ONLY START
|
||||||
|
const testcase = '/api/static';
|
||||||
|
if(embedUrl.slice(-testcase.length) == testcase) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
embedUrl = response.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TESTING ONLY END
|
||||||
|
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
|
||||||
|
const xraxParams = embedUrl.split('/').pop();
|
||||||
|
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
|
||||||
|
const nonce = await getNonce(embedUrl);
|
||||||
|
// return decrypt(secretKey, nonce, encryptedText);
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
|
||||||
|
const rawSourceData = await response.json();
|
||||||
|
const encrypted = rawSourceData?.sources;
|
||||||
|
let decryptedSources = null;
|
||||||
|
// console.log('rawSourceData', rawSourceData);
|
||||||
|
if (rawSourceData?.encrypted == false) {
|
||||||
|
decryptedSources = rawSourceData.sources;
|
||||||
|
}
|
||||||
|
if (decryptedSources == null) {
|
||||||
|
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||||
|
if (!decryptedSources) throw new Error("Failed to decrypt source");
|
||||||
|
}
|
||||||
|
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
|
||||||
|
// return the first source if it's an array
|
||||||
|
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||||
|
try {
|
||||||
|
return decryptedSources[0].file;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return {
|
||||||
|
// status: true,
|
||||||
|
// result: {
|
||||||
|
// sources: decryptedSources,
|
||||||
|
// tracks: rawSourceData.tracks,
|
||||||
|
// intro: rawSourceData.intro ?? null,
|
||||||
|
// outro: rawSourceData.outro ?? null,
|
||||||
|
// server: rawSourceData.server ?? null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: error?.message || 'Failed to get HLS link'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Computes a key based on the given secret and nonce.
|
||||||
|
* The key is used to "unlock" the encrypted data.
|
||||||
|
* The computation of the key is based on the following steps:
|
||||||
|
* 1. Concatenate the secret and nonce.
|
||||||
|
* 2. Compute a hash value of the concatenated string using a simple
|
||||||
|
* hash function (similar to Java's String.hashCode()).
|
||||||
|
* 3. Compute the remainder of the hash value divided by the maximum
|
||||||
|
* value of a 64-bit signed integer.
|
||||||
|
* 4. Use the result as a XOR mask to process the characters of the
|
||||||
|
* concatenated string.
|
||||||
|
* 5. Rotate the XOR-processed string by a shift amount equal to the
|
||||||
|
* hash value modulo the length of the XOR-processed string plus 5.
|
||||||
|
* 6. Interleave the rotated string with the reversed nonce string.
|
||||||
|
* 7. Take a substring of the interleaved string of length equal to 96
|
||||||
|
* plus the hash value modulo 33.
|
||||||
|
* 8. Convert each character of the substring to a character code
|
||||||
|
* between 32 and 126 (inclusive) by taking the remainder of the
|
||||||
|
* character code divided by 95 and adding 32.
|
||||||
|
* 9. Join the resulting array of characters into a string and return it.
|
||||||
|
* @param {string} secret - The secret string
|
||||||
|
* @param {string} nonce - The nonce string
|
||||||
|
* @returns {string} The computed key
|
||||||
|
*/
|
||||||
|
function computeKey(secret, nonce) {
|
||||||
|
const secretAndNonce = secret + nonce;
|
||||||
|
let hashValue = 0n;
|
||||||
|
for (const char of secretAndNonce) {
|
||||||
|
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
|
||||||
|
}
|
||||||
|
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
|
||||||
|
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
|
||||||
|
const xorMask = 247;
|
||||||
|
const xorProcessedString = [...secretAndNonce]
|
||||||
|
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
|
||||||
|
.join('');
|
||||||
|
const xorLen = xorProcessedString.length;
|
||||||
|
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
|
||||||
|
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
|
||||||
|
const reversedNonceString = nonce.split('').reverse().join('');
|
||||||
|
let interleavedString = '';
|
||||||
|
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
|
||||||
|
}
|
||||||
|
const length = 96 + (Number(hashValueModuloMax) % 33);
|
||||||
|
const partialString = interleavedString.substring(0, length);
|
||||||
|
return [...partialString]
|
||||||
|
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encrypts a given text using a columnar transposition cipher with a given key.
|
||||||
|
* The function arranges the text into a grid of columns and rows determined by the key length,
|
||||||
|
* fills the grid column by column based on the sorted order of the key characters,
|
||||||
|
* and returns the encrypted text by reading the grid row by row.
|
||||||
|
*
|
||||||
|
* @param {string} text - The text to be encrypted.
|
||||||
|
* @param {string} key - The key that determines the order of columns in the grid.
|
||||||
|
* @returns {string} The encrypted text.
|
||||||
|
*/
|
||||||
|
function columnarCipher(text, key) {
|
||||||
|
const columns = key.length;
|
||||||
|
const rows = Math.ceil(text.length / columns);
|
||||||
|
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
|
||||||
|
const columnOrder = [...key]
|
||||||
|
.map((char, idx) => ({ char, idx }))
|
||||||
|
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
|
||||||
|
let i = 0;
|
||||||
|
for (const { idx } of columnOrder) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
grid[row][idx] = text[i++] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid.flat().join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Deterministically unshuffles an array of characters based on a given key phrase.
|
||||||
|
* The function simulates a pseudo-random shuffling using a numeric seed derived
|
||||||
|
* from the key phrase. This ensures that the same character array and key phrase
|
||||||
|
* will always produce the same output, allowing for deterministic "unshuffling".
|
||||||
|
* @param {Array} characters - The array of characters to unshuffle.
|
||||||
|
* @param {string} keyPhrase - The key phrase used to generate the seed for the
|
||||||
|
* pseudo-random number generator.
|
||||||
|
* @returns {Array} A new array representing the deterministically unshuffled characters.
|
||||||
|
*/
|
||||||
|
function deterministicUnshuffle(characters, keyPhrase) {
|
||||||
|
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
const shuffledCharacters = characters.slice();
|
||||||
|
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
|
||||||
|
const j = randomNumberGenerator(i + 1);
|
||||||
|
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
|
||||||
|
}
|
||||||
|
return shuffledCharacters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
|
||||||
|
* The decryption process includes base64 decoding, character substitution using a pseudo-random
|
||||||
|
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
|
||||||
|
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
|
||||||
|
*
|
||||||
|
* @param {string} secretKey - The key used to decrypt the text.
|
||||||
|
* @param {string} nonce - A nonce for additional input to the decryption key.
|
||||||
|
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
|
||||||
|
* @param {number} [rounds=3] - The number of decryption rounds to perform.
|
||||||
|
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
|
||||||
|
*/
|
||||||
|
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
|
||||||
|
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
|
||||||
|
const keyPhrase = computeKey(secretKey, nonce);
|
||||||
|
for (let round = rounds; round >= 1; round--) {
|
||||||
|
const encryptionPassphrase = keyPhrase + round;
|
||||||
|
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
|
||||||
|
const randomNumberGenerator = (upperLimit) => {
|
||||||
|
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
|
||||||
|
return Number(seed % BigInt(upperLimit));
|
||||||
|
};
|
||||||
|
decryptedText = [...decryptedText]
|
||||||
|
.map(char => {
|
||||||
|
const charIndex = CHARSET.indexOf(char);
|
||||||
|
if (charIndex === -1) return char;
|
||||||
|
const offset = randomNumberGenerator(95);
|
||||||
|
return CHARSET[(charIndex - offset + 95) % 95];
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
|
||||||
|
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
|
||||||
|
const mappingArr = {};
|
||||||
|
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
|
||||||
|
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
|
||||||
|
}
|
||||||
|
const lengthString = decryptedText.slice(0, 4);
|
||||||
|
let length = parseInt(lengthString, 10);
|
||||||
|
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
|
||||||
|
console.error('Invalid length in decrypted string');
|
||||||
|
return decryptedText;
|
||||||
|
}
|
||||||
|
const decryptedString = decryptedText.slice(4, 4 + length);
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedString);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
|
||||||
|
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
|
||||||
|
const match = encryptedText.match(regex);
|
||||||
|
const matchedFile = match?.[1];
|
||||||
|
const matchType = match?.[2];
|
||||||
|
if (!matchedFile || !matchType) {
|
||||||
|
console.error('Could not match file or type in decrypted string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tries to extract the MegaCloud nonce from the given embed URL.
|
||||||
|
*
|
||||||
|
* Fetches the HTML of the page, and tries to extract the nonce from it.
|
||||||
|
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
|
||||||
|
* and tries to extract the nonce from that HTML.
|
||||||
|
*
|
||||||
|
* If all else fails, it logs the HTML of both requests and returns null.
|
||||||
|
*
|
||||||
|
* @param {string} embedUrl The URL of the MegaCloud embed
|
||||||
|
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||||
|
*/
|
||||||
|
async function getNonce(embedUrl) {
|
||||||
|
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
|
||||||
|
const html = await res.text();
|
||||||
|
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||||
|
if (match0?.[1]) {
|
||||||
|
return match0[1];
|
||||||
|
}
|
||||||
|
const match1 = html.match(/_is_th:(\S*?)\s/);
|
||||||
|
if (match1?.[1]) {
|
||||||
|
return match1[1];
|
||||||
|
}
|
||||||
|
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
|
||||||
|
if (match2?.[1]) {
|
||||||
|
return match2[1];
|
||||||
|
}
|
||||||
|
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
|
||||||
|
if (match3?.[1] && match3?.[2] && match3?.[3]) {
|
||||||
|
return "" + match3[1] + match3[2] + match3[3];
|
||||||
|
}
|
||||||
|
const match4 = html.match(/nonce="([\s\S]*?)"/);
|
||||||
|
if (match4?.[1]) {
|
||||||
|
if (match4[1].length >= 32) return match4[1];
|
||||||
|
}
|
||||||
|
const match5 = html.match(/_xy_ws = "(\S*?)"/);
|
||||||
|
if (match5?.[1]) {
|
||||||
|
return match5[1];
|
||||||
|
}
|
||||||
|
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
|
||||||
|
if (match6?.[1]) {
|
||||||
|
return match6[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function getDecryptedSourceV3(encrypted, nonce) {
|
||||||
|
let decrypted = null;
|
||||||
|
const keys = await asyncGetKeys();
|
||||||
|
for(let key in keys) {
|
||||||
|
try {
|
||||||
|
if (!encrypted) {
|
||||||
|
console.log("Encrypted source missing in response")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
decrypted = decrypt(keys[key], nonce, encrypted);
|
||||||
|
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
|
||||||
|
// Failed to decrypt source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(let source of decrypted) {
|
||||||
|
if(source != null && source?.file?.startsWith('https://')) {
|
||||||
|
// Malformed decrypted source
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Functioning key:", key);
|
||||||
|
return decrypted;
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function asyncGetKeys() {
|
||||||
|
const resolution = await Promise.allSettled([
|
||||||
|
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
|
||||||
|
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
|
||||||
|
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
|
||||||
|
]);
|
||||||
|
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
|
||||||
|
let rKey = Object.keys(r.value)[0];
|
||||||
|
let rValue = Object.values(r.value)[0];
|
||||||
|
if (typeof rValue === 'string') {
|
||||||
|
obj[rKey] = rValue.trim();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
throw new Error("Failed to fetch any decryption key");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
function fetchKey(name, url) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url, { method: 'get' });
|
||||||
|
const key = await response.text();
|
||||||
|
let trueKey = null;
|
||||||
|
try {
|
||||||
|
trueKey = JSON.parse(key);
|
||||||
|
} catch (e) {
|
||||||
|
trueKey = key;
|
||||||
|
}
|
||||||
|
resolve({ [name]: trueKey })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
/* --- mp4upload --- */
|
/* --- mp4upload --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -720,6 +1190,185 @@ async function mp4uploadExtractor(html, url = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* --- sendvid --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sendvidExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function sendvidExtractor(data, url = null) {
|
||||||
|
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
|
||||||
|
const videoUrl = match ? match[1] : null;
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
/* --- sibnet --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name sibnetExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function sibnetExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const videoMatch = html.match(
|
||||||
|
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
|
||||||
|
);
|
||||||
|
if (!videoMatch || !videoMatch[1]) {
|
||||||
|
throw new Error("Sibnet video source not found");
|
||||||
|
}
|
||||||
|
const videoPath = videoMatch[1];
|
||||||
|
const videoUrl = videoPath.startsWith("http")
|
||||||
|
? videoPath
|
||||||
|
: `https://video.sibnet.ru${videoPath}`;
|
||||||
|
return videoUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("SibNet extractor error: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamtape --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @name streamTapeExtractor
|
||||||
|
* @author ShadeOfChaos
|
||||||
|
*/
|
||||||
|
async function streamtapeExtractor(html, url) {
|
||||||
|
let promises = [];
|
||||||
|
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
|
||||||
|
const CHANGES_REGEX = /([0-9]+)/g;
|
||||||
|
if(html == null) {
|
||||||
|
if(url == null) {
|
||||||
|
throw new Error('Provided incorrect parameters.');
|
||||||
|
}
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
html = await response.text();
|
||||||
|
}
|
||||||
|
const matches = html.matchAll(LINK_REGEX);
|
||||||
|
for (const match of matches) {
|
||||||
|
let base = match?.[1];
|
||||||
|
let params = match?.[2];
|
||||||
|
const changeStr = match?.[3];
|
||||||
|
if(changeStr == null || changeStr == '') continue;
|
||||||
|
const changes = changeStr.match(CHANGES_REGEX);
|
||||||
|
for(let n of changes) {
|
||||||
|
params = params.substring(n);
|
||||||
|
}
|
||||||
|
while(base[0] == '/') {
|
||||||
|
base = base.substring(1);
|
||||||
|
}
|
||||||
|
const url = 'https://' + base + params;
|
||||||
|
promises.push(testUrl(url));
|
||||||
|
}
|
||||||
|
// Race for first success
|
||||||
|
return Promise.any(promises).then((value) => {
|
||||||
|
return value;
|
||||||
|
}).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
async function testUrl(url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Timeout version prefered, but Sora does not support it currently
|
||||||
|
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
|
||||||
|
var response = await soraFetch(url);
|
||||||
|
if(response == null) throw new Error('Connection timed out.');
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Rejected due to:', e.message);
|
||||||
|
return reject(null);
|
||||||
|
}
|
||||||
|
if(response?.ok && response?.status === 200) {
|
||||||
|
return resolve(url);
|
||||||
|
}
|
||||||
|
console.warn('Reject because of response:', response?.ok, response?.status);
|
||||||
|
return reject(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- streamup --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StreamUp Extractor
|
||||||
|
* @author Cufiy
|
||||||
|
*/
|
||||||
|
async function streamupExtractor(data, url = null) {
|
||||||
|
// if url ends with /, remove it
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
// split the url by / and get the last part
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
const videoId = urlParts[urlParts.length - 1];
|
||||||
|
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
|
||||||
|
const response = await soraFetch(apiUrl);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData && jsonData.streaming_url) {
|
||||||
|
return jsonData.streaming_url;
|
||||||
|
} else {
|
||||||
|
console.log("No streaming URL found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- supervideo --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name SuperVideo Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function supervideoExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const regex = /file:\s*"([^"]+\.m3u8)"/;
|
||||||
|
const match = regex.exec(unpackedScript);
|
||||||
|
if (match) {
|
||||||
|
const fileUrl = match[1];
|
||||||
|
console.log("File URL:" + fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
return "No stream found";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- uploadcx --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name UploadCx Extractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function uploadcxExtractor(data, url = null) {
|
||||||
|
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
|
||||||
|
return mp4Match ? mp4Match[1] : null;
|
||||||
|
}
|
||||||
|
/* --- uqload --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- videospk --- */
|
||||||
|
|
||||||
|
/* {REQUIRED PLUGINS: unbaser} */
|
||||||
|
/**
|
||||||
|
* @name videospkExtractor
|
||||||
|
* @author 50/50
|
||||||
|
*/
|
||||||
|
async function videospkExtractor(data, url = null) {
|
||||||
|
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
|
const unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
|
||||||
|
const hlsLink = streamMatch ? streamMatch[1] : null;
|
||||||
|
return "https://videospk.xyz" + hlsLink;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- vidmoly --- */
|
/* --- vidmoly --- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -898,7 +1547,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/***********************************************************
|
||||||
|
* UNPACKER MODULE
|
||||||
|
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||||
|
* https://github.com/mnsrulz/unpacker
|
||||||
|
***********************************************************/
|
||||||
class Unbaser {
|
class Unbaser {
|
||||||
constructor(base) {
|
constructor(base) {
|
||||||
this.ALPHABET = {
|
this.ALPHABET = {
|
||||||
@@ -934,6 +1587,12 @@ class Unbaser {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectUnbaser(source) {
|
||||||
|
/* Detects whether `source` is P.A.C.K.E.R. coded. */
|
||||||
|
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
|
||||||
|
}
|
||||||
|
|
||||||
function unpack(source) {
|
function unpack(source) {
|
||||||
let { payload, symtab, radix, count } = _filterargs(source);
|
let { payload, symtab, radix, count } = _filterargs(source);
|
||||||
if (count != symtab.length) {
|
if (count != symtab.length) {
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ async function extractStreamUrl(ID) {
|
|||||||
const streamdata = decryptData.result.hls;
|
const streamdata = decryptData.result.hls;
|
||||||
const stream = "https://proxy.aether.mom/m3u8-proxy?url=https://smashyplayer.top"+streamdata;
|
const stream = "https://proxy.aether.mom/m3u8-proxy?url=https://smashyplayer.top"+streamdata;
|
||||||
console.log(stream);
|
console.log(stream);
|
||||||
const subsEng = decryptData.result.subtitle?.English || "https://error.org";
|
const subsEng = decryptData?.result?.subtitle?.["English"] || "https://error.org/";
|
||||||
|
|
||||||
const streams = [];
|
const streams = [];
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ async function extractStreamUrl(ID) {
|
|||||||
: "";
|
: "";
|
||||||
const stream = baseUrl + streamdata;
|
const stream = baseUrl + streamdata;
|
||||||
console.log(stream);
|
console.log(stream);
|
||||||
const subsEng = decryptData.result.subtitle?.English || "https://error.org";
|
const subsEng = decryptData?.result?.subtitle?.["English"] || "https://error.org/";
|
||||||
|
|
||||||
const streams = [];
|
const streams = [];
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"language": "English",
|
"language": "English",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"baseUrl": "https://smashystream.xyz/i",
|
"baseUrl": "https://smashystream.xyz/",
|
||||||
"searchBaseUrl": "https://smashystream.xyz/i",
|
"searchBaseUrl": "https://smashystream.xyz/",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/smashystream/smashystream.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/smashystream/smashystream.js",
|
||||||
"type": "shows/movies/anime",
|
"type": "shows/movies/anime",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
|
|||||||
+70
-73
@@ -1,92 +1,89 @@
|
|||||||
const PLACEHOLDER_IMAGE = "https://media.istockphoto.com/id/1147544807/vector/thumbnail-image-vector-graphic.jpg?s=612x612&w=0&k=20&c=rnCKVbdxqkjlcs3xH87-9gocETqpspHFXu5dIGB4wuM=";
|
|
||||||
|
|
||||||
async function searchResults(keyword) {
|
async function searchResults(keyword) {
|
||||||
const searchUrl = `https://api-v2.soundcloud.com/search?q=${encodeURIComponent(keyword)}&facet=model&user_id=200971-112325-516393-99787&client_id=UjhhbCuNo1OQfTwkzajxQNLlJcSlUlVz&limit=30`;
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(searchUrl);
|
const response = await fetchv2(`https://api-v2.soundcloud.com/search/tracks?q=${encodeURIComponent(keyword)}&facet=genre&user_id=618848-625616-52355-612546&client_id=xwYTVSni6n4FghaI0c4uJ8T9c4pyJ3rh&limit=20&offset=0&linked_partitioning=1&app_version=1761897122&app_locale=en`);
|
||||||
const json = await JSON.parse(response);
|
const data = await response.json();
|
||||||
|
|
||||||
|
const results = data.collection
|
||||||
|
.map(track => {
|
||||||
|
const hlsLink = track.media.transcodings.find(t => t.format.protocol === "hls" && t.format.mime_type.includes("mp4"))?.url || "";
|
||||||
|
const imageUrl = track.artwork_url ? track.artwork_url.replace(/-large\.jpg$/, '-t500x500.jpg') : "";
|
||||||
|
return {
|
||||||
|
title: track.title,
|
||||||
|
image: imageUrl,
|
||||||
|
href: `${hlsLink}?title=${encodeURIComponent(track.title)}&artist=${encodeURIComponent(track.user.username)}`,
|
||||||
|
hasHls: hlsLink !== ""
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(track => track.hasHls)
|
||||||
|
.map(track => ({
|
||||||
|
title: track.title,
|
||||||
|
image: track.image,
|
||||||
|
href: track.href
|
||||||
|
}));
|
||||||
|
|
||||||
const results = json.collection.map(item => ({
|
|
||||||
title: item.title,
|
|
||||||
image: item.artwork_url || PLACEHOLDER_IMAGE,
|
|
||||||
href: item.permalink_url
|
|
||||||
}));
|
|
||||||
//console.log(results);
|
|
||||||
console.log(JSON.stringify(results));
|
|
||||||
return JSON.stringify(results);
|
return JSON.stringify(results);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Error fetching search results:", error);
|
return JSON.stringify([{
|
||||||
return JSON.stringify([]);
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractDetails(url) {
|
async function extractDetails(url) {
|
||||||
const details = [];
|
console.log(url);
|
||||||
details.push({
|
try {
|
||||||
description: 'N/A',
|
const params = url.split('?')[1] || '';
|
||||||
alias: 'N/A',
|
const pairs = params.split('&');
|
||||||
airdate: 'N/A'
|
let title = '';
|
||||||
});
|
let artist = '';
|
||||||
|
|
||||||
//#IAMLAZY
|
for (const pair of pairs) {
|
||||||
|
const [key, value] = pair.split('=');
|
||||||
console.log(JSON.stringify(details));
|
if (key === 'title') title = decodeURIComponent(value);
|
||||||
return JSON.stringify(details);
|
if (key === 'artist') artist = decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = `${title} by ${artist}`;
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "",
|
||||||
|
airdate: ""
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractEpisodes(url) {
|
async function extractEpisodes(url) {
|
||||||
const response = await fetch(url);
|
const results = [];
|
||||||
const html = await response;
|
try {
|
||||||
const tracks = [];
|
const cleanUrl = url.split('?')[0];
|
||||||
|
results.push({
|
||||||
const trackRegex = /<article itemprop="track"[^>]*>[\s\S]*?<h2 itemprop="name"><a itemprop="url" href="([^"]+)">/g;
|
href: cleanUrl,
|
||||||
|
number: 1
|
||||||
let match;
|
|
||||||
let number = 1;
|
|
||||||
|
|
||||||
while ((match = trackRegex.exec(html)) !== null) {
|
|
||||||
tracks.push({
|
|
||||||
number: number++,
|
|
||||||
href: 'https://soundcloud.com' + match[1].trim()
|
|
||||||
});
|
});
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracks.length > 0) {
|
|
||||||
return JSON.stringify(tracks);
|
|
||||||
} else {
|
|
||||||
const canonicalMatch = html.match(/\["link",\{"rel":"canonical","href":"([^"]+)"\}\]/);
|
|
||||||
if (canonicalMatch) {
|
|
||||||
return JSON.stringify([{ number: 1, href: canonicalMatch[1].trim() }]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function extractStreamUrl(url) {
|
async function extractStreamUrl(url) {
|
||||||
const clientId = "UjhhbCuNo1OQfTwkzajxQNLlJcSlUlVz";
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetchv2(url+"?client_id=xwYTVSni6n4FghaI0c4uJ8T9c4pyJ3rh&track_authorization=");
|
||||||
const html = await response;
|
const data = await response.json();
|
||||||
|
|
||||||
const urlMatch = html.match(/"url":"(https:\/\/api[^\.]*\.soundcloud\.com\/media\/soundcloud:tracks:[^"]+)"/);
|
|
||||||
const authMatch = html.match(/"track_authorization":"([^"]+)"/);
|
|
||||||
|
|
||||||
if (urlMatch && authMatch) {
|
|
||||||
const streamUrl = `${urlMatch[1]}?client_id=${clientId}&track_authorization=${authMatch[1]}`;
|
|
||||||
|
|
||||||
const responseTwo = await fetch(streamUrl);
|
|
||||||
const json = await JSON.parse(responseTwo);
|
|
||||||
|
|
||||||
return json.url;
|
return data.url || "https://error.org/";
|
||||||
} else {
|
} catch (err) {
|
||||||
console.log("No stream URL found");
|
return "https://error.org/";
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error:", error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"language": "Music lmfao",
|
"language": "Music",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"quality": "Wallah fire quality",
|
"quality": "128 kbps",
|
||||||
"baseUrl": "https://soundcloud.com/",
|
"baseUrl": "https://soundcloud.com/",
|
||||||
"searchBaseUrl": "https://bshar1865-hianime.vercel.app/api/v2/hianime/search?q=%s",
|
"searchBaseUrl": "https://soundcloud.com/",
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/soundcloud/soundcloud.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/soundcloud/soundcloud.js",
|
||||||
"asyncJS": true
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"downloadSupport": true
|
||||||
}
|
}
|
||||||
|
|||||||
+245
@@ -0,0 +1,245 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
try {
|
||||||
|
const encodedKeyword = encodeURIComponent(keyword);
|
||||||
|
const response = await fetchv2(`https://mangakatana.com/?search=${encodedKeyword}&search_by=book_name`);
|
||||||
|
const html = await response.text();
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
const itemRegex = /<div class="item"[^>]*data-genre="[^"]*"[^>]*data-id="[^"]*"[^>]*>([\s\S]*?)(?=<div class="item"|$)/g;
|
||||||
|
|
||||||
|
let itemMatch;
|
||||||
|
while ((itemMatch = itemRegex.exec(html)) !== null) {
|
||||||
|
const itemHtml = itemMatch[1];
|
||||||
|
|
||||||
|
const titleRegex = /<h3 class="title">\s*<a href="([^"]+)"[^>]*>([^<]+)<\/a>/;
|
||||||
|
const imageRegex = /<div class="wrap_img">\s*<a[^>]*><img src="([^"]+)"/;
|
||||||
|
|
||||||
|
const titleMatch = titleRegex.exec(itemHtml);
|
||||||
|
const imageMatch = imageRegex.exec(itemHtml);
|
||||||
|
|
||||||
|
if (titleMatch && imageMatch) {
|
||||||
|
const title = titleMatch[2].trim();
|
||||||
|
const href = titleMatch[1].trim();
|
||||||
|
const image = imageMatch[1].trim();
|
||||||
|
|
||||||
|
if (
|
||||||
|
title && href && image &&
|
||||||
|
!title.includes("'+") &&
|
||||||
|
!href.includes("'+") &&
|
||||||
|
href.startsWith("http")
|
||||||
|
) {
|
||||||
|
results.push({
|
||||||
|
title: title,
|
||||||
|
href: href,
|
||||||
|
image: image
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Search results for "${keyword}":`, JSON.stringify(results));
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Fetch error in searchResults:', error);
|
||||||
|
return JSON.stringify([{ title: 'Error', href: '', image: '' }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
const htmlText = await response.text();
|
||||||
|
|
||||||
|
const descMatch = htmlText.match(/<div class="label">Description<\/div>\s*<p>([\s\S]*?)<\/p>/);
|
||||||
|
|
||||||
|
let description = 'No description available';
|
||||||
|
if (descMatch && descMatch[1]) {
|
||||||
|
description = descMatch[1]
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedResults = [{
|
||||||
|
description,
|
||||||
|
aliases: 'N/A',
|
||||||
|
airdate: 'N/A'
|
||||||
|
}];
|
||||||
|
|
||||||
|
console.log(`Details for "${url}":`, JSON.stringify(transformedResults));
|
||||||
|
return JSON.stringify(transformedResults);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Details error:', error);
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: 'Error loading description',
|
||||||
|
aliases: 'N/A',
|
||||||
|
airdate: 'N/A'
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractChapters(url) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
const htmlText = await response.text();
|
||||||
|
|
||||||
|
const chapterRegex = /<tr data-jump="0">[\s\S]*?<a href="([^"]+)">([\s\S]*?)<\/a>[\s\S]*?<\/tr>/g;
|
||||||
|
const chapters = [];
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = chapterRegex.exec(htmlText)) !== null) {
|
||||||
|
const href = match[1].trim();
|
||||||
|
const titleMatch = /Chapter \d+[:\s]?.*/i.exec(match[2]);
|
||||||
|
const title = titleMatch ? decodeHtmlEntities(titleMatch[0].trim()) : "Unknown Chapter";
|
||||||
|
const numberMatch = /Chapter (\d+)/i.exec(title);
|
||||||
|
const number = numberMatch ? parseInt(numberMatch[1]) : NaN;
|
||||||
|
|
||||||
|
chapters.push({
|
||||||
|
number: number === 0 ? 1 : number,
|
||||||
|
href: href.startsWith("http") ? href : "https://mangakatana.com" + href,
|
||||||
|
title: title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters.reverse();
|
||||||
|
|
||||||
|
console.log(JSON.stringify(chapters));
|
||||||
|
return chapters;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch error in extractChapters:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractText(url) {
|
||||||
|
try {
|
||||||
|
const response = await soraFetch(url);
|
||||||
|
const htmlText = await response.text();
|
||||||
|
|
||||||
|
const testUrl = 'https://static.wikia.nocookie.net/473b884a-a6d8-43ad-9c9c-fa6b676f8126';
|
||||||
|
const imageUrls = Array(20).fill(testUrl);
|
||||||
|
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>Manga</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: #000;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
.img-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 1200px;
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
img.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${imageUrls.map((url, i) => ` <div class="img-container"><img data-src="${url}" alt="Image ${i+1}"></div>`).join('\n')}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var images = document.querySelectorAll('img[data-src]');
|
||||||
|
var containers = document.querySelectorAll('.img-container');
|
||||||
|
var currentIndex = 0;
|
||||||
|
|
||||||
|
function loadNext() {
|
||||||
|
if (currentIndex >= images.length) return;
|
||||||
|
|
||||||
|
var img = images[currentIndex];
|
||||||
|
var container = containers[currentIndex];
|
||||||
|
|
||||||
|
img.onload = function() {
|
||||||
|
container.style.minHeight = this.naturalHeight + 'px';
|
||||||
|
this.classList.add('loaded');
|
||||||
|
currentIndex++;
|
||||||
|
setTimeout(loadNext, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = function() {
|
||||||
|
currentIndex++;
|
||||||
|
setTimeout(loadNext, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (img.complete && img.naturalHeight > 0) {
|
||||||
|
container.style.minHeight = img.naturalHeight + 'px';
|
||||||
|
img.classList.add('loaded');
|
||||||
|
currentIndex++;
|
||||||
|
setTimeout(loadNext, 100);
|
||||||
|
} else {
|
||||||
|
img.src = img.dataset.src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNext();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error in extractImages:", error);
|
||||||
|
return {
|
||||||
|
error: `Error loading chapter images: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeHtmlEntities(str) {
|
||||||
|
const named = {
|
||||||
|
amp: '&',
|
||||||
|
lt: '<',
|
||||||
|
gt: '>',
|
||||||
|
quot: '"',
|
||||||
|
apos: "'",
|
||||||
|
nbsp: ' ',
|
||||||
|
hellip: '…',
|
||||||
|
rsquo: '’',
|
||||||
|
lsquo: '‘',
|
||||||
|
ndash: '–',
|
||||||
|
mdash: '—'
|
||||||
|
};
|
||||||
|
|
||||||
|
return str
|
||||||
|
.replace(/&([a-z]+);/gi, (match, name) => named[name] || match)
|
||||||
|
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function soraFetch(url, options = { headers: {}, method: 'GET', body: null }) {
|
||||||
|
try {
|
||||||
|
return await fetchv2(url, options.headers ?? {}, options.method ?? 'GET', options.body ?? null);
|
||||||
|
} catch(e) {
|
||||||
|
try {
|
||||||
|
return await fetch(url, options);
|
||||||
|
} catch(error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "MangaKatana",
|
||||||
|
"iconUrl": "https://i.pinimg.com/736x/5c/25/c1/5c25c16af08e250ee917726afafe068f.jpg",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.6",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "novels",
|
||||||
|
"quality": "N/A",
|
||||||
|
"baseUrl": "https://mangakatana.net/",
|
||||||
|
"searchBaseUrl": "https://mangakatana.net/%s",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/temp/temp.js",
|
||||||
|
"type": "novels",
|
||||||
|
"asyncJS": true,
|
||||||
|
"novel": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const results = [];
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (keyword.includes('tiktok.com')) {
|
||||||
|
const detailResponse = await fetchv2(`https://tikwm.com/api/?url=${encodeURIComponent(keyword)}`);
|
||||||
|
const detailData = await detailResponse.json();
|
||||||
|
|
||||||
|
if (detailData.code === 0) {
|
||||||
|
results.push({
|
||||||
|
title: detailData.data.title.trim(),
|
||||||
|
image: detailData.data.cover.trim(),
|
||||||
|
href: detailData.data.play.trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return JSON.stringify(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = JSON.stringify({
|
||||||
|
keywords: keyword,
|
||||||
|
count: 20,
|
||||||
|
cursor: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetchv2('https://tikwm.com/api/feed/search', headers, "POST", body);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
for (const video of data.data.videos) {
|
||||||
|
const videoUrl = `https://www.tiktok.com/@${video.author.unique_id}/video/${video.video_id}`;
|
||||||
|
const detailResponse = await fetchv2(`https://tikwm.com/api/?url=${encodeURIComponent(videoUrl)}`);
|
||||||
|
const detailData = await detailResponse.json();
|
||||||
|
|
||||||
|
if (detailData.code === 0) {
|
||||||
|
results.push({
|
||||||
|
title: detailData.data.title.trim(),
|
||||||
|
image: detailData.data.cover.trim(),
|
||||||
|
href: detailData.data.play.trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Please wait",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "N/A",
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
href: url,
|
||||||
|
number: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
return url;
|
||||||
|
} catch (err) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Tiktok",
|
||||||
|
"iconUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSrZd7mszoX8inKkbUKb6ffie8YF2-c00RL7w&s",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://www.google.com/",
|
||||||
|
"searchBaseUrl": "https://www.google.com/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/tiktok/tiktok.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true
|
||||||
|
}
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
function cleanTitle(title) {
|
||||||
|
return title
|
||||||
|
.replace(/’/g, "'")
|
||||||
|
.replace(/–/g, "-")
|
||||||
|
.replace(/&#[0-9]+;/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
async function searchResults(keyword) {
|
async function searchResults(keyword) {
|
||||||
const results = [];
|
const results = [];
|
||||||
try {
|
try {
|
||||||
@@ -8,7 +15,7 @@ async function searchResults(keyword) {
|
|||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(html)) !== null) {
|
while ((match = regex.exec(html)) !== null) {
|
||||||
results.push({
|
results.push({
|
||||||
title: match[2].trim(),
|
title: cleanTitle(match[2].trim()),
|
||||||
image: match[3] ? match[3].trim() : "",
|
image: match[3] ? match[3].trim() : "",
|
||||||
href: match[1].trim()
|
href: match[1].trim()
|
||||||
});
|
});
|
||||||
@@ -86,12 +93,35 @@ async function extractEpisodes(url) {
|
|||||||
async function extractStreamUrl(url) {
|
async function extractStreamUrl(url) {
|
||||||
|
|
||||||
const response = await fetchv2(url);
|
const response = await fetchv2(url);
|
||||||
const html = await response.text();
|
const htmlOne = await response.text();
|
||||||
let streamData = null;
|
|
||||||
|
|
||||||
streamData = voeExtractor(html);
|
const redirectMatch = htmlOne.match(/window\.location\.href\s*=\s*['"]([^'"]+)['"]/);
|
||||||
console.log("Voe Stream Data: " + streamData);
|
if (!redirectMatch) {
|
||||||
return streamData;
|
console.log("No redirect URL found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUrl = redirectMatch[1];
|
||||||
|
console.log("Redirect URL: " + redirectUrl);
|
||||||
|
|
||||||
|
const actualResponse = await fetchv2(redirectUrl);
|
||||||
|
const html = await actualResponse.text();
|
||||||
|
|
||||||
|
let streamUrl = voeExtractor(html);
|
||||||
|
console.log("Voe Stream Data: " + streamUrl);
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
title: "Server 1",
|
||||||
|
streamUrl: streamUrl,
|
||||||
|
headers: {
|
||||||
|
Referer: redirectUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SCHEME START */
|
/* SCHEME START */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "Italian",
|
"language": "Italian",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"encrypted":true,
|
"encrypted":true,
|
||||||
@@ -15,6 +15,6 @@
|
|||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/toonitalia/toonitalia.js",
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/toonitalia/toonitalia.js",
|
||||||
"type": "shows/movies",
|
"type": "shows/movies",
|
||||||
"asyncJS": true,
|
"asyncJS": true,
|
||||||
"softsub": false,
|
"softsub": true,
|
||||||
"downloadSupport": false
|
"downloadSupport": false
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ async function extractEpisodes(url) {
|
|||||||
const scriptResponse = await fetchv2(scriptUrl);
|
const scriptResponse = await fetchv2(scriptUrl);
|
||||||
const scriptContent = await scriptResponse.text();
|
const scriptContent = await scriptResponse.text();
|
||||||
|
|
||||||
const supervideRegex = /\\'(https:\/\/supervideo\.tv\/[^']+)\\'/;
|
const supervideRegex = /\\'(https:\/\/supervideo\.[^\/']+\/[^']+)\\'/;
|
||||||
const supervideMatch = scriptContent.match(supervideRegex);
|
const supervideMatch = scriptContent.match(supervideRegex);
|
||||||
|
|
||||||
if (supervideMatch) {
|
if (supervideMatch) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "50/50",
|
"name": "50/50",
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"language": "German",
|
"language": "German",
|
||||||
"streamType": "HLS",
|
"streamType": "HLS",
|
||||||
"encrypted":true,
|
"encrypted":true,
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
// Settings start
|
||||||
|
const preferedQualityOption = "Auto"; // ["Auto", "2160p", "1080p", "720p", "480p"]
|
||||||
|
const debridService = "realdebrid"; // ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink"]
|
||||||
|
const debridApiKey = "";
|
||||||
|
// Settings end
|
||||||
|
|
||||||
|
async function searchResults(keyword) {
|
||||||
|
try {
|
||||||
|
const moviesresponse = await fetchv2(
|
||||||
|
"https://v3-cinemeta.strem.io/catalog/movie/top/search=" + encodeURIComponent(keyword) + ".json"
|
||||||
|
);
|
||||||
|
const moviesdata = await moviesresponse.json();
|
||||||
|
|
||||||
|
const results = moviesdata.metas.map(item => ({
|
||||||
|
title: item.name.trim(),
|
||||||
|
image: item.poster.trim(),
|
||||||
|
href: "Movie: " + (item.id.startsWith("tt") ? item.id : "")
|
||||||
|
}));
|
||||||
|
|
||||||
|
const showsresponse = await fetchv2(
|
||||||
|
"https://v3-cinemeta.strem.io/catalog/series/top/search=" + encodeURIComponent(keyword) + ".json"
|
||||||
|
);
|
||||||
|
const showsdata = await showsresponse.json();
|
||||||
|
|
||||||
|
const showResults = showsdata.metas.map(item => ({
|
||||||
|
title: item.name.trim(),
|
||||||
|
image: item.poster.trim(),
|
||||||
|
href: "TV: " + (item.id.startsWith("tt") ? item.id : "")
|
||||||
|
}));
|
||||||
|
|
||||||
|
results.push(...showResults);
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(ID) {
|
||||||
|
try {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = "https://v3-cinemeta.strem.io/meta/" + type + "/" + actualID + ".json";
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: data.meta.description || "N/A",
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: data.meta.released || "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(ID) {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (type === "series") {
|
||||||
|
const response = await fetchv2("https://v3-cinemeta.strem.io/meta/series/" + actualID + ".json");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const videos = data.meta.videos || [];
|
||||||
|
|
||||||
|
const shouldAdjust = videos.length > 0 && videos[0].season === 0;
|
||||||
|
|
||||||
|
let currentSeason = 0;
|
||||||
|
let episodeCounter = 0;
|
||||||
|
|
||||||
|
for (const video of videos) {
|
||||||
|
const adjustedSeason = shouldAdjust ? video.season + 1 : video.season;
|
||||||
|
if (adjustedSeason !== currentSeason) {
|
||||||
|
currentSeason = adjustedSeason;
|
||||||
|
episodeCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
episodeCounter++;
|
||||||
|
|
||||||
|
let adjustedId = video.id || "";
|
||||||
|
if (adjustedId && shouldAdjust) {
|
||||||
|
const idParts = adjustedId.split(':');
|
||||||
|
if (idParts.length === 3) {
|
||||||
|
idParts[1] = String(adjustedSeason);
|
||||||
|
adjustedId = idParts.join(':');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
href: "TV: " + adjustedId,
|
||||||
|
number: episodeCounter
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} else if (type === "movie") {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Movie: " + (actualID || ""),
|
||||||
|
number: 1
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractStreamUrl(ID) {
|
||||||
|
let decodedID = decodeURIComponent(ID);
|
||||||
|
let actualID = decodedID;
|
||||||
|
let type = "movie";
|
||||||
|
|
||||||
|
if (decodedID.startsWith("Movie: ")) {
|
||||||
|
actualID = decodedID.replace("Movie: ", "");
|
||||||
|
type = "movie";
|
||||||
|
} else if (decodedID.startsWith("TV: ")) {
|
||||||
|
actualID = decodedID.replace("TV: ", "");
|
||||||
|
type = "series";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = type === "movie"
|
||||||
|
? "https://torrentio.strem.fun/" + debridService + "=" + debridApiKey + "/stream/movie/" + actualID + ".json"
|
||||||
|
: "https://torrentio.strem.fun/" + debridService + "=" + debridApiKey + "/stream/series/" + actualID + ".json";
|
||||||
|
|
||||||
|
const response = await fetchv2(endpoint);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(JSON.stringify(data));
|
||||||
|
if (!data.streams || !Array.isArray(data.streams)) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamsByQuality = {
|
||||||
|
"2160p": [],
|
||||||
|
"1080p": [],
|
||||||
|
"720p": [],
|
||||||
|
"480p": []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const stream of data.streams) {
|
||||||
|
const name = stream.name || "";
|
||||||
|
let quality = null;
|
||||||
|
|
||||||
|
if (name.includes("4k") || name.includes("2160p")) {
|
||||||
|
quality = "2160p";
|
||||||
|
} else if (name.includes("1080p")) {
|
||||||
|
quality = "1080p";
|
||||||
|
} else if (name.includes("720p")) {
|
||||||
|
quality = "720p";
|
||||||
|
} else if (name.includes("480p")) {
|
||||||
|
quality = "480p";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality && streamsByQuality[quality]) {
|
||||||
|
streamsByQuality[quality].push({
|
||||||
|
title: (stream.name || "Unknown").replace(/\n/g, " "),
|
||||||
|
streamUrl: stream.url || "",
|
||||||
|
headers: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
if (preferedQualityOption === "Auto") {
|
||||||
|
results.push(...streamsByQuality["2160p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["1080p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["720p"].slice(0, 5));
|
||||||
|
results.push(...streamsByQuality["480p"].slice(0, 5));
|
||||||
|
} else {
|
||||||
|
if (streamsByQuality[preferedQualityOption]) {
|
||||||
|
results = streamsByQuality[preferedQualityOption].slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: results,
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify({
|
||||||
|
streams: [],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "Torrentio",
|
||||||
|
"iconUrl": "https://torrentio.org/wp-content/uploads/2024/12/cropped-Torrentio-Favicon-192x192.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.1",
|
||||||
|
"language": "English",
|
||||||
|
"streamType": "MKV",
|
||||||
|
"quality": "4K",
|
||||||
|
"baseUrl": "https://www.google.com/",
|
||||||
|
"searchBaseUrl": "https://www.google.com/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/torrentio/torrentio.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true,
|
||||||
|
"settings": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
async function searchResults(keyword) {
|
||||||
|
const urls = [
|
||||||
|
`https://www.verseriesonline.net/recherche?q=${encodeURIComponent(keyword)}`,
|
||||||
|
`https://www.verseriesonline.net/recherche?q=${encodeURIComponent(keyword)}&page=2`,
|
||||||
|
`https://www.verseriesonline.net/recherche?q=${encodeURIComponent(keyword)}&page=3`,
|
||||||
|
`https://www.verseriesonline.net/recherche?q=${encodeURIComponent(keyword)}&page=4`
|
||||||
|
];
|
||||||
|
const regex = /<div class="short gridder-list">.*?<a[^>]+href="([^"]+)"[^>]*>.*?<img[^>]+src="([^"]+)"[^>]*>.*?<div class="short_title"[^>]*>.*?<a[^>]+title="([^"]+)"/gs;
|
||||||
|
try {
|
||||||
|
const fetchPromises = urls.map(url => fetchv2(url).then(r => r.text()));
|
||||||
|
const htmls = await Promise.all(fetchPromises);
|
||||||
|
const results = [];
|
||||||
|
for (const html of htmls) {
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
results.push({
|
||||||
|
href: match[1].trim(),
|
||||||
|
image: "https://www.verseriesonline.net" + match[2].trim(),
|
||||||
|
title: match[3].trim().replace("regarder ","")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
regex.lastIndex = 0;
|
||||||
|
}
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
title: "Error",
|
||||||
|
image: "Error",
|
||||||
|
href: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractDetails(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const regex = /<div class="full_content-desc">.*?<span>(.*?)<\/span>/s;
|
||||||
|
const match = regex.exec(html);
|
||||||
|
const description = match ? match[1].trim() : "N/A";
|
||||||
|
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: description,
|
||||||
|
aliases: "N/A",
|
||||||
|
airdate: "N/A"
|
||||||
|
}]);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
description: "Error",
|
||||||
|
aliases: "Error",
|
||||||
|
airdate: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEpisodes(url) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const seasonRegex = /<a class="th-hover" href="([^"]+)">.*?<div class="th-title1">temporada (\d+)<\/div>/gs;
|
||||||
|
const seasons = [];
|
||||||
|
let seasonMatch;
|
||||||
|
while ((seasonMatch = seasonRegex.exec(html)) !== null) {
|
||||||
|
seasons.push({
|
||||||
|
url: seasonMatch[1].trim(),
|
||||||
|
number: parseInt(seasonMatch[2], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(seasons.map(async (season) => {
|
||||||
|
const res = await fetchv2(season.url);
|
||||||
|
const seasonHtml = await res.text();
|
||||||
|
|
||||||
|
const episodeRegex = /<a href="([^"]+)">.*?<span class="name">Capítulo (\d+)<\/span>/gs;
|
||||||
|
let epMatch;
|
||||||
|
while ((epMatch = episodeRegex.exec(seasonHtml)) !== null) {
|
||||||
|
results.push({
|
||||||
|
season: season.number,
|
||||||
|
href: epMatch[1].trim(),
|
||||||
|
number: parseInt(epMatch[2], 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
results.sort((a, b) => {
|
||||||
|
if (a.season !== b.season) {
|
||||||
|
return a.season - b.season;
|
||||||
|
}
|
||||||
|
return a.number - b.number;
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(results);
|
||||||
|
} catch (err) {
|
||||||
|
return JSON.stringify([{
|
||||||
|
href: "Error",
|
||||||
|
number: "Error",
|
||||||
|
season: "Error"
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function extractStreamUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetchv2(url);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const blockRegex = /<div class="lien fx-row"[^>]*data-hash="([^"]+)"[^>]*>([\s\S]*?)<\/div>/g;
|
||||||
|
let hash = null;
|
||||||
|
let blockMatch;
|
||||||
|
while ((blockMatch = blockRegex.exec(html)) !== null) {
|
||||||
|
const blockContent = blockMatch[2];
|
||||||
|
if (/class="serv"[^>]*>uqload<\/span>/.test(blockContent)) {
|
||||||
|
hash = blockMatch[1].trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRegex = /_token:\s*"([^"]+)"/;
|
||||||
|
const tokenMatch = html.match(tokenRegex);
|
||||||
|
const token = tokenMatch ? tokenMatch[1].trim() : null;
|
||||||
|
|
||||||
|
console.log("Hash:"+ hash);
|
||||||
|
console.log("Token:"+ token);
|
||||||
|
|
||||||
|
const embedResponse = await fetchv2("https://www.verseriesonline.net/hashembedlink", {}, "POST", `hash=${encodeURIComponent(hash)}&_token=${encodeURIComponent(token)}`);
|
||||||
|
const embedJson = await embedResponse.json();
|
||||||
|
const uqloadUrl = embedJson.link;
|
||||||
|
|
||||||
|
const someHtml = await fetchv2(uqloadUrl);
|
||||||
|
const someText = await someHtml.text();
|
||||||
|
|
||||||
|
const finalUrl = await uqloadExtractor(someText, uqloadUrl);
|
||||||
|
const streamObj = {
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
title: "Server 1",
|
||||||
|
streamUrl: finalUrl,
|
||||||
|
headers: {
|
||||||
|
referer: "https://uqload.cx/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtitle: "https://none.com"
|
||||||
|
};
|
||||||
|
return JSON.stringify(streamObj);
|
||||||
|
} catch (err) {
|
||||||
|
return "https://error.org/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SCHEME START */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name uqloadExtractor
|
||||||
|
* @author scigward
|
||||||
|
*/
|
||||||
|
async function uqloadExtractor(html, embedUrl) {
|
||||||
|
try {
|
||||||
|
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
|
||||||
|
const videoSrc = match ? match[1] : "";
|
||||||
|
|
||||||
|
return videoSrc;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("uqloadExtractor error:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SCHEME END */
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"sourceName": "VerSeriesOnline",
|
||||||
|
"iconUrl": "https://www.verseriesonline.net/images/faveicon.png",
|
||||||
|
"author": {
|
||||||
|
"name": "50/50",
|
||||||
|
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"language": "Spanish",
|
||||||
|
"streamType": "HLS",
|
||||||
|
"quality": "1080p",
|
||||||
|
"baseUrl": "https://www.verseriesonline.net/",
|
||||||
|
"searchBaseUrl": "https://www.verseriesonline.net/",
|
||||||
|
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/verseriesonline/verseriesonline.js",
|
||||||
|
"type": "anime/movies/shows",
|
||||||
|
"asyncJS": true,
|
||||||
|
"softsub": true,
|
||||||
|
"downloadSupport": true
|
||||||
|
}
|
||||||
+10
-6
@@ -186,7 +186,7 @@ async function extractEpisodes(url) {
|
|||||||
async function extractStreamUrl(ID) {
|
async function extractStreamUrl(ID) {
|
||||||
if (ID.includes('movie')) {
|
if (ID.includes('movie')) {
|
||||||
const tmdbID = ID.replace('/movie/', '');
|
const tmdbID = ID.replace('/movie/', '');
|
||||||
const cinebyResponse = await soraFetch(`https://db.cineby.app/3/movie/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
const cinebyResponse = await soraFetch(`https://jumpfreedom.com/3/movie/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
||||||
const cinebyData = await cinebyResponse.json();
|
const cinebyData = await cinebyResponse.json();
|
||||||
|
|
||||||
const title = encodeURIComponent(cinebyData.title);
|
const title = encodeURIComponent(cinebyData.title);
|
||||||
@@ -194,7 +194,7 @@ async function extractStreamUrl(ID) {
|
|||||||
const imdbId = cinebyData.external_ids?.imdb_id || '';
|
const imdbId = cinebyData.external_ids?.imdb_id || '';
|
||||||
const tmdbId = cinebyData.id;
|
const tmdbId = cinebyData.id;
|
||||||
|
|
||||||
const fullUrl = `https://api.videasy.net/myflixerzupcloud/sources-with-title?title=${title}&mediaType=movie&year=${year}&episodeId=1&seasonId=1&tmdbId=${tmdbId}&imdbId=${imdbId}`;
|
const fullUrl = `https://api.videasy.net/cdn/sources-with-title?title=${title}&mediaType=movie&year=${year}&episodeId=1&seasonId=1&tmdbId=${tmdbId}&imdbId=${imdbId}`;
|
||||||
|
|
||||||
console.log('Full URL:' + fullUrl);
|
console.log('Full URL:' + fullUrl);
|
||||||
|
|
||||||
@@ -220,7 +220,9 @@ async function extractStreamUrl(ID) {
|
|||||||
const sources = result.sources || [];
|
const sources = result.sources || [];
|
||||||
const subtitles = result.subtitles || [];
|
const subtitles = result.subtitles || [];
|
||||||
|
|
||||||
const streams = sources.flatMap(src => [src.quality, src.url]);
|
const nonHDRSources = sources.filter(s => !s.quality.includes("HDR"));
|
||||||
|
|
||||||
|
const streams = nonHDRSources.flatMap(src => [src.quality, src.url]);
|
||||||
|
|
||||||
const englishSubtitle = subtitles.find(
|
const englishSubtitle = subtitles.find(
|
||||||
sub => sub.language.toLowerCase().includes('english')
|
sub => sub.language.toLowerCase().includes('english')
|
||||||
@@ -236,7 +238,7 @@ async function extractStreamUrl(ID) {
|
|||||||
const seasonNumber = parts[3];
|
const seasonNumber = parts[3];
|
||||||
const episodeNumber = parts[4];
|
const episodeNumber = parts[4];
|
||||||
|
|
||||||
const cinebyResponse = await soraFetch(`https://db.cineby.app/3/tv/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
const cinebyResponse = await soraFetch(`https://jumpfreedom.com/3/tv/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`);
|
||||||
const cinebyData = await cinebyResponse.json();
|
const cinebyData = await cinebyResponse.json();
|
||||||
|
|
||||||
const title = encodeURIComponent(cinebyData.name);
|
const title = encodeURIComponent(cinebyData.name);
|
||||||
@@ -244,7 +246,7 @@ async function extractStreamUrl(ID) {
|
|||||||
const imdbId = cinebyData.external_ids?.imdb_id || '';
|
const imdbId = cinebyData.external_ids?.imdb_id || '';
|
||||||
const tmdbId = cinebyData.id;
|
const tmdbId = cinebyData.id;
|
||||||
|
|
||||||
const fullUrl = `https://api.videasy.net/myflixerzupcloud/sources-with-title?title=${title}&mediaType=tv&year=${year}&episodeId=${episodeNumber}&seasonId=${seasonNumber}&tmdbId=${tmdbId}&imdbId=${imdbId}`;
|
const fullUrl = `https://api.videasy.net/cdn/sources-with-title?title=${title}&mediaType=tv&year=${year}&episodeId=${episodeNumber}&seasonId=${seasonNumber}&tmdbId=${tmdbId}&imdbId=${imdbId}`;
|
||||||
|
|
||||||
console.log('Full URL:' + fullUrl);
|
console.log('Full URL:' + fullUrl);
|
||||||
|
|
||||||
@@ -270,7 +272,9 @@ async function extractStreamUrl(ID) {
|
|||||||
const sources = result.sources || [];
|
const sources = result.sources || [];
|
||||||
const subtitles = result.subtitles || [];
|
const subtitles = result.subtitles || [];
|
||||||
|
|
||||||
const streams = sources.flatMap(src => [src.quality, src.url]);
|
const nonHDRSources = sources.filter(s => !s.quality.includes("HDR"));
|
||||||
|
|
||||||
|
const streams = nonHDRSources.flatMap(src => [src.quality, src.url]);
|
||||||
|
|
||||||
const englishSubtitle = subtitles.find(
|
const englishSubtitle = subtitles.find(
|
||||||
sub => sub.language.toLowerCase().includes('english')
|
sub => sub.language.toLowerCase().includes('english')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user