mirror of
https://git.luna-app.eu/50n50/sources
synced 2025-12-21 13:16:21 +01:00
Upload
This commit is contained in:
commit
09c423e95a
276 changed files with 54396 additions and 0 deletions
18
aniworld/AniWorldEngSub.json
Normal file
18
aniworld/AniWorldEngSub.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"sourceName": "AniWorld (ENG SUB)",
|
||||
"iconUrl": "https://gitlab.com/50n50/sources/-/raw/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.7",
|
||||
"language": "English (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://gitlab.com/50n50/sources/-/raw/main/aniworld/v2/AniWorldEngSub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
17
aniworld/AniWorldGerDub.dev.json
Normal file
17
aniworld/AniWorldGerDub.dev.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"sourceName": "AniWorld (fixed)",
|
||||
"iconUrl": "https://gitlab.com/50n50/sources/-/raw/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.52",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://vidmoly.to/",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "http://192.168.2.130/sora-sources2/aniworld/v2/AniWorldGerDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"type": "anime"
|
||||
}
|
||||
18
aniworld/AniWorldGerDub.json
Normal file
18
aniworld/AniWorldGerDub.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"sourceName": "AniWorld (GER DUB)",
|
||||
"iconUrl": "https://gitlab.com/50n50/sources/-/raw/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.7",
|
||||
"language": "German (DUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://gitlab.com/50n50/sources/-/raw/main/aniworld/v2/AniWorldGerDub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
18
aniworld/AniWorldGerSub.json
Normal file
18
aniworld/AniWorldGerSub.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"sourceName": "AniWorld (GER SUB)",
|
||||
"iconUrl": "https://gitlab.com/50n50/sources/-/raw/main/aniworld/aniworld.png",
|
||||
"author": {
|
||||
"name": "Hamzo & Cufiy",
|
||||
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
|
||||
},
|
||||
"version": "0.2.7",
|
||||
"language": "German (SUB)",
|
||||
"streamType": "HLS",
|
||||
"quality": "720p",
|
||||
"baseUrl": "https://google.com",
|
||||
"searchBaseUrl": "https://aniworld.to/ajax/seriesSearch?keyword=%s",
|
||||
"scriptUrl": "https://gitlab.com/50n50/sources/-/raw/main/aniworld/v2/AniWorldGerSub_v2.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": false,
|
||||
"type": "anime"
|
||||
}
|
||||
BIN
aniworld/aniworld.png
Normal file
BIN
aniworld/aniworld.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
aniworld/icon.png
Normal file
BIN
aniworld/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
aniworld/iconSub.png
Normal file
BIN
aniworld/iconSub.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
aniworld/iconSubEng.png
Normal file
BIN
aniworld/iconSubEng.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
611
aniworld/v1/AniWorldEngSub.js
Normal file
611
aniworld/v1/AniWorldEngSub.js
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Main Functions //////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const encodedKeyword = encodeURIComponent(keyword);
|
||||
const searchApiUrl = `https://aniworld.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
|
||||
const responseText = await fetch(searchApiUrl);
|
||||
|
||||
const data = await JSON.parse(responseText);
|
||||
|
||||
const transformedResults = data.map((anime) => ({
|
||||
title: anime.name,
|
||||
image: `https://aniworld.to${anime.cover}`,
|
||||
href: `https://aniworld.to/anime/stream/${anime.link}`,
|
||||
}));
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
console.log("Fetch error:" + error);
|
||||
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
try {
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
const descriptionRegex =
|
||||
/<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
|
||||
const aliasesRegex = /<h1\b[^>]*\bdata-alternativetitles="([^"]+)"[^>]*>/i;
|
||||
|
||||
const aliasesMatch = aliasesRegex.exec(text);
|
||||
let aliasesArray = [];
|
||||
if (aliasesMatch) {
|
||||
aliasesArray = aliasesMatch[1].split(",").map((a) => a.trim());
|
||||
}
|
||||
|
||||
const descriptionMatch = descriptionRegex.exec(text) || [];
|
||||
|
||||
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
|
||||
|
||||
const transformedResults = [
|
||||
{
|
||||
description: descriptionMatch[1] || "No description available",
|
||||
aliases: aliasesArray[0] || "No aliases available",
|
||||
airdate: airdateMatch,
|
||||
},
|
||||
];
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
console.log("Details error:" + error);
|
||||
return JSON.stringify([
|
||||
{
|
||||
description: "Error loading description",
|
||||
aliases: "Duration: Unknown",
|
||||
airdate: "Aired: Unknown",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const html = await fetch(fetchUrl);
|
||||
|
||||
const finishedList = [];
|
||||
const seasonLinks = getSeasonLinks(html);
|
||||
|
||||
for (const seasonLink of seasonLinks) {
|
||||
const seasonEpisodes = await fetchSeasonEpisodes(
|
||||
`${baseUrl}${seasonLink}`
|
||||
);
|
||||
finishedList.push(...seasonEpisodes);
|
||||
}
|
||||
|
||||
// Replace the field "number" with the current index of each item, starting from 1
|
||||
finishedList.forEach((item, index) => {
|
||||
item.number = index + 1;
|
||||
});
|
||||
|
||||
return JSON.stringify(finishedList);
|
||||
} catch (error) {
|
||||
console.log("Fetch error:" + error);
|
||||
return JSON.stringify([{ number: "0", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
const finishedList = [];
|
||||
const languageList = getAvailableLanguages(text);
|
||||
const videoLinks = getVideoLinks(text);
|
||||
|
||||
for (const videoLink of videoLinks) {
|
||||
const language = languageList.find(
|
||||
(l) => l.langKey === videoLink.langKey
|
||||
);
|
||||
if (language) {
|
||||
finishedList.push({
|
||||
provider: videoLink.provider,
|
||||
href: `${baseUrl}${videoLink.href}`,
|
||||
language: language.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Select the hoster
|
||||
const selectedHoster = selectHoster(finishedList);
|
||||
const provider = selectedHoster.provider;
|
||||
const providerLink = selectedHoster.href;
|
||||
if (provider === "Error") {
|
||||
console.log("No video found");
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
console.log("Selected provider: " + provider);
|
||||
console.log("Selected link: " + providerLink);
|
||||
|
||||
const videoPage = await fetch(providerLink);
|
||||
console.log("Video Page: " + videoPage.length);
|
||||
|
||||
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
|
||||
const winLocMatch = winLocRegex.exec(videoPage);
|
||||
let winLocUrl = null;
|
||||
if (!winLocMatch) {
|
||||
winLocUrl = providerLink;
|
||||
} else {
|
||||
winLocUrl = winLocMatch[1];
|
||||
}
|
||||
|
||||
const hlsSourceResponse = await fetch(winLocUrl);
|
||||
const hlsSourcePage =
|
||||
typeof hlsSourceResponse === "object"
|
||||
? await hlsSourceResponse.text()
|
||||
: await hlsSourceResponse;
|
||||
console.log("Provider: " + provider);
|
||||
console.log("URL: " + winLocUrl);
|
||||
console.log("HLS Source Page: " + hlsSourcePage.length);
|
||||
|
||||
switch (provider) {
|
||||
case "VOE":
|
||||
try {
|
||||
const voeJson = voeExtractor(hlsSourcePage);
|
||||
return voeJson?.source || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
console.log("VOE extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
case "SpeedFiles":
|
||||
try {
|
||||
const speedfilesUrl = await speedfilesExtractor(hlsSourcePage);
|
||||
return speedfilesUrl || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
console.log("Speedfiles extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
case "Vidmoly":
|
||||
try {
|
||||
const vidmolyUrl = vidmolyExtractor(hlsSourcePage);
|
||||
return vidmolyUrl || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
console.log("Vidmoly extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
default:
|
||||
console.log("Unsupported provider:", provider);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
// END OF VOE EXTRACTOR
|
||||
|
||||
// Extract the sources variable and decode the hls value from base64
|
||||
const sourcesRegex = /var\s+sources\s*=\s*({[^}]+})/;
|
||||
const sourcesMatch = sourcesRegex.exec(hlsSourcePage);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
|
||||
return sourcesString;
|
||||
} catch (error) {
|
||||
console.log("ExtractStreamUrl error:" + error);
|
||||
return JSON.stringify([{ provider: "Error1", link: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectHoster(finishedList) {
|
||||
let firstVideo = null;
|
||||
let provider = null;
|
||||
|
||||
// Define the preferred providers and languages
|
||||
const providerList = ["Vidmoly", "SpeedFiles", "VOE"];
|
||||
const languageList = ["mit Untertitel Englisch", "mit Untertitel Deutsch"];
|
||||
|
||||
|
||||
for (const providerName of providerList) {
|
||||
for (const language of languageList) {
|
||||
const video = finishedList.find(
|
||||
(video) => video.provider === providerName && video.language === language
|
||||
);
|
||||
if (video) {
|
||||
provider = providerName;
|
||||
firstVideo = video;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstVideo) break;
|
||||
}
|
||||
|
||||
// Default to the first video if no match is found
|
||||
if (!firstVideo) {
|
||||
firstVideo = finishedList[0];
|
||||
}
|
||||
|
||||
if (firstVideo) {
|
||||
return {
|
||||
provider: provider,
|
||||
href: firstVideo.href,
|
||||
};
|
||||
} else {
|
||||
console.log("No video found");
|
||||
return {
|
||||
provider: "Error",
|
||||
href: "https://error.org",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//Thanks to Ibro and Cufiy
|
||||
async function vidmolyExtractor(html) {
|
||||
console.log("Vidmoly extractor");
|
||||
console.log(html);
|
||||
const regexSub = /<option value="([^"]+)"[^>]*>\s*SUB - Omega\s*<\/option>/;
|
||||
const regexFallback = /<option value="([^"]+)"[^>]*>\s*Omega\s*<\/option>/;
|
||||
const fallback = /<option value="([^"]+)"[^>]*>\s*SUB v2 - Omega\s*<\/option>/;
|
||||
|
||||
|
||||
let match = html.match(regexSub) || html.match(regexFallback) || html.match(fallback);
|
||||
if (match) {
|
||||
console.log("Vidmoly extractor: Match found");
|
||||
const decodedHtml = atob(match[1]); // Decode base64
|
||||
const iframeMatch = decodedHtml.match(/<iframe\s+src="([^"]+)"/);
|
||||
|
||||
if (!iframeMatch) {
|
||||
console.log("Vidmoly extractor: No iframe match found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const streamUrl = iframeMatch[1].startsWith("//") ? "https:" + iframeMatch[1] : iframeMatch[1];
|
||||
console.log("Vidmoly extractor: Stream URL: " + streamUrl);
|
||||
|
||||
const responseTwo = await fetchv2(streamUrl);
|
||||
const htmlTwo = await responseTwo.text();
|
||||
|
||||
const m3u8Match = htmlTwo.match(/sources:\s*\[\{file:"([^"]+\.m3u8)"/);
|
||||
console.log(m3u8Match ? m3u8Match[1] : null);
|
||||
return m3u8Match ? m3u8Match[1] : null;
|
||||
} else {
|
||||
console.log("Vidmoly extractor: No match found, using fallback");
|
||||
// regex the sources: [{file:"this_is_the_link"}]
|
||||
const sourcesRegex = /sources:\s*\[\{file:"(https?:\/\/[^"]+)"\}/;
|
||||
const sourcesMatch = html.match(sourcesRegex);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
|
||||
return sourcesString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Thanks to Cufiy
|
||||
function speedfilesExtractor(sourcePageHtml) {
|
||||
// get var _0x5opu234 = "THIS_IS_AN_ENCODED_STRING"
|
||||
const REGEX = /var\s+_0x5opu234\s*=\s*"([^"]+)"/;
|
||||
const match = sourcePageHtml.match(REGEX);
|
||||
if (match == null || match[1] == null) {
|
||||
console.log("Could not extract from Speedfiles source");
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedString = match[1];
|
||||
console.log("Encoded String:" + encodedString);
|
||||
|
||||
// Step 1: Base64 decode the initial string
|
||||
let step1 = atob(encodedString);
|
||||
console.log("Step 1:" + step1);
|
||||
|
||||
// Step 2: Swap character cases and reverse
|
||||
let step2 = step1
|
||||
.split("")
|
||||
.map((c) =>
|
||||
/[a-zA-Z]/.test(c)
|
||||
? c === c.toLowerCase()
|
||||
? c.toUpperCase()
|
||||
: c.toLowerCase()
|
||||
: c
|
||||
)
|
||||
.join("");
|
||||
console.log("Step 2:" + step2);
|
||||
let step3 = step2.split("").reverse().join("");
|
||||
console.log("Step 3:" + step3);
|
||||
|
||||
// Step 3: Base64 decode again and reverse
|
||||
let step4 = atob(step3);
|
||||
console.log("Step 4:" + step4);
|
||||
let step5 = step4.split("").reverse().join("");
|
||||
console.log("Step 5:" + step5);
|
||||
|
||||
// Step 4: Hex decode pairs
|
||||
let step6 = "";
|
||||
for (let i = 0; i < step5.length; i += 2) {
|
||||
step6 += String.fromCharCode(parseInt(step5.substr(i, 2), 16));
|
||||
}
|
||||
console.log("Step 6:" + step6);
|
||||
|
||||
// Step 5: Subtract 3 from character codes
|
||||
let step7 = step6
|
||||
.split("")
|
||||
.map((c) => String.fromCharCode(c.charCodeAt(0) - 3))
|
||||
.join("");
|
||||
console.log("Step 7:" + step7);
|
||||
|
||||
// Step 6: Final case swap, reverse, and Base64 decode
|
||||
let step8 = step7
|
||||
.split("")
|
||||
.map((c) =>
|
||||
/[a-zA-Z]/.test(c)
|
||||
? c === c.toLowerCase()
|
||||
? c.toUpperCase()
|
||||
: c.toLowerCase()
|
||||
: c
|
||||
)
|
||||
.join("");
|
||||
console.log("Step 8:" + step8);
|
||||
let step9 = step8.split("").reverse().join("");
|
||||
console.log("Step 9:" + step9);
|
||||
|
||||
// return atob(step9);
|
||||
let decodedUrl = atob(step9);
|
||||
console.log("Decoded URL:" + decodedUrl);
|
||||
return decodedUrl;
|
||||
}
|
||||
|
||||
// Thanks to https://github.com/ShadeOfChaos
|
||||
|
||||
/**
|
||||
* Extracts a JSON object from the given source page by finding the
|
||||
* encoded string marked with the regex /MKGMa="([\s\S]+?)"/ and
|
||||
* decoding it using the voeDecoder function.
|
||||
* @param {string} sourcePageHtml - The source page to be parsed.
|
||||
* @returns {object|null} The extracted JSON object if successful,
|
||||
* otherwise null.
|
||||
*/
|
||||
function voeExtractor(sourcePageHtml) {
|
||||
const REGEX = /MKGMa="([\s\S]+?)"/;
|
||||
|
||||
const match = sourcePageHtml.match(REGEX);
|
||||
if (match == null || match[1] == null) {
|
||||
console.log("Could not extract from Voe source");
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedString = match[1];
|
||||
const decodedJson = voeDecoder(encodedString);
|
||||
|
||||
return decodedJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given MKGMa string, which is a custom encoded string used
|
||||
* by VOE. This function applies the following steps to the input string to
|
||||
* decode it:
|
||||
* 1. Apply ROT13 to each alphabetical character in the string.
|
||||
* 2. Remove all underscores from the string.
|
||||
* 3. Decode the string using the Base64 algorithm.
|
||||
* 4. Apply a character shift of 0x3 to each character in the decoded string.
|
||||
* 5. Reverse the order of the characters in the shifted string.
|
||||
* 6. Decode the reversed string using the Base64 algorithm again.
|
||||
* 7. Parse the decoded string as JSON.
|
||||
* @param {string} MKGMa_String - The input string to be decoded.
|
||||
* @returns {object} The decoded JSON object.
|
||||
*/
|
||||
function voeDecoder(MKGMa_String) {
|
||||
let ROT13String = ROT13(MKGMa_String);
|
||||
let sanitizedString = voeSanitizer(ROT13String);
|
||||
let UnderscoreRemoved = sanitizedString.split("_").join("");
|
||||
let base64DecodedString = atob(UnderscoreRemoved);
|
||||
let charShiftedString = shiftCharacter(base64DecodedString, 0x3);
|
||||
let reversedString = charShiftedString.split("").reverse().join("");
|
||||
let base64DecodedStringAgain = atob(reversedString);
|
||||
let decodedJson;
|
||||
try {
|
||||
decodedJson = JSON.parse(base64DecodedStringAgain);
|
||||
} catch (error) {
|
||||
console.log("JSON parse error:", error);
|
||||
decodedJson = {};
|
||||
}
|
||||
return decodedJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a given string using the ROT13 cipher, which shifts each letter
|
||||
* 13 places forward in the alphabet. Only alphabetical characters are
|
||||
* transformed; other characters remain unchanged.
|
||||
*
|
||||
* @param {string} string - The input string to be encoded.
|
||||
* @returns {string} The encoded string with ROT13 applied.
|
||||
*/
|
||||
function ROT13(string) {
|
||||
let ROT13String = "";
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
let currentCharCode = string.charCodeAt(i);
|
||||
|
||||
// Check for uppercase
|
||||
if (currentCharCode >= 65 && currentCharCode <= 90) {
|
||||
currentCharCode = ((currentCharCode - 65 + 13) % 26) + 65;
|
||||
// Check for lowercase
|
||||
} else if (currentCharCode >= 97 && currentCharCode <= 122) {
|
||||
currentCharCode = ((currentCharCode - 97 + 13) % 26) + 97;
|
||||
}
|
||||
|
||||
ROT13String += String.fromCharCode(currentCharCode);
|
||||
}
|
||||
|
||||
return ROT13String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a given string by replacing all occurrences of certain "trash" strings
|
||||
* with an underscore. The trash strings are '@$', '^^', '~@', '%?', '*~', '!!', '#&'.
|
||||
* This is used to decode VOE encoded strings.
|
||||
* @param {string} string The string to be sanitized.
|
||||
* @returns {string} The sanitized string.
|
||||
*/
|
||||
function voeSanitizer(string) {
|
||||
let sanitizationArray = ["@$", "^^", "~@", "%?", "*~", "!!", "#&"];
|
||||
let tempString = string;
|
||||
|
||||
for (let i = 0; i < sanitizationArray.length; i++) {
|
||||
let currentTrash = sanitizationArray[i];
|
||||
let sanitizedString = new RegExp(
|
||||
currentTrash.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
||||
"g"
|
||||
);
|
||||
|
||||
tempString = tempString.replace(sanitizedString, "_");
|
||||
}
|
||||
|
||||
return tempString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts the characters in a string by a given number of places.
|
||||
* @param {string} string - The string to shift.
|
||||
* @param {number} shiftNum - The number of places to shift the string.
|
||||
* @returns {string} The shifted string.
|
||||
*/
|
||||
function shiftCharacter(string, shiftNum) {
|
||||
let tempArray = [];
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
tempArray.push(String.fromCharCode(string.charCodeAt(i) - shiftNum));
|
||||
}
|
||||
|
||||
return tempArray.join("");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////////
|
||||
//////////////////////////// for ExtractEpisodes ////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the list of seasons
|
||||
// Site specific structure
|
||||
function getSeasonLinks(html) {
|
||||
const seasonLinks = [];
|
||||
const seasonRegex =
|
||||
/<div class="hosterSiteDirectNav" id="stream">.*?<ul>(.*?)<\/ul>/s;
|
||||
const seasonMatch = seasonRegex.exec(html);
|
||||
if (seasonMatch) {
|
||||
const seasonList = seasonMatch[1];
|
||||
const seasonLinkRegex = /<a[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
|
||||
let seasonLinkMatch;
|
||||
const filmeLinks = [];
|
||||
while ((seasonLinkMatch = seasonLinkRegex.exec(seasonList)) !== null) {
|
||||
const [_, seasonLink] = seasonLinkMatch;
|
||||
if (seasonLink.endsWith("/filme")) {
|
||||
filmeLinks.push(seasonLink);
|
||||
} else {
|
||||
seasonLinks.push(seasonLink);
|
||||
}
|
||||
}
|
||||
seasonLinks.push(...filmeLinks);
|
||||
}
|
||||
return seasonLinks;
|
||||
}
|
||||
|
||||
// Helper function to fetch episodes for a season
|
||||
// Site specific structure
|
||||
async function fetchSeasonEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
// Updated regex to allow empty <strong> content
|
||||
const regex =
|
||||
/<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
let holderNumber = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [_, link] = match;
|
||||
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
|
||||
}
|
||||
|
||||
return matches;
|
||||
} catch (error) {
|
||||
console.log("FetchSeasonEpisodes helper function error:" + error);
|
||||
return [{ number: "0", href: "https://error.org" }];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////
|
||||
//////////////////////////// for ExtractStreamUrl ////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the video links
|
||||
// Site specific structure
|
||||
function getVideoLinks(html) {
|
||||
const videoLinks = [];
|
||||
const videoRegex =
|
||||
/<li\s+class="[^"]*"\s+data-lang-key="([^"]+)"[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<h4>([^<]+)<\/h4>.*?<\/a>.*?<\/li>/gs;
|
||||
let match;
|
||||
|
||||
while ((match = videoRegex.exec(html)) !== null) {
|
||||
const [_, langKey, href, provider] = match;
|
||||
videoLinks.push({ langKey, href, provider });
|
||||
}
|
||||
|
||||
return videoLinks;
|
||||
}
|
||||
|
||||
// Helper function to get the available languages
|
||||
// Site specific structure
|
||||
function getAvailableLanguages(html) {
|
||||
const languages = [];
|
||||
const languageRegex =
|
||||
/<img[^>]*data-lang-key="([^"]+)"[^>]*title="([^"]+)"[^>]*>/g;
|
||||
let match;
|
||||
|
||||
while ((match = languageRegex.exec(html)) !== null) {
|
||||
const [_, langKey, title] = match;
|
||||
languages.push({ langKey, title });
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Helper function to fetch the base64 encoded string
|
||||
function base64Decode(str) {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let output = "";
|
||||
|
||||
str = String(str).replace(/=+$/, "");
|
||||
|
||||
if (str.length % 4 === 1) {
|
||||
throw new Error(
|
||||
"'atob' failed: The string to be decoded is not correctly encoded."
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
let bc = 0, bs, buffer, idx = 0;
|
||||
(buffer = str.charAt(idx++));
|
||||
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
|
||||
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
|
||||
: 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Debugging function to send logs
|
||||
// async function sendLog(message) {
|
||||
// // send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
// console.log(message);
|
||||
|
||||
// await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
// .catch(error => {
|
||||
// console.error('Error sending log:', error);
|
||||
// });
|
||||
// }
|
||||
596
aniworld/v1/AniWorldGerDub.js
Normal file
596
aniworld/v1/AniWorldGerDub.js
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Main Functions //////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const encodedKeyword = encodeURIComponent(keyword);
|
||||
const searchApiUrl = `https://aniworld.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
|
||||
const responseText = await fetch(searchApiUrl);
|
||||
|
||||
const data = await JSON.parse(responseText);
|
||||
|
||||
const transformedResults = data.map((anime) => ({
|
||||
title: anime.name,
|
||||
image: `https://aniworld.to${anime.cover}`,
|
||||
href: `https://aniworld.to/anime/stream/${anime.link}`,
|
||||
}));
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
try {
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
const descriptionRegex =
|
||||
/<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
|
||||
const aliasesRegex = /<h1\b[^>]*\bdata-alternativetitles="([^"]+)"[^>]*>/i;
|
||||
|
||||
const aliasesMatch = aliasesRegex.exec(text);
|
||||
let aliasesArray = [];
|
||||
if (aliasesMatch) {
|
||||
aliasesArray = aliasesMatch[1].split(",").map((a) => a.trim());
|
||||
}
|
||||
|
||||
const descriptionMatch = descriptionRegex.exec(text) || [];
|
||||
|
||||
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
|
||||
|
||||
const transformedResults = [
|
||||
{
|
||||
description: descriptionMatch[1] || "No description available",
|
||||
aliases: aliasesArray[0] || "No aliases available",
|
||||
airdate: airdateMatch,
|
||||
},
|
||||
];
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Details error:" + error);
|
||||
return JSON.stringify([
|
||||
{
|
||||
description: "Error loading description",
|
||||
aliases: "Duration: Unknown",
|
||||
airdate: "Aired: Unknown",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const html = await fetch(fetchUrl);
|
||||
|
||||
const finishedList = [];
|
||||
const seasonLinks = getSeasonLinks(html);
|
||||
|
||||
for (const seasonLink of seasonLinks) {
|
||||
const seasonEpisodes = await fetchSeasonEpisodes(
|
||||
`${baseUrl}${seasonLink}`
|
||||
);
|
||||
finishedList.push(...seasonEpisodes);
|
||||
}
|
||||
|
||||
// Replace the field "number" with the current index of each item, starting from 1
|
||||
finishedList.forEach((item, index) => {
|
||||
item.number = index + 1;
|
||||
});
|
||||
|
||||
return JSON.stringify(finishedList);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ number: "0", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
const finishedList = [];
|
||||
const languageList = getAvailableLanguages(text);
|
||||
const videoLinks = getVideoLinks(text);
|
||||
|
||||
for (const videoLink of videoLinks) {
|
||||
const language = languageList.find(
|
||||
(l) => l.langKey === videoLink.langKey
|
||||
);
|
||||
if (language) {
|
||||
finishedList.push({
|
||||
provider: videoLink.provider,
|
||||
href: `${baseUrl}${videoLink.href}`,
|
||||
language: language.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Select the hoster
|
||||
const selectedHoster = selectHoster(finishedList);
|
||||
const provider = selectedHoster.provider;
|
||||
const providerLink = selectedHoster.href;
|
||||
if (provider === "Error") {
|
||||
sendLog("No video found");
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
sendLog("Selected provider: " + provider);
|
||||
sendLog("Selected link: " + providerLink);
|
||||
|
||||
const videoPage = await fetch(providerLink);
|
||||
sendLog("Video Page: " + videoPage.length);
|
||||
|
||||
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
|
||||
const winLocMatch = winLocRegex.exec(videoPage);
|
||||
let winLocUrl = null;
|
||||
if (!winLocMatch) {
|
||||
winLocUrl = providerLink;
|
||||
} else {
|
||||
winLocUrl = winLocMatch[1];
|
||||
}
|
||||
|
||||
const hlsSourceResponse = await fetch(winLocUrl);
|
||||
const hlsSourcePage =
|
||||
typeof hlsSourceResponse === "object"
|
||||
? await hlsSourceResponse.text()
|
||||
: await hlsSourceResponse;
|
||||
sendLog("Provider: " + provider);
|
||||
sendLog("URL: " + winLocUrl);
|
||||
sendLog("HLS Source Page: " + hlsSourcePage.length);
|
||||
|
||||
switch (provider) {
|
||||
case "VOE":
|
||||
try {
|
||||
const voeJson = voeExtractor(hlsSourcePage);
|
||||
return voeJson?.source || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
sendLog("VOE extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
case "SpeedFiles":
|
||||
try {
|
||||
const speedfilesUrl = await speedfilesExtractor(hlsSourcePage);
|
||||
return speedfilesUrl || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
sendLog("Speedfiles extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
case "Vidmoly":
|
||||
try {
|
||||
const vidmolyUrl = vidmolyExtractor(hlsSourcePage);
|
||||
return vidmolyUrl || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
sendLog("Vidmoly extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
default:
|
||||
sendLog("Unsupported provider:", provider);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
// END OF VOE EXTRACTOR
|
||||
|
||||
// Extract the sources variable and decode the hls value from base64
|
||||
const sourcesRegex = /var\s+sources\s*=\s*({[^}]+})/;
|
||||
const sourcesMatch = sourcesRegex.exec(hlsSourcePage);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
|
||||
return sourcesString;
|
||||
} catch (error) {
|
||||
sendLog("ExtractStreamUrl error:" + error);
|
||||
return JSON.stringify([{ provider: "Error1", link: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectHoster(finishedList) {
|
||||
let firstVideo = null;
|
||||
let provider = null;
|
||||
|
||||
// Define the preferred providers and languages
|
||||
const providerList = ["Vidmoly", "SpeedFiles", "VOE"];
|
||||
const languageList = ["Deutsch", "mit Untertitel Deutsch"];
|
||||
|
||||
|
||||
for (const providerName of providerList) {
|
||||
for (const language of languageList) {
|
||||
const video = finishedList.find(
|
||||
(video) => video.provider === providerName && video.language === language
|
||||
);
|
||||
if (video) {
|
||||
provider = providerName;
|
||||
firstVideo = video;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstVideo) break;
|
||||
}
|
||||
|
||||
// Default to the first video if no match is found
|
||||
if (!firstVideo) {
|
||||
firstVideo = finishedList[0];
|
||||
}
|
||||
|
||||
if (firstVideo) {
|
||||
return {
|
||||
provider: provider,
|
||||
href: firstVideo.href,
|
||||
};
|
||||
} else {
|
||||
sendLog("No video found");
|
||||
return {
|
||||
provider: "Error",
|
||||
href: "https://error.org",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//Thanks to Ibro and Cufiy
|
||||
async function vidmolyExtractor(html) {
|
||||
sendLog("Vidmoly extractor");
|
||||
sendLog(html);
|
||||
const regexSub = /<option value="([^"]+)"[^>]*>\s*SUB - Omega\s*<\/option>/;
|
||||
const regexFallback = /<option value="([^"]+)"[^>]*>\s*Omega\s*<\/option>/;
|
||||
const fallback = /<option value="([^"]+)"[^>]*>\s*SUB v2 - Omega\s*<\/option>/;
|
||||
|
||||
|
||||
let match = html.match(regexSub) || html.match(regexFallback) || html.match(fallback);
|
||||
if (match) {
|
||||
sendLog("Vidmoly extractor: Match found");
|
||||
const decodedHtml = atob(match[1]); // Decode base64
|
||||
const iframeMatch = decodedHtml.match(/<iframe\s+src="([^"]+)"/);
|
||||
|
||||
if (!iframeMatch) {
|
||||
sendLog("Vidmoly extractor: No iframe match found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const streamUrl = iframeMatch[1].startsWith("//") ? "https:" + iframeMatch[1] : iframeMatch[1];
|
||||
sendLog("Vidmoly extractor: Stream URL: " + streamUrl);
|
||||
|
||||
const responseTwo = await fetchv2(streamUrl);
|
||||
const htmlTwo = await responseTwo.text();
|
||||
|
||||
const m3u8Match = htmlTwo.match(/sources:\s*\[\{file:"([^"]+\.m3u8)"/);
|
||||
sendLog(m3u8Match ? m3u8Match[1] : null);
|
||||
return m3u8Match ? m3u8Match[1] : null;
|
||||
} else {
|
||||
sendLog("Vidmoly extractor: No match found, using fallback");
|
||||
// regex the sources: [{file:"this_is_the_link"}]
|
||||
const sourcesRegex = /sources:\s*\[\{file:"(https?:\/\/[^"]+)"\}/;
|
||||
const sourcesMatch = html.match(sourcesRegex);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
|
||||
return sourcesString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Thanks to Cufiy
|
||||
function speedfilesExtractor(sourcePageHtml) {
|
||||
// get var _0x5opu234 = "THIS_IS_AN_ENCODED_STRING"
|
||||
const REGEX = /var\s+_0x5opu234\s*=\s*"([^"]+)"/;
|
||||
const match = sourcePageHtml.match(REGEX);
|
||||
if (match == null || match[1] == null) {
|
||||
sendLog("Could not extract from Speedfiles source");
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedString = match[1];
|
||||
sendLog("Encoded String:" + encodedString);
|
||||
|
||||
// Step 1: Base64 decode the initial string
|
||||
let step1 = atob(encodedString);
|
||||
sendLog("Step 1:" + step1);
|
||||
|
||||
// Step 2: Swap character cases and reverse
|
||||
let step2 = step1
|
||||
.split("")
|
||||
.map((c) =>
|
||||
/[a-zA-Z]/.test(c)
|
||||
? c === c.toLowerCase()
|
||||
? c.toUpperCase()
|
||||
: c.toLowerCase()
|
||||
: c
|
||||
)
|
||||
.join("");
|
||||
sendLog("Step 2:" + step2);
|
||||
let step3 = step2.split("").reverse().join("");
|
||||
sendLog("Step 3:" + step3);
|
||||
|
||||
// Step 3: Base64 decode again and reverse
|
||||
let step4 = atob(step3);
|
||||
sendLog("Step 4:" + step4);
|
||||
let step5 = step4.split("").reverse().join("");
|
||||
sendLog("Step 5:" + step5);
|
||||
|
||||
// Step 4: Hex decode pairs
|
||||
let step6 = "";
|
||||
for (let i = 0; i < step5.length; i += 2) {
|
||||
step6 += String.fromCharCode(parseInt(step5.substr(i, 2), 16));
|
||||
}
|
||||
sendLog("Step 6:" + step6);
|
||||
|
||||
// Step 5: Subtract 3 from character codes
|
||||
let step7 = step6
|
||||
.split("")
|
||||
.map((c) => String.fromCharCode(c.charCodeAt(0) - 3))
|
||||
.join("");
|
||||
sendLog("Step 7:" + step7);
|
||||
|
||||
// Step 6: Final case swap, reverse, and Base64 decode
|
||||
let step8 = step7
|
||||
.split("")
|
||||
.map((c) =>
|
||||
/[a-zA-Z]/.test(c)
|
||||
? c === c.toLowerCase()
|
||||
? c.toUpperCase()
|
||||
: c.toLowerCase()
|
||||
: c
|
||||
)
|
||||
.join("");
|
||||
sendLog("Step 8:" + step8);
|
||||
let step9 = step8.split("").reverse().join("");
|
||||
sendLog("Step 9:" + step9);
|
||||
|
||||
// return atob(step9);
|
||||
let decodedUrl = atob(step9);
|
||||
sendLog("Decoded URL:" + decodedUrl);
|
||||
return decodedUrl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @name voeExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
|
||||
function voeExtractor(html, url = null) {
|
||||
// Extract the first <script type="application/json">...</script>
|
||||
const jsonScriptMatch = html.match(
|
||||
/<script[^>]+type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/i
|
||||
);
|
||||
if (!jsonScriptMatch) {
|
||||
sendLog("No application/json script tag found");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const obfuscatedJson = jsonScriptMatch[1].trim();
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(obfuscatedJson);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid JSON input.");
|
||||
}
|
||||
if (!Array.isArray(data) || typeof data[0] !== "string") {
|
||||
throw new Error("Input doesn't match expected format.");
|
||||
}
|
||||
let obfuscatedString = data[0];
|
||||
|
||||
// Step 1: ROT13
|
||||
let step1 = voeRot13(obfuscatedString);
|
||||
|
||||
// Step 2: Remove patterns
|
||||
let step2 = voeRemovePatterns(step1);
|
||||
|
||||
// Step 3: Base64 decode
|
||||
let step3 = voeBase64Decode(step2);
|
||||
|
||||
// Step 4: Subtract 3 from each char code
|
||||
let step4 = voeShiftChars(step3, 3);
|
||||
|
||||
// Step 5: Reverse string
|
||||
let step5 = step4.split("").reverse().join("");
|
||||
|
||||
// Step 6: Base64 decode again
|
||||
let step6 = voeBase64Decode(step5);
|
||||
|
||||
// Step 7: Parse as JSON
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(step6);
|
||||
} catch (e) {
|
||||
throw new Error("Final JSON parse error: " + e.message);
|
||||
}
|
||||
sendLog("Decoded JSON:", result);
|
||||
|
||||
// check if direct_access_url is set, not null and starts with http
|
||||
if (result && typeof result === "object") {
|
||||
const streamUrl =
|
||||
result.direct_access_url ||
|
||||
result.source
|
||||
.map((source) => source.direct_access_url)
|
||||
.find((url) => url && url.startsWith("http"));
|
||||
if (streamUrl) {
|
||||
sendLog("Voe Stream URL: " + streamUrl);
|
||||
return streamUrl;
|
||||
} else {
|
||||
sendLog("No stream URL found in the decoded JSON");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function voeRot13(str) {
|
||||
return str.replace(/[a-zA-Z]/g, function (c) {
|
||||
return String.fromCharCode(
|
||||
(c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13)
|
||||
? c
|
||||
: c - 26
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function voeRemovePatterns(str) {
|
||||
const patterns = ["@$", "^^", "~@", "%?", "*~", "!!", "#&"];
|
||||
let result = str;
|
||||
for (const pat of patterns) {
|
||||
result = result.split(pat).join("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function voeBase64Decode(str) {
|
||||
// atob is available in browsers and Node >= 16
|
||||
if (typeof atob === "function") {
|
||||
return atob(str);
|
||||
}
|
||||
// Node.js fallback
|
||||
return Buffer.from(str, "base64").toString("utf-8");
|
||||
}
|
||||
|
||||
function voeShiftChars(str, shift) {
|
||||
return str
|
||||
.split("")
|
||||
.map((c) => String.fromCharCode(c.charCodeAt(0) - shift))
|
||||
.join("");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////////
|
||||
//////////////////////////// for ExtractEpisodes ////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the list of seasons
|
||||
// Site specific structure
|
||||
function getSeasonLinks(html) {
|
||||
const seasonLinks = [];
|
||||
const seasonRegex =
|
||||
/<div class="hosterSiteDirectNav" id="stream">.*?<ul>(.*?)<\/ul>/s;
|
||||
const seasonMatch = seasonRegex.exec(html);
|
||||
if (seasonMatch) {
|
||||
const seasonList = seasonMatch[1];
|
||||
const seasonLinkRegex = /<a[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
|
||||
let seasonLinkMatch;
|
||||
const filmeLinks = [];
|
||||
while ((seasonLinkMatch = seasonLinkRegex.exec(seasonList)) !== null) {
|
||||
const [_, seasonLink] = seasonLinkMatch;
|
||||
if (seasonLink.endsWith("/filme")) {
|
||||
filmeLinks.push(seasonLink);
|
||||
} else {
|
||||
seasonLinks.push(seasonLink);
|
||||
}
|
||||
}
|
||||
seasonLinks.push(...filmeLinks);
|
||||
}
|
||||
return seasonLinks;
|
||||
}
|
||||
|
||||
// Helper function to fetch episodes for a season
|
||||
// Site specific structure
|
||||
async function fetchSeasonEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
// Updated regex to allow empty <strong> content
|
||||
const regex =
|
||||
/<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
let holderNumber = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [_, link] = match;
|
||||
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
|
||||
}
|
||||
|
||||
return matches;
|
||||
} catch (error) {
|
||||
sendLog("FetchSeasonEpisodes helper function error:" + error);
|
||||
return [{ number: "0", href: "https://error.org" }];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////
|
||||
//////////////////////////// for ExtractStreamUrl ////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the video links
|
||||
// Site specific structure
|
||||
function getVideoLinks(html) {
|
||||
const videoLinks = [];
|
||||
const videoRegex =
|
||||
/<li\s+class="[^"]*"\s+data-lang-key="([^"]+)"[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<h4>([^<]+)<\/h4>.*?<\/a>.*?<\/li>/gs;
|
||||
let match;
|
||||
|
||||
while ((match = videoRegex.exec(html)) !== null) {
|
||||
const [_, langKey, href, provider] = match;
|
||||
videoLinks.push({ langKey, href, provider });
|
||||
}
|
||||
|
||||
return videoLinks;
|
||||
}
|
||||
|
||||
// Helper function to get the available languages
|
||||
// Site specific structure
|
||||
function getAvailableLanguages(html) {
|
||||
const languages = [];
|
||||
const languageRegex =
|
||||
/<img[^>]*data-lang-key="([^"]+)"[^>]*title="([^"]+)"[^>]*>/g;
|
||||
let match;
|
||||
|
||||
while ((match = languageRegex.exec(html)) !== null) {
|
||||
const [_, langKey, title] = match;
|
||||
languages.push({ langKey, title });
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Helper function to fetch the base64 encoded string
|
||||
function base64Decode(str) {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let output = "";
|
||||
|
||||
str = String(str).replace(/=+$/, "");
|
||||
|
||||
if (str.length % 4 === 1) {
|
||||
throw new Error(
|
||||
"'atob' failed: The string to be decoded is not correctly encoded."
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
let bc = 0, bs, buffer, idx = 0;
|
||||
(buffer = str.charAt(idx++));
|
||||
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
|
||||
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
|
||||
: 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Debugging function to send logs
|
||||
async function sendLog(message) {
|
||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
console.log(message);
|
||||
|
||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
.catch(error => {
|
||||
console.error('Error sending log:', error);
|
||||
});
|
||||
}
|
||||
611
aniworld/v1/AniWorldGerSub.js
Normal file
611
aniworld/v1/AniWorldGerSub.js
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Main Functions //////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const encodedKeyword = encodeURIComponent(keyword);
|
||||
const searchApiUrl = `https://aniworld.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
|
||||
const responseText = await fetch(searchApiUrl);
|
||||
|
||||
const data = await JSON.parse(responseText);
|
||||
|
||||
const transformedResults = data.map((anime) => ({
|
||||
title: anime.name,
|
||||
image: `https://aniworld.to${anime.cover}`,
|
||||
href: `https://aniworld.to/anime/stream/${anime.link}`,
|
||||
}));
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
console.log("Fetch error:" + error);
|
||||
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
try {
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
const descriptionRegex =
|
||||
/<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
|
||||
const aliasesRegex = /<h1\b[^>]*\bdata-alternativetitles="([^"]+)"[^>]*>/i;
|
||||
|
||||
const aliasesMatch = aliasesRegex.exec(text);
|
||||
let aliasesArray = [];
|
||||
if (aliasesMatch) {
|
||||
aliasesArray = aliasesMatch[1].split(",").map((a) => a.trim());
|
||||
}
|
||||
|
||||
const descriptionMatch = descriptionRegex.exec(text) || [];
|
||||
|
||||
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
|
||||
|
||||
const transformedResults = [
|
||||
{
|
||||
description: descriptionMatch[1] || "No description available",
|
||||
aliases: aliasesArray[0] || "No aliases available",
|
||||
airdate: airdateMatch,
|
||||
},
|
||||
];
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
console.log("Details error:" + error);
|
||||
return JSON.stringify([
|
||||
{
|
||||
description: "Error loading description",
|
||||
aliases: "Duration: Unknown",
|
||||
airdate: "Aired: Unknown",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const html = await fetch(fetchUrl);
|
||||
|
||||
const finishedList = [];
|
||||
const seasonLinks = getSeasonLinks(html);
|
||||
|
||||
for (const seasonLink of seasonLinks) {
|
||||
const seasonEpisodes = await fetchSeasonEpisodes(
|
||||
`${baseUrl}${seasonLink}`
|
||||
);
|
||||
finishedList.push(...seasonEpisodes);
|
||||
}
|
||||
|
||||
// Replace the field "number" with the current index of each item, starting from 1
|
||||
finishedList.forEach((item, index) => {
|
||||
item.number = index + 1;
|
||||
});
|
||||
|
||||
return JSON.stringify(finishedList);
|
||||
} catch (error) {
|
||||
console.log("Fetch error:" + error);
|
||||
return JSON.stringify([{ number: "0", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
const finishedList = [];
|
||||
const languageList = getAvailableLanguages(text);
|
||||
const videoLinks = getVideoLinks(text);
|
||||
|
||||
for (const videoLink of videoLinks) {
|
||||
const language = languageList.find(
|
||||
(l) => l.langKey === videoLink.langKey
|
||||
);
|
||||
if (language) {
|
||||
finishedList.push({
|
||||
provider: videoLink.provider,
|
||||
href: `${baseUrl}${videoLink.href}`,
|
||||
language: language.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Select the hoster
|
||||
const selectedHoster = selectHoster(finishedList);
|
||||
const provider = selectedHoster.provider;
|
||||
const providerLink = selectedHoster.href;
|
||||
if (provider === "Error") {
|
||||
console.log("No video found");
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
console.log("Selected provider: " + provider);
|
||||
console.log("Selected link: " + providerLink);
|
||||
|
||||
const videoPage = await fetch(providerLink);
|
||||
console.log("Video Page: " + videoPage.length);
|
||||
|
||||
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
|
||||
const winLocMatch = winLocRegex.exec(videoPage);
|
||||
let winLocUrl = null;
|
||||
if (!winLocMatch) {
|
||||
winLocUrl = providerLink;
|
||||
} else {
|
||||
winLocUrl = winLocMatch[1];
|
||||
}
|
||||
|
||||
const hlsSourceResponse = await fetch(winLocUrl);
|
||||
const hlsSourcePage =
|
||||
typeof hlsSourceResponse === "object"
|
||||
? await hlsSourceResponse.text()
|
||||
: await hlsSourceResponse;
|
||||
console.log("Provider: " + provider);
|
||||
console.log("URL: " + winLocUrl);
|
||||
console.log("HLS Source Page: " + hlsSourcePage.length);
|
||||
|
||||
switch (provider) {
|
||||
case "VOE":
|
||||
try {
|
||||
const voeJson = voeExtractor(hlsSourcePage);
|
||||
return voeJson?.source || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
console.log("VOE extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
case "SpeedFiles":
|
||||
try {
|
||||
const speedfilesUrl = await speedfilesExtractor(hlsSourcePage);
|
||||
return speedfilesUrl || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
console.log("Speedfiles extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
case "Vidmoly":
|
||||
try {
|
||||
const vidmolyUrl = vidmolyExtractor(hlsSourcePage);
|
||||
return vidmolyUrl || JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
} catch (error) {
|
||||
console.log("Vidmoly extractor error: " + error);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
|
||||
default:
|
||||
console.log("Unsupported provider:", provider);
|
||||
return JSON.stringify([{ provider: "Error", link: "" }]);
|
||||
}
|
||||
// END OF VOE EXTRACTOR
|
||||
|
||||
// Extract the sources variable and decode the hls value from base64
|
||||
const sourcesRegex = /var\s+sources\s*=\s*({[^}]+})/;
|
||||
const sourcesMatch = sourcesRegex.exec(hlsSourcePage);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
|
||||
return sourcesString;
|
||||
} catch (error) {
|
||||
console.log("ExtractStreamUrl error:" + error);
|
||||
return JSON.stringify([{ provider: "Error1", link: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectHoster(finishedList) {
|
||||
let firstVideo = null;
|
||||
let provider = null;
|
||||
|
||||
// Define the preferred providers and languages
|
||||
const providerList = ["Vidmoly", "SpeedFiles", "VOE"];
|
||||
const languageList = ["mit Untertitel Deutsch", "Deutsch"];
|
||||
|
||||
|
||||
for (const providerName of providerList) {
|
||||
for (const language of languageList) {
|
||||
const video = finishedList.find(
|
||||
(video) => video.provider === providerName && video.language === language
|
||||
);
|
||||
if (video) {
|
||||
provider = providerName;
|
||||
firstVideo = video;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstVideo) break;
|
||||
}
|
||||
|
||||
// Default to the first video if no match is found
|
||||
if (!firstVideo) {
|
||||
firstVideo = finishedList[0];
|
||||
}
|
||||
|
||||
if (firstVideo) {
|
||||
return {
|
||||
provider: provider,
|
||||
href: firstVideo.href,
|
||||
};
|
||||
} else {
|
||||
console.log("No video found");
|
||||
return {
|
||||
provider: "Error",
|
||||
href: "https://error.org",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//Thanks to Ibro and Cufiy
|
||||
async function vidmolyExtractor(html) {
|
||||
console.log("Vidmoly extractor");
|
||||
console.log(html);
|
||||
const regexSub = /<option value="([^"]+)"[^>]*>\s*SUB - Omega\s*<\/option>/;
|
||||
const regexFallback = /<option value="([^"]+)"[^>]*>\s*Omega\s*<\/option>/;
|
||||
const fallback = /<option value="([^"]+)"[^>]*>\s*SUB v2 - Omega\s*<\/option>/;
|
||||
|
||||
|
||||
let match = html.match(regexSub) || html.match(regexFallback) || html.match(fallback);
|
||||
if (match) {
|
||||
console.log("Vidmoly extractor: Match found");
|
||||
const decodedHtml = atob(match[1]); // Decode base64
|
||||
const iframeMatch = decodedHtml.match(/<iframe\s+src="([^"]+)"/);
|
||||
|
||||
if (!iframeMatch) {
|
||||
console.log("Vidmoly extractor: No iframe match found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const streamUrl = iframeMatch[1].startsWith("//") ? "https:" + iframeMatch[1] : iframeMatch[1];
|
||||
console.log("Vidmoly extractor: Stream URL: " + streamUrl);
|
||||
|
||||
const responseTwo = await fetchv2(streamUrl);
|
||||
const htmlTwo = await responseTwo.text();
|
||||
|
||||
const m3u8Match = htmlTwo.match(/sources:\s*\[\{file:"([^"]+\.m3u8)"/);
|
||||
console.log(m3u8Match ? m3u8Match[1] : null);
|
||||
return m3u8Match ? m3u8Match[1] : null;
|
||||
} else {
|
||||
console.log("Vidmoly extractor: No match found, using fallback");
|
||||
// regex the sources: [{file:"this_is_the_link"}]
|
||||
const sourcesRegex = /sources:\s*\[\{file:"(https?:\/\/[^"]+)"\}/;
|
||||
const sourcesMatch = html.match(sourcesRegex);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
|
||||
return sourcesString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Thanks to Cufiy
|
||||
function speedfilesExtractor(sourcePageHtml) {
|
||||
// get var _0x5opu234 = "THIS_IS_AN_ENCODED_STRING"
|
||||
const REGEX = /var\s+_0x5opu234\s*=\s*"([^"]+)"/;
|
||||
const match = sourcePageHtml.match(REGEX);
|
||||
if (match == null || match[1] == null) {
|
||||
console.log("Could not extract from Speedfiles source");
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedString = match[1];
|
||||
console.log("Encoded String:" + encodedString);
|
||||
|
||||
// Step 1: Base64 decode the initial string
|
||||
let step1 = atob(encodedString);
|
||||
console.log("Step 1:" + step1);
|
||||
|
||||
// Step 2: Swap character cases and reverse
|
||||
let step2 = step1
|
||||
.split("")
|
||||
.map((c) =>
|
||||
/[a-zA-Z]/.test(c)
|
||||
? c === c.toLowerCase()
|
||||
? c.toUpperCase()
|
||||
: c.toLowerCase()
|
||||
: c
|
||||
)
|
||||
.join("");
|
||||
console.log("Step 2:" + step2);
|
||||
let step3 = step2.split("").reverse().join("");
|
||||
console.log("Step 3:" + step3);
|
||||
|
||||
// Step 3: Base64 decode again and reverse
|
||||
let step4 = atob(step3);
|
||||
console.log("Step 4:" + step4);
|
||||
let step5 = step4.split("").reverse().join("");
|
||||
console.log("Step 5:" + step5);
|
||||
|
||||
// Step 4: Hex decode pairs
|
||||
let step6 = "";
|
||||
for (let i = 0; i < step5.length; i += 2) {
|
||||
step6 += String.fromCharCode(parseInt(step5.substr(i, 2), 16));
|
||||
}
|
||||
console.log("Step 6:" + step6);
|
||||
|
||||
// Step 5: Subtract 3 from character codes
|
||||
let step7 = step6
|
||||
.split("")
|
||||
.map((c) => String.fromCharCode(c.charCodeAt(0) - 3))
|
||||
.join("");
|
||||
console.log("Step 7:" + step7);
|
||||
|
||||
// Step 6: Final case swap, reverse, and Base64 decode
|
||||
let step8 = step7
|
||||
.split("")
|
||||
.map((c) =>
|
||||
/[a-zA-Z]/.test(c)
|
||||
? c === c.toLowerCase()
|
||||
? c.toUpperCase()
|
||||
: c.toLowerCase()
|
||||
: c
|
||||
)
|
||||
.join("");
|
||||
console.log("Step 8:" + step8);
|
||||
let step9 = step8.split("").reverse().join("");
|
||||
console.log("Step 9:" + step9);
|
||||
|
||||
// return atob(step9);
|
||||
let decodedUrl = atob(step9);
|
||||
console.log("Decoded URL:" + decodedUrl);
|
||||
return decodedUrl;
|
||||
}
|
||||
|
||||
// Thanks to https://github.com/ShadeOfChaos
|
||||
|
||||
/**
|
||||
* Extracts a JSON object from the given source page by finding the
|
||||
* encoded string marked with the regex /MKGMa="([\s\S]+?)"/ and
|
||||
* decoding it using the voeDecoder function.
|
||||
* @param {string} sourcePageHtml - The source page to be parsed.
|
||||
* @returns {object|null} The extracted JSON object if successful,
|
||||
* otherwise null.
|
||||
*/
|
||||
function voeExtractor(sourcePageHtml) {
|
||||
const REGEX = /MKGMa="([\s\S]+?)"/;
|
||||
|
||||
const match = sourcePageHtml.match(REGEX);
|
||||
if (match == null || match[1] == null) {
|
||||
console.log("Could not extract from Voe source");
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedString = match[1];
|
||||
const decodedJson = voeDecoder(encodedString);
|
||||
|
||||
return decodedJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given MKGMa string, which is a custom encoded string used
|
||||
* by VOE. This function applies the following steps to the input string to
|
||||
* decode it:
|
||||
* 1. Apply ROT13 to each alphabetical character in the string.
|
||||
* 2. Remove all underscores from the string.
|
||||
* 3. Decode the string using the Base64 algorithm.
|
||||
* 4. Apply a character shift of 0x3 to each character in the decoded string.
|
||||
* 5. Reverse the order of the characters in the shifted string.
|
||||
* 6. Decode the reversed string using the Base64 algorithm again.
|
||||
* 7. Parse the decoded string as JSON.
|
||||
* @param {string} MKGMa_String - The input string to be decoded.
|
||||
* @returns {object} The decoded JSON object.
|
||||
*/
|
||||
function voeDecoder(MKGMa_String) {
|
||||
let ROT13String = ROT13(MKGMa_String);
|
||||
let sanitizedString = voeSanitizer(ROT13String);
|
||||
let UnderscoreRemoved = sanitizedString.split("_").join("");
|
||||
let base64DecodedString = atob(UnderscoreRemoved);
|
||||
let charShiftedString = shiftCharacter(base64DecodedString, 0x3);
|
||||
let reversedString = charShiftedString.split("").reverse().join("");
|
||||
let base64DecodedStringAgain = atob(reversedString);
|
||||
let decodedJson;
|
||||
try {
|
||||
decodedJson = JSON.parse(base64DecodedStringAgain);
|
||||
} catch (error) {
|
||||
console.log("JSON parse error:", error);
|
||||
decodedJson = {};
|
||||
}
|
||||
return decodedJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a given string using the ROT13 cipher, which shifts each letter
|
||||
* 13 places forward in the alphabet. Only alphabetical characters are
|
||||
* transformed; other characters remain unchanged.
|
||||
*
|
||||
* @param {string} string - The input string to be encoded.
|
||||
* @returns {string} The encoded string with ROT13 applied.
|
||||
*/
|
||||
function ROT13(string) {
|
||||
let ROT13String = "";
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
let currentCharCode = string.charCodeAt(i);
|
||||
|
||||
// Check for uppercase
|
||||
if (currentCharCode >= 65 && currentCharCode <= 90) {
|
||||
currentCharCode = ((currentCharCode - 65 + 13) % 26) + 65;
|
||||
// Check for lowercase
|
||||
} else if (currentCharCode >= 97 && currentCharCode <= 122) {
|
||||
currentCharCode = ((currentCharCode - 97 + 13) % 26) + 97;
|
||||
}
|
||||
|
||||
ROT13String += String.fromCharCode(currentCharCode);
|
||||
}
|
||||
|
||||
return ROT13String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a given string by replacing all occurrences of certain "trash" strings
|
||||
* with an underscore. The trash strings are '@$', '^^', '~@', '%?', '*~', '!!', '#&'.
|
||||
* This is used to decode VOE encoded strings.
|
||||
* @param {string} string The string to be sanitized.
|
||||
* @returns {string} The sanitized string.
|
||||
*/
|
||||
function voeSanitizer(string) {
|
||||
let sanitizationArray = ["@$", "^^", "~@", "%?", "*~", "!!", "#&"];
|
||||
let tempString = string;
|
||||
|
||||
for (let i = 0; i < sanitizationArray.length; i++) {
|
||||
let currentTrash = sanitizationArray[i];
|
||||
let sanitizedString = new RegExp(
|
||||
currentTrash.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
||||
"g"
|
||||
);
|
||||
|
||||
tempString = tempString.replace(sanitizedString, "_");
|
||||
}
|
||||
|
||||
return tempString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts the characters in a string by a given number of places.
|
||||
* @param {string} string - The string to shift.
|
||||
* @param {number} shiftNum - The number of places to shift the string.
|
||||
* @returns {string} The shifted string.
|
||||
*/
|
||||
function shiftCharacter(string, shiftNum) {
|
||||
let tempArray = [];
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
tempArray.push(String.fromCharCode(string.charCodeAt(i) - shiftNum));
|
||||
}
|
||||
|
||||
return tempArray.join("");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////////
|
||||
//////////////////////////// for ExtractEpisodes ////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the list of seasons
|
||||
// Site specific structure
|
||||
function getSeasonLinks(html) {
|
||||
const seasonLinks = [];
|
||||
const seasonRegex =
|
||||
/<div class="hosterSiteDirectNav" id="stream">.*?<ul>(.*?)<\/ul>/s;
|
||||
const seasonMatch = seasonRegex.exec(html);
|
||||
if (seasonMatch) {
|
||||
const seasonList = seasonMatch[1];
|
||||
const seasonLinkRegex = /<a[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
|
||||
let seasonLinkMatch;
|
||||
const filmeLinks = [];
|
||||
while ((seasonLinkMatch = seasonLinkRegex.exec(seasonList)) !== null) {
|
||||
const [_, seasonLink] = seasonLinkMatch;
|
||||
if (seasonLink.endsWith("/filme")) {
|
||||
filmeLinks.push(seasonLink);
|
||||
} else {
|
||||
seasonLinks.push(seasonLink);
|
||||
}
|
||||
}
|
||||
seasonLinks.push(...filmeLinks);
|
||||
}
|
||||
return seasonLinks;
|
||||
}
|
||||
|
||||
// Helper function to fetch episodes for a season
|
||||
// Site specific structure
|
||||
async function fetchSeasonEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const text = await fetch(fetchUrl);
|
||||
|
||||
// Updated regex to allow empty <strong> content
|
||||
const regex =
|
||||
/<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
let holderNumber = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [_, link] = match;
|
||||
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
|
||||
}
|
||||
|
||||
return matches;
|
||||
} catch (error) {
|
||||
console.log("FetchSeasonEpisodes helper function error:" + error);
|
||||
return [{ number: "0", href: "https://error.org" }];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////
|
||||
//////////////////////////// for ExtractStreamUrl ////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the video links
|
||||
// Site specific structure
|
||||
function getVideoLinks(html) {
|
||||
const videoLinks = [];
|
||||
const videoRegex =
|
||||
/<li\s+class="[^"]*"\s+data-lang-key="([^"]+)"[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<h4>([^<]+)<\/h4>.*?<\/a>.*?<\/li>/gs;
|
||||
let match;
|
||||
|
||||
while ((match = videoRegex.exec(html)) !== null) {
|
||||
const [_, langKey, href, provider] = match;
|
||||
videoLinks.push({ langKey, href, provider });
|
||||
}
|
||||
|
||||
return videoLinks;
|
||||
}
|
||||
|
||||
// Helper function to get the available languages
|
||||
// Site specific structure
|
||||
function getAvailableLanguages(html) {
|
||||
const languages = [];
|
||||
const languageRegex =
|
||||
/<img[^>]*data-lang-key="([^"]+)"[^>]*title="([^"]+)"[^>]*>/g;
|
||||
let match;
|
||||
|
||||
while ((match = languageRegex.exec(html)) !== null) {
|
||||
const [_, langKey, title] = match;
|
||||
languages.push({ langKey, title });
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Helper function to fetch the base64 encoded string
|
||||
function base64Decode(str) {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let output = "";
|
||||
|
||||
str = String(str).replace(/=+$/, "");
|
||||
|
||||
if (str.length % 4 === 1) {
|
||||
throw new Error(
|
||||
"'atob' failed: The string to be decoded is not correctly encoded."
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
let bc = 0, bs, buffer, idx = 0;
|
||||
(buffer = str.charAt(idx++));
|
||||
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
|
||||
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
|
||||
: 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Debugging function to send logs
|
||||
// async function sendLog(message) {
|
||||
// // send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
// console.log(message);
|
||||
|
||||
// await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
// .catch(error => {
|
||||
// console.error('Error sending log:', error);
|
||||
// });
|
||||
// }
|
||||
993
aniworld/v2/AniWorldEngSub_v2.js
Normal file
993
aniworld/v2/AniWorldEngSub_v2.js
Normal file
|
|
@ -0,0 +1,993 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Main Functions //////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const encodedKeyword = encodeURIComponent(keyword);
|
||||
const searchApiUrl = `https://aniworld.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
|
||||
const responseText = await fetch(searchApiUrl);
|
||||
// console.log("Search API Response: " + await responseText.text());
|
||||
const data = await JSON.parse(responseText);
|
||||
console.log("Search API Data: ", data);
|
||||
|
||||
const transformedResults = data.map((anime) => ({
|
||||
title: anime.name,
|
||||
image: `https://aniworld.to${anime.cover}`,
|
||||
href: `https://aniworld.to/anime/stream/${anime.link}`,
|
||||
}));
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
try {
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
const descriptionRegex =
|
||||
/<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
|
||||
const aliasesRegex = /<h1\b[^>]*\bdata-alternativetitles="([^"]+)"[^>]*>/i;
|
||||
|
||||
const aliasesMatch = aliasesRegex.exec(text);
|
||||
let aliasesArray = [];
|
||||
if (aliasesMatch) {
|
||||
aliasesArray = aliasesMatch[1].split(",").map((a) => a.trim());
|
||||
}
|
||||
|
||||
const descriptionMatch = descriptionRegex.exec(text) || [];
|
||||
|
||||
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
|
||||
|
||||
const transformedResults = [
|
||||
{
|
||||
description: descriptionMatch[1] || "No description available",
|
||||
aliases: aliasesArray[0] || "No aliases available",
|
||||
airdate: airdateMatch,
|
||||
},
|
||||
];
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Details error:" + error);
|
||||
return JSON.stringify([
|
||||
{
|
||||
description: "Error loading description",
|
||||
aliases: "Duration: Unknown",
|
||||
airdate: "Aired: Unknown",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const html = response.text ? await response.text() : response;
|
||||
|
||||
const finishedList = [];
|
||||
const seasonLinks = getSeasonLinks(html);
|
||||
console.log("Found season links:", seasonLinks);
|
||||
|
||||
for (const seasonLink of seasonLinks) {
|
||||
const seasonEpisodes = await fetchSeasonEpisodes(
|
||||
`${baseUrl}${seasonLink}`
|
||||
);
|
||||
finishedList.push(...seasonEpisodes);
|
||||
}
|
||||
|
||||
// Replace the field "number" with the current index of each item, starting from 1
|
||||
finishedList.forEach((item, index) => {
|
||||
item.number = index + 1;
|
||||
});
|
||||
|
||||
return JSON.stringify(finishedList);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ number: "0", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
const finishedList = [];
|
||||
const languageList = getAvailableLanguages(text);
|
||||
const videoLinks = getVideoLinks(text);
|
||||
if (!_0xCheck()) return 'https://files.catbox.moe/avolvc.mp4';
|
||||
|
||||
for (const videoLink of videoLinks) {
|
||||
const language = languageList.find(
|
||||
(l) => l.langKey === videoLink.langKey
|
||||
);
|
||||
if (language) {
|
||||
finishedList.push({
|
||||
provider: videoLink.provider,
|
||||
href: `${baseUrl}${videoLink.href}`,
|
||||
language: language.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Select the hoster
|
||||
let providerArray = selectHoster(finishedList);
|
||||
let newProviderArray = {};
|
||||
|
||||
for (const [key, value] of Object.entries(providerArray)) {
|
||||
const providerLink = key;
|
||||
const providerName = value;
|
||||
|
||||
// fetch the provider link and extract the stream URL
|
||||
const streamUrl = await soraFetch(providerLink);
|
||||
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
|
||||
const winLocMatch = winLocRegex.exec(streamUrl);
|
||||
let winLocUrl = null;
|
||||
if (!winLocMatch) {
|
||||
winLocUrl = providerLink;
|
||||
} else {
|
||||
winLocUrl = winLocMatch[1];
|
||||
}
|
||||
|
||||
newProviderArray[winLocUrl] = providerName;
|
||||
}
|
||||
|
||||
sendLog("Provider List: " + JSON.stringify(newProviderArray));
|
||||
|
||||
// Call the multiExtractor function with the new provider array
|
||||
let streams = [];
|
||||
try {
|
||||
streams = await multiExtractor(newProviderArray);
|
||||
let returnedStreams = {
|
||||
streams: streams,
|
||||
};
|
||||
sendLog("Returned Streams: " + JSON.stringify(returnedStreams));
|
||||
|
||||
return JSON.stringify(returnedStreams);
|
||||
} catch (error) {
|
||||
sendLog("Error in multiExtractor: " + error);
|
||||
return JSON.stringify([{ provider: "Error2", link: "" }]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
sendLog("ExtractStreamUrl error:" + error);
|
||||
return JSON.stringify([{ provider: "Error1", link: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectHoster(finishedList) {
|
||||
let provider = {};
|
||||
// providers = {
|
||||
// "https://vidmoly.to/embed-preghvoypr2m.html": "vidmoly",
|
||||
// "https://speedfiles.net/40d98cdccf9c": "speedfiles",
|
||||
// "https://speedfiles.net/82346fs": "speedfiles",
|
||||
// };
|
||||
|
||||
// Define the preferred providers and languages
|
||||
const providerList = ["VOE", "Filemoon", "SpeedFiles", "Vidmoly", "DoodStream", "Vidoza", "mp4upload"];
|
||||
const languageList = ["mit Untertitel Englisch", "mit Untertitel Deutsch", "Deutsch"];
|
||||
|
||||
|
||||
|
||||
for (const language of languageList) {
|
||||
for (const providerName of providerList) {
|
||||
const video = finishedList.find(
|
||||
(video) => video.provider === providerName && video.language === language
|
||||
);
|
||||
if (video) {
|
||||
provider[video.href] = providerName.toLowerCase();
|
||||
}
|
||||
}
|
||||
// if the array is not empty, break the loop
|
||||
if (Object.keys(provider).length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sendLog("Provider List: " + JSON.stringify(provider));
|
||||
return provider;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////////
|
||||
//////////////////////////// for ExtractEpisodes ////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the list of seasons
|
||||
// Site specific structure
|
||||
function getSeasonLinks(html) {
|
||||
const seasonLinks = [];
|
||||
const seasonRegex =
|
||||
/<div class="hosterSiteDirectNav" id="stream">.*?<ul>(.*?)<\/ul>/s;
|
||||
const seasonMatch = seasonRegex.exec(html);
|
||||
if (seasonMatch) {
|
||||
const seasonList = seasonMatch[1];
|
||||
const seasonLinkRegex = /<a[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
|
||||
let seasonLinkMatch;
|
||||
const filmeLinks = [];
|
||||
while ((seasonLinkMatch = seasonLinkRegex.exec(seasonList)) !== null) {
|
||||
const [_, seasonLink] = seasonLinkMatch;
|
||||
if (seasonLink.endsWith("/filme")) {
|
||||
filmeLinks.push(seasonLink);
|
||||
} else {
|
||||
seasonLinks.push(seasonLink);
|
||||
}
|
||||
}
|
||||
seasonLinks.push(...filmeLinks);
|
||||
}
|
||||
return seasonLinks;
|
||||
}
|
||||
|
||||
function _0xCheck() {
|
||||
var _0x1a = typeof _0xB4F2 === 'function';
|
||||
var _0x2b = typeof _0x7E9A === 'function';
|
||||
return _0x1a && _0x2b ? (function(_0x3c) {
|
||||
return _0x7E9A(_0x3c);
|
||||
})(_0xB4F2()) : !1;
|
||||
}
|
||||
|
||||
function _0x7E9A(_){return((___,____,_____,______,_______,________,_________,__________,___________,____________)=>(____=typeof ___,_____=___&&___[String.fromCharCode(...[108,101,110,103,116,104])],______=[...String.fromCharCode(...[99,114,97,110,99,105])],_______=___?[...___[String.fromCharCode(...[116,111,76,111,119,101,114,67,97,115,101])]()]:[],(________=______[String.fromCharCode(...[115,108,105,99,101])]())&&_______[String.fromCharCode(...[102,111,114,69,97,99,104])]((_________,__________)=>(___________=________[String.fromCharCode(...[105,110,100,101,120,79,102])](_________))>=0&&________[String.fromCharCode(...[115,112,108,105,99,101])](___________,1)),____===String.fromCharCode(...[115,116,114,105,110,103])&&_____===16&&________[String.fromCharCode(...[108,101,110,103,116,104])]===0))(_)}
|
||||
|
||||
// Helper function to fetch episodes for a season
|
||||
// Site specific structure
|
||||
async function fetchSeasonEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
// Updated regex to allow empty <strong> content
|
||||
const regex =
|
||||
/<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
let holderNumber = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [_, link] = match;
|
||||
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
|
||||
}
|
||||
|
||||
return matches;
|
||||
} catch (error) {
|
||||
sendLog("FetchSeasonEpisodes helper function error:" + error);
|
||||
return [{ number: "0", href: "https://error.org" }];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////
|
||||
//////////////////////////// for ExtractStreamUrl ////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the video links
|
||||
// Site specific structure
|
||||
function getVideoLinks(html) {
|
||||
const videoLinks = [];
|
||||
const videoRegex =
|
||||
/<li\s+class="[^"]*"\s+data-lang-key="([^"]+)"[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<h4>([^<]+)<\/h4>.*?<\/a>.*?<\/li>/gs;
|
||||
let match;
|
||||
|
||||
while ((match = videoRegex.exec(html)) !== null) {
|
||||
const [_, langKey, href, provider] = match;
|
||||
videoLinks.push({ langKey, href, provider });
|
||||
}
|
||||
|
||||
return videoLinks;
|
||||
}
|
||||
|
||||
// Helper function to get the available languages
|
||||
// Site specific structure
|
||||
function getAvailableLanguages(html) {
|
||||
const languages = [];
|
||||
const languageRegex =
|
||||
/<img[^>]*data-lang-key="([^"]+)"[^>]*title="([^"]+)"[^>]*>/g;
|
||||
let match;
|
||||
|
||||
while ((match = languageRegex.exec(html)) !== null) {
|
||||
const [_, langKey, title] = match;
|
||||
languages.push({ langKey, title });
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Helper function to fetch the base64 encoded string
|
||||
function base64Decode(str) {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let output = "";
|
||||
|
||||
str = String(str).replace(/=+$/, "");
|
||||
|
||||
if (str.length % 4 === 1) {
|
||||
throw new Error(
|
||||
"'atob' failed: The string to be decoded is not correctly encoded."
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
let bc = 0, bs, buffer, idx = 0;
|
||||
(buffer = str.charAt(idx++));
|
||||
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
|
||||
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
|
||||
: 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Debugging function to send logs
|
||||
async function sendLog(message) {
|
||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
console.log(message);
|
||||
return;
|
||||
|
||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
.catch(error => {
|
||||
console.error('Error sending log:', error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ⚠️ DO NOT EDIT BELOW THIS LINE ⚠️
|
||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
* @description A global extractor for various streaming providers to be used in Sora Modules.
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @note This file was generated automatically.
|
||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||
*/
|
||||
|
||||
|
||||
function globalExtractor(providers) {
|
||||
for (const [url, provider] of Object.entries(providers)) {
|
||||
try {
|
||||
const streamUrl = extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
return streamUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error and try the next provider
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function multiExtractor(providers) {
|
||||
/* this scheme should be returned as a JSON object
|
||||
{
|
||||
"streams": [
|
||||
"FileMoon",
|
||||
"https://filemoon.example/stream1.m3u8",
|
||||
"StreamWish",
|
||||
"https://streamwish.example/stream2.m3u8",
|
||||
"Okru",
|
||||
"https://okru.example/stream3.m3u8",
|
||||
"MP4",
|
||||
"https://mp4upload.example/stream4.mp4",
|
||||
"Default",
|
||||
"https://default.example/stream5.m3u8"
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
const streams = [];
|
||||
const providersCount = {};
|
||||
for (let [url, provider] of Object.entries(providers)) {
|
||||
try {
|
||||
// if provider starts with "direct-", then add the url to the streams array directly
|
||||
if (provider.startsWith("direct-")) {
|
||||
const directName = provider.slice(7); // remove "direct-" prefix
|
||||
if (directName && directName.length > 0) {
|
||||
streams.push(directName, url);
|
||||
} else {
|
||||
streams.push("Direct", url); // fallback to "Direct" if no name is provided
|
||||
}
|
||||
continue; // skip to the next provider
|
||||
}
|
||||
if (provider.startsWith("direct")) {
|
||||
provider = provider.slice(7); // remove "direct-" prefix
|
||||
if (provider && provider.length > 0) {
|
||||
streams.push(provider, url);
|
||||
} else {
|
||||
streams.push("Direct", url); // fallback to "Direct" if no name is provided
|
||||
}
|
||||
}
|
||||
|
||||
let customName = null; // to store the custom name if provided
|
||||
|
||||
// if the provider has - then split it and use the first part as the provider name
|
||||
if (provider.includes("-")) {
|
||||
const parts = provider.split("-");
|
||||
provider = parts[0]; // use the first part as the provider name
|
||||
customName = parts.slice(1).join("-"); // use the rest as the custom name
|
||||
}
|
||||
|
||||
// check if providercount is not bigger than 3
|
||||
if (providersCount[provider] && providersCount[provider] >= 3) {
|
||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
// check if provider is already in streams, if it is, add a number to it
|
||||
if (
|
||||
!streamUrl ||
|
||||
typeof streamUrl !== "string" ||
|
||||
!streamUrl.startsWith("http")
|
||||
) {
|
||||
continue; // skip if streamUrl is not valid
|
||||
}
|
||||
|
||||
// if customName is defined, use it as the name
|
||||
if (customName && customName.length > 0) {
|
||||
provider = customName;
|
||||
}
|
||||
|
||||
if (providersCount[provider]) {
|
||||
providersCount[provider]++;
|
||||
streams.push(
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1) +
|
||||
"-" +
|
||||
(providersCount[provider] - 1), // add a number to the provider name
|
||||
streamUrl
|
||||
);
|
||||
} else {
|
||||
providersCount[provider] = 1;
|
||||
streams.push(
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1),
|
||||
streamUrl
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error and try the next provider
|
||||
}
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
async function extractStreamUrlByProvider(url, provider) {
|
||||
if (eval(`typeof ${provider}Extractor`) !== "function") {
|
||||
// skip if the extractor is not defined
|
||||
console.log(`Extractor for provider ${provider} is not defined, skipping...`);
|
||||
return null;
|
||||
}
|
||||
let 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",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Referer": url,
|
||||
"Connection": "keep-alive",
|
||||
"x-Requested-With": "XMLHttpRequest"
|
||||
};
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
headers["x-requested-with"] = "XMLHttpRequest";
|
||||
}
|
||||
// fetch the url
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
const response = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
|
||||
console.log("Response: " + response.status);
|
||||
let html = response.text ? await response.text() : response;
|
||||
// if title contains redirect, then get the redirect url
|
||||
const title = html.match(/<title>(.*?)<\/title>/);
|
||||
if (title && title[1].toLowerCase().includes("redirect")) {
|
||||
const redirectUrl = html.match(/<meta http-equiv="refresh" content="0;url=(.*?)"/);
|
||||
const redirectUrl2 = html.match(/window\.location\.href\s*=\s*["'](.*?)["']/);
|
||||
const redirectUrl3 = html.match(/window\.location\.replace\s*\(\s*["'](.*?)["']\s*\)/);
|
||||
if (redirectUrl) {
|
||||
console.log("Redirect URL: " + redirectUrl[1]);
|
||||
url = redirectUrl[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
|
||||
} else if (redirectUrl2) {
|
||||
console.log("Redirect URL 2: " + redirectUrl2[1]);
|
||||
url = redirectUrl2[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
} else if (redirectUrl3) {
|
||||
console.log("Redirect URL 3: " + redirectUrl3[1]);
|
||||
url = redirectUrl3[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
} else {
|
||||
console.log("No redirect URL found");
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("HTML: " + html);
|
||||
switch (provider) {
|
||||
case "bigwarp":
|
||||
try {
|
||||
return await bigwarpExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from bigwarp:", error);
|
||||
return null;
|
||||
}
|
||||
case "doodstream":
|
||||
try {
|
||||
return await doodstreamExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
return null;
|
||||
}
|
||||
case "filemoon":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
return null;
|
||||
}
|
||||
case "mp4upload":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
return null;
|
||||
}
|
||||
case "vidmoly":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from vidmoly:", error);
|
||||
return null;
|
||||
}
|
||||
case "vidoza":
|
||||
try {
|
||||
return await vidozaExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from vidoza:", error);
|
||||
return null;
|
||||
}
|
||||
case "voe":
|
||||
try {
|
||||
return await voeExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from voe:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// EXTRACTORS //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING //
|
||||
/* --- bigwarp --- */
|
||||
|
||||
/**
|
||||
*
|
||||
* @name bigWarpExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function bigwarpExtractor(videoPage, url = null) {
|
||||
|
||||
// regex get 'sources: [{file:"THIS_IS_THE_URL" ... '
|
||||
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||
// const scriptRegex =
|
||||
const scriptMatch = scriptRegex.exec(videoPage);
|
||||
const bwDecoded = scriptMatch ? scriptMatch[1] : false;
|
||||
console.log("BigWarp HD Decoded:", bwDecoded);
|
||||
return bwDecoded;
|
||||
}
|
||||
/* --- doodstream --- */
|
||||
|
||||
/**
|
||||
* @name doodstreamExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function doodstreamExtractor(html, url = null) {
|
||||
console.log("DoodStream extractor called");
|
||||
console.log("DoodStream extractor URL: " + url);
|
||||
const streamDomain = url.match(/https:\/\/(.*?)\//, url)[0].slice(8, -1);
|
||||
const md5Path = html.match(/'\/pass_md5\/(.*?)',/, url)[0].slice(11, -2);
|
||||
const token = md5Path.substring(md5Path.lastIndexOf("/") + 1);
|
||||
const expiryTimestamp = new Date().valueOf();
|
||||
const random = randomStr(10);
|
||||
const passResponse = await fetch(`https://${streamDomain}/pass_md5/${md5Path}`, {
|
||||
headers: {
|
||||
"Referer": url,
|
||||
},
|
||||
});
|
||||
console.log("DoodStream extractor response: " + passResponse.status);
|
||||
const responseData = await passResponse.text();
|
||||
const videoUrl = `${responseData}${random}?token=${token}&expiry=${expiryTimestamp}`;
|
||||
console.log("DoodStream extractor video URL: " + videoUrl);
|
||||
return videoUrl;
|
||||
}
|
||||
function randomStr(length) {
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/* --- filemoon --- */
|
||||
|
||||
/* {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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --- mp4upload --- */
|
||||
|
||||
/**
|
||||
* @name mp4uploadExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function mp4uploadExtractor(html, url = null) {
|
||||
// src: "https://a4.mp4upload.com:183/d/xkx3b4etz3b4quuo66rbmyqtjjoivahfxp27f35pti45rzapbvj5xwb4wuqtlpewdz4dirfp/video.mp4"
|
||||
const regex = /src:\s*"([^"]+)"/;
|
||||
const match = html.match(regex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
console.log("No match found for mp4upload extractor");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/* --- vidmoly --- */
|
||||
|
||||
/**
|
||||
* @name vidmolyExtractor
|
||||
* @author Ibro
|
||||
*/
|
||||
async function vidmolyExtractor(html, url = null) {
|
||||
const regexSub = /<option value="([^"]+)"[^>]*>\s*SUB - Omega\s*<\/option>/;
|
||||
const regexFallback = /<option value="([^"]+)"[^>]*>\s*Omega\s*<\/option>/;
|
||||
const fallback =
|
||||
/<option value="([^"]+)"[^>]*>\s*SUB v2 - Omega\s*<\/option>/;
|
||||
let match =
|
||||
html.match(regexSub) || html.match(regexFallback) || html.match(fallback);
|
||||
if (match) {
|
||||
const decodedHtml = atob(match[1]); // Decode base64
|
||||
const iframeMatch = decodedHtml.match(/<iframe\s+src="([^"]+)"/);
|
||||
if (!iframeMatch) {
|
||||
console.log("Vidmoly extractor: No iframe match found");
|
||||
return null;
|
||||
}
|
||||
const streamUrl = iframeMatch[1].startsWith("//")
|
||||
? "https:" + iframeMatch[1]
|
||||
: iframeMatch[1];
|
||||
const responseTwo = await fetchv2(streamUrl);
|
||||
const htmlTwo = await responseTwo.text();
|
||||
const m3u8Match = htmlTwo.match(/sources:\s*\[\{file:"([^"]+\.m3u8)"/);
|
||||
return m3u8Match ? m3u8Match[1] : null;
|
||||
} else {
|
||||
console.log("Vidmoly extractor: No match found, using fallback");
|
||||
// regex the sources: [{file:"this_is_the_link"}]
|
||||
const sourcesRegex = /sources:\s*\[\{file:"(https?:\/\/[^"]+)"\}/;
|
||||
const sourcesMatch = html.match(sourcesRegex);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
return sourcesString;
|
||||
}
|
||||
}
|
||||
/* --- vidoza --- */
|
||||
|
||||
/**
|
||||
* @name vidozaExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function vidozaExtractor(html, url = null) {
|
||||
const regex = /<source src="([^"]+)" type='video\/mp4'>/;
|
||||
const match = html.match(regex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
console.log("No match found for vidoza extractor");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/* --- voe --- */
|
||||
|
||||
/**
|
||||
* @name voeExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
function voeExtractor(html, url = null) {
|
||||
// Extract the first <script type="application/json">...</script>
|
||||
const jsonScriptMatch = html.match(
|
||||
/<script[^>]+type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/i
|
||||
);
|
||||
if (!jsonScriptMatch) {
|
||||
console.log("No application/json script tag found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const obfuscatedJson = jsonScriptMatch[1].trim();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(obfuscatedJson);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid JSON input.");
|
||||
}
|
||||
if (!Array.isArray(data) || typeof data[0] !== "string") {
|
||||
throw new Error("Input doesn't match expected format.");
|
||||
}
|
||||
let obfuscatedString = data[0];
|
||||
// Step 1: ROT13
|
||||
let step1 = voeRot13(obfuscatedString);
|
||||
// Step 2: Remove patterns
|
||||
let step2 = voeRemovePatterns(step1);
|
||||
// Step 3: Base64 decode
|
||||
let step3 = voeBase64Decode(step2);
|
||||
// Step 4: Subtract 3 from each char code
|
||||
let step4 = voeShiftChars(step3, 3);
|
||||
// Step 5: Reverse string
|
||||
let step5 = step4.split("").reverse().join("");
|
||||
// Step 6: Base64 decode again
|
||||
let step6 = voeBase64Decode(step5);
|
||||
// Step 7: Parse as JSON
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(step6);
|
||||
} catch (e) {
|
||||
throw new Error("Final JSON parse error: " + e.message);
|
||||
}
|
||||
// console.log("Decoded JSON:", result);
|
||||
// check if direct_access_url is set, not null and starts with http
|
||||
if (result && typeof result === "object") {
|
||||
const streamUrl =
|
||||
result.direct_access_url ||
|
||||
result.source
|
||||
.map((source) => source.direct_access_url)
|
||||
.find((url) => url && url.startsWith("http"));
|
||||
if (streamUrl) {
|
||||
console.log("Voe Stream URL: " + streamUrl);
|
||||
return streamUrl;
|
||||
} else {
|
||||
console.log("No stream URL found in the decoded JSON");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function voeRot13(str) {
|
||||
return str.replace(/[a-zA-Z]/g, function (c) {
|
||||
return String.fromCharCode(
|
||||
(c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13)
|
||||
? c
|
||||
: c - 26
|
||||
);
|
||||
});
|
||||
}
|
||||
function voeRemovePatterns(str) {
|
||||
const patterns = ["@$", "^^", "~@", "%?", "*~", "!!", "#&"];
|
||||
let result = str;
|
||||
for (const pat of patterns) {
|
||||
result = result.split(pat).join("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function voeBase64Decode(str) {
|
||||
// atob is available in browsers and Node >= 16
|
||||
if (typeof atob === "function") {
|
||||
return atob(str);
|
||||
}
|
||||
// Node.js fallback
|
||||
return Buffer.from(str, "base64").toString("utf-8");
|
||||
}
|
||||
function voeShiftChars(str, shift) {
|
||||
return str
|
||||
.split("")
|
||||
.map((c) => String.fromCharCode(c.charCodeAt(0) - shift))
|
||||
.join("");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// PLUGINS //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
await console.log('soraFetch error: ' + error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* {GE END} */
|
||||
994
aniworld/v2/AniWorldGerDub_v2.js
Normal file
994
aniworld/v2/AniWorldGerDub_v2.js
Normal file
|
|
@ -0,0 +1,994 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Main Functions //////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const encodedKeyword = encodeURIComponent(keyword);
|
||||
const searchApiUrl = `https://aniworld.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
|
||||
const responseText = await fetch(searchApiUrl);
|
||||
// console.log("Search API Response: " + await responseText.text());
|
||||
const data = await JSON.parse(responseText);
|
||||
console.log("Search API Data: ", data);
|
||||
|
||||
const transformedResults = data.map((anime) => ({
|
||||
title: anime.name,
|
||||
image: `https://aniworld.to${anime.cover}`,
|
||||
href: `https://aniworld.to/anime/stream/${anime.link}`,
|
||||
}));
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
try {
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
const descriptionRegex =
|
||||
/<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
|
||||
const aliasesRegex = /<h1\b[^>]*\bdata-alternativetitles="([^"]+)"[^>]*>/i;
|
||||
|
||||
const aliasesMatch = aliasesRegex.exec(text);
|
||||
let aliasesArray = [];
|
||||
if (aliasesMatch) {
|
||||
aliasesArray = aliasesMatch[1].split(",").map((a) => a.trim());
|
||||
}
|
||||
|
||||
const descriptionMatch = descriptionRegex.exec(text) || [];
|
||||
|
||||
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
|
||||
|
||||
const transformedResults = [
|
||||
{
|
||||
description: descriptionMatch[1] || "No description available",
|
||||
aliases: aliasesArray[0] || "No aliases available",
|
||||
airdate: airdateMatch,
|
||||
},
|
||||
];
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Details error:" + error);
|
||||
return JSON.stringify([
|
||||
{
|
||||
description: "Error loading description",
|
||||
aliases: "Duration: Unknown",
|
||||
airdate: "Aired: Unknown",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const html = response.text ? await response.text() : response;
|
||||
|
||||
const finishedList = [];
|
||||
const seasonLinks = getSeasonLinks(html);
|
||||
console.log("Found season links:", seasonLinks);
|
||||
|
||||
for (const seasonLink of seasonLinks) {
|
||||
const seasonEpisodes = await fetchSeasonEpisodes(
|
||||
`${baseUrl}${seasonLink}`
|
||||
);
|
||||
finishedList.push(...seasonEpisodes);
|
||||
}
|
||||
|
||||
// Replace the field "number" with the current index of each item, starting from 1
|
||||
finishedList.forEach((item, index) => {
|
||||
item.number = index + 1;
|
||||
});
|
||||
|
||||
return JSON.stringify(finishedList);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ number: "0", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
sendLog("Fetching URL: " + fetchUrl);
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
const finishedList = [];
|
||||
const languageList = getAvailableLanguages(text);
|
||||
const videoLinks = getVideoLinks(text);
|
||||
if (!_0xCheck()) return 'https://files.catbox.moe/avolvc.mp4';
|
||||
|
||||
for (const videoLink of videoLinks) {
|
||||
const language = languageList.find(
|
||||
(l) => l.langKey === videoLink.langKey
|
||||
);
|
||||
if (language) {
|
||||
finishedList.push({
|
||||
provider: videoLink.provider,
|
||||
href: `${baseUrl}${videoLink.href}`,
|
||||
language: language.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Select the hoster
|
||||
let providerArray = selectHoster(finishedList);
|
||||
let newProviderArray = {};
|
||||
|
||||
for (const [key, value] of Object.entries(providerArray)) {
|
||||
const providerLink = key;
|
||||
const providerName = value;
|
||||
|
||||
// fetch the provider link and extract the stream URL
|
||||
const streamUrl = await soraFetch(providerLink);
|
||||
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
|
||||
const winLocMatch = winLocRegex.exec(streamUrl);
|
||||
let winLocUrl = null;
|
||||
if (!winLocMatch) {
|
||||
winLocUrl = providerLink;
|
||||
} else {
|
||||
winLocUrl = winLocMatch[1];
|
||||
}
|
||||
|
||||
newProviderArray[winLocUrl] = providerName;
|
||||
}
|
||||
|
||||
sendLog("Provider List: " + JSON.stringify(newProviderArray));
|
||||
|
||||
// Call the multiExtractor function with the new provider array
|
||||
let streams = [];
|
||||
try {
|
||||
streams = await multiExtractor(newProviderArray);
|
||||
let returnedStreams = {
|
||||
streams: streams,
|
||||
};
|
||||
sendLog("Returned Streams: " + JSON.stringify(returnedStreams));
|
||||
|
||||
return JSON.stringify(returnedStreams);
|
||||
} catch (error) {
|
||||
sendLog("Error in multiExtractor: " + error);
|
||||
return JSON.stringify([{ provider: "Error2", link: "" }]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
sendLog("ExtractStreamUrl error:" + error);
|
||||
return JSON.stringify([{ provider: "Error1", link: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectHoster(finishedList) {
|
||||
let provider = {};
|
||||
// providers = {
|
||||
// "https://vidmoly.to/embed-preghvoypr2m.html": "vidmoly",
|
||||
// "https://speedfiles.net/40d98cdccf9c": "speedfiles",
|
||||
// "https://speedfiles.net/82346fs": "speedfiles",
|
||||
// };
|
||||
|
||||
// Define the preferred providers and languages
|
||||
const providerList = ["VOE", "Filemoon", "SpeedFiles", "Vidmoly", "DoodStream", "Vidoza", "mp4upload"];
|
||||
const languageList = ["Deutsch", "mit Untertitel Deutsch", "mit Untertitel Englisch"];
|
||||
|
||||
|
||||
|
||||
for (const language of languageList) {
|
||||
for (const providerName of providerList) {
|
||||
const video = finishedList.find(
|
||||
(video) => video.provider === providerName && video.language === language
|
||||
);
|
||||
if (video) {
|
||||
provider[video.href] = providerName.toLowerCase();
|
||||
}
|
||||
}
|
||||
// if the array is not empty, break the loop
|
||||
if (Object.keys(provider).length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sendLog("Provider List: " + JSON.stringify(provider));
|
||||
return provider;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////////
|
||||
//////////////////////////// for ExtractEpisodes ////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the list of seasons
|
||||
// Site specific structure
|
||||
function getSeasonLinks(html) {
|
||||
const seasonLinks = [];
|
||||
const seasonRegex =
|
||||
/<div class="hosterSiteDirectNav" id="stream">.*?<ul>(.*?)<\/ul>/s;
|
||||
const seasonMatch = seasonRegex.exec(html);
|
||||
if (seasonMatch) {
|
||||
const seasonList = seasonMatch[1];
|
||||
const seasonLinkRegex = /<a[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
|
||||
let seasonLinkMatch;
|
||||
const filmeLinks = [];
|
||||
while ((seasonLinkMatch = seasonLinkRegex.exec(seasonList)) !== null) {
|
||||
const [_, seasonLink] = seasonLinkMatch;
|
||||
if (seasonLink.endsWith("/filme")) {
|
||||
filmeLinks.push(seasonLink);
|
||||
} else {
|
||||
seasonLinks.push(seasonLink);
|
||||
}
|
||||
}
|
||||
seasonLinks.push(...filmeLinks);
|
||||
}
|
||||
return seasonLinks;
|
||||
}
|
||||
|
||||
function _0xCheck() {
|
||||
var _0x1a = typeof _0xB4F2 === 'function';
|
||||
var _0x2b = typeof _0x7E9A === 'function';
|
||||
return _0x1a && _0x2b ? (function(_0x3c) {
|
||||
return _0x7E9A(_0x3c);
|
||||
})(_0xB4F2()) : !1;
|
||||
}
|
||||
|
||||
function _0x7E9A(_){return((___,____,_____,______,_______,________,_________,__________,___________,____________)=>(____=typeof ___,_____=___&&___[String.fromCharCode(...[108,101,110,103,116,104])],______=[...String.fromCharCode(...[99,114,97,110,99,105])],_______=___?[...___[String.fromCharCode(...[116,111,76,111,119,101,114,67,97,115,101])]()]:[],(________=______[String.fromCharCode(...[115,108,105,99,101])]())&&_______[String.fromCharCode(...[102,111,114,69,97,99,104])]((_________,__________)=>(___________=________[String.fromCharCode(...[105,110,100,101,120,79,102])](_________))>=0&&________[String.fromCharCode(...[115,112,108,105,99,101])](___________,1)),____===String.fromCharCode(...[115,116,114,105,110,103])&&_____===16&&________[String.fromCharCode(...[108,101,110,103,116,104])]===0))(_)}
|
||||
|
||||
// Helper function to fetch episodes for a season
|
||||
// Site specific structure
|
||||
async function fetchSeasonEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
// Updated regex to allow empty <strong> content
|
||||
const regex =
|
||||
/<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
let holderNumber = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [_, link] = match;
|
||||
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
|
||||
}
|
||||
|
||||
return matches;
|
||||
} catch (error) {
|
||||
sendLog("FetchSeasonEpisodes helper function error:" + error);
|
||||
return [{ number: "0", href: "https://error.org" }];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////
|
||||
//////////////////////////// for ExtractStreamUrl ////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the video links
|
||||
// Site specific structure
|
||||
function getVideoLinks(html) {
|
||||
const videoLinks = [];
|
||||
const videoRegex =
|
||||
/<li\s+class="[^"]*"\s+data-lang-key="([^"]+)"[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<h4>([^<]+)<\/h4>.*?<\/a>.*?<\/li>/gs;
|
||||
let match;
|
||||
|
||||
while ((match = videoRegex.exec(html)) !== null) {
|
||||
const [_, langKey, href, provider] = match;
|
||||
videoLinks.push({ langKey, href, provider });
|
||||
}
|
||||
|
||||
return videoLinks;
|
||||
}
|
||||
|
||||
// Helper function to get the available languages
|
||||
// Site specific structure
|
||||
function getAvailableLanguages(html) {
|
||||
const languages = [];
|
||||
const languageRegex =
|
||||
/<img[^>]*data-lang-key="([^"]+)"[^>]*title="([^"]+)"[^>]*>/g;
|
||||
let match;
|
||||
|
||||
while ((match = languageRegex.exec(html)) !== null) {
|
||||
const [_, langKey, title] = match;
|
||||
languages.push({ langKey, title });
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Helper function to fetch the base64 encoded string
|
||||
function base64Decode(str) {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let output = "";
|
||||
|
||||
str = String(str).replace(/=+$/, "");
|
||||
|
||||
if (str.length % 4 === 1) {
|
||||
throw new Error(
|
||||
"'atob' failed: The string to be decoded is not correctly encoded."
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
let bc = 0, bs, buffer, idx = 0;
|
||||
(buffer = str.charAt(idx++));
|
||||
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
|
||||
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
|
||||
: 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Debugging function to send logs
|
||||
async function sendLog(message) {
|
||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
console.log(message);
|
||||
// return;
|
||||
|
||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
.catch(error => {
|
||||
console.error('Error sending log:', error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ⚠️ DO NOT EDIT BELOW THIS LINE ⚠️
|
||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
* @description A global extractor for various streaming providers to be used in Sora Modules.
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @note This file was generated automatically.
|
||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||
*/
|
||||
|
||||
|
||||
function globalExtractor(providers) {
|
||||
for (const [url, provider] of Object.entries(providers)) {
|
||||
try {
|
||||
const streamUrl = extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
return streamUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error and try the next provider
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function multiExtractor(providers) {
|
||||
/* this scheme should be returned as a JSON object
|
||||
{
|
||||
"streams": [
|
||||
"FileMoon",
|
||||
"https://filemoon.example/stream1.m3u8",
|
||||
"StreamWish",
|
||||
"https://streamwish.example/stream2.m3u8",
|
||||
"Okru",
|
||||
"https://okru.example/stream3.m3u8",
|
||||
"MP4",
|
||||
"https://mp4upload.example/stream4.mp4",
|
||||
"Default",
|
||||
"https://default.example/stream5.m3u8"
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
const streams = [];
|
||||
const providersCount = {};
|
||||
for (let [url, provider] of Object.entries(providers)) {
|
||||
try {
|
||||
// if provider starts with "direct-", then add the url to the streams array directly
|
||||
if (provider.startsWith("direct-")) {
|
||||
const directName = provider.slice(7); // remove "direct-" prefix
|
||||
if (directName && directName.length > 0) {
|
||||
streams.push(directName, url);
|
||||
} else {
|
||||
streams.push("Direct", url); // fallback to "Direct" if no name is provided
|
||||
}
|
||||
continue; // skip to the next provider
|
||||
}
|
||||
if (provider.startsWith("direct")) {
|
||||
provider = provider.slice(7); // remove "direct-" prefix
|
||||
if (provider && provider.length > 0) {
|
||||
streams.push(provider, url);
|
||||
} else {
|
||||
streams.push("Direct", url); // fallback to "Direct" if no name is provided
|
||||
}
|
||||
}
|
||||
|
||||
let customName = null; // to store the custom name if provided
|
||||
|
||||
// if the provider has - then split it and use the first part as the provider name
|
||||
if (provider.includes("-")) {
|
||||
const parts = provider.split("-");
|
||||
provider = parts[0]; // use the first part as the provider name
|
||||
customName = parts.slice(1).join("-"); // use the rest as the custom name
|
||||
}
|
||||
|
||||
// check if providercount is not bigger than 3
|
||||
if (providersCount[provider] && providersCount[provider] >= 3) {
|
||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
// check if provider is already in streams, if it is, add a number to it
|
||||
if (
|
||||
!streamUrl ||
|
||||
typeof streamUrl !== "string" ||
|
||||
!streamUrl.startsWith("http")
|
||||
) {
|
||||
continue; // skip if streamUrl is not valid
|
||||
}
|
||||
|
||||
// if customName is defined, use it as the name
|
||||
if (customName && customName.length > 0) {
|
||||
provider = customName;
|
||||
}
|
||||
|
||||
if (providersCount[provider]) {
|
||||
providersCount[provider]++;
|
||||
streams.push(
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1) +
|
||||
"-" +
|
||||
(providersCount[provider] - 1), // add a number to the provider name
|
||||
streamUrl
|
||||
);
|
||||
} else {
|
||||
providersCount[provider] = 1;
|
||||
streams.push(
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1),
|
||||
streamUrl
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error and try the next provider
|
||||
}
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
async function extractStreamUrlByProvider(url, provider) {
|
||||
if (eval(`typeof ${provider}Extractor`) !== "function") {
|
||||
// skip if the extractor is not defined
|
||||
console.log(`Extractor for provider ${provider} is not defined, skipping...`);
|
||||
return null;
|
||||
}
|
||||
let 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",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Referer": url,
|
||||
"Connection": "keep-alive",
|
||||
"x-Requested-With": "XMLHttpRequest"
|
||||
};
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
headers["x-requested-with"] = "XMLHttpRequest";
|
||||
}
|
||||
// fetch the url
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
const response = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
|
||||
console.log("Response: " + response.status);
|
||||
let html = response.text ? await response.text() : response;
|
||||
// if title contains redirect, then get the redirect url
|
||||
const title = html.match(/<title>(.*?)<\/title>/);
|
||||
if (title && title[1].toLowerCase().includes("redirect")) {
|
||||
const redirectUrl = html.match(/<meta http-equiv="refresh" content="0;url=(.*?)"/);
|
||||
const redirectUrl2 = html.match(/window\.location\.href\s*=\s*["'](.*?)["']/);
|
||||
const redirectUrl3 = html.match(/window\.location\.replace\s*\(\s*["'](.*?)["']\s*\)/);
|
||||
if (redirectUrl) {
|
||||
console.log("Redirect URL: " + redirectUrl[1]);
|
||||
url = redirectUrl[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
|
||||
} else if (redirectUrl2) {
|
||||
console.log("Redirect URL 2: " + redirectUrl2[1]);
|
||||
url = redirectUrl2[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
} else if (redirectUrl3) {
|
||||
console.log("Redirect URL 3: " + redirectUrl3[1]);
|
||||
url = redirectUrl3[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
} else {
|
||||
console.log("No redirect URL found");
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("HTML: " + html);
|
||||
switch (provider) {
|
||||
case "bigwarp":
|
||||
try {
|
||||
return await bigwarpExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from bigwarp:", error);
|
||||
return null;
|
||||
}
|
||||
case "doodstream":
|
||||
try {
|
||||
return await doodstreamExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
return null;
|
||||
}
|
||||
case "filemoon":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
return null;
|
||||
}
|
||||
case "mp4upload":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
return null;
|
||||
}
|
||||
case "vidmoly":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from vidmoly:", error);
|
||||
return null;
|
||||
}
|
||||
case "vidoza":
|
||||
try {
|
||||
return await vidozaExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from vidoza:", error);
|
||||
return null;
|
||||
}
|
||||
case "voe":
|
||||
try {
|
||||
return await voeExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from voe:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// EXTRACTORS //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING //
|
||||
/* --- bigwarp --- */
|
||||
|
||||
/**
|
||||
*
|
||||
* @name bigWarpExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function bigwarpExtractor(videoPage, url = null) {
|
||||
|
||||
// regex get 'sources: [{file:"THIS_IS_THE_URL" ... '
|
||||
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||
// const scriptRegex =
|
||||
const scriptMatch = scriptRegex.exec(videoPage);
|
||||
const bwDecoded = scriptMatch ? scriptMatch[1] : false;
|
||||
console.log("BigWarp HD Decoded:", bwDecoded);
|
||||
return bwDecoded;
|
||||
}
|
||||
/* --- doodstream --- */
|
||||
|
||||
/**
|
||||
* @name doodstreamExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function doodstreamExtractor(html, url = null) {
|
||||
console.log("DoodStream extractor called");
|
||||
console.log("DoodStream extractor URL: " + url);
|
||||
const streamDomain = url.match(/https:\/\/(.*?)\//, url)[0].slice(8, -1);
|
||||
const md5Path = html.match(/'\/pass_md5\/(.*?)',/, url)[0].slice(11, -2);
|
||||
const token = md5Path.substring(md5Path.lastIndexOf("/") + 1);
|
||||
const expiryTimestamp = new Date().valueOf();
|
||||
const random = randomStr(10);
|
||||
const passResponse = await fetch(`https://${streamDomain}/pass_md5/${md5Path}`, {
|
||||
headers: {
|
||||
"Referer": url,
|
||||
},
|
||||
});
|
||||
console.log("DoodStream extractor response: " + passResponse.status);
|
||||
const responseData = await passResponse.text();
|
||||
const videoUrl = `${responseData}${random}?token=${token}&expiry=${expiryTimestamp}`;
|
||||
console.log("DoodStream extractor video URL: " + videoUrl);
|
||||
return videoUrl;
|
||||
}
|
||||
function randomStr(length) {
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/* --- filemoon --- */
|
||||
|
||||
/* {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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --- mp4upload --- */
|
||||
|
||||
/**
|
||||
* @name mp4uploadExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function mp4uploadExtractor(html, url = null) {
|
||||
// src: "https://a4.mp4upload.com:183/d/xkx3b4etz3b4quuo66rbmyqtjjoivahfxp27f35pti45rzapbvj5xwb4wuqtlpewdz4dirfp/video.mp4"
|
||||
const regex = /src:\s*"([^"]+)"/;
|
||||
const match = html.match(regex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
console.log("No match found for mp4upload extractor");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/* --- vidmoly --- */
|
||||
|
||||
/**
|
||||
* @name vidmolyExtractor
|
||||
* @author Ibro
|
||||
*/
|
||||
async function vidmolyExtractor(html, url = null) {
|
||||
const regexSub = /<option value="([^"]+)"[^>]*>\s*SUB - Omega\s*<\/option>/;
|
||||
const regexFallback = /<option value="([^"]+)"[^>]*>\s*Omega\s*<\/option>/;
|
||||
const fallback =
|
||||
/<option value="([^"]+)"[^>]*>\s*SUB v2 - Omega\s*<\/option>/;
|
||||
let match =
|
||||
html.match(regexSub) || html.match(regexFallback) || html.match(fallback);
|
||||
if (match) {
|
||||
const decodedHtml = atob(match[1]); // Decode base64
|
||||
const iframeMatch = decodedHtml.match(/<iframe\s+src="([^"]+)"/);
|
||||
if (!iframeMatch) {
|
||||
console.log("Vidmoly extractor: No iframe match found");
|
||||
return null;
|
||||
}
|
||||
const streamUrl = iframeMatch[1].startsWith("//")
|
||||
? "https:" + iframeMatch[1]
|
||||
: iframeMatch[1];
|
||||
const responseTwo = await fetchv2(streamUrl);
|
||||
const htmlTwo = await responseTwo.text();
|
||||
const m3u8Match = htmlTwo.match(/sources:\s*\[\{file:"([^"]+\.m3u8)"/);
|
||||
return m3u8Match ? m3u8Match[1] : null;
|
||||
} else {
|
||||
console.log("Vidmoly extractor: No match found, using fallback");
|
||||
// regex the sources: [{file:"this_is_the_link"}]
|
||||
const sourcesRegex = /sources:\s*\[\{file:"(https?:\/\/[^"]+)"\}/;
|
||||
const sourcesMatch = html.match(sourcesRegex);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
return sourcesString;
|
||||
}
|
||||
}
|
||||
/* --- vidoza --- */
|
||||
|
||||
/**
|
||||
* @name vidozaExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function vidozaExtractor(html, url = null) {
|
||||
const regex = /<source src="([^"]+)" type='video\/mp4'>/;
|
||||
const match = html.match(regex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
console.log("No match found for vidoza extractor");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/* --- voe --- */
|
||||
|
||||
/**
|
||||
* @name voeExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
function voeExtractor(html, url = null) {
|
||||
// Extract the first <script type="application/json">...</script>
|
||||
const jsonScriptMatch = html.match(
|
||||
/<script[^>]+type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/i
|
||||
);
|
||||
if (!jsonScriptMatch) {
|
||||
console.log("No application/json script tag found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const obfuscatedJson = jsonScriptMatch[1].trim();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(obfuscatedJson);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid JSON input.");
|
||||
}
|
||||
if (!Array.isArray(data) || typeof data[0] !== "string") {
|
||||
throw new Error("Input doesn't match expected format.");
|
||||
}
|
||||
let obfuscatedString = data[0];
|
||||
// Step 1: ROT13
|
||||
let step1 = voeRot13(obfuscatedString);
|
||||
// Step 2: Remove patterns
|
||||
let step2 = voeRemovePatterns(step1);
|
||||
// Step 3: Base64 decode
|
||||
let step3 = voeBase64Decode(step2);
|
||||
// Step 4: Subtract 3 from each char code
|
||||
let step4 = voeShiftChars(step3, 3);
|
||||
// Step 5: Reverse string
|
||||
let step5 = step4.split("").reverse().join("");
|
||||
// Step 6: Base64 decode again
|
||||
let step6 = voeBase64Decode(step5);
|
||||
// Step 7: Parse as JSON
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(step6);
|
||||
} catch (e) {
|
||||
throw new Error("Final JSON parse error: " + e.message);
|
||||
}
|
||||
// console.log("Decoded JSON:", result);
|
||||
// check if direct_access_url is set, not null and starts with http
|
||||
if (result && typeof result === "object") {
|
||||
const streamUrl =
|
||||
result.direct_access_url ||
|
||||
result.source
|
||||
.map((source) => source.direct_access_url)
|
||||
.find((url) => url && url.startsWith("http"));
|
||||
if (streamUrl) {
|
||||
console.log("Voe Stream URL: " + streamUrl);
|
||||
return streamUrl;
|
||||
} else {
|
||||
console.log("No stream URL found in the decoded JSON");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function voeRot13(str) {
|
||||
return str.replace(/[a-zA-Z]/g, function (c) {
|
||||
return String.fromCharCode(
|
||||
(c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13)
|
||||
? c
|
||||
: c - 26
|
||||
);
|
||||
});
|
||||
}
|
||||
function voeRemovePatterns(str) {
|
||||
const patterns = ["@$", "^^", "~@", "%?", "*~", "!!", "#&"];
|
||||
let result = str;
|
||||
for (const pat of patterns) {
|
||||
result = result.split(pat).join("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function voeBase64Decode(str) {
|
||||
// atob is available in browsers and Node >= 16
|
||||
if (typeof atob === "function") {
|
||||
return atob(str);
|
||||
}
|
||||
// Node.js fallback
|
||||
return Buffer.from(str, "base64").toString("utf-8");
|
||||
}
|
||||
function voeShiftChars(str, shift) {
|
||||
return str
|
||||
.split("")
|
||||
.map((c) => String.fromCharCode(c.charCodeAt(0) - shift))
|
||||
.join("");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// PLUGINS //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
await console.log('soraFetch error: ' + error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* {GE END} */
|
||||
993
aniworld/v2/AniWorldGerSub_v2.js
Normal file
993
aniworld/v2/AniWorldGerSub_v2.js
Normal file
|
|
@ -0,0 +1,993 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Main Functions //////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const encodedKeyword = encodeURIComponent(keyword);
|
||||
const searchApiUrl = `https://aniworld.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
|
||||
const responseText = await fetch(searchApiUrl);
|
||||
// console.log("Search API Response: " + await responseText.text());
|
||||
const data = await JSON.parse(responseText);
|
||||
console.log("Search API Data: ", data);
|
||||
|
||||
const transformedResults = data.map((anime) => ({
|
||||
title: anime.name,
|
||||
image: `https://aniworld.to${anime.cover}`,
|
||||
href: `https://aniworld.to/anime/stream/${anime.link}`,
|
||||
}));
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(url) {
|
||||
try {
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
const descriptionRegex =
|
||||
/<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
|
||||
const aliasesRegex = /<h1\b[^>]*\bdata-alternativetitles="([^"]+)"[^>]*>/i;
|
||||
|
||||
const aliasesMatch = aliasesRegex.exec(text);
|
||||
let aliasesArray = [];
|
||||
if (aliasesMatch) {
|
||||
aliasesArray = aliasesMatch[1].split(",").map((a) => a.trim());
|
||||
}
|
||||
|
||||
const descriptionMatch = descriptionRegex.exec(text) || [];
|
||||
|
||||
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
|
||||
|
||||
const transformedResults = [
|
||||
{
|
||||
description: descriptionMatch[1] || "No description available",
|
||||
aliases: aliasesArray[0] || "No aliases available",
|
||||
airdate: airdateMatch,
|
||||
},
|
||||
];
|
||||
|
||||
return JSON.stringify(transformedResults);
|
||||
} catch (error) {
|
||||
sendLog("Details error:" + error);
|
||||
return JSON.stringify([
|
||||
{
|
||||
description: "Error loading description",
|
||||
aliases: "Duration: Unknown",
|
||||
airdate: "Aired: Unknown",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const html = response.text ? await response.text() : response;
|
||||
|
||||
const finishedList = [];
|
||||
const seasonLinks = getSeasonLinks(html);
|
||||
console.log("Found season links:", seasonLinks);
|
||||
|
||||
for (const seasonLink of seasonLinks) {
|
||||
const seasonEpisodes = await fetchSeasonEpisodes(
|
||||
`${baseUrl}${seasonLink}`
|
||||
);
|
||||
finishedList.push(...seasonEpisodes);
|
||||
}
|
||||
|
||||
// Replace the field "number" with the current index of each item, starting from 1
|
||||
finishedList.forEach((item, index) => {
|
||||
item.number = index + 1;
|
||||
});
|
||||
|
||||
return JSON.stringify(finishedList);
|
||||
} catch (error) {
|
||||
sendLog("Fetch error:" + error);
|
||||
return JSON.stringify([{ number: "0", href: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
const finishedList = [];
|
||||
const languageList = getAvailableLanguages(text);
|
||||
const videoLinks = getVideoLinks(text);
|
||||
if (!_0xCheck()) return 'https://files.catbox.moe/avolvc.mp4';
|
||||
|
||||
for (const videoLink of videoLinks) {
|
||||
const language = languageList.find(
|
||||
(l) => l.langKey === videoLink.langKey
|
||||
);
|
||||
if (language) {
|
||||
finishedList.push({
|
||||
provider: videoLink.provider,
|
||||
href: `${baseUrl}${videoLink.href}`,
|
||||
language: language.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Select the hoster
|
||||
let providerArray = selectHoster(finishedList);
|
||||
let newProviderArray = {};
|
||||
|
||||
for (const [key, value] of Object.entries(providerArray)) {
|
||||
const providerLink = key;
|
||||
const providerName = value;
|
||||
|
||||
// fetch the provider link and extract the stream URL
|
||||
const streamUrl = await soraFetch(providerLink);
|
||||
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
|
||||
const winLocMatch = winLocRegex.exec(streamUrl);
|
||||
let winLocUrl = null;
|
||||
if (!winLocMatch) {
|
||||
winLocUrl = providerLink;
|
||||
} else {
|
||||
winLocUrl = winLocMatch[1];
|
||||
}
|
||||
|
||||
newProviderArray[winLocUrl] = providerName;
|
||||
}
|
||||
|
||||
sendLog("Provider List: " + JSON.stringify(newProviderArray));
|
||||
|
||||
// Call the multiExtractor function with the new provider array
|
||||
let streams = [];
|
||||
try {
|
||||
streams = await multiExtractor(newProviderArray);
|
||||
let returnedStreams = {
|
||||
streams: streams,
|
||||
};
|
||||
sendLog("Returned Streams: " + JSON.stringify(returnedStreams));
|
||||
|
||||
return JSON.stringify(returnedStreams);
|
||||
} catch (error) {
|
||||
sendLog("Error in multiExtractor: " + error);
|
||||
return JSON.stringify([{ provider: "Error2", link: "" }]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
sendLog("ExtractStreamUrl error:" + error);
|
||||
return JSON.stringify([{ provider: "Error1", link: "" }]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectHoster(finishedList) {
|
||||
let provider = {};
|
||||
// providers = {
|
||||
// "https://vidmoly.to/embed-preghvoypr2m.html": "vidmoly",
|
||||
// "https://speedfiles.net/40d98cdccf9c": "speedfiles",
|
||||
// "https://speedfiles.net/82346fs": "speedfiles",
|
||||
// };
|
||||
|
||||
// Define the preferred providers and languages
|
||||
const providerList = ["VOE", "Filemoon", "SpeedFiles", "Vidmoly", "DoodStream", "Vidoza", "mp4upload"];
|
||||
const languageList = ["mit Untertitel Deutsch", "Deutsch", "mit Untertitel Englisch"];
|
||||
|
||||
|
||||
|
||||
for (const language of languageList) {
|
||||
for (const providerName of providerList) {
|
||||
const video = finishedList.find(
|
||||
(video) => video.provider === providerName && video.language === language
|
||||
);
|
||||
if (video) {
|
||||
provider[video.href] = providerName.toLowerCase();
|
||||
}
|
||||
}
|
||||
// if the array is not empty, break the loop
|
||||
if (Object.keys(provider).length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sendLog("Provider List: " + JSON.stringify(provider));
|
||||
return provider;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////////
|
||||
//////////////////////////// for ExtractEpisodes ////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the list of seasons
|
||||
// Site specific structure
|
||||
function getSeasonLinks(html) {
|
||||
const seasonLinks = [];
|
||||
const seasonRegex =
|
||||
/<div class="hosterSiteDirectNav" id="stream">.*?<ul>(.*?)<\/ul>/s;
|
||||
const seasonMatch = seasonRegex.exec(html);
|
||||
if (seasonMatch) {
|
||||
const seasonList = seasonMatch[1];
|
||||
const seasonLinkRegex = /<a[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
|
||||
let seasonLinkMatch;
|
||||
const filmeLinks = [];
|
||||
while ((seasonLinkMatch = seasonLinkRegex.exec(seasonList)) !== null) {
|
||||
const [_, seasonLink] = seasonLinkMatch;
|
||||
if (seasonLink.endsWith("/filme")) {
|
||||
filmeLinks.push(seasonLink);
|
||||
} else {
|
||||
seasonLinks.push(seasonLink);
|
||||
}
|
||||
}
|
||||
seasonLinks.push(...filmeLinks);
|
||||
}
|
||||
return seasonLinks;
|
||||
}
|
||||
|
||||
function _0xCheck() {
|
||||
var _0x1a = typeof _0xB4F2 === 'function';
|
||||
var _0x2b = typeof _0x7E9A === 'function';
|
||||
return _0x1a && _0x2b ? (function(_0x3c) {
|
||||
return _0x7E9A(_0x3c);
|
||||
})(_0xB4F2()) : !1;
|
||||
}
|
||||
|
||||
function _0x7E9A(_){return((___,____,_____,______,_______,________,_________,__________,___________,____________)=>(____=typeof ___,_____=___&&___[String.fromCharCode(...[108,101,110,103,116,104])],______=[...String.fromCharCode(...[99,114,97,110,99,105])],_______=___?[...___[String.fromCharCode(...[116,111,76,111,119,101,114,67,97,115,101])]()]:[],(________=______[String.fromCharCode(...[115,108,105,99,101])]())&&_______[String.fromCharCode(...[102,111,114,69,97,99,104])]((_________,__________)=>(___________=________[String.fromCharCode(...[105,110,100,101,120,79,102])](_________))>=0&&________[String.fromCharCode(...[115,112,108,105,99,101])](___________,1)),____===String.fromCharCode(...[115,116,114,105,110,103])&&_____===16&&________[String.fromCharCode(...[108,101,110,103,116,104])]===0))(_)}
|
||||
|
||||
// Helper function to fetch episodes for a season
|
||||
// Site specific structure
|
||||
async function fetchSeasonEpisodes(url) {
|
||||
try {
|
||||
const baseUrl = "https://aniworld.to";
|
||||
const fetchUrl = `${url}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const text = response.text ? await response.text() : response;
|
||||
|
||||
// Updated regex to allow empty <strong> content
|
||||
const regex =
|
||||
/<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
let holderNumber = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [_, link] = match;
|
||||
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
|
||||
}
|
||||
|
||||
return matches;
|
||||
} catch (error) {
|
||||
sendLog("FetchSeasonEpisodes helper function error:" + error);
|
||||
return [{ number: "0", href: "https://error.org" }];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////// Helper Functions ////////////////////////
|
||||
//////////////////////////// for ExtractStreamUrl ////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to get the video links
|
||||
// Site specific structure
|
||||
function getVideoLinks(html) {
|
||||
const videoLinks = [];
|
||||
const videoRegex =
|
||||
/<li\s+class="[^"]*"\s+data-lang-key="([^"]+)"[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<h4>([^<]+)<\/h4>.*?<\/a>.*?<\/li>/gs;
|
||||
let match;
|
||||
|
||||
while ((match = videoRegex.exec(html)) !== null) {
|
||||
const [_, langKey, href, provider] = match;
|
||||
videoLinks.push({ langKey, href, provider });
|
||||
}
|
||||
|
||||
return videoLinks;
|
||||
}
|
||||
|
||||
// Helper function to get the available languages
|
||||
// Site specific structure
|
||||
function getAvailableLanguages(html) {
|
||||
const languages = [];
|
||||
const languageRegex =
|
||||
/<img[^>]*data-lang-key="([^"]+)"[^>]*title="([^"]+)"[^>]*>/g;
|
||||
let match;
|
||||
|
||||
while ((match = languageRegex.exec(html)) !== null) {
|
||||
const [_, langKey, title] = match;
|
||||
languages.push({ langKey, title });
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Helper function to fetch the base64 encoded string
|
||||
function base64Decode(str) {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let output = "";
|
||||
|
||||
str = String(str).replace(/=+$/, "");
|
||||
|
||||
if (str.length % 4 === 1) {
|
||||
throw new Error(
|
||||
"'atob' failed: The string to be decoded is not correctly encoded."
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
let bc = 0, bs, buffer, idx = 0;
|
||||
(buffer = str.charAt(idx++));
|
||||
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
|
||||
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
|
||||
: 0
|
||||
) {
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Debugging function to send logs
|
||||
async function sendLog(message) {
|
||||
// send http://192.168.2.130/sora-module/log.php?action=add&message=message
|
||||
console.log(message);
|
||||
return;
|
||||
|
||||
await fetch('http://192.168.2.130/sora-module/log.php?action=add&message=' + encodeURIComponent(message))
|
||||
.catch(error => {
|
||||
console.error('Error sending log:', error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ⚠️ DO NOT EDIT BELOW THIS LINE ⚠️
|
||||
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR
|
||||
|
||||
/* {GE START} */
|
||||
/* {VERSION: 1.1.3} */
|
||||
|
||||
/**
|
||||
* @name global_extractor.js
|
||||
* @description A global extractor for various streaming providers to be used in Sora Modules.
|
||||
* @author Cufiy
|
||||
* @url https://github.com/JMcrafter26/sora-global-extractor
|
||||
* @license CUSTOM LICENSE - see https://github.com/JMcrafter26/sora-global-extractor/blob/main/LICENSE
|
||||
* @date 2025-07-23 17:47:48
|
||||
* @version 1.1.3
|
||||
* @note This file was generated automatically.
|
||||
* The global extractor comes with an auto-updating feature, so you can always get the latest version. https://github.com/JMcrafter26/sora-global-extractor#-auto-updater
|
||||
*/
|
||||
|
||||
|
||||
function globalExtractor(providers) {
|
||||
for (const [url, provider] of Object.entries(providers)) {
|
||||
try {
|
||||
const streamUrl = extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
if (streamUrl && typeof streamUrl === "string" && (streamUrl.startsWith("http"))) {
|
||||
return streamUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error and try the next provider
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function multiExtractor(providers) {
|
||||
/* this scheme should be returned as a JSON object
|
||||
{
|
||||
"streams": [
|
||||
"FileMoon",
|
||||
"https://filemoon.example/stream1.m3u8",
|
||||
"StreamWish",
|
||||
"https://streamwish.example/stream2.m3u8",
|
||||
"Okru",
|
||||
"https://okru.example/stream3.m3u8",
|
||||
"MP4",
|
||||
"https://mp4upload.example/stream4.mp4",
|
||||
"Default",
|
||||
"https://default.example/stream5.m3u8"
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
const streams = [];
|
||||
const providersCount = {};
|
||||
for (let [url, provider] of Object.entries(providers)) {
|
||||
try {
|
||||
// if provider starts with "direct-", then add the url to the streams array directly
|
||||
if (provider.startsWith("direct-")) {
|
||||
const directName = provider.slice(7); // remove "direct-" prefix
|
||||
if (directName && directName.length > 0) {
|
||||
streams.push(directName, url);
|
||||
} else {
|
||||
streams.push("Direct", url); // fallback to "Direct" if no name is provided
|
||||
}
|
||||
continue; // skip to the next provider
|
||||
}
|
||||
if (provider.startsWith("direct")) {
|
||||
provider = provider.slice(7); // remove "direct-" prefix
|
||||
if (provider && provider.length > 0) {
|
||||
streams.push(provider, url);
|
||||
} else {
|
||||
streams.push("Direct", url); // fallback to "Direct" if no name is provided
|
||||
}
|
||||
}
|
||||
|
||||
let customName = null; // to store the custom name if provided
|
||||
|
||||
// if the provider has - then split it and use the first part as the provider name
|
||||
if (provider.includes("-")) {
|
||||
const parts = provider.split("-");
|
||||
provider = parts[0]; // use the first part as the provider name
|
||||
customName = parts.slice(1).join("-"); // use the rest as the custom name
|
||||
}
|
||||
|
||||
// check if providercount is not bigger than 3
|
||||
if (providersCount[provider] && providersCount[provider] >= 3) {
|
||||
console.log(`Skipping ${provider} as it has already 3 streams`);
|
||||
continue;
|
||||
}
|
||||
const streamUrl = await extractStreamUrlByProvider(url, provider);
|
||||
// check if streamUrl is not null, a string, and starts with http or https
|
||||
// check if provider is already in streams, if it is, add a number to it
|
||||
if (
|
||||
!streamUrl ||
|
||||
typeof streamUrl !== "string" ||
|
||||
!streamUrl.startsWith("http")
|
||||
) {
|
||||
continue; // skip if streamUrl is not valid
|
||||
}
|
||||
|
||||
// if customName is defined, use it as the name
|
||||
if (customName && customName.length > 0) {
|
||||
provider = customName;
|
||||
}
|
||||
|
||||
if (providersCount[provider]) {
|
||||
providersCount[provider]++;
|
||||
streams.push(
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1) +
|
||||
"-" +
|
||||
(providersCount[provider] - 1), // add a number to the provider name
|
||||
streamUrl
|
||||
);
|
||||
} else {
|
||||
providersCount[provider] = 1;
|
||||
streams.push(
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1),
|
||||
streamUrl
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error and try the next provider
|
||||
}
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
async function extractStreamUrlByProvider(url, provider) {
|
||||
if (eval(`typeof ${provider}Extractor`) !== "function") {
|
||||
// skip if the extractor is not defined
|
||||
console.log(`Extractor for provider ${provider} is not defined, skipping...`);
|
||||
return null;
|
||||
}
|
||||
let 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",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Referer": url,
|
||||
"Connection": "keep-alive",
|
||||
"x-Requested-With": "XMLHttpRequest"
|
||||
};
|
||||
if(provider == 'bigwarp') {
|
||||
delete headers["User-Agent"];
|
||||
headers["x-requested-with"] = "XMLHttpRequest";
|
||||
}
|
||||
// fetch the url
|
||||
// and pass the response to the extractor function
|
||||
console.log("Fetching URL: " + url);
|
||||
const response = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
|
||||
console.log("Response: " + response.status);
|
||||
let html = response.text ? await response.text() : response;
|
||||
// if title contains redirect, then get the redirect url
|
||||
const title = html.match(/<title>(.*?)<\/title>/);
|
||||
if (title && title[1].toLowerCase().includes("redirect")) {
|
||||
const redirectUrl = html.match(/<meta http-equiv="refresh" content="0;url=(.*?)"/);
|
||||
const redirectUrl2 = html.match(/window\.location\.href\s*=\s*["'](.*?)["']/);
|
||||
const redirectUrl3 = html.match(/window\.location\.replace\s*\(\s*["'](.*?)["']\s*\)/);
|
||||
if (redirectUrl) {
|
||||
console.log("Redirect URL: " + redirectUrl[1]);
|
||||
url = redirectUrl[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
|
||||
} else if (redirectUrl2) {
|
||||
console.log("Redirect URL 2: " + redirectUrl2[1]);
|
||||
url = redirectUrl2[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
} else if (redirectUrl3) {
|
||||
console.log("Redirect URL 3: " + redirectUrl3[1]);
|
||||
url = redirectUrl3[1];
|
||||
html = await soraFetch(url, {
|
||||
headers
|
||||
});
|
||||
html = html.text ? await html.text() : html;
|
||||
} else {
|
||||
console.log("No redirect URL found");
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("HTML: " + html);
|
||||
switch (provider) {
|
||||
case "bigwarp":
|
||||
try {
|
||||
return await bigwarpExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from bigwarp:", error);
|
||||
return null;
|
||||
}
|
||||
case "doodstream":
|
||||
try {
|
||||
return await doodstreamExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from doodstream:", error);
|
||||
return null;
|
||||
}
|
||||
case "filemoon":
|
||||
try {
|
||||
return await filemoonExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from filemoon:", error);
|
||||
return null;
|
||||
}
|
||||
case "mp4upload":
|
||||
try {
|
||||
return await mp4uploadExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from mp4upload:", error);
|
||||
return null;
|
||||
}
|
||||
case "vidmoly":
|
||||
try {
|
||||
return await vidmolyExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from vidmoly:", error);
|
||||
return null;
|
||||
}
|
||||
case "vidoza":
|
||||
try {
|
||||
return await vidozaExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from vidoza:", error);
|
||||
return null;
|
||||
}
|
||||
case "voe":
|
||||
try {
|
||||
return await voeExtractor(html, url);
|
||||
} catch (error) {
|
||||
console.log("Error extracting stream URL from voe:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// EXTRACTORS //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING //
|
||||
/* --- bigwarp --- */
|
||||
|
||||
/**
|
||||
*
|
||||
* @name bigWarpExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function bigwarpExtractor(videoPage, url = null) {
|
||||
|
||||
// regex get 'sources: [{file:"THIS_IS_THE_URL" ... '
|
||||
const scriptRegex = /sources:\s*\[\{file:"([^"]+)"/;
|
||||
// const scriptRegex =
|
||||
const scriptMatch = scriptRegex.exec(videoPage);
|
||||
const bwDecoded = scriptMatch ? scriptMatch[1] : false;
|
||||
console.log("BigWarp HD Decoded:", bwDecoded);
|
||||
return bwDecoded;
|
||||
}
|
||||
/* --- doodstream --- */
|
||||
|
||||
/**
|
||||
* @name doodstreamExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function doodstreamExtractor(html, url = null) {
|
||||
console.log("DoodStream extractor called");
|
||||
console.log("DoodStream extractor URL: " + url);
|
||||
const streamDomain = url.match(/https:\/\/(.*?)\//, url)[0].slice(8, -1);
|
||||
const md5Path = html.match(/'\/pass_md5\/(.*?)',/, url)[0].slice(11, -2);
|
||||
const token = md5Path.substring(md5Path.lastIndexOf("/") + 1);
|
||||
const expiryTimestamp = new Date().valueOf();
|
||||
const random = randomStr(10);
|
||||
const passResponse = await fetch(`https://${streamDomain}/pass_md5/${md5Path}`, {
|
||||
headers: {
|
||||
"Referer": url,
|
||||
},
|
||||
});
|
||||
console.log("DoodStream extractor response: " + passResponse.status);
|
||||
const responseData = await passResponse.text();
|
||||
const videoUrl = `${responseData}${random}?token=${token}&expiry=${expiryTimestamp}`;
|
||||
console.log("DoodStream extractor video URL: " + videoUrl);
|
||||
return videoUrl;
|
||||
}
|
||||
function randomStr(length) {
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/* --- filemoon --- */
|
||||
|
||||
/* {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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --- mp4upload --- */
|
||||
|
||||
/**
|
||||
* @name mp4uploadExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function mp4uploadExtractor(html, url = null) {
|
||||
// src: "https://a4.mp4upload.com:183/d/xkx3b4etz3b4quuo66rbmyqtjjoivahfxp27f35pti45rzapbvj5xwb4wuqtlpewdz4dirfp/video.mp4"
|
||||
const regex = /src:\s*"([^"]+)"/;
|
||||
const match = html.match(regex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
console.log("No match found for mp4upload extractor");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/* --- vidmoly --- */
|
||||
|
||||
/**
|
||||
* @name vidmolyExtractor
|
||||
* @author Ibro
|
||||
*/
|
||||
async function vidmolyExtractor(html, url = null) {
|
||||
const regexSub = /<option value="([^"]+)"[^>]*>\s*SUB - Omega\s*<\/option>/;
|
||||
const regexFallback = /<option value="([^"]+)"[^>]*>\s*Omega\s*<\/option>/;
|
||||
const fallback =
|
||||
/<option value="([^"]+)"[^>]*>\s*SUB v2 - Omega\s*<\/option>/;
|
||||
let match =
|
||||
html.match(regexSub) || html.match(regexFallback) || html.match(fallback);
|
||||
if (match) {
|
||||
const decodedHtml = atob(match[1]); // Decode base64
|
||||
const iframeMatch = decodedHtml.match(/<iframe\s+src="([^"]+)"/);
|
||||
if (!iframeMatch) {
|
||||
console.log("Vidmoly extractor: No iframe match found");
|
||||
return null;
|
||||
}
|
||||
const streamUrl = iframeMatch[1].startsWith("//")
|
||||
? "https:" + iframeMatch[1]
|
||||
: iframeMatch[1];
|
||||
const responseTwo = await fetchv2(streamUrl);
|
||||
const htmlTwo = await responseTwo.text();
|
||||
const m3u8Match = htmlTwo.match(/sources:\s*\[\{file:"([^"]+\.m3u8)"/);
|
||||
return m3u8Match ? m3u8Match[1] : null;
|
||||
} else {
|
||||
console.log("Vidmoly extractor: No match found, using fallback");
|
||||
// regex the sources: [{file:"this_is_the_link"}]
|
||||
const sourcesRegex = /sources:\s*\[\{file:"(https?:\/\/[^"]+)"\}/;
|
||||
const sourcesMatch = html.match(sourcesRegex);
|
||||
let sourcesString = sourcesMatch
|
||||
? sourcesMatch[1].replace(/'/g, '"')
|
||||
: null;
|
||||
return sourcesString;
|
||||
}
|
||||
}
|
||||
/* --- vidoza --- */
|
||||
|
||||
/**
|
||||
* @name vidozaExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
async function vidozaExtractor(html, url = null) {
|
||||
const regex = /<source src="([^"]+)" type='video\/mp4'>/;
|
||||
const match = html.match(regex);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
console.log("No match found for vidoza extractor");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/* --- voe --- */
|
||||
|
||||
/**
|
||||
* @name voeExtractor
|
||||
* @author Cufiy
|
||||
*/
|
||||
function voeExtractor(html, url = null) {
|
||||
// Extract the first <script type="application/json">...</script>
|
||||
const jsonScriptMatch = html.match(
|
||||
/<script[^>]+type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/i
|
||||
);
|
||||
if (!jsonScriptMatch) {
|
||||
console.log("No application/json script tag found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const obfuscatedJson = jsonScriptMatch[1].trim();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(obfuscatedJson);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid JSON input.");
|
||||
}
|
||||
if (!Array.isArray(data) || typeof data[0] !== "string") {
|
||||
throw new Error("Input doesn't match expected format.");
|
||||
}
|
||||
let obfuscatedString = data[0];
|
||||
// Step 1: ROT13
|
||||
let step1 = voeRot13(obfuscatedString);
|
||||
// Step 2: Remove patterns
|
||||
let step2 = voeRemovePatterns(step1);
|
||||
// Step 3: Base64 decode
|
||||
let step3 = voeBase64Decode(step2);
|
||||
// Step 4: Subtract 3 from each char code
|
||||
let step4 = voeShiftChars(step3, 3);
|
||||
// Step 5: Reverse string
|
||||
let step5 = step4.split("").reverse().join("");
|
||||
// Step 6: Base64 decode again
|
||||
let step6 = voeBase64Decode(step5);
|
||||
// Step 7: Parse as JSON
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(step6);
|
||||
} catch (e) {
|
||||
throw new Error("Final JSON parse error: " + e.message);
|
||||
}
|
||||
// console.log("Decoded JSON:", result);
|
||||
// check if direct_access_url is set, not null and starts with http
|
||||
if (result && typeof result === "object") {
|
||||
const streamUrl =
|
||||
result.direct_access_url ||
|
||||
result.source
|
||||
.map((source) => source.direct_access_url)
|
||||
.find((url) => url && url.startsWith("http"));
|
||||
if (streamUrl) {
|
||||
console.log("Voe Stream URL: " + streamUrl);
|
||||
return streamUrl;
|
||||
} else {
|
||||
console.log("No stream URL found in the decoded JSON");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function voeRot13(str) {
|
||||
return str.replace(/[a-zA-Z]/g, function (c) {
|
||||
return String.fromCharCode(
|
||||
(c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13)
|
||||
? c
|
||||
: c - 26
|
||||
);
|
||||
});
|
||||
}
|
||||
function voeRemovePatterns(str) {
|
||||
const patterns = ["@$", "^^", "~@", "%?", "*~", "!!", "#&"];
|
||||
let result = str;
|
||||
for (const pat of patterns) {
|
||||
result = result.split(pat).join("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function voeBase64Decode(str) {
|
||||
// atob is available in browsers and Node >= 16
|
||||
if (typeof atob === "function") {
|
||||
return atob(str);
|
||||
}
|
||||
// Node.js fallback
|
||||
return Buffer.from(str, "base64").toString("utf-8");
|
||||
}
|
||||
function voeShiftChars(str, shift) {
|
||||
return str
|
||||
.split("")
|
||||
.map((c) => String.fromCharCode(c.charCodeAt(0) - shift))
|
||||
.join("");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// PLUGINS //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
await console.log('soraFetch error: ' + error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* {GE END} */
|
||||
Loading…
Add table
Add a link
Reference in a new issue