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"
|
||||
}];
|
||||
}
|
||||
const movieData = [{ name: "MovieID", data: movieIDMatch }];
|
||||
const tokenResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(movieData)
|
||||
);
|
||||
const temp = await tokenResponse.json();
|
||||
const token = temp[0]?.data;
|
||||
|
||||
const movieIdApiUrl = `https://enc-dec.app/api/enc-movies-flix?text=${movieIDMatch}`;
|
||||
const movieIdTokenResponse = await fetchv2(movieIdApiUrl);
|
||||
const movieIdTokenData = await movieIdTokenResponse.json();
|
||||
const token = movieIdTokenData.result;
|
||||
|
||||
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
|
||||
const episodeListResponse = await fetchv2(episodeListUrl);
|
||||
const episodeListData = await episodeListResponse.json();
|
||||
@@ -115,23 +112,16 @@ async function extractEpisodes(movieUrl) {
|
||||
const episodeRegex = /<a[^>]+eid="([^"]+)"[^>]+num="([^"]+)"[^>]*>/g;
|
||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||
|
||||
const episodeData = episodeMatches.map(([_, episodeToken, episodeNum]) => ({
|
||||
name: `Episode ${episodeNum}`,
|
||||
data: episodeToken
|
||||
}));
|
||||
const episodeTokenPromises = episodeMatches.map(([_, episodeToken]) => {
|
||||
const apiUrl = `https://enc-dec.app/api/enc-movies-flix?text=${episodeToken}`;
|
||||
return fetchv2(apiUrl).then(response => response.json());
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(episodeData));
|
||||
const batchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(episodeData)
|
||||
);
|
||||
const batchResults = await batchResponse.json();
|
||||
const episodeTokenResults = await Promise.all(episodeTokenPromises);
|
||||
|
||||
const episodes = batchResults.map((result, index) => ({
|
||||
number: parseInt(episodeMatches[index][2], 10),
|
||||
href: `https://1movies.bz/ajax/links/list?eid=${episodeMatches[index][1]}&_=${result.data}`
|
||||
const episodes = episodeMatches.map(([_, episodeToken, episodeNum], index) => ({
|
||||
number: parseInt(episodeNum, 10),
|
||||
href: `https://1movies.bz/ajax/links/list?eid=${episodeToken}&_=${episodeTokenResults[index].result}`
|
||||
}));
|
||||
|
||||
return JSON.stringify(episodes);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"language": "English",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"streamAsyncJS": false,
|
||||
"softsub": false,
|
||||
"type": "anime",
|
||||
"downloadSupport": true
|
||||
"downloadSupport": false
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ async function searchResults(keyword) {
|
||||
const headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"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;
|
||||
|
||||
try {
|
||||
const response = await fetchv2(
|
||||
"https://anime-sama.fr/template-php/defaut/fetch.php",
|
||||
"https://anime-sama.org/template-php/defaut/fetch.php",
|
||||
headers,
|
||||
"POST",
|
||||
`query=${encodeURIComponent(keyword)}`
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"language": "French",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
|
||||
@@ -80,7 +80,6 @@ async function extractEpisodes(url) {
|
||||
}
|
||||
|
||||
async function extractStreamUrl(id) {
|
||||
if (!_0xCheck()) return 'https://files.catbox.moe/avolvc.mp4';
|
||||
|
||||
const cookieHeader = `key=${id}`;
|
||||
const headers = {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"language": "English (SUB)",
|
||||
"streamType": "HLS",
|
||||
"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 {
|
||||
const response = await fetchv2(animeUrl);
|
||||
const htmlText = await response.text();
|
||||
|
||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||
if (!animeIdMatch) {
|
||||
return [{
|
||||
error: "AniID not found"
|
||||
}];
|
||||
}
|
||||
|
||||
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`);
|
||||
const token = await tokenResponse.text();
|
||||
|
||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||
|
||||
const episodeListResponse = await fetchv2(episodeListUrl);
|
||||
const episodeListData = await episodeListResponse.json();
|
||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
||||
|
||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||
|
||||
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
||||
name: `Episode ${episodeNum}`,
|
||||
data: episodeToken
|
||||
}));
|
||||
|
||||
const batchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
||||
{},
|
||||
"POST",
|
||||
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);
|
||||
const actualUrl = url.replace("Animekai:", "").trim();
|
||||
const htmlText = await (await fetchv2(actualUrl)).text();
|
||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
||||
|
||||
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
||||
const tokenData = await tokenResponse.json();
|
||||
const token = tokenData.result;
|
||||
|
||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
||||
|
||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||
|
||||
const recentEpisodeMatches = episodeMatches.slice(-50);
|
||||
|
||||
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
||||
name: `Episode ${episodeNum}`,
|
||||
data: episodeToken
|
||||
}));
|
||||
|
||||
const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
|
||||
|
||||
const episodes = batchResults.map((result, index) => ({
|
||||
number: parseInt(recentEpisodeMatches[index][1], 10),
|
||||
href: `https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
|
||||
}));
|
||||
|
||||
return JSON.stringify(episodes);
|
||||
} catch (err) {
|
||||
console.error("Error fetching episodes:" + err);
|
||||
return [{
|
||||
@@ -159,13 +158,13 @@ async function extractStreamUrl(url) {
|
||||
{ name: "Sub", data: serverIdSub }
|
||||
].filter(item => item.data);
|
||||
|
||||
const tokenBatchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(tokenRequestData)
|
||||
const tokenPromises = tokenRequestData.map(item =>
|
||||
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => ({ name: item.name, data: json.result }))
|
||||
.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 serverIdMap = {
|
||||
@@ -210,13 +209,13 @@ async function extractStreamUrl(url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const decryptBatchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(decryptRequestData)
|
||||
const decryptPromises = decryptRequestData.map(item =>
|
||||
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||
);
|
||||
const decryptResults = await decryptBatchResponse.json();
|
||||
const decryptResults = await Promise.all(decryptPromises);
|
||||
|
||||
const finalResults = {};
|
||||
decryptResults.forEach(result => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"language": "English",
|
||||
"streamType": "HLS",
|
||||
"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 {
|
||||
const response = await fetchv2(animeUrl);
|
||||
const htmlText = await response.text();
|
||||
|
||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||
if (!animeIdMatch) {
|
||||
return [{
|
||||
error: "AniID not found"
|
||||
}];
|
||||
}
|
||||
|
||||
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`);
|
||||
const token = await tokenResponse.text();
|
||||
|
||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||
|
||||
const episodeListResponse = await fetchv2(episodeListUrl);
|
||||
const episodeListData = await episodeListResponse.json();
|
||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
||||
|
||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||
|
||||
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
||||
name: `Episode ${episodeNum}`,
|
||||
data: episodeToken
|
||||
}));
|
||||
|
||||
const batchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
||||
{},
|
||||
"POST",
|
||||
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);
|
||||
const actualUrl = url.replace("Animekai:", "").trim();
|
||||
const htmlText = await (await fetchv2(actualUrl)).text();
|
||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
||||
|
||||
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
||||
const tokenData = await tokenResponse.json();
|
||||
const token = tokenData.result;
|
||||
|
||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
||||
|
||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
||||
|
||||
const recentEpisodeMatches = episodeMatches.slice(-50);
|
||||
|
||||
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
||||
name: `Episode ${episodeNum}`,
|
||||
data: episodeToken
|
||||
}));
|
||||
|
||||
const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
|
||||
|
||||
const episodes = batchResults.map((result, index) => ({
|
||||
number: parseInt(recentEpisodeMatches[index][1], 10),
|
||||
href: `https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
|
||||
}));
|
||||
|
||||
return JSON.stringify(episodes);
|
||||
} catch (err) {
|
||||
console.error("Error fetching episodes:" + err);
|
||||
return [{
|
||||
@@ -159,13 +158,13 @@ async function extractStreamUrl(url) {
|
||||
{ name: "Sub", data: serverIdSub }
|
||||
].filter(item => item.data);
|
||||
|
||||
const tokenBatchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(tokenRequestData)
|
||||
const tokenPromises = tokenRequestData.map(item =>
|
||||
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => ({ name: item.name, data: json.result }))
|
||||
.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 serverIdMap = {
|
||||
@@ -210,13 +209,13 @@ async function extractStreamUrl(url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const decryptBatchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(decryptRequestData)
|
||||
const decryptPromises = decryptRequestData.map(item =>
|
||||
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||
);
|
||||
const decryptResults = await decryptBatchResponse.json();
|
||||
const decryptResults = await Promise.all(decryptPromises);
|
||||
|
||||
const finalResults = {};
|
||||
decryptResults.forEach(result => {
|
||||
@@ -234,16 +233,16 @@ async function extractStreamUrl(url) {
|
||||
};
|
||||
|
||||
const decryptedUrls = await processStreams(streamUrls);
|
||||
const decryptedSub = decryptedUrls.Sub || decryptedUrls.Softsub || decryptedUrls.Dub;
|
||||
|
||||
console.log(decryptedSub);
|
||||
const decryptedDub = decryptedUrls.Sub || decryptedUrls.Dub || decryptedUrls.Softsub;
|
||||
|
||||
console.log(decryptedDub);
|
||||
const headers = {
|
||||
"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"
|
||||
};
|
||||
|
||||
if (decryptedSub) {
|
||||
const response = await fetchv2(decryptedSub.replace("/e/", "/media/"), headers);
|
||||
if (decryptedDub) {
|
||||
const response = await fetchv2(decryptedDub.replace("/e/", "/media/"), headers);
|
||||
const responseJson = await response.json();
|
||||
|
||||
const result = responseJson?.result;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"language": "English",
|
||||
"streamType": "HLS",
|
||||
"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) {
|
||||
const results = [];
|
||||
try {
|
||||
@@ -18,7 +25,7 @@ async function searchResults(keyword) {
|
||||
results.push({
|
||||
href: "https://animenana.com" + hrefMatch[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({
|
||||
href: "https://animenana.com" + hrefMatch[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({
|
||||
href: "https://animenana.com" + match[1].trim(),
|
||||
image: "https://animenana.com" + match[2].trim(),
|
||||
title: match[3].trim()
|
||||
title: cleanTitle(match[3].trim())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "English (Hardsub)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
|
||||
+16
-16
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"sourceName": "AnimeToast",
|
||||
"iconUrl": "https://www.animetoast.cc/wp-content/uploads/2018/03/toastfavi-300x300.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.2.12",
|
||||
"language": "German (DUB/SUB)",
|
||||
"streamType": "MP4",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://www.animetoast.cc/",
|
||||
"searchBaseUrl": "https://www.animetoast.cc/?s=the%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animetoast/animetoast_v2.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
"sourceName": "AnimeToast",
|
||||
"iconUrl": "https://www.animetoast.cc/wp-content/uploads/2018/03/toastfavi-300x300.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.2.13",
|
||||
"language": "German (DUB/SUB)",
|
||||
"streamType": "MP4",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://www.animetoast.cc/",
|
||||
"searchBaseUrl": "https://www.animetoast.cc/?s=the%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animetoast/animetoast_v2.js",
|
||||
"asyncJS": true,
|
||||
"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
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -321,8 +321,8 @@ async function sendLog(message) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -335,7 +335,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -454,7 +470,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -513,6 +536,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -520,6 +550,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -527,6 +571,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -610,6 +710,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -681,6 +1151,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -895,6 +1548,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
if (count != symtab.length) {
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"streamAsyncJS": false,
|
||||
"softsub": false,
|
||||
"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)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.7",
|
||||
"language": "English (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldEngSub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
"sourceName": "AniWorld (ENG SUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.8",
|
||||
"language": "English (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldEngSub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"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",
|
||||
"author": {
|
||||
"name": "Cufiy",
|
||||
@@ -11,7 +11,7 @@
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://vidmoly.to/",
|
||||
"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,
|
||||
"type": "anime"
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"sourceName": "AniWorld (GER DUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.7",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
"sourceName": "AniWorld (GER DUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.8",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"sourceName": "AniWorld (GER SUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.7",
|
||||
"language": "German (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerSub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
"sourceName": "AniWorld (GER SUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.8",
|
||||
"language": "German (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerSub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
@@ -589,6 +589,8 @@ async function sendLog(message) {
|
||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
console.log(message);
|
||||
|
||||
return;
|
||||
|
||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
.catch(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
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -358,8 +358,8 @@ async function sendLog(message) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -372,7 +372,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -491,7 +507,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -550,6 +573,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -557,6 +587,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -564,6 +608,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -647,6 +747,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -718,6 +1188,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -932,6 +1585,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
if (count != symtab.length) {
|
||||
|
||||
@@ -338,7 +338,7 @@ function base64Decode(str) {
|
||||
async function sendLog(message) {
|
||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
console.log(message);
|
||||
// return;
|
||||
return;
|
||||
|
||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
.catch(error => {
|
||||
@@ -351,7 +351,7 @@ async function sendLog(message) {
|
||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -359,8 +359,8 @@ async function sendLog(message) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -373,7 +373,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -492,7 +508,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -551,6 +574,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -558,6 +588,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -565,6 +609,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -648,6 +748,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -719,6 +1189,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -933,6 +1586,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
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
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -358,8 +358,8 @@ async function sendLog(message) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -372,7 +372,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -491,7 +507,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -550,6 +573,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -557,6 +587,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -564,6 +608,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -647,6 +747,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -718,6 +1188,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -932,6 +1585,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
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
|
||||
.filter(r => r.score > 50) // Increased threshold to filter out weak matches
|
||||
.sort((a, b) => b.score - a.score) // Sort by pre-calculated scores
|
||||
.filter(r => r.score > 50)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map(({ score, ...rest }) => rest);
|
||||
|
||||
return JSON.stringify(filteredResults.length > 0 ? filteredResults : [{
|
||||
@@ -330,18 +330,13 @@ async function extractDetails(url) {
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
const sendEpisodes = async (endpoint, episodeData) => {
|
||||
if (episodeData.length > 45) {
|
||||
const promises = episodeData.map(item =>
|
||||
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
|
||||
.then(res => res.text())
|
||||
.then(data => ({ name: item.name, data }))
|
||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||
);
|
||||
return Promise.all(promises);
|
||||
} else {
|
||||
const resp = await fetchv2(endpoint, {}, "POST", JSON.stringify(episodeData));
|
||||
return resp.json();
|
||||
}
|
||||
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 {
|
||||
@@ -351,8 +346,9 @@ async function extractEpisodes(url) {
|
||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
||||
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
||||
|
||||
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`);
|
||||
const token = await tokenResponse.text();
|
||||
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
||||
const tokenData = await tokenResponse.json();
|
||||
const token = tokenData.result;
|
||||
|
||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||
@@ -361,16 +357,18 @@ async function extractEpisodes(url) {
|
||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
||||
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}`,
|
||||
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) => ({
|
||||
number: parseInt(episodeMatches[index][1], 10),
|
||||
href: `Animekai:https://animekai.to/ajax/links/list?token=${episodeMatches[index][2]}&_=${result.data}`
|
||||
number: parseInt(recentEpisodeMatches[index][1], 10),
|
||||
href: `Animekai:https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
|
||||
}));
|
||||
|
||||
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];
|
||||
if (!movieIDMatch) return JSON.stringify([{ error: "MovieID not found" }]);
|
||||
|
||||
const movieData = [{ name: "MovieID", data: movieIDMatch }];
|
||||
const tokenResponse = await fetchv2("https://ilovekai.simplepostrequest.workers.dev/ilovethighs", {}, "POST", JSON.stringify(movieData));
|
||||
const tokenResponse = await fetchv2("https://enc-dec.app/api/enc-movies-flix?text=" + encodeURIComponent(movieIDMatch));
|
||||
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 episodeListData = await (await fetchv2(episodeListUrl)).json();
|
||||
@@ -396,7 +393,7 @@ async function extractEpisodes(url) {
|
||||
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) => ({
|
||||
number: parseInt(episodeMatches[index][2], 10),
|
||||
@@ -450,13 +447,13 @@ async function extractStreamUrl(url) {
|
||||
{ name: "Sub", data: serverIdSub }
|
||||
].filter(item => item.data);
|
||||
|
||||
const tokenBatchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(tokenRequestData)
|
||||
const tokenPromises = tokenRequestData.map(item =>
|
||||
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => ({ name: item.name, data: json.result }))
|
||||
.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 serverIdMap = {
|
||||
@@ -501,13 +498,13 @@ async function extractStreamUrl(url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const decryptBatchResponse = await fetchv2(
|
||||
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits",
|
||||
{},
|
||||
"POST",
|
||||
JSON.stringify(decryptRequestData)
|
||||
const decryptPromises = decryptRequestData.map(item =>
|
||||
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
||||
);
|
||||
const decryptResults = await decryptBatchResponse.json();
|
||||
const decryptResults = await Promise.all(decryptPromises);
|
||||
|
||||
const finalResults = {};
|
||||
decryptResults.forEach(result => {
|
||||
@@ -569,7 +566,7 @@ async function extractStreamUrl(url) {
|
||||
if (dubStream) streams.push("Dubbed English", dubStream);
|
||||
|
||||
const rawStream = decryptedRaw ? await getStream(decryptedRaw) : null;
|
||||
if (rawStream) streams.push("Japanese", rawStream);
|
||||
if (rawStream) streams.push("Original audio", rawStream);
|
||||
|
||||
const final = {
|
||||
streams,
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.8",
|
||||
"language": "English",
|
||||
"streamType": "HLS",
|
||||
"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 iframeUrl = iframeMatch ? iframeMatch[1] : null;
|
||||
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 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) {
|
||||
const filemoon = atob(fileMonMatch[1]);
|
||||
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();
|
||||
return filemoonExtractor(fileHtml, filemoon);
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "French",
|
||||
"streamType": "HLS",
|
||||
"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
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.4} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -155,12 +155,13 @@ function cleanHtmlSymbols(string) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-08-13 03:44:07
|
||||
* @version 1.1.4
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
||||
function globalExtractor(providers) {
|
||||
for (const [url, provider] of Object.entries(providers)) {
|
||||
try {
|
||||
@@ -307,6 +308,8 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
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
|
||||
@@ -367,6 +370,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -374,6 +384,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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);
|
||||
@@ -388,6 +405,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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);
|
||||
@@ -395,6 +419,34 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
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);
|
||||
@@ -402,6 +454,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -430,17 +489,11 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// EXTRACTORS //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING //
|
||||
|
||||
|
||||
/* --- bigwarp --- */
|
||||
|
||||
/**
|
||||
@@ -458,8 +511,6 @@ async function bigwarpExtractor(videoPage, url = null) {
|
||||
console.log("BigWarp HD Decoded:", bwDecoded);
|
||||
return bwDecoded;
|
||||
}
|
||||
|
||||
|
||||
/* --- doodstream --- */
|
||||
|
||||
/**
|
||||
@@ -493,7 +544,27 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
@@ -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 --- */
|
||||
|
||||
/**
|
||||
@@ -560,16 +641,28 @@ async function filemoonExtractor(html, url = null) {
|
||||
|
||||
// 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 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 encrypted = rawSourceData?.sources;
|
||||
let decryptedSources = null;
|
||||
// console.log('rawSourceData', rawSourceData);
|
||||
if (rawSourceData?.encrypted == false) {
|
||||
decryptedSources = rawSourceData.sources;
|
||||
}
|
||||
@@ -577,14 +670,14 @@ async function megacloudExtractor(html, embedUrl) {
|
||||
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
|
||||
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
|
||||
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
|
||||
try {
|
||||
return decryptedSources[0].file;
|
||||
} catch (error) {
|
||||
console.log("Error extracting MegaCloud stream URL:" + error);
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// return {
|
||||
@@ -774,7 +867,7 @@ async function megacloudExtractor(html, embedUrl) {
|
||||
* @returns {string|null} The extracted nonce, or null if it couldn't be found
|
||||
*/
|
||||
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 match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
|
||||
if (match0?.[1]) {
|
||||
@@ -857,10 +950,10 @@ async function megacloudExtractor(html, embedUrl) {
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function fetchKey(name, url, timeout = 1000) {
|
||||
function fetchKey(name, url) {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'get', timeout: timeout });
|
||||
const response = await soraFetch(url, { method: 'get' });
|
||||
const key = await response.text();
|
||||
let trueKey = null;
|
||||
try {
|
||||
@@ -875,8 +968,6 @@ async function megacloudExtractor(html, embedUrl) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --- mp4upload --- */
|
||||
|
||||
/**
|
||||
@@ -894,8 +985,17 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -920,8 +1020,119 @@ async function sibnetExtractor(html, embedUrl) {
|
||||
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 --- */
|
||||
|
||||
/**
|
||||
@@ -938,7 +1149,20 @@ async function uqloadExtractor(html, embedUrl) {
|
||||
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 --- */
|
||||
|
||||
@@ -978,8 +1202,6 @@ async function vidmolyExtractor(html, url = null) {
|
||||
return sourcesString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --- vidoza --- */
|
||||
|
||||
/**
|
||||
@@ -996,8 +1218,6 @@ async function vidozaExtractor(html, url = null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --- voe --- */
|
||||
|
||||
/**
|
||||
@@ -1093,10 +1313,6 @@ function voeShiftChars(str, shift) {
|
||||
.join("");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// 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 {
|
||||
constructor(base) {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
|
||||
+18
-18
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"sourceName": "DoraBash",
|
||||
"iconUrl": "https://dorabash.com/wp-content/uploads/2023/06/cropped-Untitled_design-removebg-192x192.png",
|
||||
"author": {
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"language": "Hindi",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://dorabash.com/",
|
||||
"searchBaseUrl": "https://dorabash.com/",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/dorabash/dorabash.js",
|
||||
"type": "anime",
|
||||
"asyncJS": true,
|
||||
"softsub": false,
|
||||
"downloadSupport": false
|
||||
}
|
||||
"sourceName": "DoraBash",
|
||||
"iconUrl": "https://dorabash.com/wp-content/uploads/2023/06/cropped-Untitled_design-removebg-192x192.png",
|
||||
"author": {
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.2",
|
||||
"language": "Hindi",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://dorabash.com/",
|
||||
"searchBaseUrl": "https://dorabash.com/",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/dorabash/dorabash.js",
|
||||
"type": "anime",
|
||||
"asyncJS": true,
|
||||
"softsub": false,
|
||||
"downloadSupport": false
|
||||
}
|
||||
@@ -72,6 +72,7 @@ async function extractEpisodes(url) {
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
const response = await fetchv2(url);
|
||||
@@ -85,7 +86,7 @@ async function extractStreamUrl(url) {
|
||||
const serversJson = jsonMatch[1].replace(/\\/g, '');
|
||||
const servers = JSON.parse(serversJson);
|
||||
|
||||
const fdewsdcServer = servers.find(server => server.name === "fdewsdc");
|
||||
const fdewsdcServer = servers.find(server => server.name === "EarnVids");
|
||||
if (fdewsdcServer) {
|
||||
console.log("Stream URL: " + fdewsdcServer.url);
|
||||
return await extractEarnVids(fdewsdcServer.url);
|
||||
@@ -97,8 +98,12 @@ async function extractStreamUrl(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 {
|
||||
const response = await fetchv2(url);
|
||||
const response = await fetchv2(url, headers);
|
||||
const html = await response.text();
|
||||
|
||||
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "Arabic",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
+41
-60
@@ -80,72 +80,21 @@ async function extractEpisodes(url) {
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
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 response = await fetchv2(url, header, "POST", postData);
|
||||
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[^"']*)["']/);
|
||||
if (hlsObjectMatch) {
|
||||
hlsLink = hlsObjectMatch[1];
|
||||
console.log("Found HLS object link:", hlsLink);
|
||||
return hlsLink;
|
||||
const earnvidsMatch = html.match(/<li[^>]*data-link=["']([^"']+)["'][^>]*>\s*<span>\s*<p>\s*EarnVids\s*<\/p>/i);
|
||||
const embedResponse = await fetchv2(earnvidsMatch[1], header);
|
||||
const embedHtml = await embedResponse.text();
|
||||
if (earnvidsMatch) {
|
||||
const stream = await earnvidsExtractor(embedHtml, url);
|
||||
return stream;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error("Error in extractStreamUrl:", err);
|
||||
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
|
||||
* Credit to GitHub user "mnsrulz" for Unpacker Node library
|
||||
@@ -271,3 +248,7 @@ function unpack(source) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
/* REMOVE_END */
|
||||
|
||||
/* SCHEME END */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "Arabic",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
|
||||
+16
-16
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"sourceName": "FireAnime SUB",
|
||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.1.2",
|
||||
"language": "German (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://fireani.me/",
|
||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerSub.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
"sourceName": "FireAnime SUB",
|
||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.1.3",
|
||||
"language": "German (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://fireani.me/",
|
||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerSub.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"sourceName": "FireAnime DUB",
|
||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.1.2",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://fireani.me/",
|
||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerDub.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
"sourceName": "FireAnime DUB",
|
||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.1.3",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://fireani.me/",
|
||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerDub.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
+16
-16
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"sourceName": "FireAnime English (SUB)",
|
||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.1.2",
|
||||
"language": "English (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://fireani.me/",
|
||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeEngSub.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
"sourceName": "FireAnime English (SUB)",
|
||||
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
|
||||
"author": {
|
||||
"name": "50/50 & Cufiy",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.1.3",
|
||||
"language": "English (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://fireani.me/",
|
||||
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeEngSub.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
@@ -190,7 +190,7 @@ async function sendLog(message) {
|
||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -198,8 +198,8 @@ async function sendLog(message) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -212,7 +212,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -331,7 +347,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -390,6 +413,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -397,6 +427,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -404,6 +448,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -487,6 +587,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -558,6 +1028,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -772,6 +1425,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
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
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -199,8 +199,8 @@ async function sendLog(message) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -213,7 +213,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -332,7 +348,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -391,6 +414,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -398,6 +428,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -405,6 +449,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -488,6 +588,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -559,6 +1029,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -773,6 +1426,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
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
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -198,8 +198,8 @@ async function sendLog(message) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -212,7 +212,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -331,7 +347,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -390,6 +413,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -397,6 +427,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -404,6 +448,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -487,6 +587,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -558,6 +1028,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -772,6 +1425,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
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) {
|
||||
const searchUrl = `https://kimcartoon.com.co/?s=${encodeURIComponent(keyword)}`;
|
||||
try {
|
||||
const response = await fetch(searchUrl);
|
||||
const html = await response;
|
||||
const response = await fetchv2(searchUrl);
|
||||
const html = await response;text();
|
||||
const results = [];
|
||||
const articleRegex = /<article[^>]*class="bs styletwo"[\s\S]*?<\/article>/g;
|
||||
const items = html.match(articleRegex) || [];
|
||||
@@ -32,8 +32,8 @@ async function searchResults(keyword) {
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
const response = await fetch(url);
|
||||
const html = await response;
|
||||
const response = await fetchv2(url);
|
||||
const html = await response.text();
|
||||
const details = [];
|
||||
const descriptionMatch = html.match(/<div class="entry-content" itemprop="description">\s*<p>(.*?)<\/p>/);
|
||||
const description = descriptionMatch ? descriptionMatch[1].trim() : `N/A`;
|
||||
@@ -48,8 +48,8 @@ async function extractDetails(url) {
|
||||
return JSON.stringify(details);
|
||||
}
|
||||
async function extractEpisodes(url) {
|
||||
const response = await fetch(url);
|
||||
const html = await response;
|
||||
const response = await fetchv2(url);
|
||||
const html = await response.text();
|
||||
const episodes = [];
|
||||
|
||||
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) {
|
||||
const embedResponse = await fetch(url);
|
||||
const data = await embedResponse;
|
||||
const embedResponse = await fetchv2(url);
|
||||
const data = await embedResponse.text();
|
||||
|
||||
const embedMatch = data.match(/<div class="pembed" data-embed="(.*?)"/);
|
||||
if (embedMatch && embedMatch[1]) {
|
||||
@@ -97,17 +97,29 @@ async function extractStreamUrl(url) {
|
||||
}
|
||||
|
||||
console.log(fullEmbedUrl);
|
||||
const embedPageResponse = await fetch(fullEmbedUrl);
|
||||
const embedPageData = await embedPageResponse;
|
||||
|
||||
const embedPageResponse = await fetchv2(fullEmbedUrl);
|
||||
const embedPageData = await embedPageResponse.text();
|
||||
|
||||
console.log(embedPageData);
|
||||
const m3u8Match = embedPageData.match(/sources:\s*\[\{file:"(https:\/\/[^"]*\.m3u8)"/);
|
||||
if (m3u8Match && m3u8Match[1]) {
|
||||
const m3u8Url = m3u8Match[1];
|
||||
console.log(m3u8Url);
|
||||
return m3u8Url;
|
||||
|
||||
const iframeMatch = embedPageData.match(/<iframe[^>]*src="([^"]+)"/);
|
||||
if (iframeMatch && iframeMatch[1]) {
|
||||
const iframeUrl = iframeMatch[1];
|
||||
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 {
|
||||
throw new Error("M3U8 URL not found.");
|
||||
throw new Error("Iframe URL not found in embedPageData.");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Embed URL not found.");
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "English (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
@@ -13,5 +13,6 @@
|
||||
"searchBaseUrl": "https://kimcartoon.com.co/?s=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/kimcartoon/kimcartoon.js",
|
||||
"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}`;
|
||||
console.log("Form Data: " + formData);
|
||||
const headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Origin": "https://wwv.monoschinos2.net",
|
||||
"Referer": url
|
||||
"Referer": "https://wvv.monoschinos2.net/",
|
||||
"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 pageHtml = await response.text();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "Spanish (DUB/SUB)",
|
||||
"streamType": "HLS",
|
||||
"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)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
||||
"author": {
|
||||
"name": "Cufiy",
|
||||
"icon": "https://files.catbox.moe/ttj4fc.gif"
|
||||
},
|
||||
"version": "0.3.14",
|
||||
"language": "English (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToEngDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "shows"
|
||||
}
|
||||
"sourceName": "s.to (ENG DUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
||||
"author": {
|
||||
"name": "Cufiy",
|
||||
"icon": "https://files.catbox.moe/ttj4fc.gif"
|
||||
},
|
||||
"version": "0.3.15",
|
||||
"language": "English (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToEngDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "shows"
|
||||
}
|
||||
+665
-6
@@ -352,7 +352,7 @@ function base64Decode(str) {
|
||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -360,8 +360,8 @@ function base64Decode(str) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -374,7 +374,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -493,7 +509,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -552,6 +575,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -559,6 +589,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -566,6 +610,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -649,6 +749,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -720,6 +1190,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -934,6 +1587,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
if (count != symtab.length) {
|
||||
|
||||
+17
-17
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"sourceName": "s.to (GER DUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.3.14",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToGerDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "shows"
|
||||
}
|
||||
"sourceName": "s.to (GER DUB)",
|
||||
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.3.15",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToGerDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "shows"
|
||||
}
|
||||
+665
-6
@@ -352,7 +352,7 @@ function base64Decode(str) {
|
||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
/* {VERSION: 1.1.8} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
@@ -360,8 +360,8 @@ function base64Decode(str) {
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @date 2025-11-05 15:44:57
|
||||
* @version 1.1.8
|
||||
* @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
|
||||
*/
|
||||
@@ -374,7 +374,17 @@ function globalExtractor(providers) {
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
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) {
|
||||
// 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`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
let streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
|
||||
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
|
||||
if (
|
||||
!streamUrl ||
|
||||
@@ -493,7 +509,14 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
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
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
@@ -552,6 +575,13 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
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":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
@@ -559,6 +589,20 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
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":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
@@ -566,6 +610,62 @@ async function extractStreamUrlByProvider(url, provider) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
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":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
@@ -649,6 +749,28 @@ function randomStr(length) {
|
||||
}
|
||||
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 --- */
|
||||
|
||||
/* {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 --- */
|
||||
|
||||
/**
|
||||
@@ -720,6 +1190,185 @@ async function mp4uploadExtractor(html, url = 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 --- */
|
||||
|
||||
/**
|
||||
@@ -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 {
|
||||
constructor(base) {
|
||||
this.ALPHABET = {
|
||||
@@ -934,6 +1587,12 @@ class Unbaser {
|
||||
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) {
|
||||
let { payload, symtab, radix, count } = _filterargs(source);
|
||||
if (count != symtab.length) {
|
||||
|
||||
@@ -223,7 +223,7 @@ async function extractStreamUrl(ID) {
|
||||
const streamdata = decryptData.result.hls;
|
||||
const stream = "https://proxy.aether.mom/m3u8-proxy?url=https://smashyplayer.top"+streamdata;
|
||||
console.log(stream);
|
||||
const subsEng = decryptData.result.subtitle?.English || "https://error.org";
|
||||
const subsEng = decryptData?.result?.subtitle?.["English"] || "https://error.org/";
|
||||
|
||||
const streams = [];
|
||||
|
||||
@@ -283,7 +283,7 @@ async function extractStreamUrl(ID) {
|
||||
: "";
|
||||
const stream = baseUrl + streamdata;
|
||||
console.log(stream);
|
||||
const subsEng = decryptData.result.subtitle?.English || "https://error.org";
|
||||
const subsEng = decryptData?.result?.subtitle?.["English"] || "https://error.org/";
|
||||
|
||||
const streams = [];
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"language": "English",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://smashystream.xyz/i",
|
||||
"searchBaseUrl": "https://smashystream.xyz/i",
|
||||
"baseUrl": "https://smashystream.xyz/",
|
||||
"searchBaseUrl": "https://smashystream.xyz/",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/smashystream/smashystream.js",
|
||||
"type": "shows/movies/anime",
|
||||
"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) {
|
||||
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 {
|
||||
const response = await fetch(searchUrl);
|
||||
const json = await JSON.parse(response);
|
||||
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 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);
|
||||
} catch (error) {
|
||||
console.error("Error fetching search results:", error);
|
||||
return JSON.stringify([]);
|
||||
} catch (err) {
|
||||
return JSON.stringify([{
|
||||
title: "Error",
|
||||
image: "Error",
|
||||
href: "Error"
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
const details = [];
|
||||
details.push({
|
||||
description: 'N/A',
|
||||
alias: 'N/A',
|
||||
airdate: 'N/A'
|
||||
});
|
||||
|
||||
//#IAMLAZY
|
||||
|
||||
console.log(JSON.stringify(details));
|
||||
return JSON.stringify(details);
|
||||
console.log(url);
|
||||
try {
|
||||
const params = url.split('?')[1] || '';
|
||||
const pairs = params.split('&');
|
||||
let title = '';
|
||||
let artist = '';
|
||||
|
||||
for (const pair of pairs) {
|
||||
const [key, value] = pair.split('=');
|
||||
if (key === 'title') title = decodeURIComponent(value);
|
||||
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) {
|
||||
const response = await fetch(url);
|
||||
const html = await response;
|
||||
const tracks = [];
|
||||
|
||||
const trackRegex = /<article itemprop="track"[^>]*>[\s\S]*?<h2 itemprop="name"><a itemprop="url" href="([^"]+)">/g;
|
||||
|
||||
let match;
|
||||
let number = 1;
|
||||
|
||||
while ((match = trackRegex.exec(html)) !== null) {
|
||||
tracks.push({
|
||||
number: number++,
|
||||
href: 'https://soundcloud.com' + match[1].trim()
|
||||
const results = [];
|
||||
try {
|
||||
const cleanUrl = url.split('?')[0];
|
||||
results.push({
|
||||
href: cleanUrl,
|
||||
number: 1
|
||||
});
|
||||
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) {
|
||||
const clientId = "UjhhbCuNo1OQfTwkzajxQNLlJcSlUlVz";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const html = await response;
|
||||
|
||||
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);
|
||||
const response = await fetchv2(url+"?client_id=xwYTVSni6n4FghaI0c4uJ8T9c4pyJ3rh&track_authorization=");
|
||||
const data = await response.json();
|
||||
|
||||
return json.url;
|
||||
} else {
|
||||
console.log("No stream URL found");
|
||||
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
return null;
|
||||
return data.url || "https://error.org/";
|
||||
} catch (err) {
|
||||
return "https://error.org/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"language": "Music lmfao",
|
||||
"language": "Music",
|
||||
"streamType": "HLS",
|
||||
"quality": "Wallah fire quality",
|
||||
"quality": "128 kbps",
|
||||
"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",
|
||||
"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) {
|
||||
const results = [];
|
||||
try {
|
||||
@@ -8,7 +15,7 @@ async function searchResults(keyword) {
|
||||
let match;
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
results.push({
|
||||
title: match[2].trim(),
|
||||
title: cleanTitle(match[2].trim()),
|
||||
image: match[3] ? match[3].trim() : "",
|
||||
href: match[1].trim()
|
||||
});
|
||||
@@ -86,12 +93,35 @@ async function extractEpisodes(url) {
|
||||
async function extractStreamUrl(url) {
|
||||
|
||||
const response = await fetchv2(url);
|
||||
const html = await response.text();
|
||||
let streamData = null;
|
||||
const htmlOne = await response.text();
|
||||
|
||||
streamData = voeExtractor(html);
|
||||
console.log("Voe Stream Data: " + streamData);
|
||||
return streamData;
|
||||
const redirectMatch = htmlOne.match(/window\.location\.href\s*=\s*['"]([^'"]+)['"]/);
|
||||
if (!redirectMatch) {
|
||||
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 */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "Italian",
|
||||
"streamType": "HLS",
|
||||
"encrypted":true,
|
||||
@@ -15,6 +15,6 @@
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/toonitalia/toonitalia.js",
|
||||
"type": "shows/movies",
|
||||
"asyncJS": true,
|
||||
"softsub": false,
|
||||
"softsub": true,
|
||||
"downloadSupport": false
|
||||
}
|
||||
@@ -102,7 +102,7 @@ async function extractEpisodes(url) {
|
||||
const scriptResponse = await fetchv2(scriptUrl);
|
||||
const scriptContent = await scriptResponse.text();
|
||||
|
||||
const supervideRegex = /\\'(https:\/\/supervideo\.tv\/[^']+)\\'/;
|
||||
const supervideRegex = /\\'(https:\/\/supervideo\.[^\/']+\/[^']+)\\'/;
|
||||
const supervideMatch = scriptContent.match(supervideRegex);
|
||||
|
||||
if (supervideMatch) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "50/50",
|
||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"language": "German",
|
||||
"streamType": "HLS",
|
||||
"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) {
|
||||
if (ID.includes('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 title = encodeURIComponent(cinebyData.title);
|
||||
@@ -194,7 +194,7 @@ async function extractStreamUrl(ID) {
|
||||
const imdbId = cinebyData.external_ids?.imdb_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);
|
||||
|
||||
@@ -220,7 +220,9 @@ async function extractStreamUrl(ID) {
|
||||
const sources = result.sources || [];
|
||||
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(
|
||||
sub => sub.language.toLowerCase().includes('english')
|
||||
@@ -236,7 +238,7 @@ async function extractStreamUrl(ID) {
|
||||
const seasonNumber = parts[3];
|
||||
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 title = encodeURIComponent(cinebyData.name);
|
||||
@@ -244,7 +246,7 @@ async function extractStreamUrl(ID) {
|
||||
const imdbId = cinebyData.external_ids?.imdb_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);
|
||||
|
||||
@@ -270,7 +272,9 @@ async function extractStreamUrl(ID) {
|
||||
const sources = result.sources || [];
|
||||
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(
|
||||
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