mirror of
https://git.luna-app.eu/50n50/sources
synced 2025-12-21 13:16:21 +01:00
aa
This commit is contained in:
parent
def28c29e0
commit
05ffcfb54d
16 changed files with 1 additions and 2250 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,4 +9,5 @@
|
||||||
*.tmp
|
*.tmp
|
||||||
*.tmp.driveupload
|
*.tmp.driveupload
|
||||||
.tmp.driveupload/
|
.tmp.driveupload/
|
||||||
|
.tmp.drivedownload/
|
||||||
update_global_extractor.py
|
update_global_extractor.py
|
||||||
Binary file not shown.
|
|
@ -1,296 +0,0 @@
|
||||||
async function searchResults(query) {
|
|
||||||
const encodeQuery = keyword => encodeURIComponent(keyword);
|
|
||||||
const searchBaseUrl = "https://animekai.to/browser?keyword=";
|
|
||||||
const baseUrl = "https://animekai.to";
|
|
||||||
|
|
||||||
const posterHrefRegex = /href="[^"]*" class="poster"/g;
|
|
||||||
const titleRegex = /class="title"[^>]*title="[^"]*"/g;
|
|
||||||
const imageRegex = /data-src="[^"]*"/g;
|
|
||||||
const extractHrefRegex = /href="([^"]*)"/;
|
|
||||||
const extractImageRegex = /data-src="([^"]*)"/;
|
|
||||||
const extractTitleRegex = /title="([^"]*)"/;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encodedQuery = encodeQuery(query);
|
|
||||||
const searchUrl = searchBaseUrl + encodedQuery;
|
|
||||||
const response = await fetchv2(searchUrl);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
const posterMatches = htmlText.match(posterHrefRegex) || [];
|
|
||||||
const titleMatches = htmlText.match(titleRegex) || [];
|
|
||||||
const imageMatches = htmlText.match(imageRegex) || [];
|
|
||||||
|
|
||||||
const minLength = Math.min(posterMatches.length, titleMatches.length, imageMatches.length);
|
|
||||||
|
|
||||||
for (let index = 0; index < minLength; index++) {
|
|
||||||
const hrefMatch = posterMatches[index].match(extractHrefRegex);
|
|
||||||
const fullHref = hrefMatch ?
|
|
||||||
(hrefMatch[1].startsWith("http") ? hrefMatch[1] : baseUrl + hrefMatch[1]) :
|
|
||||||
null;
|
|
||||||
|
|
||||||
const imageMatch = imageMatches[index].match(extractImageRegex);
|
|
||||||
const imageSrc = imageMatch ? imageMatch[1] : null;
|
|
||||||
|
|
||||||
const titleMatch = titleMatches[index].match(extractTitleRegex);
|
|
||||||
const cleanTitle = titleMatch ?
|
|
||||||
decodeHtmlEntities(titleMatch[1]) :
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (fullHref && imageSrc && cleanTitle) {
|
|
||||||
results.push({
|
|
||||||
href: fullHref,
|
|
||||||
image: imageSrc,
|
|
||||||
title: cleanTitle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(results);
|
|
||||||
} catch (error) {
|
|
||||||
return JSON.stringify([{
|
|
||||||
href: "",
|
|
||||||
image: "",
|
|
||||||
title: "Search failed: " + error.message
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractDetails(url) {
|
|
||||||
try {
|
|
||||||
const response = await fetchv2(url);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
console.log(htmlText);
|
|
||||||
|
|
||||||
const descriptionMatch = (/<div class="desc text-expand">([\s\S]*?)<\/div>/.exec(htmlText) || [])[1];
|
|
||||||
const aliasesMatch = (/<small class="al-title text-expand">([\s\S]*?)<\/small>/.exec(htmlText) || [])[1];
|
|
||||||
|
|
||||||
return JSON.stringify([{
|
|
||||||
description: descriptionMatch ? cleanHtmlSymbols(descriptionMatch) : "Not available",
|
|
||||||
aliases: aliasesMatch ? cleanHtmlSymbols(aliasesMatch) : "Not available",
|
|
||||||
airdate: "If stream doesn't load try later or disable VPN/DNS"
|
|
||||||
}]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching details:" + error);
|
|
||||||
return [{
|
|
||||||
description: "Error loading description",
|
|
||||||
aliases: "Aliases: Unknown",
|
|
||||||
airdate: "Aired: Unknown"
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractEpisodes(url) {
|
|
||||||
try {
|
|
||||||
const actualUrl = url.replace("Animekai:", "").trim();
|
|
||||||
const htmlText = await (await fetchv2(actualUrl)).text();
|
|
||||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
|
||||||
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
|
||||||
|
|
||||||
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
const token = tokenData.result;
|
|
||||||
|
|
||||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
|
||||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
|
||||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
|
||||||
|
|
||||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
|
||||||
|
|
||||||
const episodes = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
|
||||||
number: parseInt(episodeNum, 10),
|
|
||||||
href: `https://animekai.to/ajax/links/list?token=${episodeToken}&_=ENCRYPT_ME`
|
|
||||||
}));
|
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching episodes:" + err);
|
|
||||||
return [{
|
|
||||||
number: 1,
|
|
||||||
href: "Error fetching episodes"
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractStreamUrl(url) {
|
|
||||||
try {
|
|
||||||
const tokenMatch = url.match(/token=([^&]+)/);
|
|
||||||
if (tokenMatch && tokenMatch[1]) {
|
|
||||||
const rawToken = tokenMatch[1];
|
|
||||||
const encryptResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(rawToken)}`);
|
|
||||||
const encryptData = await encryptResponse.json();
|
|
||||||
const encryptedToken = encryptData.result;
|
|
||||||
url = url.replace('&_=ENCRYPT_ME', `&_=${encryptedToken}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchUrl = `${url}`;
|
|
||||||
const response = await fetchv2(fetchUrl);
|
|
||||||
const text = await response.text();
|
|
||||||
const cleanedHtml = cleanJsonHtml(text);
|
|
||||||
const subRegex = /<div class="server-items lang-group" data-id="sub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const softsubRegex = /<div class="server-items lang-group" data-id="softsub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const dubRegex = /<div class="server-items lang-group" data-id="dub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const subMatch = subRegex.exec(cleanedHtml);
|
|
||||||
const softsubMatch = softsubRegex.exec(cleanedHtml);
|
|
||||||
const dubMatch = dubRegex.exec(cleanedHtml);
|
|
||||||
const subContent = subMatch ? subMatch[1].trim() : "";
|
|
||||||
const softsubContent = softsubMatch ? softsubMatch[1].trim() : "";
|
|
||||||
const dubContent = dubMatch ? dubMatch[1].trim() : "";
|
|
||||||
const serverSpanRegex = /<span class="server"[^>]*data-lid="([^"]+)"[^>]*>Server 1<\/span>/;
|
|
||||||
const serverIdDub = serverSpanRegex.exec(dubContent)?.[1];
|
|
||||||
const serverIdSoftsub = serverSpanRegex.exec(softsubContent)?.[1];
|
|
||||||
const serverIdSub = serverSpanRegex.exec(subContent)?.[1];
|
|
||||||
|
|
||||||
const tokenRequestData = [
|
|
||||||
{ name: "Dub", data: serverIdDub },
|
|
||||||
{ name: "Softsub", data: serverIdSoftsub },
|
|
||||||
{ name: "Sub", data: serverIdSub }
|
|
||||||
].filter(item => item.data);
|
|
||||||
|
|
||||||
const tokenPromises = tokenRequestData.map(item =>
|
|
||||||
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(json => ({ name: item.name, data: json.result }))
|
|
||||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
|
||||||
);
|
|
||||||
const tokenResults = await Promise.all(tokenPromises);
|
|
||||||
|
|
||||||
const streamUrls = tokenResults.map(result => {
|
|
||||||
const serverIdMap = {
|
|
||||||
"Dub": serverIdDub,
|
|
||||||
"Softsub": serverIdSoftsub,
|
|
||||||
"Sub": serverIdSub
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
type: result.name,
|
|
||||||
url: `https://animekai.to/ajax/links/view?id=${serverIdMap[result.name]}&_=${result.data}`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const processStreams = async (streamUrls) => {
|
|
||||||
const streamResponses = await Promise.all(
|
|
||||||
streamUrls.map(async ({ type, url }) => {
|
|
||||||
try {
|
|
||||||
const res = await fetchv2(url);
|
|
||||||
const json = await res.json();
|
|
||||||
return {
|
|
||||||
type: type,
|
|
||||||
result: json.result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error fetching ${type} stream:`, error);
|
|
||||||
return {
|
|
||||||
type: type,
|
|
||||||
result: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const decryptRequestData = streamResponses
|
|
||||||
.filter(item => item.result)
|
|
||||||
.map(item => ({
|
|
||||||
name: item.type,
|
|
||||||
data: item.result
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (decryptRequestData.length === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptPromises = decryptRequestData.map(item =>
|
|
||||||
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
|
||||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
|
||||||
);
|
|
||||||
const decryptResults = await Promise.all(decryptPromises);
|
|
||||||
|
|
||||||
const finalResults = {};
|
|
||||||
decryptResults.forEach(result => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(result.data);
|
|
||||||
finalResults[result.name] = parsed.url;
|
|
||||||
console.log(`decrypted${result.name} URL:` + parsed.url);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error parsing ${result.name} result:`, error);
|
|
||||||
finalResults[result.name] = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalResults;
|
|
||||||
};
|
|
||||||
|
|
||||||
const decryptedUrls = await processStreams(streamUrls);
|
|
||||||
const decryptedSub = decryptedUrls.Sub || decryptedUrls.Dub || decryptedUrls.Softsub;
|
|
||||||
|
|
||||||
console.log(decryptedSub);
|
|
||||||
const headers = {
|
|
||||||
"Referer": "https://animekai.to/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (decryptedSub) {
|
|
||||||
const response = await fetchv2(decryptedSub.replace("/e/", "/media/").replace("megaup22", "megaup.site"), headers);
|
|
||||||
const responseJson = await response.json();
|
|
||||||
|
|
||||||
const result = responseJson?.result;
|
|
||||||
|
|
||||||
const postData = {
|
|
||||||
"text": result,
|
|
||||||
"Useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalResponse = await fetchv2("https://ilovekai.simplepostrequest.workers.dev/ilovebush", {}, "POST", JSON.stringify(postData));
|
|
||||||
const finalJson = await finalResponse.json();
|
|
||||||
const m3u8Link = finalJson?.result?.sources?.[0]?.file;
|
|
||||||
|
|
||||||
return m3u8Link;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "error";
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Fetch error:"+ error);
|
|
||||||
return "https://error.org";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanHtmlSymbols(string) {
|
|
||||||
if (!string) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
.replace(/’/g, "'")
|
|
||||||
.replace(/–/g, "-")
|
|
||||||
.replace(/&#[0-9]+;/g, "")
|
|
||||||
.replace(/\r?\n|\r/g, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanJsonHtml(jsonHtml) {
|
|
||||||
if (!jsonHtml) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return jsonHtml
|
|
||||||
.replace(/\\"/g, "\"")
|
|
||||||
.replace(/\\'/g, "'")
|
|
||||||
.replace(/\\\\/g, "\\")
|
|
||||||
.replace(/\\n/g, "\n")
|
|
||||||
.replace(/\\t/g, "\t")
|
|
||||||
.replace(/\\r/g, "\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeHtmlEntities(text) {
|
|
||||||
if (!text) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/"/g, "\"")
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/ /g, " ");
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"sourceName": "AnimeKai (Hardsub)",
|
|
||||||
"iconUrl": "https://apktodo.io/uploads/2025/5/animekai-icon.jpg",
|
|
||||||
"author": {
|
|
||||||
"name": "50/50",
|
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
|
||||||
},
|
|
||||||
"version": "1.0.5",
|
|
||||||
"language": "English",
|
|
||||||
"streamType": "HLS",
|
|
||||||
"quality": "1080p",
|
|
||||||
"baseUrl": "https://animekai.to/",
|
|
||||||
"searchBaseUrl": "https://animekai.to/",
|
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animekai/hardsub/animekai.js",
|
|
||||||
"type": "anime",
|
|
||||||
"asyncJS": true,
|
|
||||||
"softsub": false,
|
|
||||||
"downloadSupport": true,
|
|
||||||
"note": "Make sure you're on the latest version of Sora."
|
|
||||||
}
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
async function searchResults(query) {
|
|
||||||
const encodeQuery = keyword => encodeURIComponent(keyword);
|
|
||||||
const searchBaseUrl = "https://animekai.to/browser?keyword=";
|
|
||||||
const baseUrl = "https://animekai.to";
|
|
||||||
|
|
||||||
const posterHrefRegex = /href="[^"]*" class="poster"/g;
|
|
||||||
const titleRegex = /class="title"[^>]*title="[^"]*"/g;
|
|
||||||
const imageRegex = /data-src="[^"]*"/g;
|
|
||||||
const extractHrefRegex = /href="([^"]*)"/;
|
|
||||||
const extractImageRegex = /data-src="([^"]*)"/;
|
|
||||||
const extractTitleRegex = /title="([^"]*)"/;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encodedQuery = encodeQuery(query);
|
|
||||||
const searchUrl = searchBaseUrl + encodedQuery;
|
|
||||||
const response = await fetchv2(searchUrl);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
const posterMatches = htmlText.match(posterHrefRegex) || [];
|
|
||||||
const titleMatches = htmlText.match(titleRegex) || [];
|
|
||||||
const imageMatches = htmlText.match(imageRegex) || [];
|
|
||||||
|
|
||||||
const minLength = Math.min(posterMatches.length, titleMatches.length, imageMatches.length);
|
|
||||||
|
|
||||||
for (let index = 0; index < minLength; index++) {
|
|
||||||
const hrefMatch = posterMatches[index].match(extractHrefRegex);
|
|
||||||
const fullHref = hrefMatch ?
|
|
||||||
(hrefMatch[1].startsWith("http") ? hrefMatch[1] : baseUrl + hrefMatch[1]) :
|
|
||||||
null;
|
|
||||||
|
|
||||||
const imageMatch = imageMatches[index].match(extractImageRegex);
|
|
||||||
const imageSrc = imageMatch ? imageMatch[1] : null;
|
|
||||||
|
|
||||||
const titleMatch = titleMatches[index].match(extractTitleRegex);
|
|
||||||
const cleanTitle = titleMatch ?
|
|
||||||
decodeHtmlEntities(titleMatch[1]) :
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (fullHref && imageSrc && cleanTitle) {
|
|
||||||
results.push({
|
|
||||||
href: fullHref,
|
|
||||||
image: imageSrc,
|
|
||||||
title: cleanTitle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(results);
|
|
||||||
} catch (error) {
|
|
||||||
return JSON.stringify([{
|
|
||||||
href: "",
|
|
||||||
image: "",
|
|
||||||
title: "Search failed: " + error.message
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractDetails(url) {
|
|
||||||
try {
|
|
||||||
const response = await fetchv2(url);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
console.log(htmlText);
|
|
||||||
|
|
||||||
const descriptionMatch = (/<div class="desc text-expand">([\s\S]*?)<\/div>/.exec(htmlText) || [])[1];
|
|
||||||
const aliasesMatch = (/<small class="al-title text-expand">([\s\S]*?)<\/small>/.exec(htmlText) || [])[1];
|
|
||||||
|
|
||||||
return JSON.stringify([{
|
|
||||||
description: descriptionMatch ? cleanHtmlSymbols(descriptionMatch) : "Not available",
|
|
||||||
aliases: aliasesMatch ? cleanHtmlSymbols(aliasesMatch) : "Not available",
|
|
||||||
airdate: "If stream doesn't load try later or disable VPN/DNS"
|
|
||||||
}]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching details:" + error);
|
|
||||||
return [{
|
|
||||||
description: "Error loading description",
|
|
||||||
aliases: "Aliases: Unknown",
|
|
||||||
airdate: "Aired: Unknown"
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractEpisodes(url) {
|
|
||||||
try {
|
|
||||||
const actualUrl = url.replace("Animekai:", "").trim();
|
|
||||||
const htmlText = await (await fetchv2(actualUrl)).text();
|
|
||||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
|
||||||
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
|
||||||
|
|
||||||
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
const token = tokenData.result;
|
|
||||||
|
|
||||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
|
||||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
|
||||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
|
||||||
|
|
||||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
|
||||||
|
|
||||||
const episodes = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
|
||||||
number: parseInt(episodeNum, 10),
|
|
||||||
href: `https://animekai.to/ajax/links/list?token=${episodeToken}&_=ENCRYPT_ME`
|
|
||||||
}));
|
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching episodes:" + err);
|
|
||||||
return [{
|
|
||||||
number: 1,
|
|
||||||
href: "Error fetching episodes"
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractStreamUrl(url) {
|
|
||||||
try {
|
|
||||||
const tokenMatch = url.match(/token=([^&]+)/);
|
|
||||||
if (tokenMatch && tokenMatch[1]) {
|
|
||||||
const rawToken = tokenMatch[1];
|
|
||||||
const encryptResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(rawToken)}`);
|
|
||||||
const encryptData = await encryptResponse.json();
|
|
||||||
const encryptedToken = encryptData.result;
|
|
||||||
url = url.replace('&_=ENCRYPT_ME', `&_=${encryptedToken}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchUrl = `${url}`;
|
|
||||||
const response = await fetchv2(fetchUrl);
|
|
||||||
const text = await response.text();
|
|
||||||
const cleanedHtml = cleanJsonHtml(text);
|
|
||||||
const subRegex = /<div class="server-items lang-group" data-id="sub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const softsubRegex = /<div class="server-items lang-group" data-id="softsub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const dubRegex = /<div class="server-items lang-group" data-id="dub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const subMatch = subRegex.exec(cleanedHtml);
|
|
||||||
const softsubMatch = softsubRegex.exec(cleanedHtml);
|
|
||||||
const dubMatch = dubRegex.exec(cleanedHtml);
|
|
||||||
const subContent = subMatch ? subMatch[1].trim() : "";
|
|
||||||
const softsubContent = softsubMatch ? softsubMatch[1].trim() : "";
|
|
||||||
const dubContent = dubMatch ? dubMatch[1].trim() : "";
|
|
||||||
const serverSpanRegex = /<span class="server"[^>]*data-lid="([^"]+)"[^>]*>Server 1<\/span>/;
|
|
||||||
const serverIdDub = serverSpanRegex.exec(dubContent)?.[1];
|
|
||||||
const serverIdSoftsub = serverSpanRegex.exec(softsubContent)?.[1];
|
|
||||||
const serverIdSub = serverSpanRegex.exec(subContent)?.[1];
|
|
||||||
|
|
||||||
const tokenRequestData = [
|
|
||||||
{ name: "Dub", data: serverIdDub },
|
|
||||||
{ name: "Softsub", data: serverIdSoftsub },
|
|
||||||
{ name: "Sub", data: serverIdSub }
|
|
||||||
].filter(item => item.data);
|
|
||||||
|
|
||||||
const tokenPromises = tokenRequestData.map(item =>
|
|
||||||
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(json => ({ name: item.name, data: json.result }))
|
|
||||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
|
||||||
);
|
|
||||||
const tokenResults = await Promise.all(tokenPromises);
|
|
||||||
|
|
||||||
const streamUrls = tokenResults.map(result => {
|
|
||||||
const serverIdMap = {
|
|
||||||
"Dub": serverIdDub,
|
|
||||||
"Softsub": serverIdSoftsub,
|
|
||||||
"Sub": serverIdSub
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
type: result.name,
|
|
||||||
url: `https://animekai.to/ajax/links/view?id=${serverIdMap[result.name]}&_=${result.data}`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const processStreams = async (streamUrls) => {
|
|
||||||
const streamResponses = await Promise.all(
|
|
||||||
streamUrls.map(async ({ type, url }) => {
|
|
||||||
try {
|
|
||||||
const res = await fetchv2(url);
|
|
||||||
const json = await res.json();
|
|
||||||
return {
|
|
||||||
type: type,
|
|
||||||
result: json.result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error fetching ${type} stream:`, error);
|
|
||||||
return {
|
|
||||||
type: type,
|
|
||||||
result: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const decryptRequestData = streamResponses
|
|
||||||
.filter(item => item.result)
|
|
||||||
.map(item => ({
|
|
||||||
name: item.type,
|
|
||||||
data: item.result
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (decryptRequestData.length === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptPromises = decryptRequestData.map(item =>
|
|
||||||
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
|
||||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
|
||||||
);
|
|
||||||
const decryptResults = await Promise.all(decryptPromises);
|
|
||||||
|
|
||||||
const finalResults = {};
|
|
||||||
decryptResults.forEach(result => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(result.data);
|
|
||||||
finalResults[result.name] = parsed.url;
|
|
||||||
console.log(`decrypted${result.name} URL:` + parsed.url);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error parsing ${result.name} result:`, error);
|
|
||||||
finalResults[result.name] = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalResults;
|
|
||||||
};
|
|
||||||
|
|
||||||
const decryptedUrls = await processStreams(streamUrls);
|
|
||||||
const decryptedDub = decryptedUrls.Dub || decryptedUrls.Sub || decryptedUrls.Softsub;
|
|
||||||
|
|
||||||
console.log(decryptedDub);
|
|
||||||
const headers = {
|
|
||||||
"Referer": "https://animekai.to/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (decryptedDub) {
|
|
||||||
const response = await fetchv2(decryptedDub.replace("/e/", "/media/").replace("megaup22", "megaup.site"), headers);
|
|
||||||
const responseJson = await response.json();
|
|
||||||
|
|
||||||
const result = responseJson?.result;
|
|
||||||
|
|
||||||
const postData = {
|
|
||||||
"text": result,
|
|
||||||
"Useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalResponse = await fetchv2("https://ilovekai.simplepostrequest.workers.dev/ilovebush", {}, "POST", JSON.stringify(postData));
|
|
||||||
const finalJson = await finalResponse.json();
|
|
||||||
const m3u8Link = finalJson?.result?.sources?.[0]?.file;
|
|
||||||
|
|
||||||
return m3u8Link;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "error";
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Fetch error:"+ error);
|
|
||||||
return "https://error.org";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanHtmlSymbols(string) {
|
|
||||||
if (!string) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
.replace(/’/g, "'")
|
|
||||||
.replace(/–/g, "-")
|
|
||||||
.replace(/&#[0-9]+;/g, "")
|
|
||||||
.replace(/\r?\n|\r/g, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanJsonHtml(jsonHtml) {
|
|
||||||
if (!jsonHtml) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return jsonHtml
|
|
||||||
.replace(/\\"/g, "\"")
|
|
||||||
.replace(/\\'/g, "'")
|
|
||||||
.replace(/\\\\/g, "\\")
|
|
||||||
.replace(/\\n/g, "\n")
|
|
||||||
.replace(/\\t/g, "\t")
|
|
||||||
.replace(/\\r/g, "\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeHtmlEntities(text) {
|
|
||||||
if (!text) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/"/g, "\"")
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/ /g, " ");
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"sourceName": "AnimeKai (Dub)",
|
|
||||||
"iconUrl": "https://apktodo.io/uploads/2025/5/animekai-icon.jpg",
|
|
||||||
"author": {
|
|
||||||
"name": "50/50",
|
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
|
||||||
},
|
|
||||||
"version": "1.0.6",
|
|
||||||
"language": "English",
|
|
||||||
"streamType": "HLS",
|
|
||||||
"quality": "1080p",
|
|
||||||
"baseUrl": "https://animekai.to/",
|
|
||||||
"searchBaseUrl": "https://animekai.to/",
|
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/animekai/dub/animekai.js",
|
|
||||||
"type": "anime",
|
|
||||||
"asyncJS": true,
|
|
||||||
"softsub": false,
|
|
||||||
"downloadSupport": true,
|
|
||||||
"note": "Make sure you're on the latest version of Sora."
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"sourceName": "1Movies",
|
|
||||||
"iconUrl": "https://1movies.bz/assets/uploads/675b5c22f2829fc8e3a4030af7f4284acad017e5241280b3dc21.png",
|
|
||||||
"author": {
|
|
||||||
"name": "50/50",
|
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
|
||||||
},
|
|
||||||
"version": "1.0.9",
|
|
||||||
"language": "English",
|
|
||||||
"streamType": "HLS",
|
|
||||||
"quality": "1080p",
|
|
||||||
"baseUrl": "https://animekai.to/",
|
|
||||||
"searchBaseUrl": "https://1movies.bz/home",
|
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/1movies/1movies.js",
|
|
||||||
"type": "shows/movies",
|
|
||||||
"asyncJS": true,
|
|
||||||
"softsub": true,
|
|
||||||
"downloadSupport": true
|
|
||||||
}
|
|
||||||
|
|
@ -1,318 +0,0 @@
|
||||||
async function searchResults(query) {
|
|
||||||
const encodeQuery = keyword => encodeURIComponent(keyword);
|
|
||||||
const searchBaseUrl = "https://1movies.bz/browser?keyword=";
|
|
||||||
const baseUrl = "https://1movies.bz";
|
|
||||||
|
|
||||||
const posterHrefRegex = /href="([^"]*)" class="poster"/g;
|
|
||||||
const titleRegex = /class="title" href="[^"]*">([^<]*)</g;
|
|
||||||
const imageRegex = /data-src="([^"]*)"/g;
|
|
||||||
|
|
||||||
const extractResultsFromHTML = (htmlText) => {
|
|
||||||
const results = [];
|
|
||||||
const posterMatches = [...htmlText.matchAll(posterHrefRegex)];
|
|
||||||
const titleMatches = [...htmlText.matchAll(titleRegex)];
|
|
||||||
const imageMatches = [...htmlText.matchAll(imageRegex)];
|
|
||||||
|
|
||||||
const minLength = Math.min(posterMatches.length, titleMatches.length, imageMatches.length);
|
|
||||||
|
|
||||||
for (let index = 0; index < minLength; index++) {
|
|
||||||
const href = posterMatches[index][1];
|
|
||||||
const fullHref = href.startsWith("http") ? href : baseUrl + href;
|
|
||||||
|
|
||||||
const imageSrc = imageMatches[index][1];
|
|
||||||
|
|
||||||
const title = titleMatches[index][1];
|
|
||||||
const cleanTitle = decodeHtmlEntities(title);
|
|
||||||
|
|
||||||
if (fullHref && imageSrc && cleanTitle) {
|
|
||||||
results.push({
|
|
||||||
href: fullHref,
|
|
||||||
image: imageSrc,
|
|
||||||
title: cleanTitle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encodedQuery = encodeQuery(query);
|
|
||||||
|
|
||||||
const urls = [
|
|
||||||
`${searchBaseUrl}${encodedQuery}`,
|
|
||||||
`${searchBaseUrl}${encodedQuery}&page=2`,
|
|
||||||
`${searchBaseUrl}${encodedQuery}&page=3`
|
|
||||||
];
|
|
||||||
|
|
||||||
const responses = await Promise.all(urls.map(url => fetchv2(url)));
|
|
||||||
|
|
||||||
const htmlTexts = await Promise.all(responses.map(response => response.text()));
|
|
||||||
|
|
||||||
const allResults = [];
|
|
||||||
htmlTexts.forEach(htmlText => {
|
|
||||||
const pageResults = extractResultsFromHTML(htmlText);
|
|
||||||
allResults.push(...pageResults);
|
|
||||||
});
|
|
||||||
|
|
||||||
return JSON.stringify(allResults);
|
|
||||||
} catch (error) {
|
|
||||||
return JSON.stringify([{
|
|
||||||
href: "",
|
|
||||||
image: "",
|
|
||||||
title: "Search failed: " + error.message
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractDetails(url) {
|
|
||||||
try {
|
|
||||||
const response = await fetchv2(url);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
|
|
||||||
const descriptionMatch = (/<div class="description text-expand">([\s\S]*?)<\/div>/.exec(htmlText) || [])[1];
|
|
||||||
const aliasesMatch = (/<small class="al-title text-expand">([\s\S]*?)<\/small>/.exec(htmlText) || [])[1];
|
|
||||||
const airdateMatch = (/<li>Released:\s*<span[^>]*>(.*?)<\/span>/.exec(htmlText) || [])[1];
|
|
||||||
|
|
||||||
return JSON.stringify([{
|
|
||||||
description: descriptionMatch ? cleanHtmlSymbols(descriptionMatch) : "Not available",
|
|
||||||
aliases: aliasesMatch ? cleanHtmlSymbols(aliasesMatch) : "Not aliases",
|
|
||||||
airdate: airdateMatch ? cleanHtmlSymbols(airdateMatch) : "Not available"
|
|
||||||
}]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching details:" + error);
|
|
||||||
return [{
|
|
||||||
description: "Error loading description",
|
|
||||||
aliases: "Not available",
|
|
||||||
airdate: "Not available"
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractEpisodes(movieUrl) {
|
|
||||||
try {
|
|
||||||
const response = await fetchv2(movieUrl);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
const movieIDMatch = (htmlText.match(/<div class="detail-lower"[^>]*id="movie-rating"[^>]*data-id="([^"]+)"/) || [])[1];
|
|
||||||
if (!movieIDMatch) {
|
|
||||||
return [{
|
|
||||||
error: "MovieID not found"
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
const movieIdApiUrl = `https://enc-dec.app/api/enc-movies-flix?text=${movieIDMatch}`;
|
|
||||||
const movieIdTokenResponse = await fetchv2(movieIdApiUrl);
|
|
||||||
const movieIdTokenData = await movieIdTokenResponse.json();
|
|
||||||
const token = movieIdTokenData.result;
|
|
||||||
|
|
||||||
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
|
|
||||||
const episodeListResponse = await fetchv2(episodeListUrl);
|
|
||||||
const episodeListData = await episodeListResponse.json();
|
|
||||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
|
||||||
|
|
||||||
const episodeRegex = /<a[^>]+eid="([^"]+)"[^>]+num="([^"]+)"[^>]*>/g;
|
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
|
||||||
|
|
||||||
const episodes = episodeMatches.map(([_, episodeToken, episodeNum]) => ({
|
|
||||||
number: parseInt(episodeNum, 10),
|
|
||||||
href: `https://1movies.bz/ajax/links/list?eid=${episodeToken}&_=ENCRYPT_ME`
|
|
||||||
}));
|
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching episodes:" + err);
|
|
||||||
return [{
|
|
||||||
number: 1,
|
|
||||||
href: "Error fetching episodes"
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractStreamUrl(url) {
|
|
||||||
try {
|
|
||||||
const eidMatch = url.match(/eid=([^&]+)/);
|
|
||||||
if (eidMatch && eidMatch[1]) {
|
|
||||||
const rawEpisodeToken = eidMatch[1];
|
|
||||||
const encryptResponse = await fetchv2(`https://enc-dec.app/api/enc-movies-flix?text=${rawEpisodeToken}`);
|
|
||||||
const encryptData = await encryptResponse.json();
|
|
||||||
const encryptedToken = encryptData.result;
|
|
||||||
url = url.replace('&_=ENCRYPT_ME', `&_=${encryptedToken}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchUrl = `${url}`;
|
|
||||||
const response = await fetchv2(fetchUrl);
|
|
||||||
const responseData = await response.json();
|
|
||||||
const cleanedHtml = cleanJsonHtml(responseData.result);
|
|
||||||
|
|
||||||
const server1Regex = /<div class="server wnav-item"[^>]*data-lid="([^"]+)"[^>]*>\s*<span>Server 1<\/span>/;
|
|
||||||
const server1Match = server1Regex.exec(cleanedHtml);
|
|
||||||
|
|
||||||
if (!server1Match) {
|
|
||||||
console.log("Server 1 not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverId = server1Match[1];
|
|
||||||
|
|
||||||
const tokenRequestData = [{ name: "Server1", data: serverId }];
|
|
||||||
|
|
||||||
const tokenBatchResponse = await fetchv2(
|
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs",
|
|
||||||
{},
|
|
||||||
"POST",
|
|
||||||
JSON.stringify(tokenRequestData)
|
|
||||||
);
|
|
||||||
const tokenResults = await tokenBatchResponse.json();
|
|
||||||
const token = tokenResults[0]?.data;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.log("Token not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamUrl = `https://1movies.bz/ajax/links/view?id=${serverId}&_=${token}`;
|
|
||||||
const streamResponse = await fetchv2(streamUrl);
|
|
||||||
const streamData = await streamResponse.json();
|
|
||||||
|
|
||||||
if (!streamData.result) {
|
|
||||||
console.log("Stream result not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptRequestData = [{ name: "Server1", data: streamData.result }];
|
|
||||||
|
|
||||||
const decryptBatchResponse = await fetchv2(
|
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/iloveboobs",
|
|
||||||
{},
|
|
||||||
"POST",
|
|
||||||
JSON.stringify(decryptRequestData)
|
|
||||||
);
|
|
||||||
const decryptedResponse = await decryptBatchResponse.json();
|
|
||||||
const decryptedUrl = decryptedResponse[0]?.data.url;
|
|
||||||
|
|
||||||
const subListEncoded = decryptedUrl.split("sub.list=")[1]?.split("&")[0];
|
|
||||||
let subtitles = "N/A";
|
|
||||||
|
|
||||||
if (subListEncoded) {
|
|
||||||
try {
|
|
||||||
const subListUrl = decodeURIComponent(subListEncoded);
|
|
||||||
const subResponse = await fetchv2(subListUrl);
|
|
||||||
subtitles = await subResponse.json();
|
|
||||||
} catch {
|
|
||||||
subtitles = "N/A";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const englishSubUrl = Array.isArray(subtitles)
|
|
||||||
? subtitles.find(sub => sub.label === "English")?.file.replace(/\\\//g, "/")
|
|
||||||
: "N/A";
|
|
||||||
|
|
||||||
if (!decryptedUrl) {
|
|
||||||
console.log("Decryption failed");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Referer": "https://1movies.bz/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
};
|
|
||||||
|
|
||||||
const mediaResponse = await fetchv2(decryptedUrl.replace("/e/", "/media/"), headers);
|
|
||||||
const mediaJson = await mediaResponse.json();
|
|
||||||
|
|
||||||
const result = mediaJson?.result;
|
|
||||||
if (!result) {
|
|
||||||
console.log("Media result not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const postData = {
|
|
||||||
"text": result,
|
|
||||||
"Useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
};
|
|
||||||
|
|
||||||
const finalResponse = await fetchv2("https://ilovekai.simplepostrequest.workers.dev/ilovebush", {}, "POST", JSON.stringify(postData));
|
|
||||||
const finalJson = await finalResponse.json();
|
|
||||||
|
|
||||||
const m3u8Link = finalJson?.result?.sources?.[0]?.file;
|
|
||||||
|
|
||||||
const m3u8Response = await fetchv2(m3u8Link);
|
|
||||||
const m3u8Text = await m3u8Response.text();
|
|
||||||
|
|
||||||
const baseUrl = m3u8Link.substring(0, m3u8Link.lastIndexOf('/') + 1);
|
|
||||||
|
|
||||||
const streams = [];
|
|
||||||
const lines = m3u8Text.split('\n');
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i].trim();
|
|
||||||
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
|
||||||
const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
|
|
||||||
let quality = 'Unknown';
|
|
||||||
|
|
||||||
if (resolutionMatch) {
|
|
||||||
const [width, height] = resolutionMatch[1].split('x');
|
|
||||||
quality = `${height}p`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i + 1 < lines.length) {
|
|
||||||
const streamPath = lines[i + 1].trim();
|
|
||||||
const streamUrl = baseUrl + streamPath;
|
|
||||||
|
|
||||||
streams.push({
|
|
||||||
title: quality,
|
|
||||||
streamUrl: streamUrl
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const returnValue = {
|
|
||||||
streams: streams,
|
|
||||||
subtitle: englishSubUrl !== "N/A" ? englishSubUrl : ""
|
|
||||||
};
|
|
||||||
console.log("RETURN: " + JSON.stringify(returnValue));
|
|
||||||
return JSON.stringify(returnValue);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Fetch error:"+ error);
|
|
||||||
return "https://error.org";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanHtmlSymbols(string) {
|
|
||||||
if (!string) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
.replace(/’/g, "'")
|
|
||||||
.replace(/–/g, "-")
|
|
||||||
.replace(/&#[0-9]+;/g, "")
|
|
||||||
.replace(/\r?\n|\r/g, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanJsonHtml(jsonHtml) {
|
|
||||||
if (!jsonHtml) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return jsonHtml
|
|
||||||
.replace(/\\"/g, "\"")
|
|
||||||
.replace(/\\'/g, "'")
|
|
||||||
.replace(/\\\\/g, "\\")
|
|
||||||
.replace(/\\n/g, "\n")
|
|
||||||
.replace(/\\t/g, "\t")
|
|
||||||
.replace(/\\r/g, "\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeHtmlEntities(text) {
|
|
||||||
if (!text) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/"/g, "\"")
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/ /g, " ");
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div style="border-left: 6px solid #f39c12; background: #fff8e5; padding: 12px 16px; border-radius: 6px; font-family: sans-serif;">
|
|
||||||
<p style="margin:0; font-size: 16px;">⚠️ <b style="color:#b57400;">IMPORTANT</b></p>
|
|
||||||
<p style="margin:8px 0 0 0;">Any app meeting the following criteria is free to use my modules:</p>
|
|
||||||
<ol style="margin:8px 0 0 20px;">
|
|
||||||
<li><b>No paywall, subscription, or payment</b> required for my modules
|
|
||||||
(you may charge for other parts of your app).</li>
|
|
||||||
<li><b>No advertisements</b> during the usage of my modules.</li>
|
|
||||||
<li><b>Open source</b>: your app’s source code must be publicly available.</li>
|
|
||||||
</ol>
|
|
||||||
<p style="margin:8px 0 0 0;">All the above terms are mandatory unless given permission by me.<br>
|
|
||||||
In short: <b>no commercial use</b> and <b>transparency is required</b>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div style="border-left: 6px solid #e74c3c; background: #fdeaea; padding: 12px 16px; border-radius: 6px; font-family: sans-serif;">
|
|
||||||
<p style="margin:0; font-size: 16px;">🚫 <b style="color:#a40000;">CAUTION</b></p>
|
|
||||||
<p style="margin:8px 0 0 0;">Do not pay to use these modules — if someone is charging you, it's a scam!</p>
|
|
||||||
<p style="margin:8px 0 0 0;">Neither should you bear watching ads to use these modules. Please report apps that do this forcibly!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[](https://discord.com/users/1072985316916469870)
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
[](https://ko-fi.com/50n50)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"sourceName": "Ashi (あし) - Literally Everything",
|
|
||||||
"iconUrl": "https://files.catbox.moe/y8v199.png",
|
|
||||||
"author": {
|
|
||||||
"name": "50/50",
|
|
||||||
"icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s"
|
|
||||||
},
|
|
||||||
"version": "1.1.1",
|
|
||||||
"language": "English",
|
|
||||||
"streamType": "HLS",
|
|
||||||
"quality": "1080p",
|
|
||||||
"baseUrl": "https://animekai.to/",
|
|
||||||
"searchBaseUrl": "https://animekai.to/",
|
|
||||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/ashi/ashi.js",
|
|
||||||
"type": "anime/movies/shows",
|
|
||||||
"asyncJS": true,
|
|
||||||
"softsub": true,
|
|
||||||
"downloadSupport": true
|
|
||||||
}
|
|
||||||
|
|
@ -1,761 +0,0 @@
|
||||||
//
|
|
||||||
//
|
|
||||||
// Main functions
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
async function searchResults(query) {
|
|
||||||
const encodeQuery = keyword => encodeURIComponent(keyword);
|
|
||||||
|
|
||||||
const decodeHtmlEntities = (str) => {
|
|
||||||
if (!str) return str;
|
|
||||||
return str.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
};
|
|
||||||
|
|
||||||
const fuzzyMatch = (query, title) => {
|
|
||||||
const q = query.toLowerCase().trim();
|
|
||||||
const t = title.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (t === q) return 1000;
|
|
||||||
|
|
||||||
if (t.startsWith(q + ' ') || t.startsWith(q + ':') || t.startsWith(q + '-')) return 950;
|
|
||||||
|
|
||||||
const wordBoundaryRegex = new RegExp(`\\b${q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
||||||
if (wordBoundaryRegex.test(t)) return 900;
|
|
||||||
|
|
||||||
const qTokens = q.split(/\s+/).filter(token => token.length > 0);
|
|
||||||
const tTokens = t.split(/[\s\-:]+/).filter(token => token.length > 0);
|
|
||||||
|
|
||||||
const stopwords = new Set(['the', 'a', 'an', 'and', 'or', 'of', 'in', 'on', 'at', 'to', 'for', 'with']);
|
|
||||||
|
|
||||||
let score = 0;
|
|
||||||
let exactMatches = 0;
|
|
||||||
let partialMatches = 0;
|
|
||||||
let significantMatches = 0;
|
|
||||||
|
|
||||||
qTokens.forEach(qToken => {
|
|
||||||
const isStopword = stopwords.has(qToken);
|
|
||||||
let bestMatch = 0;
|
|
||||||
let hasExactMatch = false;
|
|
||||||
|
|
||||||
tTokens.forEach(tToken => {
|
|
||||||
let matchScore = 0;
|
|
||||||
|
|
||||||
if (tToken === qToken) {
|
|
||||||
matchScore = isStopword ? 25 : 120;
|
|
||||||
hasExactMatch = true;
|
|
||||||
if (!isStopword) significantMatches++;
|
|
||||||
}
|
|
||||||
else if (qToken.includes(tToken) && tToken.length >= 3 && qToken.length <= tToken.length + 2) {
|
|
||||||
matchScore = isStopword ? 8 : 40;
|
|
||||||
if (!isStopword) significantMatches++;
|
|
||||||
}
|
|
||||||
else if (tToken.startsWith(qToken) && qToken.length >= 3) {
|
|
||||||
matchScore = isStopword ? 12 : 70;
|
|
||||||
if (!isStopword) significantMatches++;
|
|
||||||
}
|
|
||||||
else if (qToken.length >= 4 && tToken.length >= 4) {
|
|
||||||
const dist = levenshteinDistance(qToken, tToken);
|
|
||||||
const maxLen = Math.max(qToken.length, tToken.length);
|
|
||||||
const similarity = 1 - (dist / maxLen);
|
|
||||||
|
|
||||||
if (similarity > 0.8) {
|
|
||||||
matchScore = Math.floor(similarity * 60);
|
|
||||||
if (!isStopword) significantMatches++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bestMatch = Math.max(bestMatch, matchScore);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bestMatch > 0) {
|
|
||||||
score += bestMatch;
|
|
||||||
if (hasExactMatch) exactMatches++;
|
|
||||||
else partialMatches++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const significantTokens = qTokens.filter(t => !stopwords.has(t)).length;
|
|
||||||
|
|
||||||
const requiredMatches = Math.max(1, Math.ceil(significantTokens * 0.8));
|
|
||||||
if (significantMatches < requiredMatches) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exactMatches + partialMatches >= qTokens.length) {
|
|
||||||
score += 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
score += exactMatches * 20;
|
|
||||||
|
|
||||||
const extraWords = tTokens.length - qTokens.length;
|
|
||||||
if (extraWords > 2) {
|
|
||||||
score -= (extraWords - 2) * 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
let orderBonus = 0;
|
|
||||||
for (let i = 0; i < qTokens.length - 1; i++) {
|
|
||||||
const currentTokenIndex = tTokens.findIndex(t => t.includes(qTokens[i]));
|
|
||||||
const nextTokenIndex = tTokens.findIndex(t => t.includes(qTokens[i + 1]));
|
|
||||||
|
|
||||||
if (currentTokenIndex !== -1 && nextTokenIndex !== -1 && currentTokenIndex < nextTokenIndex) {
|
|
||||||
orderBonus += 15;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
score += orderBonus;
|
|
||||||
|
|
||||||
return Math.max(0, score);
|
|
||||||
};
|
|
||||||
|
|
||||||
const levenshteinDistance = (a, b) => {
|
|
||||||
const matrix = [];
|
|
||||||
|
|
||||||
for (let i = 0; i <= b.length; i++) {
|
|
||||||
matrix[i] = [i];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = 0; j <= a.length; j++) {
|
|
||||||
matrix[0][j] = j;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i <= b.length; i++) {
|
|
||||||
for (let j = 1; j <= a.length; j++) {
|
|
||||||
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
||||||
matrix[i][j] = matrix[i - 1][j - 1];
|
|
||||||
} else {
|
|
||||||
matrix[i][j] = Math.min(
|
|
||||||
matrix[i - 1][j - 1] + 1,
|
|
||||||
matrix[i][j - 1] + 1,
|
|
||||||
matrix[i - 1][j] + 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matrix[b.length][a.length];
|
|
||||||
};
|
|
||||||
|
|
||||||
const animekaiSearch = async () => {
|
|
||||||
const searchBaseUrl = "https://animekai.to/browser?keyword=";
|
|
||||||
const baseUrl = "https://animekai.to";
|
|
||||||
|
|
||||||
const posterHrefRegex = /href="[^"]*" class="poster"/g;
|
|
||||||
const titleRegex = /class="title"[^>]*title="[^"]*"/g;
|
|
||||||
const imageRegex = /data-src="[^"]*"/g;
|
|
||||||
const extractHrefRegex = /href="([^"]*)"/;
|
|
||||||
const extractImageRegex = /data-src="([^"]*)"/;
|
|
||||||
const extractTitleRegex = /title="([^"]*)"/;
|
|
||||||
|
|
||||||
const extractResultsFromHTML = (htmlText) => {
|
|
||||||
const results = [];
|
|
||||||
const posterMatches = htmlText.match(posterHrefRegex) || [];
|
|
||||||
const titleMatches = htmlText.match(titleRegex) || [];
|
|
||||||
const imageMatches = htmlText.match(imageRegex) || [];
|
|
||||||
const minLength = Math.min(posterMatches.length, titleMatches.length, imageMatches.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < minLength; i++) {
|
|
||||||
const hrefMatch = posterMatches[i].match(extractHrefRegex);
|
|
||||||
const fullHref = hrefMatch ? (hrefMatch[1].startsWith("http") ? hrefMatch[1] : baseUrl + hrefMatch[1]) : null;
|
|
||||||
|
|
||||||
const imageMatch = imageMatches[i].match(extractImageRegex);
|
|
||||||
const imageSrc = imageMatch ? imageMatch[1] : null;
|
|
||||||
|
|
||||||
const titleMatch = titleMatches[i].match(extractTitleRegex);
|
|
||||||
const cleanTitle = titleMatch ? decodeHtmlEntities(titleMatch[1]) : null;
|
|
||||||
|
|
||||||
if (fullHref && imageSrc && cleanTitle) {
|
|
||||||
results.push({
|
|
||||||
href: `Animekai:${fullHref}`,
|
|
||||||
image: imageSrc,
|
|
||||||
title: cleanTitle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encodedQuery = encodeQuery(query);
|
|
||||||
const urls = [
|
|
||||||
`${searchBaseUrl}${encodedQuery}`,
|
|
||||||
`${searchBaseUrl}${encodedQuery}&page=2`,
|
|
||||||
`${searchBaseUrl}${encodedQuery}&page=3`
|
|
||||||
];
|
|
||||||
|
|
||||||
const responses = await Promise.all(urls.map(url => fetchv2(url)));
|
|
||||||
const htmlTexts = await Promise.all(responses.map(res => res.text()));
|
|
||||||
|
|
||||||
const allResults = [];
|
|
||||||
htmlTexts.forEach(html => allResults.push(...extractResultsFromHTML(html)));
|
|
||||||
return allResults;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Animekai search error:" + error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const oneMoviesSearch = async () => {
|
|
||||||
const searchBaseUrl = "https://1movies.bz/browser?keyword=";
|
|
||||||
const baseUrl = "https://1movies.bz";
|
|
||||||
|
|
||||||
const posterHrefRegex = /href="([^"]*)" class="poster"/g;
|
|
||||||
const titleRegex = /class="title" href="[^"]*">([^<]*)</g;
|
|
||||||
const imageRegex = /data-src="([^"]*)"/g;
|
|
||||||
|
|
||||||
const extractResultsFromHTML = (htmlText) => {
|
|
||||||
const results = [];
|
|
||||||
const posterMatches = [...htmlText.matchAll(posterHrefRegex)];
|
|
||||||
const titleMatches = [...htmlText.matchAll(titleRegex)];
|
|
||||||
const imageMatches = [...htmlText.matchAll(imageRegex)];
|
|
||||||
const minLength = Math.min(posterMatches.length, titleMatches.length, imageMatches.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < minLength; i++) {
|
|
||||||
const href = posterMatches[i][1];
|
|
||||||
const fullHref = href.startsWith("http") ? href : baseUrl + href;
|
|
||||||
|
|
||||||
const imageSrc = imageMatches[i][1];
|
|
||||||
const title = decodeHtmlEntities(titleMatches[i][1]);
|
|
||||||
|
|
||||||
results.push({ href: fullHref, image: imageSrc, title });
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encodedQuery = encodeQuery(query);
|
|
||||||
const urls = [
|
|
||||||
`${searchBaseUrl}${encodedQuery}`,
|
|
||||||
`${searchBaseUrl}${encodedQuery}&page=2`,
|
|
||||||
`${searchBaseUrl}${encodedQuery}&page=3`
|
|
||||||
];
|
|
||||||
|
|
||||||
const responses = await Promise.all(urls.map(url => fetchv2(url)));
|
|
||||||
const htmlTexts = await Promise.all(responses.map(res => res.text()));
|
|
||||||
|
|
||||||
const allResults = [];
|
|
||||||
htmlTexts.forEach(html => allResults.push(...extractResultsFromHTML(html)));
|
|
||||||
return allResults;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("1Movies search error:" + error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [animekaiResults, oneMoviesResults] = await Promise.all([
|
|
||||||
animekaiSearch(),
|
|
||||||
oneMoviesSearch()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const mergedResults = [...animekaiResults, ...oneMoviesResults];
|
|
||||||
|
|
||||||
const scoredResults = mergedResults.map(r => ({
|
|
||||||
...r,
|
|
||||||
score: fuzzyMatch(query, r.title)
|
|
||||||
}));
|
|
||||||
|
|
||||||
const filteredResults = scoredResults
|
|
||||||
.filter(r => r.score > 50)
|
|
||||||
.sort((a, b) => b.score - a.score)
|
|
||||||
.map(({ score, ...rest }) => rest);
|
|
||||||
|
|
||||||
return JSON.stringify(filteredResults.length > 0 ? filteredResults : [{
|
|
||||||
href: "",
|
|
||||||
image: "",
|
|
||||||
title: "No results found, please refine query."
|
|
||||||
}]);
|
|
||||||
} catch (error) {
|
|
||||||
return JSON.stringify([{
|
|
||||||
href: "",
|
|
||||||
image: "",
|
|
||||||
title: "Search failed: " + error.message
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractDetails(url) {
|
|
||||||
|
|
||||||
if (url.startsWith("Animekai:")) {
|
|
||||||
const actualUrl = url.replace("Animekai:", "").trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchv2(actualUrl);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
|
|
||||||
const descriptionMatch = (/<div class="desc text-expand">([\s\S]*?)<\/div>/.exec(htmlText) || [])[1];
|
|
||||||
const aliasesMatch = (/<small class="al-title text-expand">([\s\S]*?)<\/small>/.exec(htmlText) || [])[1];
|
|
||||||
|
|
||||||
return JSON.stringify([{
|
|
||||||
description: descriptionMatch ? cleanHtmlSymbols(descriptionMatch) : "Not available",
|
|
||||||
aliases: aliasesMatch ? cleanHtmlSymbols(aliasesMatch) : "Not available",
|
|
||||||
airdate: "If stream doesn't load try later or disable VPN/DNS"
|
|
||||||
}]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching Animekai details:" + error);
|
|
||||||
return JSON.stringify([{
|
|
||||||
description: "Error loading description",
|
|
||||||
aliases: "Aliases: Unknown",
|
|
||||||
airdate: "Aired: Unknown"
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const response = await fetchv2(url);
|
|
||||||
const htmlText = await response.text();
|
|
||||||
|
|
||||||
const descriptionMatch = (/<div class="description text-expand">([\s\S]*?)<\/div>/.exec(htmlText) || [])[1];
|
|
||||||
const aliasesMatch = (/<small class="al-title text-expand">([\s\S]*?)<\/small>/.exec(htmlText) || [])[1];
|
|
||||||
const airdateMatch = (/<li>Released:\s*<span[^>]*>(.*?)<\/span>/.exec(htmlText) || [])[1];
|
|
||||||
|
|
||||||
return JSON.stringify([{
|
|
||||||
description: descriptionMatch ? cleanHtmlSymbols(descriptionMatch) : "Not available",
|
|
||||||
aliases: aliasesMatch ? cleanHtmlSymbols(aliasesMatch) : "Not aliases",
|
|
||||||
airdate: airdateMatch ? cleanHtmlSymbols(airdateMatch) : "Not available"
|
|
||||||
}]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching 1Movies details:"+ error);
|
|
||||||
return JSON.stringify([{
|
|
||||||
description: "Error loading description",
|
|
||||||
aliases: "Not available",
|
|
||||||
airdate: "Not available"
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractEpisodes(url) {
|
|
||||||
try {
|
|
||||||
if (url.startsWith("Animekai:")) {
|
|
||||||
const actualUrl = url.replace("Animekai:", "").trim();
|
|
||||||
const htmlText = await (await fetchv2(actualUrl)).text();
|
|
||||||
const animeIdMatch = (htmlText.match(/<div class="rate-box"[^>]*data-id="([^"]+)"/) || [])[1];
|
|
||||||
if (!animeIdMatch) return JSON.stringify([{ error: "AniID not found" }]);
|
|
||||||
|
|
||||||
const tokenResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(animeIdMatch)}`);
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
const token = tokenData.result;
|
|
||||||
|
|
||||||
const episodeListUrl = `https://animekai.to/ajax/episodes/list?ani_id=${animeIdMatch}&_=${token}`;
|
|
||||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
|
||||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
|
||||||
|
|
||||||
const episodeRegex = /<a[^>]+num="([^"]+)"[^>]+token="([^"]+)"[^>]*>/g;
|
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
|
||||||
|
|
||||||
const episodes = episodeMatches.map(([_, episodeNum, episodeToken]) => ({
|
|
||||||
number: parseInt(episodeNum, 10),
|
|
||||||
href: `Animekai:https://animekai.to/ajax/links/list?token=${episodeToken}&_=ENCRYPT_ME`
|
|
||||||
}));
|
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
|
||||||
} else {
|
|
||||||
const htmlText = await (await fetchv2(url)).text();
|
|
||||||
const movieIDMatch = (htmlText.match(/<div class="detail-lower"[^>]*id="movie-rating"[^>]*data-id="([^"]+)"/) || [])[1];
|
|
||||||
if (!movieIDMatch) return JSON.stringify([{ error: "MovieID not found" }]);
|
|
||||||
|
|
||||||
const tokenResponse = await fetchv2("https://enc-dec.app/api/enc-movies-flix?text=" + encodeURIComponent(movieIDMatch));
|
|
||||||
const temp = await tokenResponse.json();
|
|
||||||
const token = temp.result;
|
|
||||||
|
|
||||||
const episodeListUrl = `https://1movies.bz/ajax/episodes/list?id=${movieIDMatch}&_=${token}`;
|
|
||||||
const episodeListData = await (await fetchv2(episodeListUrl)).json();
|
|
||||||
const cleanedHtml = cleanJsonHtml(episodeListData.result);
|
|
||||||
|
|
||||||
const episodeRegex = /<a[^>]+eid="([^"]+)"[^>]+num="([^"]+)"[^>]*>/g;
|
|
||||||
const episodeMatches = [...cleanedHtml.matchAll(episodeRegex)];
|
|
||||||
|
|
||||||
const episodes = episodeMatches.map(([_, episodeToken, episodeNum]) => ({
|
|
||||||
number: parseInt(episodeNum, 10),
|
|
||||||
href: `https://1movies.bz/ajax/links/list?eid=${episodeToken}&_=ENCRYPT_ME`
|
|
||||||
}));
|
|
||||||
|
|
||||||
return JSON.stringify(episodes);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching episodes:" + err);
|
|
||||||
return JSON.stringify([{ number: 1, href: "Error fetching episodes" }]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractStreamUrl(url) {
|
|
||||||
let source, actualUrl;
|
|
||||||
|
|
||||||
if (url.startsWith("Animekai:")) {
|
|
||||||
source = "Animekai";
|
|
||||||
actualUrl = url.replace("Animekai:", "").trim();
|
|
||||||
} else if (url.includes("1movies.bz")) {
|
|
||||||
source = "1Movies";
|
|
||||||
actualUrl = url.trim();
|
|
||||||
} else {
|
|
||||||
console.log("Failed to match URL:", url);
|
|
||||||
return "Invalid URL format: " + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source === "Animekai") {
|
|
||||||
try {
|
|
||||||
const tokenMatch = actualUrl.match(/token=([^&]+)/);
|
|
||||||
if (tokenMatch && tokenMatch[1]) {
|
|
||||||
const rawToken = tokenMatch[1];
|
|
||||||
const encryptResponse = await fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(rawToken)}`);
|
|
||||||
const encryptData = await encryptResponse.json();
|
|
||||||
const encryptedToken = encryptData.result;
|
|
||||||
actualUrl = actualUrl.replace('&_=ENCRYPT_ME', `&_=${encryptedToken}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetchv2(actualUrl);
|
|
||||||
const text = await response.text();
|
|
||||||
const cleanedHtml = cleanJsonHtml(text);
|
|
||||||
const subRegex = /<div class="server-items lang-group" data-id="sub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const softsubRegex = /<div class="server-items lang-group" data-id="softsub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const dubRegex = /<div class="server-items lang-group" data-id="dub"[^>]*>([\s\S]*?)<\/div>/;
|
|
||||||
const subMatch = subRegex.exec(cleanedHtml);
|
|
||||||
const softsubMatch = softsubRegex.exec(cleanedHtml);
|
|
||||||
const dubMatch = dubRegex.exec(cleanedHtml);
|
|
||||||
const subContent = subMatch ? subMatch[1].trim() : "";
|
|
||||||
const softsubContent = softsubMatch ? softsubMatch[1].trim() : "";
|
|
||||||
const dubContent = dubMatch ? dubMatch[1].trim() : "";
|
|
||||||
const serverSpanRegex = /<span class="server"[^>]*data-lid="([^"]+)"[^>]*>Server 1<\/span>/;
|
|
||||||
const serverIdDub = serverSpanRegex.exec(dubContent)?.[1];
|
|
||||||
const serverIdSoftsub = serverSpanRegex.exec(softsubContent)?.[1];
|
|
||||||
const serverIdSub = serverSpanRegex.exec(subContent)?.[1];
|
|
||||||
|
|
||||||
const tokenRequestData = [
|
|
||||||
{ name: "Dub", data: serverIdDub },
|
|
||||||
{ name: "Softsub", data: serverIdSoftsub },
|
|
||||||
{ name: "Sub", data: serverIdSub }
|
|
||||||
].filter(item => item.data);
|
|
||||||
|
|
||||||
const tokenPromises = tokenRequestData.map(item =>
|
|
||||||
fetchv2(`https://enc-dec.app/api/enc-kai?text=${encodeURIComponent(item.data)}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(json => ({ name: item.name, data: json.result }))
|
|
||||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
|
||||||
);
|
|
||||||
const tokenResults = await Promise.all(tokenPromises);
|
|
||||||
|
|
||||||
const streamUrls = tokenResults.map(result => {
|
|
||||||
const serverIdMap = {
|
|
||||||
"Dub": serverIdDub,
|
|
||||||
"Softsub": serverIdSoftsub,
|
|
||||||
"Sub": serverIdSub
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
type: result.name,
|
|
||||||
url: `https://animekai.to/ajax/links/view?id=${serverIdMap[result.name]}&_=${result.data}`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const processStreams = async (streamUrls) => {
|
|
||||||
const streamResponses = await Promise.all(
|
|
||||||
streamUrls.map(async ({ type, url }) => {
|
|
||||||
try {
|
|
||||||
const res = await fetchv2(url);
|
|
||||||
const json = await res.json();
|
|
||||||
return {
|
|
||||||
type: type,
|
|
||||||
result: json.result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error fetching ${type} stream:` + error);
|
|
||||||
return {
|
|
||||||
type: type,
|
|
||||||
result: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const decryptRequestData = streamResponses
|
|
||||||
.filter(item => item.result)
|
|
||||||
.map(item => ({
|
|
||||||
name: item.type,
|
|
||||||
data: item.result
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (decryptRequestData.length === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptPromises = decryptRequestData.map(item =>
|
|
||||||
fetchv2(`https://enc-dec.app/api/dec-kai?text=${encodeURIComponent(item.data)}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(json => ({ name: item.name, data: JSON.stringify(json.result) }))
|
|
||||||
.catch(err => ({ name: item.name, error: err.toString() }))
|
|
||||||
);
|
|
||||||
const decryptResults = await Promise.all(decryptPromises);
|
|
||||||
|
|
||||||
const finalResults = {};
|
|
||||||
decryptResults.forEach(result => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(result.data);
|
|
||||||
finalResults[result.name] = parsed.url;
|
|
||||||
console.log(`decrypted${result.name} URL:` + parsed.url);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error parsing ${result.name} result:` + error);
|
|
||||||
finalResults[result.name] = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalResults;
|
|
||||||
};
|
|
||||||
|
|
||||||
const decryptedUrls = await processStreams(streamUrls);
|
|
||||||
const decryptedSub = decryptedUrls.Sub;
|
|
||||||
const decryptedDub = decryptedUrls.Dub;
|
|
||||||
const decryptedRaw = decryptedUrls.Softsub;
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Referer": "https://animekai.to/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getStream(url) {
|
|
||||||
try {
|
|
||||||
const response = await fetchv2(url.replace("/e/", "/media/"), headers);
|
|
||||||
const responseJson = await response.json();
|
|
||||||
|
|
||||||
const result = responseJson?.result;
|
|
||||||
|
|
||||||
const postData = {
|
|
||||||
"text": result,
|
|
||||||
"Useragent": headers["User-Agent"]
|
|
||||||
};
|
|
||||||
|
|
||||||
const finalResponse = await fetchv2(
|
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/ilovebush",
|
|
||||||
{},
|
|
||||||
"POST",
|
|
||||||
JSON.stringify(postData)
|
|
||||||
);
|
|
||||||
|
|
||||||
const finalJson = await finalResponse.json();
|
|
||||||
return finalJson?.result?.sources?.[0]?.file || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const streams = [];
|
|
||||||
|
|
||||||
const subStream = decryptedSub ? await getStream(decryptedSub.replace("megaup22", "megaup.site")) : null;
|
|
||||||
if (subStream) streams.push("Hardsub English", subStream);
|
|
||||||
|
|
||||||
const dubStream = decryptedDub ? await getStream(decryptedDub.replace("megaup22", "megaup.site")) : null;
|
|
||||||
if (dubStream) streams.push("Dubbed English", dubStream);
|
|
||||||
|
|
||||||
const rawStream = decryptedRaw ? await getStream(decryptedRaw.replace("megaup22", "megaup.site")) : null;
|
|
||||||
if (rawStream) streams.push("Original audio", rawStream);
|
|
||||||
|
|
||||||
const final = {
|
|
||||||
streams,
|
|
||||||
subtitles: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("RETURN: " + JSON.stringify(final));
|
|
||||||
return JSON.stringify(final);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Animekai fetch error:" + error);
|
|
||||||
return "https://error.org";
|
|
||||||
}
|
|
||||||
} else if (source === "1Movies") {
|
|
||||||
try {
|
|
||||||
const eidMatch = actualUrl.match(/eid=([^&]+)/);
|
|
||||||
if (eidMatch && eidMatch[1]) {
|
|
||||||
const rawEpisodeToken = eidMatch[1];
|
|
||||||
const encryptResponse = await fetchv2(`https://enc-dec.app/api/enc-movies-flix?text=${rawEpisodeToken}`);
|
|
||||||
const encryptData = await encryptResponse.json();
|
|
||||||
const encryptedToken = encryptData.result;
|
|
||||||
actualUrl = actualUrl.replace('&_=ENCRYPT_ME', `&_=${encryptedToken}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetchv2(actualUrl);
|
|
||||||
const responseData = await response.json();
|
|
||||||
const cleanedHtml = cleanJsonHtml(responseData.result);
|
|
||||||
|
|
||||||
const server1Regex = /<div class="server wnav-item"[^>]*data-lid="([^"]+)"[^>]*>\s*<span>Server 1<\/span>/;
|
|
||||||
const server1Match = server1Regex.exec(cleanedHtml);
|
|
||||||
|
|
||||||
if (!server1Match) {
|
|
||||||
console.log("Server 1 not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverId = server1Match[1];
|
|
||||||
|
|
||||||
const tokenRequestData = [{ name: "Server1", data: serverId }];
|
|
||||||
|
|
||||||
const tokenBatchResponse = await fetchv2(
|
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/ilovethighs",
|
|
||||||
{},
|
|
||||||
"POST",
|
|
||||||
JSON.stringify(tokenRequestData)
|
|
||||||
);
|
|
||||||
const tokenResults = await tokenBatchResponse.json();
|
|
||||||
const token = tokenResults[0]?.data;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.log("Token not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamUrl = `https://1movies.bz/ajax/links/view?id=${serverId}&_=${token}`;
|
|
||||||
const streamResponse = await fetchv2(streamUrl);
|
|
||||||
const streamData = await streamResponse.json();
|
|
||||||
|
|
||||||
if (!streamData.result) {
|
|
||||||
console.log("Stream result not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptRequestData = [{ name: "Server1", data: streamData.result }];
|
|
||||||
|
|
||||||
const decryptBatchResponse = await fetchv2(
|
|
||||||
"https://ilovekai.simplepostrequest.workers.dev/iloveboobs",
|
|
||||||
{},
|
|
||||||
"POST",
|
|
||||||
JSON.stringify(decryptRequestData)
|
|
||||||
);
|
|
||||||
const decryptedResponse = await decryptBatchResponse.json();
|
|
||||||
console.log("Decrypted response:" + JSON.stringify(decryptedResponse));
|
|
||||||
const decryptedUrl = decryptedResponse[0]?.data.url;
|
|
||||||
|
|
||||||
const subListEncoded = decryptedUrl.split("sub.list=")[1]?.split("&")[0];
|
|
||||||
let subtitles = "N/A";
|
|
||||||
|
|
||||||
if (subListEncoded) {
|
|
||||||
try {
|
|
||||||
const subListUrl = decodeURIComponent(subListEncoded);
|
|
||||||
const subResponse = await fetchv2(subListUrl);
|
|
||||||
subtitles = await subResponse.json();
|
|
||||||
} catch {
|
|
||||||
subtitles = "N/A";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const englishSubUrl = Array.isArray(subtitles)
|
|
||||||
? subtitles.find(sub => sub.label === "English")?.file.replace(/\\\//g, "/")
|
|
||||||
: "N/A";
|
|
||||||
|
|
||||||
if (!decryptedUrl) {
|
|
||||||
console.log("Decryption failed");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Referer": "https://1movies.bz/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
};
|
|
||||||
|
|
||||||
const mediaResponse = await fetchv2(decryptedUrl.replace("/e/", "/media/"), headers);
|
|
||||||
const mediaJson = await mediaResponse.json();
|
|
||||||
|
|
||||||
const result = mediaJson?.result;
|
|
||||||
if (!result) {
|
|
||||||
console.log("Media result not found");
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const postData = {
|
|
||||||
"text": result,
|
|
||||||
"Useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
|
||||||
};
|
|
||||||
|
|
||||||
const finalResponse = await fetchv2("https://ilovekai.simplepostrequest.workers.dev/iloveass", {}, "POST", JSON.stringify(postData));
|
|
||||||
const finalJson = await finalResponse.json();
|
|
||||||
|
|
||||||
const m3u8Link = finalJson?.result?.sources?.[0]?.file;
|
|
||||||
|
|
||||||
const m3u8Response = await fetchv2(m3u8Link);
|
|
||||||
const m3u8Text = await m3u8Response.text();
|
|
||||||
|
|
||||||
const baseUrl = m3u8Link.substring(0, m3u8Link.lastIndexOf('/') + 1);
|
|
||||||
|
|
||||||
const streams = [];
|
|
||||||
const lines = m3u8Text.split('\n');
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i].trim();
|
|
||||||
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
|
||||||
const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
|
|
||||||
let quality = 'Unknown';
|
|
||||||
|
|
||||||
if (resolutionMatch) {
|
|
||||||
const [width, height] = resolutionMatch[1].split('x');
|
|
||||||
quality = `${height}p`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i + 1 < lines.length) {
|
|
||||||
const streamPath = lines[i + 1].trim();
|
|
||||||
const streamUrl = baseUrl + streamPath;
|
|
||||||
|
|
||||||
streams.push({
|
|
||||||
title: quality,
|
|
||||||
streamUrl: streamUrl
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const returnValue = {
|
|
||||||
streams: streams,
|
|
||||||
subtitle: englishSubUrl !== "N/A" ? englishSubUrl : ""
|
|
||||||
};
|
|
||||||
console.log("RETURN: " + JSON.stringify(returnValue));
|
|
||||||
return JSON.stringify(returnValue);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("1Movies fetch error:" + error);
|
|
||||||
return "https://error.org";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// Helper functions
|
|
||||||
///
|
|
||||||
///
|
|
||||||
|
|
||||||
function cleanHtmlSymbols(string) {
|
|
||||||
if (!string) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
.replace(/’/g, "'")
|
|
||||||
.replace(/–/g, "-")
|
|
||||||
.replace(/&#[0-9]+;/g, "")
|
|
||||||
.replace(/\r?\n|\r/g, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanJsonHtml(jsonHtml) {
|
|
||||||
if (!jsonHtml) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return jsonHtml
|
|
||||||
.replace(/\\"/g, "\"")
|
|
||||||
.replace(/\\'/g, "'")
|
|
||||||
.replace(/\\\\/g, "\\")
|
|
||||||
.replace(/\\n/g, "\n")
|
|
||||||
.replace(/\\t/g, "\t")
|
|
||||||
.replace(/\\r/g, "\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeHtmlEntities(text) {
|
|
||||||
if (!text) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/"/g, "\"")
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/ /g, " ");
|
|
||||||
}
|
|
||||||
|
|
@ -1,439 +0,0 @@
|
||||||
async function searchContent(input,page=0){
|
|
||||||
function parseSearchResults(html) {
|
|
||||||
const results = [];
|
|
||||||
const regex = /<div class="unit item-\d+">[\s\S]*?<a href="\/manga\/([\w\-\.]+)"[^>]*class="poster"[\s\S]*?<img src="([^"]*)"[^>]*alt="([^"]*)"/g;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = regex.exec(html)) !== null) {
|
|
||||||
results.push({
|
|
||||||
title: match[3],
|
|
||||||
imageURL: match[2],
|
|
||||||
id: match[1]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
} const vrf = generate_vrf(input);
|
|
||||||
const response = await fetch("https://mangafire.to/filter?keyword=" + encodeURIComponent(input) + "&vrf=" + vrf);
|
|
||||||
const data = await response.text();
|
|
||||||
console.log(JSON.stringify(parseSearchResults(data)));
|
|
||||||
return parseSearchResults(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getContentData(url) {
|
|
||||||
function parseHtmlData(htmlContent) {
|
|
||||||
const genreRegex = /<a href="\/genre\/[^"]*">([^<]+)<\/a>/g;
|
|
||||||
const tags = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = genreRegex.exec(htmlContent)) !== null) {
|
|
||||||
if (match[1].trim()) tags.push(match[1].trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueTags = [...new Set(tags)];
|
|
||||||
|
|
||||||
const ogDescriptionRegex =
|
|
||||||
/<meta property="og:description" content=['"]([^'"]*)['"]/i;
|
|
||||||
const ogMatch = htmlContent.match(ogDescriptionRegex);
|
|
||||||
|
|
||||||
let description = ogMatch ? ogMatch[1] : "";
|
|
||||||
|
|
||||||
if (!description) {
|
|
||||||
const metaDescriptionRegex =
|
|
||||||
/<meta name="description" content=['"]([^'"]*)['"]/i;
|
|
||||||
const metaMatch = htmlContent.match(metaDescriptionRegex);
|
|
||||||
description = metaMatch ? metaMatch[1] : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
description = description
|
|
||||||
.replace(/(")/g, '"')
|
|
||||||
.replace(/(&)/g, '&')
|
|
||||||
.replace(/(<)/g, '<')
|
|
||||||
.replace(/(>)/g, '>')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (uniqueTags.length === 0) {
|
|
||||||
uniqueTags.push("Unknown");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
description: description,
|
|
||||||
tags: uniqueTags
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`https://mangafire.to${url}`);
|
|
||||||
const data = await response.text();
|
|
||||||
console.log(JSON.stringify(parseHtmlData(data)));
|
|
||||||
return parseHtmlData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getChapters(url) {
|
|
||||||
const mangaIdMatch = url.match(/\.([a-z0-9]+)$/);
|
|
||||||
const mangaId = mangaIdMatch ? mangaIdMatch[1] : null;
|
|
||||||
|
|
||||||
vrf = generate_vrf(`${mangaId}@chapter@en`);
|
|
||||||
if (!mangaId) {
|
|
||||||
console.error("Could not extract manga ID from URL");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseChapters(htmlContent) {
|
|
||||||
const chapters = {};
|
|
||||||
|
|
||||||
const chapterRegex =
|
|
||||||
/<a href="\/read\/[^"]+\/([^/]+)\/chapter-[^"]*" data-number="([^"]+)" data-id="([^"]+)"[^>]*>(.*?)<\/a>/gs;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = chapterRegex.exec(htmlContent)) !== null) {
|
|
||||||
const langCode = match[1];
|
|
||||||
const chapterNumber = match[2];
|
|
||||||
const chapterId = match[3];
|
|
||||||
const title = match[4].replace(/<[^>]*>/g, '').trim();
|
|
||||||
|
|
||||||
if (!chapters[langCode]) chapters[langCode] = [];
|
|
||||||
|
|
||||||
chapters[langCode].push([
|
|
||||||
chapterNumber,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: chapterId,
|
|
||||||
title: title,
|
|
||||||
chapter: Number(chapterNumber),
|
|
||||||
scanlation_group: "Mangafire"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(chapters).forEach(lang => chapters[lang].reverse());
|
|
||||||
|
|
||||||
return chapters;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://mangafire.to/ajax/read/${mangaId}/chapter/en?vrf=${vrf}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.status === 200 && data.result && data.result.html) {
|
|
||||||
const chapters = parseChapters(data.result.html);
|
|
||||||
console.log(JSON.stringify(chapters));
|
|
||||||
return chapters;
|
|
||||||
} else {
|
|
||||||
console.error("Invalid response from server");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching chapters:" + error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getChapterImages(ID) {
|
|
||||||
vrf = generate_vrf(`chapter@${ID}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://mangafire.to/ajax/read/chapter/${ID}?vrf=${vrf}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.status === 200 && data.result && data.result.images) {
|
|
||||||
const images = data.result.images.map(img => img[0]);
|
|
||||||
console.log(JSON.stringify(images));
|
|
||||||
return images;
|
|
||||||
} else {
|
|
||||||
console.error("Invalid response from server");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching chapters:" + error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function b64encode(data) {
|
|
||||||
const keystr =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
|
|
||||||
function atobLookup(chr) {
|
|
||||||
const index = keystr.indexOf(chr);
|
|
||||||
return index < 0 ? undefined : index;
|
|
||||||
}
|
|
||||||
|
|
||||||
data = `${data}`;
|
|
||||||
data = data.replace(/[ \t\n\f\r]/g, "");
|
|
||||||
if (data.length % 4 === 0) {
|
|
||||||
data = data.replace(/==?$/, "");
|
|
||||||
}
|
|
||||||
if (data.length % 4 === 1 || /[^+/0-9A-Za-z]/.test(data)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let output = "";
|
|
||||||
let buffer = 0;
|
|
||||||
let accumulatedBits = 0;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
buffer <<= 6;
|
|
||||||
buffer |= atobLookup(data[i]);
|
|
||||||
accumulatedBits += 6;
|
|
||||||
if (accumulatedBits === 24) {
|
|
||||||
output += String.fromCharCode((buffer & 0xff0000) >> 16);
|
|
||||||
output += String.fromCharCode((buffer & 0xff00) >> 8);
|
|
||||||
output += String.fromCharCode(buffer & 0xff);
|
|
||||||
buffer = accumulatedBits = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (accumulatedBits === 12) {
|
|
||||||
buffer >>= 4;
|
|
||||||
output += String.fromCharCode(buffer);
|
|
||||||
} else if (accumulatedBits === 18) {
|
|
||||||
buffer >>= 2;
|
|
||||||
output += String.fromCharCode((buffer & 0xff00) >> 8);
|
|
||||||
output += String.fromCharCode(buffer & 0xff);
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function b64decode(s) {
|
|
||||||
const keystr =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
|
|
||||||
function btoaLookup(index) {
|
|
||||||
if (index >= 0 && index < 64) {
|
|
||||||
return keystr[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let i;
|
|
||||||
s = `${s}`;
|
|
||||||
for (i = 0; i < s.length; i++) {
|
|
||||||
if (s.charCodeAt(i) > 255) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let out = "";
|
|
||||||
for (i = 0; i < s.length; i += 3) {
|
|
||||||
const groupsOfSix = [undefined, undefined, undefined, undefined];
|
|
||||||
groupsOfSix[0] = s.charCodeAt(i) >> 2;
|
|
||||||
groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4;
|
|
||||||
if (s.length > i + 1) {
|
|
||||||
groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4;
|
|
||||||
groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2;
|
|
||||||
}
|
|
||||||
if (s.length > i + 2) {
|
|
||||||
groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6;
|
|
||||||
groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f;
|
|
||||||
}
|
|
||||||
for (let j = 0; j < groupsOfSix.length; j++) {
|
|
||||||
if (typeof groupsOfSix[j] === "undefined") {
|
|
||||||
out += "=";
|
|
||||||
} else {
|
|
||||||
out += btoaLookup(groupsOfSix[j]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toBytes = (str) => Array.from(str, (c) => c.charCodeAt(0) & 0xff);
|
|
||||||
const fromBytes = (bytes) =>
|
|
||||||
bytes.map((b) => String.fromCharCode(b & 0xff)).join("");
|
|
||||||
|
|
||||||
function rc4Bytes(key, input) {
|
|
||||||
const s = Array.from({ length: 256 }, (_, i) => i);
|
|
||||||
let j = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
j = (j + s[i] + key.charCodeAt(i % key.length)) & 0xff;
|
|
||||||
[s[i], s[j]] = [s[j], s[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
const out = new Array(input.length);
|
|
||||||
let i = 0;
|
|
||||||
j = 0;
|
|
||||||
for (let y = 0; y < input.length; y++) {
|
|
||||||
i = (i + 1) & 0xff;
|
|
||||||
j = (j + s[i]) & 0xff;
|
|
||||||
[s[i], s[j]] = [s[j], s[i]];
|
|
||||||
const k = s[(s[i] + s[j]) & 0xff];
|
|
||||||
out[y] = (input[y] ^ k) & 0xff;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function transform(input, initSeedBytes, prefixKeyBytes, prefixLen, schedule) {
|
|
||||||
const out = [];
|
|
||||||
for (let i = 0; i < input.length; i++) {
|
|
||||||
if (i < prefixLen) out.push(prefixKeyBytes[i]);
|
|
||||||
|
|
||||||
out.push(
|
|
||||||
schedule[i % 10]((input[i] ^ initSeedBytes[i % 32]) & 0xff) & 0xff
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
const add8 = (n) => (c) => (c + n) & 0xff;
|
|
||||||
const sub8 = (n) => (c) => (c - n + 256) & 0xff;
|
|
||||||
const xor8 = (n) => (c) => (c ^ n) & 0xff;
|
|
||||||
const rotl8 = (n) => (c) => ((c << n) | (c >>> (8 - n))) & 0xff;
|
|
||||||
|
|
||||||
function base64UrlEncodeBytes(bytes) {
|
|
||||||
const std = b64decode(fromBytes(bytes));
|
|
||||||
return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function bytesFromBase64(b64) {
|
|
||||||
return toBytes(b64encode(b64));
|
|
||||||
}
|
|
||||||
|
|
||||||
const rotr8 = (n) => (c) => ((c >>> n) | (c << (8 - n))) & 0xff;
|
|
||||||
|
|
||||||
function generate_vrf(input) {
|
|
||||||
const schedule0 = [
|
|
||||||
sub8(223),
|
|
||||||
rotr8(4),
|
|
||||||
rotr8(4),
|
|
||||||
add8(234),
|
|
||||||
rotr8(7),
|
|
||||||
rotr8(2),
|
|
||||||
rotr8(7),
|
|
||||||
sub8(223),
|
|
||||||
rotr8(7),
|
|
||||||
rotr8(6),
|
|
||||||
];
|
|
||||||
|
|
||||||
const schedule1 = [
|
|
||||||
add8(19),
|
|
||||||
rotr8(7),
|
|
||||||
add8(19),
|
|
||||||
rotr8(6),
|
|
||||||
add8(19),
|
|
||||||
rotr8(1),
|
|
||||||
add8(19),
|
|
||||||
rotr8(6),
|
|
||||||
rotr8(7),
|
|
||||||
rotr8(4),
|
|
||||||
];
|
|
||||||
|
|
||||||
const schedule2 = [
|
|
||||||
sub8(223),
|
|
||||||
rotr8(1),
|
|
||||||
add8(19),
|
|
||||||
sub8(223),
|
|
||||||
rotl8(2),
|
|
||||||
sub8(223),
|
|
||||||
add8(19),
|
|
||||||
rotl8(1),
|
|
||||||
rotl8(2),
|
|
||||||
rotl8(1),
|
|
||||||
];
|
|
||||||
|
|
||||||
const schedule3 = [
|
|
||||||
add8(19),
|
|
||||||
rotl8(1),
|
|
||||||
rotl8(1),
|
|
||||||
rotr8(1),
|
|
||||||
add8(234),
|
|
||||||
rotl8(1),
|
|
||||||
sub8(223),
|
|
||||||
rotl8(6),
|
|
||||||
rotl8(4),
|
|
||||||
rotl8(1),
|
|
||||||
];
|
|
||||||
|
|
||||||
const schedule4 = [
|
|
||||||
rotr8(1),
|
|
||||||
rotl8(1),
|
|
||||||
rotl8(6),
|
|
||||||
rotr8(1),
|
|
||||||
rotl8(2),
|
|
||||||
rotr8(4),
|
|
||||||
rotl8(1),
|
|
||||||
rotl8(1),
|
|
||||||
sub8(223),
|
|
||||||
rotl8(2),
|
|
||||||
];
|
|
||||||
|
|
||||||
const CONST = {
|
|
||||||
"rc4Keys": [
|
|
||||||
"FgxyJUQDPUGSzwbAq/ToWn4/e8jYzvabE+dLMb1XU1o=",
|
|
||||||
"CQx3CLwswJAnM1VxOqX+y+f3eUns03ulxv8Z+0gUyik=",
|
|
||||||
"fAS+otFLkKsKAJzu3yU+rGOlbbFVq+u+LaS6+s1eCJs=",
|
|
||||||
"Oy45fQVK9kq9019+VysXVlz1F9S1YwYKgXyzGlZrijo=",
|
|
||||||
"aoDIdXezm2l3HrcnQdkPJTDT8+W6mcl2/02ewBHfPzg=",
|
|
||||||
],
|
|
||||||
"seeds32": [
|
|
||||||
"yH6MXnMEcDVWO/9a6P9W92BAh1eRLVFxFlWTHUqQ474=",
|
|
||||||
"RK7y4dZ0azs9Uqz+bbFB46Bx2K9EHg74ndxknY9uknA=",
|
|
||||||
"rqr9HeTQOg8TlFiIGZpJaxcvAaKHwMwrkqojJCpcvoc=",
|
|
||||||
"/4GPpmZXYpn5RpkP7FC/dt8SXz7W30nUZTe8wb+3xmU=",
|
|
||||||
"wsSGSBXKWA9q1oDJpjtJddVxH+evCfL5SO9HZnUDFU8=",
|
|
||||||
],
|
|
||||||
"prefixKeys": [
|
|
||||||
"l9PavRg=",
|
|
||||||
"Ml2v7ag1Jg==",
|
|
||||||
"i/Va0UxrbMo=",
|
|
||||||
"WFjKAHGEkQM=",
|
|
||||||
"5Rr27rWd",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stage 0: normalize to URI-encoded bytes
|
|
||||||
let bytes = toBytes(encodeURIComponent(input));
|
|
||||||
|
|
||||||
// RC4
|
|
||||||
bytes = rc4Bytes(b64encode(CONST.rc4Keys[0]), bytes);
|
|
||||||
const prefixKey0 = bytesFromBase64(CONST.prefixKeys[0]);
|
|
||||||
bytes = transform(
|
|
||||||
bytes,
|
|
||||||
bytesFromBase64(CONST.seeds32[0]),
|
|
||||||
prefixKey0,
|
|
||||||
prefixKey0.length,
|
|
||||||
schedule0
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes = rc4Bytes(b64encode(CONST.rc4Keys[1]), bytes);
|
|
||||||
const prefixKey1 = bytesFromBase64(CONST.prefixKeys[1]);
|
|
||||||
bytes = transform(
|
|
||||||
bytes,
|
|
||||||
bytesFromBase64(CONST.seeds32[1]),
|
|
||||||
prefixKey1,
|
|
||||||
prefixKey1.length,
|
|
||||||
schedule1
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes = rc4Bytes(b64encode(CONST.rc4Keys[2]), bytes);
|
|
||||||
const prefixKey2 = bytesFromBase64(CONST.prefixKeys[2]);
|
|
||||||
bytes = transform(
|
|
||||||
bytes,
|
|
||||||
bytesFromBase64(CONST.seeds32[2]),
|
|
||||||
prefixKey2,
|
|
||||||
prefixKey2.length,
|
|
||||||
schedule2
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes = rc4Bytes(b64encode(CONST.rc4Keys[3]), bytes);
|
|
||||||
const prefixKey3 = bytesFromBase64(CONST.prefixKeys[3]);
|
|
||||||
bytes = transform(
|
|
||||||
bytes,
|
|
||||||
bytesFromBase64(CONST.seeds32[3]),
|
|
||||||
prefixKey3,
|
|
||||||
prefixKey3.length,
|
|
||||||
schedule3
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes = rc4Bytes(b64encode(CONST.rc4Keys[4]), bytes);
|
|
||||||
const prefixKey4 = bytesFromBase64(CONST.prefixKeys[4]);
|
|
||||||
bytes = transform(
|
|
||||||
bytes,
|
|
||||||
bytesFromBase64(CONST.seeds32[4]),
|
|
||||||
prefixKey4,
|
|
||||||
prefixKey4.length,
|
|
||||||
schedule4
|
|
||||||
);
|
|
||||||
|
|
||||||
return base64UrlEncodeBytes(bytes);
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
baaf3c1d221e67bbcf3ebda4bb52f4e520cd0974
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
312c50343fe37511a49ec4a312b393069fbfde08
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
0000000000000000000000000000000000000000 09c423e95a233553b774b005d8f6e6bdb31fc8f1 50/50 <80717571+50n50@users.noreply.github.com> 1760195925 +0200 commit (initial): Upload
|
|
||||||
09c423e95a233553b774b005d8f6e6bdb31fc8f1 4bdd7bbd2beccee5506770f77db7626a69b74f12 50/50 <80717571+50n50@users.noreply.github.com> 1760196216 +0200 commit: Fix script url
|
|
||||||
4bdd7bbd2beccee5506770f77db7626a69b74f12 9eacd16f54e6f8a4a51e1ac7aa87b2e1d942c072 50/50 <80717571+50n50@users.noreply.github.com> 1760287264 +0200 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
9eacd16f54e6f8a4a51e1ac7aa87b2e1d942c072 5c721916282c3ea70c4dbec930204eccbf270d6d 50/50 <80717571+50n50@users.noreply.github.com> 1760474661 +0200 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
5c721916282c3ea70c4dbec930204eccbf270d6d b9ea9732f6939fe3cd04049c7e640654511dbed8 50/50 <80717571+50n50@users.noreply.github.com> 1760474668 +0200 commit: test
|
|
||||||
b9ea9732f6939fe3cd04049c7e640654511dbed8 9e3a7a915a80ce7500fdaf5f651d43338760a561 50/50 <80717571+50n50@users.noreply.github.com> 1761677670 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
9e3a7a915a80ce7500fdaf5f651d43338760a561 e5b8e16feb85c3fa48aac5dc68758d7b62963aa2 50/50 <80717571+50n50@users.noreply.github.com> 1762117625 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
e5b8e16feb85c3fa48aac5dc68758d7b62963aa2 7069fa8ca49507d435963f2a92f3fe1823c535d9 50/50 <80717571+50n50@users.noreply.github.com> 1762117632 +0100 revert: Revert "Update mediafusion/mediafusion.js"
|
|
||||||
7069fa8ca49507d435963f2a92f3fe1823c535d9 866a5b750628eb0612f91ec3773200b754a3a8c6 50/50 <80717571+50n50@users.noreply.github.com> 1762980132 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
866a5b750628eb0612f91ec3773200b754a3a8c6 ac2ff67c141519c770b52bbcc46c1c8323b4e053 50/50 <80717571+50n50@users.noreply.github.com> 1764355727 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
ac2ff67c141519c770b52bbcc46c1c8323b4e053 baaf3c1d221e67bbcf3ebda4bb52f4e520cd0974 50/50 <80717571+50n50@users.noreply.github.com> 1764535876 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
baaf3c1d221e67bbcf3ebda4bb52f4e520cd0974 312c50343fe37511a49ec4a312b393069fbfde08 50/50 <80717571+50n50@users.noreply.github.com> 1765037272 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
0000000000000000000000000000000000000000 09c423e95a233553b774b005d8f6e6bdb31fc8f1 50/50 <80717571+50n50@users.noreply.github.com> 1760195925 +0200 commit (initial): Upload
|
|
||||||
09c423e95a233553b774b005d8f6e6bdb31fc8f1 4bdd7bbd2beccee5506770f77db7626a69b74f12 50/50 <80717571+50n50@users.noreply.github.com> 1760196216 +0200 commit: Fix script url
|
|
||||||
4bdd7bbd2beccee5506770f77db7626a69b74f12 9eacd16f54e6f8a4a51e1ac7aa87b2e1d942c072 50/50 <80717571+50n50@users.noreply.github.com> 1760287264 +0200 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
9eacd16f54e6f8a4a51e1ac7aa87b2e1d942c072 5c721916282c3ea70c4dbec930204eccbf270d6d 50/50 <80717571+50n50@users.noreply.github.com> 1760474661 +0200 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
5c721916282c3ea70c4dbec930204eccbf270d6d b9ea9732f6939fe3cd04049c7e640654511dbed8 50/50 <80717571+50n50@users.noreply.github.com> 1760474668 +0200 commit: test
|
|
||||||
b9ea9732f6939fe3cd04049c7e640654511dbed8 9e3a7a915a80ce7500fdaf5f651d43338760a561 50/50 <80717571+50n50@users.noreply.github.com> 1761677670 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
9e3a7a915a80ce7500fdaf5f651d43338760a561 e5b8e16feb85c3fa48aac5dc68758d7b62963aa2 50/50 <80717571+50n50@users.noreply.github.com> 1762117625 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
e5b8e16feb85c3fa48aac5dc68758d7b62963aa2 7069fa8ca49507d435963f2a92f3fe1823c535d9 50/50 <80717571+50n50@users.noreply.github.com> 1762117632 +0100 revert: Revert "Update mediafusion/mediafusion.js"
|
|
||||||
7069fa8ca49507d435963f2a92f3fe1823c535d9 866a5b750628eb0612f91ec3773200b754a3a8c6 50/50 <80717571+50n50@users.noreply.github.com> 1762980132 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
866a5b750628eb0612f91ec3773200b754a3a8c6 ac2ff67c141519c770b52bbcc46c1c8323b4e053 50/50 <80717571+50n50@users.noreply.github.com> 1764355727 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
ac2ff67c141519c770b52bbcc46c1c8323b4e053 baaf3c1d221e67bbcf3ebda4bb52f4e520cd0974 50/50 <80717571+50n50@users.noreply.github.com> 1764535876 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
baaf3c1d221e67bbcf3ebda4bb52f4e520cd0974 312c50343fe37511a49ec4a312b393069fbfde08 50/50 <80717571+50n50@users.noreply.github.com> 1765037272 +0100 pull --ff --recurse-submodules --progress origin: Fast-forward
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue