1
0
forked from 50n50/sources

Merge ; commit '2fbfa76208b5a32145c3c9753d6069ccefca99ef'

This commit is contained in:
2025-11-08 15:57:29 +01:00
113 changed files with 26102 additions and 638 deletions
+11
View File
@@ -0,0 +1,11 @@
*.dev.json
*.tmp.json
*.temp.json
*.test.json
*.log
*.bak
*.old
*.temp
*.tmp
update_global_extractor.py
+14 -24
View File
@@ -98,15 +98,12 @@ async function extractEpisodes(movieUrl) {
error: "MovieID not found" error: "MovieID not found"
}]; }];
} }
const movieData = [{ name: "MovieID", data: movieIDMatch }];
const tokenResponse = await fetchv2( const movieIdApiUrl = `https://enc-dec.app/api/enc-movies-flix?text=${movieIDMatch}`;
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs", const movieIdTokenResponse = await fetchv2(movieIdApiUrl);
{}, const movieIdTokenData = await movieIdTokenResponse.json();
"POST", const token = movieIdTokenData.result;
JSON.stringify(movieData)
);
const temp = await tokenResponse.json();
const token = temp[0]?.data;
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`; const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
const episodeListResponse = await fetchv2(episodeListUrl); const episodeListResponse = await fetchv2(episodeListUrl);
const episodeListData = await episodeListResponse.json(); const episodeListData = await episodeListResponse.json();
@@ -115,23 +112,16 @@ async function extractEpisodes(movieUrl) {
const episodeRegex = /<a[^>]+eid="([^"]+)"[^>]+num="([^"]+)"[^>]*>/g; const episodeRegex = /<a[^>]+eid="([^"]+)"[^>]+num="([^"]+)"[^>]*>/g;
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)]; const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
const episodeData = episodeMatches.map(([_, episodeToken, episodeNum]) => ({ const episodeTokenPromises = episodeMatches.map(([_, episodeToken]) => {
name: `Episode ${episodeNum}`, const apiUrl = `https://enc-dec.app/api/enc-movies-flix?text=${episodeToken}`;
data: episodeToken return fetchv2(apiUrl).then(response => response.json());
})); });
console.log(JSON.stringify(episodeData)); const episodeTokenResults = await Promise.all(episodeTokenPromises);
const batchResponse = await fetchv2(
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs",
{},
"POST",
JSON.stringify(episodeData)
);
const batchResults = await batchResponse.json();
const episodes = batchResults.map((result, index) => ({ const episodes = episodeMatches.map(([_, episodeToken, episodeNum], index) => ({
number: parseInt(episodeMatches[index][2], 10), number: parseInt(episodeNum, 10),
href: `https://1movies.bz/ajax/links/list?eid=${episodeMatches[index][1]}&_=${result.data}` href: `https://1movies.bz/ajax/links/list?eid=${episodeToken}&_=${episodeTokenResults[index].result}`
})); }));
return JSON.stringify(episodes); return JSON.stringify(episodes);
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.6", "version": "1.0.7",
"language": "English", "language": "English",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+1 -1
View File
@@ -16,5 +16,5 @@
"streamAsyncJS": false, "streamAsyncJS": false,
"softsub": false, "softsub": false,
"type": "anime", "type": "anime",
"downloadSupport": true "downloadSupport": false
} }
+2 -2
View File
@@ -3,14 +3,14 @@ async function searchResults(keyword) {
const headers = { const headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
"referer": "https://anime-sama.fr/" "referer": "https://anime-sama.org/"
}; };
const regex = /<a[^>]+href="([^"]+)"[\s\S]*?<img[^>]+src="([^"]+)"[\s\S]*?<h3[^>]*>(.*?)<\/h3>/gi; const regex = /<a[^>]+href="([^"]+)"[\s\S]*?<img[^>]+src="([^"]+)"[\s\S]*?<h3[^>]*>(.*?)<\/h3>/gi;
try { try {
const response = await fetchv2( const response = await fetchv2(
"https://anime-sama.fr/template-php/defaut/fetch.php", "https://anime-sama.org/template-php/defaut/fetch.php",
headers, headers,
"POST", "POST",
`query=${encodeURIComponent(keyword)}` `query=${encodeURIComponent(keyword)}`
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.1", "version": "1.0.2",
"language": "French", "language": "French",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
-1
View File
@@ -80,7 +80,6 @@ async function extractEpisodes(url) {
} }
async function extractStreamUrl(id) { async function extractStreamUrl(id) {
if (!_0xCheck()) return 'https://files.catbox.moe/avolvc.mp4';
const cookieHeader = `key=${id}`; const cookieHeader = `key=${id}`;
const headers = { const headers = {
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.5", "version": "1.0.6",
"language": "English (SUB)", "language": "English (SUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",
+368
View File
@@ -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(/&quot;/g, '"')
.replace(/\\\//g, '/')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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 */
+19
View File
@@ -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
View File
@@ -80,50 +80,49 @@ async function extractDetails(url) {
} }
} }
async function extractEpisodes(animeUrl) { async function extractEpisodes(url) {
const sendEpisodes = async (endpoint, episodeData) => {
const promises = episodeData.map(item =>
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
.then(res => res.json())
.then(json => ({ name: item.name, data: json.result }))
.catch(err => ({ name: item.name, error: err.toString() }))
);
return Promise.all(promises);
};
try { try {
const response = await fetchv2(animeUrl); const actualUrl = url.replace("Animekai:", "").trim();
const htmlText = await response.text(); const htmlText = await (await fetchv2(actualUrl)).text();
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1]; if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
if (!animeIdMatch) {
return [{ const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
error: "AniID not found" const tokenData = await tokenResponse.json();
}]; const token = tokenData.result;
}
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`); const episodeListData = await (await fetchv2(episodeListUrl)).json();
const token = await tokenResponse.text(); const cleanedHtml = cleanJsonHtml(episodeListData.result);
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`; const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
const episodeListResponse = await fetchv2(episodeListUrl);
const episodeListData = await episodeListResponse.json(); const recentEpisodeMatches = episodeMatches.slice(-50);
const cleanedHtml = cleanJsonHtml(episodeListData.result);
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g; name: `Episode ${episodeNum}`,
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)]; data: episodeToken
}));
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
name: `Episode ${episodeNum}`, const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
data: episodeToken
})); const episodes = batchResults.map((result, index) => ({
number: parseInt(recentEpisodeMatches[index][1], 10),
const batchResponse = await fetchv2( href: `https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet", }));
{},
"POST", return JSON.stringify(episodes);
JSON.stringify(episodeData)
);
const batchResults = await batchResponse.json();
const episodes = batchResults.map((result, index) => ({
number: parseInt(episodeMatches[index][1], 10),
href: `https://animekai.to/ajax/links/list?token=${episodeMatches[index][2]}&_=${result.data}`
}));
return JSON.stringify(episodes);
} catch (err) { } catch (err) {
console.error("Error fetching episodes:" + err); console.error("Error fetching episodes:" + err);
return [{ return [{
@@ -159,13 +158,13 @@ async function extractStreamUrl(url) {
{ name: "Sub", data: serverIdSub } { name: "Sub", data: serverIdSub }
].filter(item => item.data); ].filter(item => item.data);
const tokenBatchResponse = await fetchv2( const tokenPromises = tokenRequestData.map(item =>
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet", fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
{}, .then(res => res.json())
"POST", .then(json => ({ name: item.name, data: json.result }))
JSON.stringify(tokenRequestData) .catch(err => ({ name: item.name, error: err.toString() }))
); );
const tokenResults = await tokenBatchResponse.json(); const tokenResults = await Promise.all(tokenPromises);
const streamUrls = tokenResults.map(result => { const streamUrls = tokenResults.map(result => {
const serverIdMap = { const serverIdMap = {
@@ -210,13 +209,13 @@ async function extractStreamUrl(url) {
return {}; return {};
} }
const decryptBatchResponse = await fetchv2( const decryptPromises = decryptRequestData.map(item =>
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits", fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
{}, .then(res => res.json())
"POST", .then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
JSON.stringify(decryptRequestData) .catch(err => ({ name: item.name, error: err.toString() }))
); );
const decryptResults = await decryptBatchResponse.json(); const decryptResults = await Promise.all(decryptPromises);
const finalResults = {}; const finalResults = {};
decryptResults.forEach(result => { decryptResults.forEach(result => {
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.3", "version": "1.0.4",
"language": "English", "language": "English",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+59 -60
View File
@@ -80,50 +80,49 @@ async function extractDetails(url) {
} }
} }
async function extractEpisodes(animeUrl) { async function extractEpisodes(url) {
const sendEpisodes = async (endpoint, episodeData) => {
const promises = episodeData.map(item =>
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
.then(res => res.json())
.then(json => ({ name: item.name, data: json.result }))
.catch(err => ({ name: item.name, error: err.toString() }))
);
return Promise.all(promises);
};
try { try {
const response = await fetchv2(animeUrl); const actualUrl = url.replace("Animekai:", "").trim();
const htmlText = await response.text(); const htmlText = await (await fetchv2(actualUrl)).text();
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1]; if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
if (!animeIdMatch) {
return [{ const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
error: "AniID not found" const tokenData = await tokenResponse.json();
}]; const token = tokenData.result;
}
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`); const episodeListData = await (await fetchv2(episodeListUrl)).json();
const token = await tokenResponse.text(); const cleanedHtml = cleanJsonHtml(episodeListData.result);
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`; const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
const episodeListResponse = await fetchv2(episodeListUrl);
const episodeListData = await episodeListResponse.json(); const recentEpisodeMatches = episodeMatches.slice(-50);
const cleanedHtml = cleanJsonHtml(episodeListData.result);
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g; name: `Episode ${episodeNum}`,
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)]; data: episodeToken
}));
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
name: `Episode ${episodeNum}`, const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
data: episodeToken
})); const episodes = batchResults.map((result, index) => ({
number: parseInt(recentEpisodeMatches[index][1], 10),
const batchResponse = await fetchv2( href: `https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet", }));
{},
"POST", return JSON.stringify(episodes);
JSON.stringify(episodeData)
);
const batchResults = await batchResponse.json();
const episodes = batchResults.map((result, index) => ({
number: parseInt(episodeMatches[index][1], 10),
href: `https://animekai.to/ajax/links/list?token=${episodeMatches[index][2]}&_=${result.data}`
}));
return JSON.stringify(episodes);
} catch (err) { } catch (err) {
console.error("Error fetching episodes:" + err); console.error("Error fetching episodes:" + err);
return [{ return [{
@@ -159,13 +158,13 @@ async function extractStreamUrl(url) {
{ name: "Sub", data: serverIdSub } { name: "Sub", data: serverIdSub }
].filter(item => item.data); ].filter(item => item.data);
const tokenBatchResponse = await fetchv2( const tokenPromises = tokenRequestData.map(item =>
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet", fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
{}, .then(res => res.json())
"POST", .then(json => ({ name: item.name, data: json.result }))
JSON.stringify(tokenRequestData) .catch(err => ({ name: item.name, error: err.toString() }))
); );
const tokenResults = await tokenBatchResponse.json(); const tokenResults = await Promise.all(tokenPromises);
const streamUrls = tokenResults.map(result => { const streamUrls = tokenResults.map(result => {
const serverIdMap = { const serverIdMap = {
@@ -210,13 +209,13 @@ async function extractStreamUrl(url) {
return {}; return {};
} }
const decryptBatchResponse = await fetchv2( const decryptPromises = decryptRequestData.map(item =>
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits", fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
{}, .then(res => res.json())
"POST", .then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
JSON.stringify(decryptRequestData) .catch(err => ({ name: item.name, error: err.toString() }))
); );
const decryptResults = await decryptBatchResponse.json(); const decryptResults = await Promise.all(decryptPromises);
const finalResults = {}; const finalResults = {};
decryptResults.forEach(result => { decryptResults.forEach(result => {
@@ -234,16 +233,16 @@ async function extractStreamUrl(url) {
}; };
const decryptedUrls = await processStreams(streamUrls); const decryptedUrls = await processStreams(streamUrls);
const decryptedSub = decryptedUrls.Sub || decryptedUrls.Softsub || decryptedUrls.Dub; const decryptedDub = decryptedUrls.Sub || decryptedUrls.Dub || decryptedUrls.Softsub;
console.log(decryptedSub); console.log(decryptedDub);
const headers = { const headers = {
"Referer": "https://animekai.to/", "Referer": "https://animekai.to/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36" "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}; };
if (decryptedSub) { if (decryptedDub) {
const response = await fetchv2(decryptedSub.replace("/e/", "/media/"), headers); const response = await fetchv2(decryptedDub.replace("/e/", "/media/"), headers);
const responseJson = await response.json(); const responseJson = await response.json();
const result = responseJson?.result; const result = responseJson?.result;
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.2", "version": "1.0.3",
"language": "English", "language": "English",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+217
View File
@@ -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 = {
'&quot;': '"',
'&amp;': '&',
'&apos;': "'",
'&lt;': '<',
'&gt;': '>'
};
for (const entity in entities) {
text = text.replace(new RegExp(entity, 'g'), entities[entity]);
}
return text;
}
+19
View File
@@ -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
}
+236
View File
@@ -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;
}
+19
View File
@@ -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
View File
@@ -1,3 +1,10 @@
function cleanTitle(title) {
return title
.replace(/&#8217;/g, "'")
.replace(/&#8211;/g, "-")
.replace(/&#[0-9]+;/g, "");
}
async function searchResults(keyword) { async function searchResults(keyword) {
const results = []; const results = [];
try { try {
@@ -18,7 +25,7 @@ async function searchResults(keyword) {
results.push({ results.push({
href: "https://animenana.com" + hrefMatch[1].trim(), href: "https://animenana.com" + hrefMatch[1].trim(),
image: "https://animenana.com" + imgMatch[1].trim(), image: "https://animenana.com" + imgMatch[1].trim(),
title: titleMatch[1].trim() title: cleanTitle(titleMatch[1].trim())
}); });
} }
} }
@@ -37,7 +44,7 @@ async function searchResults(keyword) {
results.push({ results.push({
href: "https://animenana.com" + hrefMatch[1].trim(), href: "https://animenana.com" + hrefMatch[1].trim(),
image: "https://animenana.com" + imgMatch[1].trim(), image: "https://animenana.com" + imgMatch[1].trim(),
title: titleMatch[1].trim() title: cleanTitle(titleMatch[1].trim())
}); });
} }
} }
@@ -51,7 +58,7 @@ async function searchResults(keyword) {
results.push({ results.push({
href: "https://animenana.com" + match[1].trim(), href: "https://animenana.com" + match[1].trim(),
image: "https://animenana.com" + match[2].trim(), image: "https://animenana.com" + match[2].trim(),
title: match[3].trim() title: cleanTitle(match[3].trim())
}); });
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "English (Hardsub)", "language": "English (Hardsub)",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+16 -16
View File
@@ -1,17 +1,17 @@
{ {
"sourceName": "AnimeToast", "sourceName": "AnimeToast",
"iconUrl": "https://www.animetoast.cc/wp-content/uploads/2018/03/toastfavi-300x300.png", "iconUrl": "https://www.animetoast.cc/wp-content/uploads/2018/03/toastfavi-300x300.png",
"author": { "author": {
"name": "50/50 & Cufiy", "name": "50/50 & Cufiy",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.2.12", "version": "1.2.13",
"language": "German (DUB/SUB)", "language": "German (DUB/SUB)",
"streamType": "MP4", "streamType": "MP4",
"quality": "1080p", "quality": "1080p",
"baseUrl": "https://www.animetoast.cc/", "baseUrl": "https://www.animetoast.cc/",
"searchBaseUrl": "https://www.animetoast.cc/?s=the%s", "searchBaseUrl": "https://www.animetoast.cc/?s=the%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animetoast/animetoast_v2.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animetoast/animetoast_v2.js",
"asyncJS": true, "asyncJS": true,
"type": "anime" "type": "anime"
} }
+665 -6
View File
@@ -313,7 +313,7 @@ async function sendLog(message) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -321,8 +321,8 @@ async function sendLog(message) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -335,7 +335,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -398,8 +408,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -454,7 +470,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -513,6 +536,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -520,6 +550,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -527,6 +571,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -610,6 +710,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -664,6 +786,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -681,6 +1151,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -859,7 +1508,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -895,6 +1548,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+1 -1
View File
@@ -16,5 +16,5 @@
"streamAsyncJS": false, "streamAsyncJS": false,
"softsub": false, "softsub": false,
"type": "anime", "type": "anime",
"downloadSupport": true "downloadSupport": false
} }
+130
View File
@@ -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(/&nbsp;/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": ""});
}
}
+19
View File
@@ -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
}
+17 -17
View File
@@ -1,18 +1,18 @@
{ {
"sourceName": "AniWorld (ENG SUB)", "sourceName": "AniWorld (ENG SUB)",
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png", "iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
"author": { "author": {
"name": "Hamzo & Cufiy", "name": "Hamzo & Cufiy",
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024" "icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
}, },
"version": "0.2.7", "version": "0.2.8",
"language": "English (SUB)", "language": "English (SUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",
"baseUrl": "https://google.com", "baseUrl": "https://google.com",
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s", "searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldEngSub_v2.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldEngSub_v2.js",
"asyncJS": true, "asyncJS": true,
"streamAsyncJS": false, "streamAsyncJS": false,
"type": "anime" "type": "anime"
} }
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"sourceName": "AniWorld (fixed)", "sourceName": "AniWorld (Local Test)",
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png", "iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
"author": { "author": {
"name": "Cufiy", "name": "Cufiy",
@@ -11,7 +11,7 @@
"quality": "720p", "quality": "720p",
"baseUrl": "https://vidmoly.to/", "baseUrl": "https://vidmoly.to/",
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s", "searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
"scriptUrl": "http://192.168.2.130/sora-sources2/aniworld/v2/AniWorldGerDub_v2.js", "scriptUrl": "http://192.168.2.130/sora-module-repos/sources/aniworld/v2/AniWorldGerDub_v2.js",
"asyncJS": true, "asyncJS": true,
"type": "anime" "type": "anime"
} }
+17 -17
View File
@@ -1,18 +1,18 @@
{ {
"sourceName": "AniWorld (GER DUB)", "sourceName": "AniWorld (GER DUB)",
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png", "iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
"author": { "author": {
"name": "Hamzo & Cufiy", "name": "Hamzo & Cufiy",
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024" "icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
}, },
"version": "0.2.7", "version": "0.2.8",
"language": "German (DUB)", "language": "German (DUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",
"baseUrl": "https://google.com", "baseUrl": "https://google.com",
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s", "searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerDub_v2.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerDub_v2.js",
"asyncJS": true, "asyncJS": true,
"streamAsyncJS": false, "streamAsyncJS": false,
"type": "anime" "type": "anime"
} }
+17 -17
View File
@@ -1,18 +1,18 @@
{ {
"sourceName": "AniWorld (GER SUB)", "sourceName": "AniWorld (GER SUB)",
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png", "iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/aniworld.png",
"author": { "author": {
"name": "Hamzo & Cufiy", "name": "Hamzo & Cufiy",
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024" "icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
}, },
"version": "0.2.7", "version": "0.2.8",
"language": "German (SUB)", "language": "German (SUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",
"baseUrl": "https://google.com", "baseUrl": "https://google.com",
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s", "searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerSub_v2.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/aniworld/v2/AniWorldGerSub_v2.js",
"asyncJS": true, "asyncJS": true,
"streamAsyncJS": false, "streamAsyncJS": false,
"type": "anime" "type": "anime"
} }
+2
View File
@@ -589,6 +589,8 @@ async function sendLog(message) {
// send http://192.168.2.130/sora-module/log.php?action=add&message=message // send http://192.168.2.130/sora-module/log.php?action=add&message=message
console.log(message); console.log(message);
return;
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message)) await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
.catch(error => { .catch(error => {
console.error('Error sending log:', error); console.error('Error sending log:', error);
+665 -6
View File
@@ -350,7 +350,7 @@ async function sendLog(message) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -358,8 +358,8 @@ async function sendLog(message) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -372,7 +372,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -435,8 +445,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -491,7 +507,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -550,6 +573,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -557,6 +587,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -564,6 +608,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -647,6 +747,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -701,6 +823,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -718,6 +1188,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -896,7 +1545,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -932,6 +1585,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+666 -7
View File
@@ -338,7 +338,7 @@ function base64Decode(str) {
async function sendLog(message) { async function sendLog(message) {
// send http://192.168.2.130/sora-module/log.php?action=add&message=message // send http://192.168.2.130/sora-module/log.php?action=add&message=message
console.log(message); console.log(message);
// return; return;
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message)) await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
.catch(error => { .catch(error => {
@@ -351,7 +351,7 @@ async function sendLog(message) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -359,8 +359,8 @@ async function sendLog(message) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -373,7 +373,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -436,8 +446,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -492,7 +508,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -551,6 +574,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -558,6 +588,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -565,6 +609,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -648,6 +748,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -702,6 +824,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -719,6 +1189,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -897,7 +1546,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -933,6 +1586,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+665 -6
View File
@@ -350,7 +350,7 @@ async function sendLog(message) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -358,8 +358,8 @@ async function sendLog(message) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -372,7 +372,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -435,8 +445,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -491,7 +507,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -550,6 +573,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -557,6 +587,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -564,6 +608,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -647,6 +747,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -701,6 +823,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -718,6 +1188,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -896,7 +1545,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -932,6 +1585,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+175
View File
@@ -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/";
}
}
+19
View File
@@ -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
View File
@@ -260,8 +260,8 @@ async function searchResults(query) {
})); }));
const filteredResults = scoredResults const filteredResults = scoredResults
.filter(r => r.score > 50) // Increased threshold to filter out weak matches .filter(r => r.score > 50)
.sort((a, b) => b.score - a.score) // Sort by pre-calculated scores .sort((a, b) => b.score - a.score)
.map(({ score, ...rest }) => rest); .map(({ score, ...rest }) => rest);
return JSON.stringify(filteredResults.length > 0 ? filteredResults : [{ return JSON.stringify(filteredResults.length > 0 ? filteredResults : [{
@@ -330,18 +330,13 @@ async function extractDetails(url) {
async function extractEpisodes(url) { async function extractEpisodes(url) {
const sendEpisodes = async (endpoint, episodeData) => { const sendEpisodes = async (endpoint, episodeData) => {
if (episodeData.length > 45) { const promises = episodeData.map(item =>
const promises = episodeData.map(item => fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`)
fetchv2(`${endpoint}=${encodeURIComponent(item.data)}`) .then(res => res.json())
.then(res => res.text()) .then(json => ({ name: item.name, data: json.result }))
.then(data => ({ name: item.name, data })) .catch(err => ({ name: item.name, error: err.toString() }))
.catch(err => ({ name: item.name, error: err.toString() })) );
); return Promise.all(promises);
return Promise.all(promises);
} else {
const resp = await fetchv2(endpoint, {}, "POST", JSON.stringify(episodeData));
return resp.json();
}
}; };
try { try {
@@ -351,8 +346,9 @@ async function extractEpisodes(url) {
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1]; const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]); if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
const tokenResponse = await fetchv2(`https://ilovekai.simplepostrequest.workers.dev/?ilovefeet=${encodeURIComponent(animeIdMatch)}`); const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
const token = await tokenResponse.text(); const tokenData = await tokenResponse.json();
const token = tokenData.result;
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`; const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
const episodeListData = await (await fetchv2(episodeListUrl)).json(); const episodeListData = await (await fetchv2(episodeListUrl)).json();
@@ -361,16 +357,18 @@ async function extractEpisodes(url) {
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g; const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)]; const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
const episodeData = episodeMatches.map(([_, episodeNum, episodeToken]) => ({ const recentEpisodeMatches = episodeMatches.slice(-50);
const episodeData = recentEpisodeMatches.map(([_, episodeNum, episodeToken]) => ({
name: `Episode ${episodeNum}`, name: `Episode ${episodeNum}`,
data: episodeToken data: episodeToken
})); }));
const batchResults = await sendEpisodes("https://ilovekai.simplepostrequest.workers.dev/?ilovefeet", episodeData); const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-kai?text", episodeData);
const episodes = batchResults.map((result, index) => ({ const episodes = batchResults.map((result, index) => ({
number: parseInt(episodeMatches[index][1], 10), number: parseInt(recentEpisodeMatches[index][1], 10),
href: `Animekai:https://animekai.to/ajax/links/list?token=${episodeMatches[index][2]}&_=${result.data}` href: `Animekai:https://animekai.to/ajax/links/list?token=${recentEpisodeMatches[index][2]}&_=${result.data}`
})); }));
return JSON.stringify(episodes); return JSON.stringify(episodes);
@@ -379,10 +377,9 @@ async function extractEpisodes(url) {
const movieIDMatch = (htmlText.match(/<div class="detail-lower"[^>]*id="movie-rating"[^>]*data-id="([^"]+)"/) || [])[1]; const movieIDMatch = (htmlText.match(/<div class="detail-lower"[^>]*id="movie-rating"[^>]*data-id="([^"]+)"/) || [])[1];
if (!movieIDMatch) return JSON.stringify([{ error: "MovieID not found" }]); if (!movieIDMatch) return JSON.stringify([{ error: "MovieID not found" }]);
const movieData = [{ name: "MovieID", data: movieIDMatch }]; const tokenResponse = await fetchv2("https://enc-dec.app/api/enc-movies-flix?text=" + encodeURIComponent(movieIDMatch));
const tokenResponse = await fetchv2("https://ilovekai.simplepostrequest.workers.dev/ilovethighs", {}, "POST", JSON.stringify(movieData));
const temp = await tokenResponse.json(); const temp = await tokenResponse.json();
const token = temp[0]?.data; const token = temp.result;
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`; const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
const episodeListData = await (await fetchv2(episodeListUrl)).json(); const episodeListData = await (await fetchv2(episodeListUrl)).json();
@@ -396,7 +393,7 @@ async function extractEpisodes(url) {
data: episodeToken data: episodeToken
})); }));
const batchResults = await sendEpisodes("https://ilovekai.simplepostrequest.workers.dev/?ilovethighs", episodeData); const batchResults = await sendEpisodes("https://enc-dec.app/api/enc-movies-flix?text", episodeData);
const episodes = batchResults.map((result, index) => ({ const episodes = batchResults.map((result, index) => ({
number: parseInt(episodeMatches[index][2], 10), number: parseInt(episodeMatches[index][2], 10),
@@ -450,13 +447,13 @@ async function extractStreamUrl(url) {
{ name: "Sub", data: serverIdSub } { name: "Sub", data: serverIdSub }
].filter(item => item.data); ].filter(item => item.data);
const tokenBatchResponse = await fetchv2( const tokenPromises = tokenRequestData.map(item =>
"https://ilovekai.simplepostrequest.workers.dev/?ilovefeet", fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
{}, .then(res => res.json())
"POST", .then(json => ({ name: item.name, data: json.result }))
JSON.stringify(tokenRequestData) .catch(err => ({ name: item.name, error: err.toString() }))
); );
const tokenResults = await tokenBatchResponse.json(); const tokenResults = await Promise.all(tokenPromises);
const streamUrls = tokenResults.map(result => { const streamUrls = tokenResults.map(result => {
const serverIdMap = { const serverIdMap = {
@@ -501,13 +498,13 @@ async function extractStreamUrl(url) {
return {}; return {};
} }
const decryptBatchResponse = await fetchv2( const decryptPromises = decryptRequestData.map(item =>
"https://ilovekai.simplepostrequest.workers.dev/?ilovearmpits", fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
{}, .then(res => res.json())
"POST", .then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
JSON.stringify(decryptRequestData) .catch(err => ({ name: item.name, error: err.toString() }))
); );
const decryptResults = await decryptBatchResponse.json(); const decryptResults = await Promise.all(decryptPromises);
const finalResults = {}; const finalResults = {};
decryptResults.forEach(result => { decryptResults.forEach(result => {
@@ -569,7 +566,7 @@ async function extractStreamUrl(url) {
if (dubStream) streams.push("Dubbed English", dubStream); if (dubStream) streams.push("Dubbed English", dubStream);
const rawStream = decryptedRaw ? await getStream(decryptedRaw) : null; const rawStream = decryptedRaw ? await getStream(decryptedRaw) : null;
if (rawStream) streams.push("Japanese", rawStream); if (rawStream) streams.push("Original audio", rawStream);
const final = { const final = {
streams, streams,
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.6", "version": "1.0.8",
"language": "English", "language": "English",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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
View File
@@ -107,8 +107,14 @@ async function extractStreamUrl(url) {
const iframeMatch = html.match(/<iframe[^>]+src="([^"]+)"/i); const iframeMatch = html.match(/<iframe[^>]+src="([^"]+)"/i);
const iframeUrl = iframeMatch ? iframeMatch[1] : null; const iframeUrl = iframeMatch ? iframeMatch[1] : null;
if (!iframeUrl) throw new Error("Iframe not found"); if (!iframeUrl) throw new Error("Iframe not found");
const headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Referer": url,
"origin": "https://coflix.cc"
};
const iframeResp = await fetchv2(iframeUrl); const iframeResp = await fetchv2(iframeUrl, headers);
const iframeHtml = await iframeResp.text(); const iframeHtml = await iframeResp.text();
const uqloadMatch = iframeHtml.match(/<li[^>]*onclick="showVideo\('([^']+)',\s*'[^']+'\)">\s*<img[^>]*src="static\/server\/uqload[^"]*"[^>]*>\s*<span>Uqload/i); const uqloadMatch = iframeHtml.match(/<li[^>]*onclick="showVideo\('([^']+)',\s*'[^']+'\)">\s*<img[^>]*src="static\/server\/uqload[^"]*"[^>]*>\s*<span>Uqload/i);
@@ -130,7 +136,12 @@ async function extractStreamUrl(url) {
if (fileMonMatch) { if (fileMonMatch) {
const filemoon = atob(fileMonMatch[1]); const filemoon = atob(fileMonMatch[1]);
console.log("FileMon URL:" + filemoon); console.log("FileMon URL:" + filemoon);
const fileResp = await fetchv2(filemoon); const headers2 = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Referer": "https://lecteurvideo.com/"
};
const fileResp = await fetchv2(filemoon, headers2);
const fileHtml = await fileResp.text(); const fileHtml = await fileResp.text();
return filemoonExtractor(fileHtml, filemoon); return filemoonExtractor(fileHtml, filemoon);
} }
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "French", "language": "French",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+250
View File
@@ -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"
});
}
}
+20
View File
@@ -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
}
+152
View File
@@ -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(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&amp;/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: ""
});
}
+19
View File
@@ -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
}
+107
View File
@@ -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/";
}
}
+19
View File
@@ -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
View File
@@ -147,7 +147,7 @@ function cleanHtmlSymbols(string) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.4} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -155,12 +155,13 @@ function cleanHtmlSymbols(string) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-08-13 03:44:07 * @date 2025-11-05 15:44:57
* @version 1.1.4 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
function globalExtractor(providers) { function globalExtractor(providers) {
for (const [url, provider] of Object.entries(providers)) { for (const [url, provider] of Object.entries(providers)) {
try { try {
@@ -307,6 +308,8 @@ async function extractStreamUrlByProvider(url, provider) {
headers["encoding"] = "windows-1251"; // required headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') { } else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
@@ -367,6 +370,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -374,6 +384,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud": case "megacloud":
try { try {
return await megacloudExtractor(html, url); return await megacloudExtractor(html, url);
@@ -388,6 +405,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet": case "sibnet":
try { try {
return await sibnetExtractor(html, url); return await sibnetExtractor(html, url);
@@ -395,6 +419,34 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from sibnet:", error); console.log("Error extracting stream URL from sibnet:", error);
return null; return null;
} }
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload": case "uqload":
try { try {
return await uqloadExtractor(html, url); return await uqloadExtractor(html, url);
@@ -402,6 +454,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from uqload:", error); console.log("Error extracting stream URL from uqload:", error);
return null; return null;
} }
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -430,17 +489,11 @@ async function extractStreamUrlByProvider(url, provider) {
} }
//////////////////////////////////////////////// ////////////////////////////////////////////////
// EXTRACTORS // // EXTRACTORS //
//////////////////////////////////////////////// ////////////////////////////////////////////////
// DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING // // DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING //
/* --- bigwarp --- */ /* --- bigwarp --- */
/** /**
@@ -458,8 +511,6 @@ async function bigwarpExtractor(videoPage, url = null) {
console.log("BigWarp HD Decoded:", bwDecoded); console.log("BigWarp HD Decoded:", bwDecoded);
return bwDecoded; return bwDecoded;
} }
/* --- doodstream --- */ /* --- doodstream --- */
/** /**
@@ -493,7 +544,27 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
@@ -549,8 +620,18 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */ /* --- megacloud --- */
/** /**
@@ -560,16 +641,28 @@ async function filemoonExtractor(html, url = null) {
// Megacloud V3 specific // Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) { async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32)); const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop(); const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams; const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl); const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText); // return decrypt(secretKey, nonce, encryptedText);
try { try {
const response = await fetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`); const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json(); const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources; const encrypted = rawSourceData?.sources;
let decryptedSources = null; let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) { if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources; decryptedSources = rawSourceData.sources;
} }
@@ -577,14 +670,14 @@ async function megacloudExtractor(html, embedUrl) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce); decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source"); if (!decryptedSources) throw new Error("Failed to decrypt source");
} }
console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2)); // console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array // return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) { if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try { try {
return decryptedSources[0].file; return decryptedSources[0].file;
} catch (error) { } catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error); console.log("Error extracting MegaCloud stream URL:" + error);
return null; return false;
} }
} }
// return { // return {
@@ -774,7 +867,7 @@ async function megacloudExtractor(html, embedUrl) {
* @returns {string|null} The extracted nonce, or null if it couldn't be found * @returns {string|null} The extracted nonce, or null if it couldn't be found
*/ */
async function getNonce(embedUrl) { async function getNonce(embedUrl) {
const res = await fetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } }); const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text(); const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/); const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) { if (match0?.[1]) {
@@ -857,10 +950,10 @@ async function megacloudExtractor(html, embedUrl) {
} }
return keys; return keys;
} }
function fetchKey(name, url, timeout = 1000) { function fetchKey(name, url) {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
try { try {
const response = await fetch(url, { method: 'get', timeout: timeout }); const response = await soraFetch(url, { method: 'get' });
const key = await response.text(); const key = await response.text();
let trueKey = null; let trueKey = null;
try { try {
@@ -875,8 +968,6 @@ async function megacloudExtractor(html, embedUrl) {
}); });
} }
} }
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -894,8 +985,17 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */ /* --- sibnet --- */
/** /**
@@ -920,8 +1020,119 @@ async function sibnetExtractor(html, embedUrl) {
return null; return null;
} }
} }
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */ /* --- uqload --- */
/** /**
@@ -938,7 +1149,20 @@ async function uqloadExtractor(html, embedUrl) {
return null; return null;
} }
} }
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
@@ -978,8 +1202,6 @@ async function vidmolyExtractor(html, url = null) {
return sourcesString; return sourcesString;
} }
} }
/* --- vidoza --- */ /* --- vidoza --- */
/** /**
@@ -996,8 +1218,6 @@ async function vidozaExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- voe --- */ /* --- voe --- */
/** /**
@@ -1093,10 +1313,6 @@ function voeShiftChars(str, shift) {
.join(""); .join("");
} }
//////////////////////////////////////////////// ////////////////////////////////////////////////
// PLUGINS // // PLUGINS //
//////////////////////////////////////////////// ////////////////////////////////////////////////
@@ -1126,7 +1342,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -1163,6 +1383,10 @@ class Unbaser {
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
+18 -18
View File
@@ -1,19 +1,19 @@
{ {
"sourceName": "DoraBash", "sourceName": "DoraBash",
"iconUrl": "https://dorabash.com/wp-content/uploads/2023/06/cropped-Untitled_design-removebg-192x192.png", "iconUrl": "https://dorabash.com/wp-content/uploads/2023/06/cropped-Untitled_design-removebg-192x192.png",
"author": { "author": {
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.1", "version": "1.0.2",
"language": "Hindi", "language": "Hindi",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
"baseUrl": "https://dorabash.com/", "baseUrl": "https://dorabash.com/",
"searchBaseUrl": "https://dorabash.com/", "searchBaseUrl": "https://dorabash.com/",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/dorabash/dorabash.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/dorabash/dorabash.js",
"type": "anime", "type": "anime",
"asyncJS": true, "asyncJS": true,
"softsub": false, "softsub": false,
"downloadSupport": false "downloadSupport": false
} }
@@ -72,6 +72,7 @@ async function extractEpisodes(url) {
}]); }]);
} }
} }
async function extractStreamUrl(url) { async function extractStreamUrl(url) {
try { try {
const response = await fetchv2(url); const response = await fetchv2(url);
@@ -85,7 +86,7 @@ async function extractStreamUrl(url) {
const serversJson = jsonMatch[1].replace(/\\/g, ''); const serversJson = jsonMatch[1].replace(/\\/g, '');
const servers = JSON.parse(serversJson); const servers = JSON.parse(serversJson);
const fdewsdcServer = servers.find(server => server.name === "fdewsdc"); const fdewsdcServer = servers.find(server => server.name === "EarnVids");
if (fdewsdcServer) { if (fdewsdcServer) {
console.log("Stream URL: " + fdewsdcServer.url); console.log("Stream URL: " + fdewsdcServer.url);
return await extractEarnVids(fdewsdcServer.url); return await extractEarnVids(fdewsdcServer.url);
@@ -97,8 +98,12 @@ async function extractStreamUrl(url) {
} }
async function extractEarnVids(url) { async function extractEarnVids(url) {
const headers = {
"Referer": "https://shahed4u.day/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"
};
try { try {
const response = await fetchv2(url); const response = await fetchv2(url, headers);
const html = await response.text(); const html = await response.text();
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/); const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "Arabic", "language": "Arabic",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+41 -60
View File
@@ -80,72 +80,21 @@ async function extractEpisodes(url) {
async function extractStreamUrl(url) { async function extractStreamUrl(url) {
try { try {
const header = { const header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/",
"Referer": url
}; };
const postData = "View=1"; const postData = "View=1";
const response = await fetchv2(url, header, "POST", postData); const response = await fetchv2(url, header, "POST", postData);
const html = await response.text(); const html = await response.text();
const match = html.match(/<li data-link="(https:\/\/mivalyo\.com\/v\/[^"]+)"/);
const streamUrl = match ? match[1] : "https://files.catbox.moe/avolvc.mp4";
console.log("Stream URL:", streamUrl);
const responseTwo = await fetchv2(streamUrl);
const htmlTwo = await responseTwo.text();
const obfuscatedScript = htmlTwo.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
if (!obfuscatedScript) {
console.log("No obfuscated script found, using fallback");
return "https://files.catbox.moe/avolvc.mp4";
}
const unpackedScript = unpack(obfuscatedScript[1]);
console.log("Unpacked script:", unpackedScript.substring(0, 200) + "...");
let hlsLink = null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
if (streamMatch) {
hlsLink = streamMatch[1];
console.log("Found stream path:", hlsLink);
return baseUrl + hlsLink;
}
const hlsObjectMatch = unpackedScript.match(/["']hls\d?["']\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/); const earnvidsMatch = html.match(/<li[^>]*data-link=["']([^"']+)["'][^>]*>\s*<span>\s*<p>\s*EarnVids\s*<\/p>/i);
if (hlsObjectMatch) { const embedResponse = await fetchv2(earnvidsMatch[1], header);
hlsLink = hlsObjectMatch[1]; const embedHtml = await embedResponse.text();
console.log("Found HLS object link:", hlsLink); if (earnvidsMatch) {
return hlsLink; const stream = await earnvidsExtractor(embedHtml, url);
return stream;
} }
return null;
const m3u8Match = unpackedScript.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/);
if (m3u8Match) {
hlsLink = m3u8Match[1];
console.log("Found m3u8 link:", hlsLink);
return hlsLink;
}
const cdnMatch = unpackedScript.match(/["'](https?:\/\/[^"']*(?:cdn|stream)[^"']*\.m3u8[^"']*)["']/i);
if (cdnMatch) {
hlsLink = cdnMatch[1];
console.log("Found CDN link:", hlsLink);
return hlsLink;
}
const linksMatch = unpackedScript.match(/links\s*[=:]\s*({[^}]+}|\[[^\]]+\])/);
if (linksMatch) {
const linksStr = linksMatch[1];
const urlInLinks = linksStr.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/);
if (urlInLinks) {
hlsLink = urlInLinks[1];
console.log("Found link in links object:", hlsLink);
return hlsLink;
}
}
console.log("No HLS link found in unpacked script, using fallback");
return "https://files.catbox.moe/avolvc.mp4";
} catch (err) { } catch (err) {
console.error("Error in extractStreamUrl:", err); console.error("Error in extractStreamUrl:", err);
return "https://files.catbox.moe/avolvc.mp4"; return "https://files.catbox.moe/avolvc.mp4";
@@ -153,6 +102,34 @@ async function extractStreamUrl(url) {
} }
/* SCHEME START */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* REMOVE_START */
/*********************************************************** /***********************************************************
* UNPACKER MODULE * UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library * Credit to GitHub user "mnsrulz" for Unpacker Node library
@@ -271,3 +248,7 @@ function unpack(source) {
return source; return source;
} }
} }
/* REMOVE_END */
/* SCHEME END */
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "Arabic", "language": "Arabic",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+16 -16
View File
@@ -1,17 +1,17 @@
{ {
"sourceName": "FireAnime SUB", "sourceName": "FireAnime SUB",
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png", "iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
"author": { "author": {
"name": "50/50 & Cufiy", "name": "50/50 & Cufiy",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.1.2", "version": "1.1.3",
"language": "German (SUB)", "language": "German (SUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
"baseUrl": "https://fireani.me/", "baseUrl": "https://fireani.me/",
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s", "searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerSub.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerSub.js",
"asyncJS": true, "asyncJS": true,
"type": "anime" "type": "anime"
} }
+16 -16
View File
@@ -1,17 +1,17 @@
{ {
"sourceName": "FireAnime DUB", "sourceName": "FireAnime DUB",
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png", "iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
"author": { "author": {
"name": "50/50 & Cufiy", "name": "50/50 & Cufiy",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.1.2", "version": "1.1.3",
"language": "German (DUB)", "language": "German (DUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
"baseUrl": "https://fireani.me/", "baseUrl": "https://fireani.me/",
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s", "searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerDub.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeGerDub.js",
"asyncJS": true, "asyncJS": true,
"type": "anime" "type": "anime"
} }
+16 -16
View File
@@ -1,17 +1,17 @@
{ {
"sourceName": "FireAnime English (SUB)", "sourceName": "FireAnime English (SUB)",
"iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png", "iconUrl": "https://i.ibb.co/dJ1SN5ch/favicon.png",
"author": { "author": {
"name": "50/50 & Cufiy", "name": "50/50 & Cufiy",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.1.2", "version": "1.1.3",
"language": "English (SUB)", "language": "English (SUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
"baseUrl": "https://fireani.me/", "baseUrl": "https://fireani.me/",
"searchBaseUrl": "https://fireani.me/api/anime/search?q=%s", "searchBaseUrl": "https://fireani.me/api/anime/search?q=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeEngSub.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/fireanime/v2/FireAnimeEngSub.js",
"asyncJS": true, "asyncJS": true,
"type": "anime" "type": "anime"
} }
+665 -6
View File
@@ -190,7 +190,7 @@ async function sendLog(message) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -198,8 +198,8 @@ async function sendLog(message) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -212,7 +212,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -275,8 +285,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -331,7 +347,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -390,6 +413,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -397,6 +427,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -404,6 +448,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -487,6 +587,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -541,6 +663,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -558,6 +1028,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -736,7 +1385,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -772,6 +1425,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+665 -6
View File
@@ -191,7 +191,7 @@ async function sendLog(message) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -199,8 +199,8 @@ async function sendLog(message) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -213,7 +213,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -276,8 +286,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -332,7 +348,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -391,6 +414,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -398,6 +428,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -405,6 +449,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -488,6 +588,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -542,6 +664,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -559,6 +1029,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -737,7 +1386,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -773,6 +1426,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+665 -6
View File
@@ -190,7 +190,7 @@ async function sendLog(message) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -198,8 +198,8 @@ async function sendLog(message) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -212,7 +212,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -275,8 +285,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -331,7 +347,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -390,6 +413,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -397,6 +427,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -404,6 +448,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -487,6 +587,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -541,6 +663,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -558,6 +1028,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -736,7 +1385,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -772,6 +1425,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+235
View File
@@ -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;
}
}
+19
View File
@@ -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
}
+94
View File
@@ -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/";
}
}
+20
View File
@@ -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
View File
@@ -1,8 +1,8 @@
async function searchResults(keyword) { async function searchResults(keyword) {
const searchUrl = `https://kimcartoon.com.co/?s=${encodeURIComponent(keyword)}`; const searchUrl = `https://kimcartoon.com.co/?s=${encodeURIComponent(keyword)}`;
try { try {
const response = await fetch(searchUrl); const response = await fetchv2(searchUrl);
const html = await response; const html = await response;text();
const results = []; const results = [];
const articleRegex = /<article[^>]*class="bs styletwo"[\s\S]*?<\/article>/g; const articleRegex = /<article[^>]*class="bs styletwo"[\s\S]*?<\/article>/g;
const items = html.match(articleRegex) || []; const items = html.match(articleRegex) || [];
@@ -32,8 +32,8 @@ async function searchResults(keyword) {
} }
async function extractDetails(url) { async function extractDetails(url) {
const response = await fetch(url); const response = await fetchv2(url);
const html = await response; const html = await response.text();
const details = []; const details = [];
const descriptionMatch = html.match(/<div class="entry-content" itemprop="description">\s*<p>(.*?)<\/p>/); const descriptionMatch = html.match(/<div class="entry-content" itemprop="description">\s*<p>(.*?)<\/p>/);
const description = descriptionMatch ? descriptionMatch[1].trim() : `N/A`; const description = descriptionMatch ? descriptionMatch[1].trim() : `N/A`;
@@ -48,8 +48,8 @@ async function extractDetails(url) {
return JSON.stringify(details); return JSON.stringify(details);
} }
async function extractEpisodes(url) { async function extractEpisodes(url) {
const response = await fetch(url); const response = await fetchv2(url);
const html = await response; const html = await response.text();
const episodes = []; const episodes = [];
const allMatches = [...html.matchAll(/<li[^>]*>\s*<a href="([^"]+)">\s*<div class="epl-title">([^<]*) <\/div>/g)]; const allMatches = [...html.matchAll(/<li[^>]*>\s*<a href="([^"]+)">\s*<div class="epl-title">([^<]*) <\/div>/g)];
@@ -80,8 +80,8 @@ async function extractEpisodes(url) {
} }
async function extractStreamUrl(url) { async function extractStreamUrl(url) {
const embedResponse = await fetch(url); const embedResponse = await fetchv2(url);
const data = await embedResponse; const data = await embedResponse.text();
const embedMatch = data.match(/<div class="pembed" data-embed="(.*?)"/); const embedMatch = data.match(/<div class="pembed" data-embed="(.*?)"/);
if (embedMatch && embedMatch[1]) { if (embedMatch && embedMatch[1]) {
@@ -97,17 +97,29 @@ async function extractStreamUrl(url) {
} }
console.log(fullEmbedUrl); console.log(fullEmbedUrl);
const embedPageResponse = await fetch(fullEmbedUrl); const embedPageResponse = await fetchv2(fullEmbedUrl);
const embedPageData = await embedPageResponse; const embedPageData = await embedPageResponse.text();
console.log(embedPageData); console.log(embedPageData);
const m3u8Match = embedPageData.match(/sources:\s*\[\{file:"(https:\/\/[^"]*\.m3u8)"/);
if (m3u8Match && m3u8Match[1]) { const iframeMatch = embedPageData.match(/<iframe[^>]*src="([^"]+)"/);
const m3u8Url = m3u8Match[1]; if (iframeMatch && iframeMatch[1]) {
console.log(m3u8Url); const iframeUrl = iframeMatch[1];
return m3u8Url; console.log("Iframe URL:", iframeUrl);
const iframeResponse = await fetchv2(iframeUrl);
const iframeData = await iframeResponse.text();
const m3u8Match = iframeData.match(/sources:\s*\[\{"file":"(https:\/\/[^"]*\.m3u8)"/);
if (m3u8Match && m3u8Match[1]) {
const m3u8Url = m3u8Match[1];
console.log(m3u8Url);
return m3u8Url;
} else {
throw new Error("M3U8 URL not found in iframe content.");
}
} else { } else {
throw new Error("M3U8 URL not found."); throw new Error("Iframe URL not found in embedPageData.");
} }
} else { } else {
throw new Error("Embed URL not found."); throw new Error("Embed URL not found.");
+3 -2
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "English (DUB)", "language": "English (DUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
@@ -13,5 +13,6 @@
"searchBaseUrl": "https://kimcartoon.com.co/?s=%s", "searchBaseUrl": "https://kimcartoon.com.co/?s=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/kimcartoon/kimcartoon.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/kimcartoon/kimcartoon.js",
"asyncJS": true, "asyncJS": true,
"type": "anime/shows/movies" "type": "anime/shows/movies",
"downloadSupport": false
} }
+146
View File
@@ -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;
}
}
}
+19
View File
@@ -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
}
+138
View File
@@ -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(/&amp;/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*'[^']+'/, '');
}
+19
View File
@@ -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
}
+457
View File
@@ -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;
}
}
}
+19
View File
@@ -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
}
+357
View File
@@ -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;
}
}
+20
View File
@@ -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
}
+3 -3
View File
@@ -77,10 +77,10 @@ async function extractEpisodes(url) {
const formData = `acc=episodes&i=${i}&u=${u}&p=${page}`; const formData = `acc=episodes&i=${i}&u=${u}&p=${page}`;
console.log("Form Data: " + formData); console.log("Form Data: " + formData);
const headers = { const headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
"Origin": "https://wwv.monoschinos2.net", "Referer": "https://wvv.monoschinos2.net/",
"Referer": url "Host": "wvv.monoschinos2.net",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
}; };
const response = await fetchv2("https://wwv.monoschinos2.net/ajax_pagination", headers, "POST", formData); const response = await fetchv2("https://wwv.monoschinos2.net/ajax_pagination", headers, "POST", formData);
const pageHtml = await response.text(); const pageHtml = await response.text();
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "Spanish (DUB/SUB)", "language": "Spanish (DUB/SUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
+175
View File
@@ -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(/&nbsp;/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"
});
}
}
+19
View File
@@ -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
View File
@@ -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: ""
});
}
}
+19
View File
@@ -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
View File
@@ -1,18 +1,18 @@
{ {
"sourceName": "s.to (ENG DUB)", "sourceName": "s.to (ENG DUB)",
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png", "iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
"author": { "author": {
"name": "Cufiy", "name": "Cufiy",
"icon": "https://files.catbox.moe/ttj4fc.gif" "icon": "https://files.catbox.moe/ttj4fc.gif"
}, },
"version": "0.3.14", "version": "0.3.15",
"language": "English (DUB)", "language": "English (DUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",
"baseUrl": "https://google.com", "baseUrl": "https://google.com",
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s", "searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToEngDub_v2.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToEngDub_v2.js",
"asyncJS": true, "asyncJS": true,
"streamAsyncJS": false, "streamAsyncJS": false,
"type": "shows" "type": "shows"
} }
+665 -6
View File
@@ -352,7 +352,7 @@ function base64Decode(str) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -360,8 +360,8 @@ function base64Decode(str) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -374,7 +374,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -437,8 +447,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -493,7 +509,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -552,6 +575,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -559,6 +589,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -566,6 +610,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -649,6 +749,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -703,6 +825,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -720,6 +1190,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -898,7 +1547,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -934,6 +1587,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+17 -17
View File
@@ -1,18 +1,18 @@
{ {
"sourceName": "s.to (GER DUB)", "sourceName": "s.to (GER DUB)",
"iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png", "iconUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sto.png",
"author": { "author": {
"name": "Hamzo & Cufiy", "name": "Hamzo & Cufiy",
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024" "icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
}, },
"version": "0.3.14", "version": "0.3.15",
"language": "German (DUB)", "language": "German (DUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",
"baseUrl": "https://google.com", "baseUrl": "https://google.com",
"searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s", "searchBaseUrl": "https://s.to/ajax/seriesSearch?keyword=%s",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToGerDub_v2.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/s.to/sToGerDub_v2.js",
"asyncJS": true, "asyncJS": true,
"streamAsyncJS": false, "streamAsyncJS": false,
"type": "shows" "type": "shows"
} }
+665 -6
View File
@@ -352,7 +352,7 @@ function base64Decode(str) {
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
/* {GE START} */ /* {GE START} */
/* {VERSION: 1.1.3} */ /* {VERSION: 1.1.8} */
/** /**
* @name global_extractor.js * @name global_extractor.js
@@ -360,8 +360,8 @@ function base64Decode(str) {
* @author Cufiy * @author Cufiy
* @url https://github.com/JMcrafter26/sora-global-extractor * @url https://github.com/JMcrafter26/sora-global-extractor
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE * @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
* @date 2025-07-23 17:47:48 * @date 2025-11-05 15:44:57
* @version 1.1.3 * @version 1.1.8
* @note This file was generated automatically. * @note This file was generated automatically.
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater * The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
*/ */
@@ -374,7 +374,17 @@ function globalExtractor(providers) {
// check if streamUrl is not null, a string, and starts with http or https // check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) { if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
return streamUrl; return streamUrl;
// if its an array, get the value that starts with http
} else if (Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
return httpStream;
}
} else if (streamUrl || typeof streamUrl !== "string") {
// check if it's a valid stream URL
return null;
} }
} catch (error) { } catch (error) {
// Ignore the error and try the next provider // Ignore the error and try the next provider
} }
@@ -437,8 +447,14 @@ async function multiExtractor(providers) {
console.log(`Skipping ${provider} as it has already 3 streams`); console.log(`Skipping ${provider} as it has already 3 streams`);
continue; continue;
} }
const streamUrl = await extractStreamUrlByProvider(url, provider); let streamUrl = await extractStreamUrlByProvider(url, provider);
// check if streamUrl is not null, a string, and starts with http or https
if (streamUrl && Array.isArray(streamUrl)) {
const httpStream = streamUrl.find(url => url.startsWith("http"));
if (httpStream) {
streamUrl = httpStream;
}
}
// check if provider is already in streams, if it is, add a number to it // check if provider is already in streams, if it is, add a number to it
if ( if (
!streamUrl || !streamUrl ||
@@ -493,7 +509,14 @@ async function extractStreamUrlByProvider(url, provider) {
if(provider == 'bigwarp') { if(provider == 'bigwarp') {
delete headers["User-Agent"]; delete headers["User-Agent"];
headers["x-requested-with"] = "XMLHttpRequest"; headers["x-requested-with"] = "XMLHttpRequest";
} else if (provider == 'vk') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'sibnet') {
headers["encoding"] = "windows-1251"; // required
} else if (provider == 'supervideo') {
delete headers["User-Agent"];
} }
// fetch the url // fetch the url
// and pass the response to the extractor function // and pass the response to the extractor function
console.log("Fetching URL: " + url); console.log("Fetching URL: " + url);
@@ -552,6 +575,13 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from doodstream:", error); console.log("Error extracting stream URL from doodstream:", error);
return null; return null;
} }
case "earnvids":
try {
return await earnvidsExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from earnvids:", error);
return null;
}
case "filemoon": case "filemoon":
try { try {
return await filemoonExtractor(html, url); return await filemoonExtractor(html, url);
@@ -559,6 +589,20 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from filemoon:", error); console.log("Error extracting stream URL from filemoon:", error);
return null; return null;
} }
case "lulustream":
try {
return await lulustreamExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from lulustream:", error);
return null;
}
case "megacloud":
try {
return await megacloudExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from megacloud:", error);
return null;
}
case "mp4upload": case "mp4upload":
try { try {
return await mp4uploadExtractor(html, url); return await mp4uploadExtractor(html, url);
@@ -566,6 +610,62 @@ async function extractStreamUrlByProvider(url, provider) {
console.log("Error extracting stream URL from mp4upload:", error); console.log("Error extracting stream URL from mp4upload:", error);
return null; return null;
} }
case "sendvid":
try {
return await sendvidExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sendvid:", error);
return null;
}
case "sibnet":
try {
return await sibnetExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from sibnet:", error);
return null;
}
case "streamtape":
try {
return await streamtapeExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamtape:", error);
return null;
}
case "streamup":
try {
return await streamupExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from streamup:", error);
return null;
}
case "supervideo":
try {
return await supervideoExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from supervideo:", error);
return null;
}
case "uploadcx":
try {
return await uploadcxExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uploadcx:", error);
return null;
}
case "uqload":
try {
return await uqloadExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from uqload:", error);
return null;
}
case "videospk":
try {
return await videospkExtractor(html, url);
} catch (error) {
console.log("Error extracting stream URL from videospk:", error);
return null;
}
case "vidmoly": case "vidmoly":
try { try {
return await vidmolyExtractor(html, url); return await vidmolyExtractor(html, url);
@@ -649,6 +749,28 @@ function randomStr(length) {
} }
return result; return result;
} }
/* --- earnvids --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name earnvidsExtractor
* @author 50/50
*/
async function earnvidsExtractor(html, url = null) {
try {
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
const baseUrl = url.match(/^(https?:\/\/[^/]+)/)[1];
console.log("HLS Link:" + baseUrl + hlsLink);
return baseUrl + hlsLink;
} catch (err) {
console.log(err);
return "https://files.catbox.moe/avolvc.mp4";
}
}
/* --- filemoon --- */ /* --- filemoon --- */
/* {REQUIRED PLUGINS: unbaser} */ /* {REQUIRED PLUGINS: unbaser} */
@@ -703,6 +825,354 @@ async function filemoonExtractor(html, url = null) {
} }
/* --- lulustream --- */
/**
* @name LuluStream Extractor
* @author Cufiy
*/
async function lulustreamExtractor(data, url = null) {
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
const scriptMatch = scriptRegex.exec(data);
const decoded = scriptMatch ? scriptMatch[1] : false;
return decoded;
}
/* --- megacloud --- */
/**
* @name megacloudExtractor
* @author ShadeOfChaos
*/
// Megacloud V3 specific
async function megacloudExtractor(html, embedUrl) {
// TESTING ONLY START
const testcase = '/api/static';
if(embedUrl.slice(-testcase.length) == testcase) {
try {
const response = await soraFetch(embedUrl, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
embedUrl = response.url;
} catch (error) {
throw new Error("[TESTING ONLY] Megacloud extraction error:", error);
}
}
// TESTING ONLY END
const CHARSET = Array.from({ length: 95 }, (_, i) => String.fromCharCode(i + 32));
const xraxParams = embedUrl.split('/').pop();
const xrax = xraxParams.includes('?') ? xraxParams.split('?')[0] : xraxParams;
const nonce = await getNonce(embedUrl);
// return decrypt(secretKey, nonce, encryptedText);
try {
const response = await soraFetch(`https://megacloud.blog/embed-2/v3/e-1/getSources?id=${xrax}&_k=${nonce}`, { method: 'GET', headers: { "referer": "https://megacloud.blog/" } });
const rawSourceData = await response.json();
const encrypted = rawSourceData?.sources;
let decryptedSources = null;
// console.log('rawSourceData', rawSourceData);
if (rawSourceData?.encrypted == false) {
decryptedSources = rawSourceData.sources;
}
if (decryptedSources == null) {
decryptedSources = await getDecryptedSourceV3(encrypted, nonce);
if (!decryptedSources) throw new Error("Failed to decrypt source");
}
// console.log("Decrypted sources:" + JSON.stringify(decryptedSources, null, 2));
// return the first source if it's an array
if (Array.isArray(decryptedSources) && decryptedSources.length > 0) {
try {
return decryptedSources[0].file;
} catch (error) {
console.log("Error extracting MegaCloud stream URL:" + error);
return false;
}
}
// return {
// status: true,
// result: {
// sources: decryptedSources,
// tracks: rawSourceData.tracks,
// intro: rawSourceData.intro ?? null,
// outro: rawSourceData.outro ?? null,
// server: rawSourceData.server ?? null
// }
// }
} catch (error) {
console.error(`[ERROR][decryptSources] Error decrypting ${embedUrl}:`, error);
return {
status: false,
error: error?.message || 'Failed to get HLS link'
};
}
/**
* Computes a key based on the given secret and nonce.
* The key is used to "unlock" the encrypted data.
* The computation of the key is based on the following steps:
* 1. Concatenate the secret and nonce.
* 2. Compute a hash value of the concatenated string using a simple
* hash function (similar to Java's String.hashCode()).
* 3. Compute the remainder of the hash value divided by the maximum
* value of a 64-bit signed integer.
* 4. Use the result as a XOR mask to process the characters of the
* concatenated string.
* 5. Rotate the XOR-processed string by a shift amount equal to the
* hash value modulo the length of the XOR-processed string plus 5.
* 6. Interleave the rotated string with the reversed nonce string.
* 7. Take a substring of the interleaved string of length equal to 96
* plus the hash value modulo 33.
* 8. Convert each character of the substring to a character code
* between 32 and 126 (inclusive) by taking the remainder of the
* character code divided by 95 and adding 32.
* 9. Join the resulting array of characters into a string and return it.
* @param {string} secret - The secret string
* @param {string} nonce - The nonce string
* @returns {string} The computed key
*/
function computeKey(secret, nonce) {
const secretAndNonce = secret + nonce;
let hashValue = 0n;
for (const char of secretAndNonce) {
hashValue = BigInt(char.charCodeAt(0)) + hashValue * 31n + (hashValue << 7n) - hashValue;
}
const maximum64BitSignedIntegerValue = 0x7fffffffffffffffn;
const hashValueModuloMax = hashValue % maximum64BitSignedIntegerValue;
const xorMask = 247;
const xorProcessedString = [...secretAndNonce]
.map(char => String.fromCharCode(char.charCodeAt(0) ^ xorMask))
.join('');
const xorLen = xorProcessedString.length;
const shiftAmount = (Number(hashValueModuloMax) % xorLen) + 5;
const rotatedString = xorProcessedString.slice(shiftAmount) + xorProcessedString.slice(0, shiftAmount);
const reversedNonceString = nonce.split('').reverse().join('');
let interleavedString = '';
const maxLen = Math.max(rotatedString.length, reversedNonceString.length);
for (let i = 0; i < maxLen; i++) {
interleavedString += (rotatedString[i] || '') + (reversedNonceString[i] || '');
}
const length = 96 + (Number(hashValueModuloMax) % 33);
const partialString = interleavedString.substring(0, length);
return [...partialString]
.map(ch => String.fromCharCode((ch.charCodeAt(0) % 95) + 32))
.join('');
}
/**
* Encrypts a given text using a columnar transposition cipher with a given key.
* The function arranges the text into a grid of columns and rows determined by the key length,
* fills the grid column by column based on the sorted order of the key characters,
* and returns the encrypted text by reading the grid row by row.
*
* @param {string} text - The text to be encrypted.
* @param {string} key - The key that determines the order of columns in the grid.
* @returns {string} The encrypted text.
*/
function columnarCipher(text, key) {
const columns = key.length;
const rows = Math.ceil(text.length / columns);
const grid = Array.from({ length: rows }, () => Array(columns).fill(''));
const columnOrder = [...key]
.map((char, idx) => ({ char, idx }))
.sort((a, b) => a.char.charCodeAt(0) - b.char.charCodeAt(0));
let i = 0;
for (const { idx } of columnOrder) {
for (let row = 0; row < rows; row++) {
grid[row][idx] = text[i++] || '';
}
}
return grid.flat().join('');
}
/**
* Deterministically unshuffles an array of characters based on a given key phrase.
* The function simulates a pseudo-random shuffling using a numeric seed derived
* from the key phrase. This ensures that the same character array and key phrase
* will always produce the same output, allowing for deterministic "unshuffling".
* @param {Array} characters - The array of characters to unshuffle.
* @param {string} keyPhrase - The key phrase used to generate the seed for the
* pseudo-random number generator.
* @returns {Array} A new array representing the deterministically unshuffled characters.
*/
function deterministicUnshuffle(characters, keyPhrase) {
let seed = [...keyPhrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
const shuffledCharacters = characters.slice();
for (let i = shuffledCharacters.length - 1; i > 0; i--) {
const j = randomNumberGenerator(i + 1);
[shuffledCharacters[i], shuffledCharacters[j]] = [shuffledCharacters[j], shuffledCharacters[i]];
}
return shuffledCharacters;
}
/**
* Decrypts an encrypted text using a secret key and a nonce through multiple rounds of decryption.
* The decryption process includes base64 decoding, character substitution using a pseudo-random
* number generator, a columnar transposition cipher, and deterministic unshuffling of the character set.
* Finally, it extracts and parses the decrypted JSON string or verifies it using a regex pattern.
*
* @param {string} secretKey - The key used to decrypt the text.
* @param {string} nonce - A nonce for additional input to the decryption key.
* @param {string} encryptedText - The text to be decrypted, encoded in base64.
* @param {number} [rounds=3] - The number of decryption rounds to perform.
* @returns {Object|null} The decrypted JSON object if successful, or null if parsing fails.
*/
function decrypt(secretKey, nonce, encryptedText, rounds = 3) {
let decryptedText = Buffer.from(encryptedText, 'base64').toString('utf-8');
const keyPhrase = computeKey(secretKey, nonce);
for (let round = rounds; round >= 1; round--) {
const encryptionPassphrase = keyPhrase + round;
let seed = [...encryptionPassphrase].reduce((acc, char) => (acc * 31n + BigInt(char.charCodeAt(0))) & 0xffffffffn, 0n);
const randomNumberGenerator = (upperLimit) => {
seed = (seed * 1103515245n + 12345n) & 0x7fffffffn;
return Number(seed % BigInt(upperLimit));
};
decryptedText = [...decryptedText]
.map(char => {
const charIndex = CHARSET.indexOf(char);
if (charIndex === -1) return char;
const offset = randomNumberGenerator(95);
return CHARSET[(charIndex - offset + 95) % 95];
})
.join('');
decryptedText = columnarCipher(decryptedText, encryptionPassphrase);
const shuffledCharset = deterministicUnshuffle(CHARSET, encryptionPassphrase);
const mappingArr = {};
shuffledCharset.forEach((c, i) => (mappingArr[c] = CHARSET[i]));
decryptedText = [...decryptedText].map(char => mappingArr[char] || char).join('');
}
const lengthString = decryptedText.slice(0, 4);
let length = parseInt(lengthString, 10);
if (isNaN(length) || length <= 0 || length > decryptedText.length - 4) {
console.error('Invalid length in decrypted string');
return decryptedText;
}
const decryptedString = decryptedText.slice(4, 4 + length);
try {
return JSON.parse(decryptedString);
} catch (e) {
console.warn('Could not parse decrypted string, unlikely to be valid. Using regex to verify');
const regex = /"file":"(.*?)".*?"type":"(.*?)"/;
const match = encryptedText.match(regex);
const matchedFile = match?.[1];
const matchType = match?.[2];
if (!matchedFile || !matchType) {
console.error('Could not match file or type in decrypted string');
return null;
}
return decryptedString;
}
}
/**
* Tries to extract the MegaCloud nonce from the given embed URL.
*
* Fetches the HTML of the page, and tries to extract the nonce from it.
* If that fails, it sends a request with the "x-requested-with" header set to "XMLHttpRequest"
* and tries to extract the nonce from that HTML.
*
* If all else fails, it logs the HTML of both requests and returns null.
*
* @param {string} embedUrl The URL of the MegaCloud embed
* @returns {string|null} The extracted nonce, or null if it couldn't be found
*/
async function getNonce(embedUrl) {
const res = await soraFetch(embedUrl, { headers: { "referer": "https://anicrush.to/", "x-requested-with": "XMLHttpRequest" } });
const html = await res.text();
const match0 = html.match(/\<meta[\s\S]*?name="_gg_fb"[\s\S]*?content="([\s\S]*?)">/);
if (match0?.[1]) {
return match0[1];
}
const match1 = html.match(/_is_th:(\S*?)\s/);
if (match1?.[1]) {
return match1[1];
}
const match2 = html.match(/data-dpi="([\s\S]*?)"/);
if (match2?.[1]) {
return match2[1];
}
const match3 = html.match(/_lk_db[\s]?=[\s\S]*?x:[\s]"([\S]*?)"[\s\S]*?y:[\s]"([\S]*?)"[\s\S]*?z:[\s]"([\S]*?)"/);
if (match3?.[1] && match3?.[2] && match3?.[3]) {
return "" + match3[1] + match3[2] + match3[3];
}
const match4 = html.match(/nonce="([\s\S]*?)"/);
if (match4?.[1]) {
if (match4[1].length >= 32) return match4[1];
}
const match5 = html.match(/_xy_ws = "(\S*?)"/);
if (match5?.[1]) {
return match5[1];
}
const match6 = html.match(/[a-zA-Z0-9]{48}]/);
if (match6?.[1]) {
return match6[1];
}
return null;
}
async function getDecryptedSourceV3(encrypted, nonce) {
let decrypted = null;
const keys = await asyncGetKeys();
for(let key in keys) {
try {
if (!encrypted) {
console.log("Encrypted source missing in response")
return null;
}
decrypted = decrypt(keys[key], nonce, encrypted);
if(!Array.isArray(decrypted) || decrypted.length <= 0) {
// Failed to decrypt source
continue;
}
for(let source of decrypted) {
if(source != null && source?.file?.startsWith('https://')) {
// Malformed decrypted source
continue;
}
}
console.log("Functioning key:", key);
return decrypted;
} catch(error) {
console.error('Error:', error);
console.error(`[${ new Date().toLocaleString() }] Key did not work: ${ key }`);
continue;
}
}
return null;
}
async function asyncGetKeys() {
const resolution = await Promise.allSettled([
fetchKey("ofchaos", "https://ac-api.ofchaos.com/api/key"),
fetchKey("yogesh", "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"),
fetchKey("esteven", "https://raw.githubusercontent.com/carlosesteven/e1-player-deobf/refs/heads/main/output/key.json")
]);
const keys = resolution.filter(r => r.status === 'fulfilled' && r.value != null).reduce((obj, r) => {
let rKey = Object.keys(r.value)[0];
let rValue = Object.values(r.value)[0];
if (typeof rValue === 'string') {
obj[rKey] = rValue.trim();
return obj;
}
obj[rKey] = rValue?.mega ?? rValue?.decryptKey ?? rValue?.MegaCloud?.Anime?.Key ?? rValue?.megacloud?.key ?? rValue?.key ?? rValue?.megacloud?.anime?.key ?? rValue?.megacloud;
return obj;
}, {});
if (keys.length === 0) {
throw new Error("Failed to fetch any decryption key");
}
return keys;
}
function fetchKey(name, url) {
return new Promise(async (resolve) => {
try {
const response = await soraFetch(url, { method: 'get' });
const key = await response.text();
let trueKey = null;
try {
trueKey = JSON.parse(key);
} catch (e) {
trueKey = key;
}
resolve({ [name]: trueKey })
} catch (error) {
resolve(null);
}
});
}
}
/* --- mp4upload --- */ /* --- mp4upload --- */
/** /**
@@ -720,6 +1190,185 @@ async function mp4uploadExtractor(html, url = null) {
return null; return null;
} }
} }
/* --- sendvid --- */
/**
* @name sendvidExtractor
* @author 50/50
*/
async function sendvidExtractor(data, url = null) {
const match = data.match(/var\s+video_source\s*=\s*"([^"]+)"/);
const videoUrl = match ? match[1] : null;
return videoUrl;
}
/* --- sibnet --- */
/**
* @name sibnetExtractor
* @author scigward
*/
async function sibnetExtractor(html, embedUrl) {
try {
const videoMatch = html.match(
/player\.src\s*\(\s*\[\s*\{\s*src\s*:\s*["']([^"']+)["']/i
);
if (!videoMatch || !videoMatch[1]) {
throw new Error("Sibnet video source not found");
}
const videoPath = videoMatch[1];
const videoUrl = videoPath.startsWith("http")
? videoPath
: `https://video.sibnet.ru${videoPath}`;
return videoUrl;
} catch (error) {
console.log("SibNet extractor error: " + error.message);
return null;
}
}
/* --- streamtape --- */
/**
*
* @name streamTapeExtractor
* @author ShadeOfChaos
*/
async function streamtapeExtractor(html, url) {
let promises = [];
const LINK_REGEX = /link['"]{1}\).innerHTML *= *['"]{1}([\s\S]*?)["'][\s\S]*?\(["']([\s\S]*?)["']([\s\S]*?);/g;
const CHANGES_REGEX = /([0-9]+)/g;
if(html == null) {
if(url == null) {
throw new Error('Provided incorrect parameters.');
}
const response = await soraFetch(url);
html = await response.text();
}
const matches = html.matchAll(LINK_REGEX);
for (const match of matches) {
let base = match?.[1];
let params = match?.[2];
const changeStr = match?.[3];
if(changeStr == null || changeStr == '') continue;
const changes = changeStr.match(CHANGES_REGEX);
for(let n of changes) {
params = params.substring(n);
}
while(base[0] == '/') {
base = base.substring(1);
}
const url = 'https://' + base + params;
promises.push(testUrl(url));
}
// Race for first success
return Promise.any(promises).then((value) => {
return value;
}).catch((error) => {
return null;
});
async function testUrl(url) {
return new Promise(async (resolve, reject) => {
try {
// Timeout version prefered, but Sora does not support it currently
// var response = await soraFetch(url, { method: 'GET', signal: AbortSignal.timeout(2000) });
var response = await soraFetch(url);
if(response == null) throw new Error('Connection timed out.');
} catch(e) {
console.error('Rejected due to:', e.message);
return reject(null);
}
if(response?.ok && response?.status === 200) {
return resolve(url);
}
console.warn('Reject because of response:', response?.ok, response?.status);
return reject(null);
});
}
}
/* --- streamup --- */
/**
* @name StreamUp Extractor
* @author Cufiy
*/
async function streamupExtractor(data, url = null) {
// if url ends with /, remove it
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
// split the url by / and get the last part
const urlParts = url.split("/");
const videoId = urlParts[urlParts.length - 1];
const apiUrl = `https://strmup.to/ajax/stream?filecode=${videoId}`;
const response = await soraFetch(apiUrl);
const jsonData = await response.json();
if (jsonData && jsonData.streaming_url) {
return jsonData.streaming_url;
} else {
console.log("No streaming URL found in the response.");
return null;
}
}
/* --- supervideo --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name SuperVideo Extractor
* @author 50/50
*/
async function supervideoExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const regex = /file:\s*"([^"]+\.m3u8)"/;
const match = regex.exec(unpackedScript);
if (match) {
const fileUrl = match[1];
console.log("File URL:" + fileUrl);
return fileUrl;
}
return "No stream found";
}
/* --- uploadcx --- */
/**
* @name UploadCx Extractor
* @author 50/50
*/
async function uploadcxExtractor(data, url = null) {
const mp4Match = /sources:\s*\["([^"]+\.mp4)"]/i.exec(data);
return mp4Match ? mp4Match[1] : null;
}
/* --- uqload --- */
/**
* @name uqloadExtractor
* @author scigward
*/
async function uqloadExtractor(html, embedUrl) {
try {
const match = html.match(/sources:\s*\[\s*"([^"]+\.mp4)"\s*\]/);
const videoSrc = match ? match[1] : "";
return videoSrc;
} catch (error) {
console.log("uqloadExtractor error:", error.message);
return null;
}
}
/* --- videospk --- */
/* {REQUIRED PLUGINS: unbaser} */
/**
* @name videospkExtractor
* @author 50/50
*/
async function videospkExtractor(data, url = null) {
const obfuscatedScript = data.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
const unpackedScript = unpack(obfuscatedScript[1]);
const streamMatch = unpackedScript.match(/["'](\/stream\/[^"']+)["']/);
const hlsLink = streamMatch ? streamMatch[1] : null;
return "https://videospk.xyz" + hlsLink;
}
/* --- vidmoly --- */ /* --- vidmoly --- */
/** /**
@@ -898,7 +1547,11 @@ async function soraFetch(url, options = { headers: {}, method: 'GET', body: null
} }
} }
} }
/***********************************************************
* UNPACKER MODULE
* Credit to GitHub user "mnsrulz" for Unpacker Node library
* https://github.com/mnsrulz/unpacker
***********************************************************/
class Unbaser { class Unbaser {
constructor(base) { constructor(base) {
this.ALPHABET = { this.ALPHABET = {
@@ -934,6 +1587,12 @@ class Unbaser {
return ret; return ret;
} }
} }
function detectUnbaser(source) {
/* Detects whether `source` is P.A.C.K.E.R. coded. */
return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,");
}
function unpack(source) { function unpack(source) {
let { payload, symtab, radix, count } = _filterargs(source); let { payload, symtab, radix, count } = _filterargs(source);
if (count != symtab.length) { if (count != symtab.length) {
+2 -2
View File
@@ -223,7 +223,7 @@ async function extractStreamUrl(ID) {
const streamdata = decryptData.result.hls; const streamdata = decryptData.result.hls;
const stream = "https://proxy.aether.mom/m3u8-proxy?url=https://smashyplayer.top"+streamdata; const stream = "https://proxy.aether.mom/m3u8-proxy?url=https://smashyplayer.top"+streamdata;
console.log(stream); console.log(stream);
const subsEng = decryptData.result.subtitle?.English || "https://error.org"; const subsEng = decryptData?.result?.subtitle?.["English"] || "https://error.org/";
const streams = []; const streams = [];
@@ -283,7 +283,7 @@ async function extractStreamUrl(ID) {
: ""; : "";
const stream = baseUrl + streamdata; const stream = baseUrl + streamdata;
console.log(stream); console.log(stream);
const subsEng = decryptData.result.subtitle?.English || "https://error.org"; const subsEng = decryptData?.result?.subtitle?.["English"] || "https://error.org/";
const streams = []; const streams = [];
+3 -3
View File
@@ -5,12 +5,12 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.1", "version": "1.0.2",
"language": "English", "language": "English",
"streamType": "HLS", "streamType": "HLS",
"quality": "1080p", "quality": "1080p",
"baseUrl": "https://smashystream.xyz/i", "baseUrl": "https://smashystream.xyz/",
"searchBaseUrl": "https://smashystream.xyz/i", "searchBaseUrl": "https://smashystream.xyz/",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/smashystream/smashystream.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/smashystream/smashystream.js",
"type": "shows/movies/anime", "type": "shows/movies/anime",
"asyncJS": true, "asyncJS": true,
+70 -73
View File
@@ -1,92 +1,89 @@
const PLACEHOLDER_IMAGE = "https://media.istockphoto.com/id/1147544807/vector/thumbnail-image-vector-graphic.jpg?s=612x612&w=0&k=20&c=rnCKVbdxqkjlcs3xH87-9gocETqpspHFXu5dIGB4wuM=";
async function searchResults(keyword) { async function searchResults(keyword) {
const searchUrl = `https://api-v2.soundcloud.com/search?q=${encodeURIComponent(keyword)}&facet=model&user_id=200971-112325-516393-99787&client_id=UjhhbCuNo1OQfTwkzajxQNLlJcSlUlVz&limit=30`;
try { try {
const response = await fetch(searchUrl); const response = await fetchv2(`https://api-v2.soundcloud.com/search/tracks?q=${encodeURIComponent(keyword)}&facet=genre&user_id=618848-625616-52355-612546&client_id=xwYTVSni6n4FghaI0c4uJ8T9c4pyJ3rh&limit=20&offset=0&linked_partitioning=1&app_version=1761897122&app_locale=en`);
const json = await JSON.parse(response); const data = await response.json();
const results = data.collection
.map(track => {
const hlsLink = track.media.transcodings.find(t => t.format.protocol === "hls" && t.format.mime_type.includes("mp4"))?.url || "";
const imageUrl = track.artwork_url ? track.artwork_url.replace(/-large\.jpg$/, '-t500x500.jpg') : "";
return {
title: track.title,
image: imageUrl,
href: `${hlsLink}?title=${encodeURIComponent(track.title)}&artist=${encodeURIComponent(track.user.username)}`,
hasHls: hlsLink !== ""
};
})
.filter(track => track.hasHls)
.map(track => ({
title: track.title,
image: track.image,
href: track.href
}));
const results = json.collection.map(item => ({
title: item.title,
image: item.artwork_url || PLACEHOLDER_IMAGE,
href: item.permalink_url
}));
//console.log(results);
console.log(JSON.stringify(results));
return JSON.stringify(results); return JSON.stringify(results);
} catch (error) { } catch (err) {
console.error("Error fetching search results:", error); return JSON.stringify([{
return JSON.stringify([]); title: "Error",
image: "Error",
href: "Error"
}]);
} }
} }
async function extractDetails(url) { async function extractDetails(url) {
const details = []; console.log(url);
details.push({ try {
description: 'N/A', const params = url.split('?')[1] || '';
alias: 'N/A', const pairs = params.split('&');
airdate: 'N/A' let title = '';
}); let artist = '';
//#IAMLAZY for (const pair of pairs) {
const [key, value] = pair.split('=');
console.log(JSON.stringify(details)); if (key === 'title') title = decodeURIComponent(value);
return JSON.stringify(details); if (key === 'artist') artist = decodeURIComponent(value);
}
const description = `${title} by ${artist}`;
return JSON.stringify([{
description: description,
aliases: "",
airdate: ""
}]);
} catch (err) {
return JSON.stringify([{
description: "Error",
aliases: "Error",
airdate: "Error"
}]);
}
} }
async function extractEpisodes(url) { async function extractEpisodes(url) {
const response = await fetch(url); const results = [];
const html = await response; try {
const tracks = []; const cleanUrl = url.split('?')[0];
results.push({
const trackRegex = /<article itemprop="track"[^>]*>[\s\S]*?<h2 itemprop="name"><a itemprop="url" href="([^"]+)">/g; href: cleanUrl,
number: 1
let match;
let number = 1;
while ((match = trackRegex.exec(html)) !== null) {
tracks.push({
number: number++,
href: 'https://soundcloud.com' + match[1].trim()
}); });
return JSON.stringify(results);
} catch (err) {
return JSON.stringify([{
href: "Error",
number: "Error"
}]);
} }
if (tracks.length > 0) {
return JSON.stringify(tracks);
} else {
const canonicalMatch = html.match(/\["link",\{"rel":"canonical","href":"([^"]+)"\}\]/);
if (canonicalMatch) {
return JSON.stringify([{ number: 1, href: canonicalMatch[1].trim() }]);
}
}
return JSON.stringify([]);
} }
async function extractStreamUrl(url) { async function extractStreamUrl(url) {
const clientId = "UjhhbCuNo1OQfTwkzajxQNLlJcSlUlVz";
try { try {
const response = await fetch(url); const response = await fetchv2(url+"?client_id=xwYTVSni6n4FghaI0c4uJ8T9c4pyJ3rh&track_authorization=");
const html = await response; const data = await response.json();
const urlMatch = html.match(/"url":"(https:\/\/api[^\.]*\.soundcloud\.com\/media\/soundcloud:tracks:[^"]+)"/);
const authMatch = html.match(/"track_authorization":"([^"]+)"/);
if (urlMatch && authMatch) {
const streamUrl = `${urlMatch[1]}?client_id=${clientId}&track_authorization=${authMatch[1]}`;
const responseTwo = await fetch(streamUrl);
const json = await JSON.parse(responseTwo);
return json.url; return data.url || "https://error.org/";
} else { } catch (err) {
console.log("No stream URL found"); return "https://error.org/";
return null;
}
} catch (error) {
console.error("Error:", error);
return null;
} }
} }
+7 -4
View File
@@ -6,11 +6,14 @@
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.0",
"language": "Music lmfao", "language": "Music",
"streamType": "HLS", "streamType": "HLS",
"quality": "Wallah fire quality", "quality": "128 kbps",
"baseUrl": "https://soundcloud.com/", "baseUrl": "https://soundcloud.com/",
"searchBaseUrl": "https://bshar1865-hianime.vercel.app/api/v2/hianime/search?q=%s", "searchBaseUrl": "https://soundcloud.com/",
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/soundcloud/soundcloud.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/soundcloud/soundcloud.js",
"asyncJS": true "asyncJS": true,
"softsub": true,
"type": "anime/movies/shows",
"downloadSupport": true
} }
+245
View File
@@ -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;
}
}
}
+18
View File
@@ -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
}
+101
View File
@@ -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/";
}
}
+19
View File
@@ -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
}
+36 -6
View File
@@ -1,3 +1,10 @@
function cleanTitle(title) {
return title
.replace(/&#8217;/g, "'")
.replace(/&#8211;/g, "-")
.replace(/&#[0-9]+;/g, "");
}
async function searchResults(keyword) { async function searchResults(keyword) {
const results = []; const results = [];
try { try {
@@ -8,7 +15,7 @@ async function searchResults(keyword) {
let match; let match;
while ((match = regex.exec(html)) !== null) { while ((match = regex.exec(html)) !== null) {
results.push({ results.push({
title: match[2].trim(), title: cleanTitle(match[2].trim()),
image: match[3] ? match[3].trim() : "", image: match[3] ? match[3].trim() : "",
href: match[1].trim() href: match[1].trim()
}); });
@@ -86,12 +93,35 @@ async function extractEpisodes(url) {
async function extractStreamUrl(url) { async function extractStreamUrl(url) {
const response = await fetchv2(url); const response = await fetchv2(url);
const html = await response.text(); const htmlOne = await response.text();
let streamData = null;
streamData = voeExtractor(html); const redirectMatch = htmlOne.match(/window\.location\.href\s*=\s*['"]([^'"]+)['"]/);
console.log("Voe Stream Data: " + streamData); if (!redirectMatch) {
return streamData; console.log("No redirect URL found");
return null;
}
const redirectUrl = redirectMatch[1];
console.log("Redirect URL: " + redirectUrl);
const actualResponse = await fetchv2(redirectUrl);
const html = await actualResponse.text();
let streamUrl = voeExtractor(html);
console.log("Voe Stream Data: " + streamUrl);
return JSON.stringify({
streams: [
{
title: "Server 1",
streamUrl: streamUrl,
headers: {
Referer: redirectUrl
}
}
],
subtitle: "https://none.com"
});
} }
/* SCHEME START */ /* SCHEME START */
+2 -2
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "Italian", "language": "Italian",
"streamType": "HLS", "streamType": "HLS",
"encrypted":true, "encrypted":true,
@@ -15,6 +15,6 @@
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/toonitalia/toonitalia.js", "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/toonitalia/toonitalia.js",
"type": "shows/movies", "type": "shows/movies",
"asyncJS": true, "asyncJS": true,
"softsub": false, "softsub": true,
"downloadSupport": false "downloadSupport": false
} }
+1 -1
View File
@@ -102,7 +102,7 @@ async function extractEpisodes(url) {
const scriptResponse = await fetchv2(scriptUrl); const scriptResponse = await fetchv2(scriptUrl);
const scriptContent = await scriptResponse.text(); const scriptContent = await scriptResponse.text();
const supervideRegex = /\\'(https:\/\/supervideo\.tv\/[^']+)\\'/; const supervideRegex = /\\'(https:\/\/supervideo\.[^\/']+\/[^']+)\\'/;
const supervideMatch = scriptContent.match(supervideRegex); const supervideMatch = scriptContent.match(supervideRegex);
if (supervideMatch) { if (supervideMatch) {
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "50/50", "name": "50/50",
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
}, },
"version": "1.0.0", "version": "1.0.1",
"language": "German", "language": "German",
"streamType": "HLS", "streamType": "HLS",
"encrypted":true, "encrypted":true,
+222
View File
@@ -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"
});
}
}
+20
View File
@@ -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
}
+175
View File
@@ -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 */
+19
View File
@@ -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
View File
@@ -186,7 +186,7 @@ async function extractEpisodes(url) {
async function extractStreamUrl(ID) { async function extractStreamUrl(ID) {
if (ID.includes('movie')) { if (ID.includes('movie')) {
const tmdbID = ID.replace('/movie/', ''); const tmdbID = ID.replace('/movie/', '');
const cinebyResponse = await soraFetch(`https://db.cineby.app/3/movie/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`); const cinebyResponse = await soraFetch(`https://jumpfreedom.com/3/movie/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`);
const cinebyData = await cinebyResponse.json(); const cinebyData = await cinebyResponse.json();
const title = encodeURIComponent(cinebyData.title); const title = encodeURIComponent(cinebyData.title);
@@ -194,7 +194,7 @@ async function extractStreamUrl(ID) {
const imdbId = cinebyData.external_ids?.imdb_id || ''; const imdbId = cinebyData.external_ids?.imdb_id || '';
const tmdbId = cinebyData.id; const tmdbId = cinebyData.id;
const fullUrl = `https://api.videasy.net/myflixerzupcloud/sources-with-title?title=${title}&mediaType=movie&year=${year}&episodeId=1&seasonId=1&tmdbId=${tmdbId}&imdbId=${imdbId}`; const fullUrl = `https://api.videasy.net/cdn/sources-with-title?title=${title}&mediaType=movie&year=${year}&episodeId=1&seasonId=1&tmdbId=${tmdbId}&imdbId=${imdbId}`;
console.log('Full URL:' + fullUrl); console.log('Full URL:' + fullUrl);
@@ -220,7 +220,9 @@ async function extractStreamUrl(ID) {
const sources = result.sources || []; const sources = result.sources || [];
const subtitles = result.subtitles || []; const subtitles = result.subtitles || [];
const streams = sources.flatMap(src => [src.quality, src.url]); const nonHDRSources = sources.filter(s => !s.quality.includes("HDR"));
const streams = nonHDRSources.flatMap(src => [src.quality, src.url]);
const englishSubtitle = subtitles.find( const englishSubtitle = subtitles.find(
sub => sub.language.toLowerCase().includes('english') sub => sub.language.toLowerCase().includes('english')
@@ -236,7 +238,7 @@ async function extractStreamUrl(ID) {
const seasonNumber = parts[3]; const seasonNumber = parts[3];
const episodeNumber = parts[4]; const episodeNumber = parts[4];
const cinebyResponse = await soraFetch(`https://db.cineby.app/3/tv/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`); const cinebyResponse = await soraFetch(`https://jumpfreedom.com/3/tv/${tmdbID}?append_to_response=external_ids&language=en&api_key=ad301b7cc82ffe19273e55e4d4206885`);
const cinebyData = await cinebyResponse.json(); const cinebyData = await cinebyResponse.json();
const title = encodeURIComponent(cinebyData.name); const title = encodeURIComponent(cinebyData.name);
@@ -244,7 +246,7 @@ async function extractStreamUrl(ID) {
const imdbId = cinebyData.external_ids?.imdb_id || ''; const imdbId = cinebyData.external_ids?.imdb_id || '';
const tmdbId = cinebyData.id; const tmdbId = cinebyData.id;
const fullUrl = `https://api.videasy.net/myflixerzupcloud/sources-with-title?title=${title}&mediaType=tv&year=${year}&episodeId=${episodeNumber}&seasonId=${seasonNumber}&tmdbId=${tmdbId}&imdbId=${imdbId}`; const fullUrl = `https://api.videasy.net/cdn/sources-with-title?title=${title}&mediaType=tv&year=${year}&episodeId=${episodeNumber}&seasonId=${seasonNumber}&tmdbId=${tmdbId}&imdbId=${imdbId}`;
console.log('Full URL:' + fullUrl); console.log('Full URL:' + fullUrl);
@@ -270,7 +272,9 @@ async function extractStreamUrl(ID) {
const sources = result.sources || []; const sources = result.sources || [];
const subtitles = result.subtitles || []; const subtitles = result.subtitles || [];
const streams = sources.flatMap(src => [src.quality, src.url]); const nonHDRSources = sources.filter(s => !s.quality.includes("HDR"));
const streams = nonHDRSources.flatMap(src => [src.quality, src.url]);
const englishSubtitle = subtitles.find( const englishSubtitle = subtitles.find(
sub => sub.language.toLowerCase().includes('english') sub => sub.language.toLowerCase().includes('english')

Some files were not shown because too many files have changed in this diff Show More