Update providers and deduplicate streams
This commit is contained in:
@@ -28,59 +28,178 @@ const cheerio = require("cheerio");
|
||||
// variables
|
||||
const baseUrl = "https://animepahe.pw";
|
||||
|
||||
function notifyRenderer(channel, payload) {
|
||||
if (typeof global.sendToRenderer === "function") {
|
||||
try {
|
||||
global.sendToRenderer(channel, payload);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
let lastRequestTime = 0;
|
||||
const MIN_REQUEST_INTERVAL = 1500;
|
||||
|
||||
async function safeGet(url, config = {}, maxRetries = 5) {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
const now = Date.now();
|
||||
const timeSinceLast = now - lastRequestTime;
|
||||
if (timeSinceLast < MIN_REQUEST_INTERVAL) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, MIN_REQUEST_INTERVAL - timeSinceLast),
|
||||
);
|
||||
}
|
||||
lastRequestTime = Date.now();
|
||||
|
||||
const isApi = url.includes("/api?");
|
||||
const mergedHeaders = {
|
||||
Referer: baseUrl + "/",
|
||||
...(isApi
|
||||
? {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Accept: "application/json, text/javascript, */*; q=0.01",
|
||||
}
|
||||
: {
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
}),
|
||||
...(config.headers || {}),
|
||||
};
|
||||
const reqConfig = {
|
||||
...config,
|
||||
headers: mergedHeaders,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await global.axios.get(url, reqConfig);
|
||||
let data = response?.data;
|
||||
|
||||
if (isApi && typeof data === "string") {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
response.data = data;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const isRateLimited =
|
||||
response?.status === 429 ||
|
||||
data?.status === 429 ||
|
||||
data?.error_code === 1015 ||
|
||||
data?.title?.includes("rate limited") ||
|
||||
(typeof data === "string" &&
|
||||
(data.includes("error code: 1015") || data.includes("rate limited")));
|
||||
|
||||
if (isRateLimited) {
|
||||
if (attempt < maxRetries) {
|
||||
const totalWaitSecs =
|
||||
Math.max(4, Math.ceil(data?.retry_after || 4)) * attempt;
|
||||
console.warn(
|
||||
`[AnimePahe] Rate limited (Attempt ${attempt}/${maxRetries}). Waiting ${totalWaitSecs}s...`,
|
||||
);
|
||||
for (let sec = totalWaitSecs; sec > 0; sec--) {
|
||||
notifyRenderer("catalog-loading-status", {
|
||||
text: `Rate limited by AnimePahe. Retrying in ${sec}s...`,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
notifyRenderer("catalog-loading-status", {
|
||||
text: "Retrying AnimePahe fetch...",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
notifyRenderer("catalog-loading-status", {
|
||||
text: "",
|
||||
});
|
||||
return response;
|
||||
} catch (err) {
|
||||
const status = err.response?.status;
|
||||
if (status === 404) {
|
||||
notifyRenderer("catalog-loading-status", { text: "" });
|
||||
throw err;
|
||||
}
|
||||
if (status === 403 && attempt < maxRetries) {
|
||||
console.warn(
|
||||
`[AnimePahe] HTTP 403 on attempt ${attempt}/${maxRetries}. Checking clearance...`,
|
||||
);
|
||||
try {
|
||||
if (global.cloudflarebypass) {
|
||||
await global.cloudflarebypass(url, false, baseUrl + "/");
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (attempt < maxRetries) {
|
||||
const totalWaitSecs = status === 429 ? 8 * attempt : 3 * attempt;
|
||||
console.warn(
|
||||
`[AnimePahe] HTTP ${status || "Error"}. Waiting ${totalWaitSecs}s before retry ${attempt}/${maxRetries}...`,
|
||||
);
|
||||
for (let sec = totalWaitSecs; sec > 0; sec--) {
|
||||
notifyRenderer("catalog-loading-status", {
|
||||
text:
|
||||
status === 429
|
||||
? `Rate limited by AnimePahe. Retrying in ${sec}s...`
|
||||
: `Retrying AnimePahe in ${sec}s...`,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
notifyRenderer("catalog-loading-status", {
|
||||
text: "Retrying AnimePahe fetch...",
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
notifyRenderer("catalog-loading-status", {
|
||||
text: "",
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anime Search
|
||||
async function SearchAnime(query, filters = {}) {
|
||||
try {
|
||||
const { data } = await global.axios.get(
|
||||
`${baseUrl}/api?m=search&q=${encodeURIComponent(query)}`,
|
||||
{
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
const { data } = await safeGet(
|
||||
`${baseUrl}/api?m=search&q=${encodeURIComponent(query)}`,
|
||||
{
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
);
|
||||
const res = {
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
totalPages: 1,
|
||||
results: data.data.map((item) => ({
|
||||
id: `${item.session}`,
|
||||
title: item.title,
|
||||
image: item?.poster,
|
||||
})),
|
||||
};
|
||||
return res;
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
const res = {
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
totalPages: 1,
|
||||
results: (data?.data || []).map((item) => ({
|
||||
id: `${item.session}`,
|
||||
title: item.title,
|
||||
image: item?.poster,
|
||||
})),
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
// Recent Episodes
|
||||
async function fetchRecentEpisodes(filters = {}) {
|
||||
try {
|
||||
const { data } = await global.axios.get(
|
||||
`${baseUrl}/api?m=airing&page=${filters.page}`,
|
||||
{
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
},
|
||||
);
|
||||
const res = {
|
||||
currentPage: filters.page,
|
||||
hasNextPage: data?.next_page_url?.length > 0 ? true : false,
|
||||
totalPages: data?.last_page ?? 0,
|
||||
results: data.data.map((item) => ({
|
||||
id: `${item.anime_session}`,
|
||||
title: item.anime_title,
|
||||
image: item?.snapshot,
|
||||
episode: item.episode,
|
||||
})),
|
||||
};
|
||||
return res;
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
const pageNum = filters.page || 1;
|
||||
const { data } = await safeGet(`${baseUrl}/api?m=airing&page=${pageNum}`, {
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
});
|
||||
const res = {
|
||||
currentPage: pageNum,
|
||||
hasNextPage: data?.next_page_url?.length > 0 ? true : false,
|
||||
totalPages: data?.last_page ?? 0,
|
||||
results: (data?.data || []).map((item) => ({
|
||||
id: `${item.anime_session}`,
|
||||
title: item.anime_title,
|
||||
image: item?.snapshot,
|
||||
episode: item.episode,
|
||||
})),
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
// Animeinfo
|
||||
@@ -91,11 +210,25 @@ async function AnimeInfo(id) {
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await global.axios.get(`${baseUrl}/anime/${id}`, {
|
||||
const res = await safeGet(`${baseUrl}/anime/${id}`, {
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
});
|
||||
let data = res?.data;
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
(data.status === 404 || data.status >= 400)
|
||||
) {
|
||||
const err404 = new Error(
|
||||
`Request failed with status code ${data.status}`,
|
||||
);
|
||||
err404.status = data.status;
|
||||
err404.response = { status: data.status, data };
|
||||
throw err404;
|
||||
}
|
||||
|
||||
const $ = (0, cheerio.load)(data);
|
||||
|
||||
let MalId =
|
||||
@@ -138,6 +271,120 @@ async function AnimeInfo(id) {
|
||||
|
||||
return animeInfo;
|
||||
} catch (error) {
|
||||
const is404 =
|
||||
error?.status === 404 ||
|
||||
error?.response?.status === 404 ||
|
||||
error?.message?.includes("404");
|
||||
|
||||
if (is404 && id) {
|
||||
console.warn(
|
||||
`[AnimePahe] UUID ${id} returned 404. Fetching /anime directory to auto-heal UUID...`,
|
||||
);
|
||||
notifyRenderer("info-loading-status", {
|
||||
text: "Auto-healing AnimePahe UUID from directory, please wait...",
|
||||
});
|
||||
let resolvedNewUuid = null;
|
||||
let resolvedVersion = null;
|
||||
try {
|
||||
let html = "";
|
||||
if (typeof global.scrapperFetch === "function") {
|
||||
try {
|
||||
html = await global.scrapperFetch(`${baseUrl}/anime`);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!html) {
|
||||
const catalogRes = await safeGet(`${baseUrl}/anime`);
|
||||
html = typeof catalogRes?.data === "string" ? catalogRes.data : "";
|
||||
}
|
||||
|
||||
let links = [];
|
||||
let tsv = "";
|
||||
if (html) {
|
||||
try {
|
||||
const $ = (0, cheerio.load)(html);
|
||||
$("a[href*='/anime/']").each((_, el) => {
|
||||
const href = $(el).attr("href") || "";
|
||||
const match = href.match(
|
||||
/\/anime\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i,
|
||||
);
|
||||
if (match) {
|
||||
const u = match[1].toLowerCase().trim();
|
||||
const n = ($(el).text().trim() || $(el).attr("title") || "")
|
||||
.replace(/[\r\n\t]+/g, " ")
|
||||
.trim();
|
||||
if (u && n) {
|
||||
links.push({ uuid: u, name: n });
|
||||
tsv += `${u}\t${n}\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
|
||||
if (links.length === 0) {
|
||||
const linkRegex =
|
||||
/<a\s+[^>]*href="\/anime\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
let match;
|
||||
while ((match = linkRegex.exec(html)) !== null) {
|
||||
const u = match[1].toLowerCase().trim();
|
||||
const n = match[2]
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/[\r\n\t]+/g, " ")
|
||||
.trim();
|
||||
if (u && n) {
|
||||
links.push({ uuid: u, name: n });
|
||||
tsv += `${u}\t${n}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (links.length > 0 && global.axios) {
|
||||
const syncRes = await global.axios
|
||||
.post(
|
||||
"https://strawverse.theyogmehta.online/api/pahe/index",
|
||||
{ brokenUuid: id, catalog: tsv, links },
|
||||
{ timeout: 30000 },
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
"[AnimePahe] Failed to post index to server:",
|
||||
err?.message,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
if (syncRes?.data?.resolvedNewUuid) {
|
||||
resolvedNewUuid = syncRes.data.resolvedNewUuid;
|
||||
console.log(
|
||||
`[AnimePahe] Server auto-healed UUID ${id} -> ${resolvedNewUuid}`,
|
||||
);
|
||||
}
|
||||
if (syncRes?.data?.version) {
|
||||
resolvedVersion = syncRes.data.version;
|
||||
}
|
||||
}
|
||||
} catch (recoveryErr) {
|
||||
console.error(
|
||||
"[AnimePahe] Failed to recover from /anime index:",
|
||||
recoveryErr.message,
|
||||
);
|
||||
notifyRenderer("info-loading-status", {
|
||||
text: "",
|
||||
});
|
||||
}
|
||||
|
||||
notifyRenderer("info-loading-status", {
|
||||
text: "Updating database with healed mapping, please wait...",
|
||||
});
|
||||
|
||||
return {
|
||||
needsMappingSync: true,
|
||||
brokenUuid: id,
|
||||
newUuid: resolvedNewUuid || null,
|
||||
dataId: resolvedNewUuid || id,
|
||||
version: resolvedVersion || null,
|
||||
};
|
||||
}
|
||||
|
||||
console.error("Error fetching data from AnimePahe:", error);
|
||||
throw error;
|
||||
}
|
||||
@@ -150,7 +397,7 @@ async function getFirstEpisodeNumber(id, lastPage) {
|
||||
return firstEpCache[id];
|
||||
}
|
||||
try {
|
||||
const { data } = await global.axios.get(
|
||||
const { data } = await safeGet(
|
||||
`${baseUrl}/api?m=release&id=${id}&sort=episode_desc&page=${lastPage}`,
|
||||
{
|
||||
headers: {
|
||||
@@ -174,17 +421,28 @@ async function getFirstEpisodeNumber(id, lastPage) {
|
||||
async function fetchEpisode(id, page = 1) {
|
||||
try {
|
||||
let episodes = [];
|
||||
|
||||
let { last_page, data, total } = (
|
||||
await global.axios.get(
|
||||
`${baseUrl}/api?m=release&id=${id}&sort=episode_desc&page=${page}`,
|
||||
{
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
const resp = await safeGet(
|
||||
`${baseUrl}/api?m=release&id=${id}&sort=episode_desc&page=${page}`,
|
||||
{
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
)
|
||||
).data;
|
||||
},
|
||||
);
|
||||
const respData = resp?.data;
|
||||
if (
|
||||
!respData ||
|
||||
typeof respData !== "object" ||
|
||||
!Array.isArray(respData.data)
|
||||
) {
|
||||
return {
|
||||
episodes: [],
|
||||
totalPages: 0,
|
||||
total: 0,
|
||||
currentPage: page,
|
||||
};
|
||||
}
|
||||
const { last_page, data, total } = respData;
|
||||
|
||||
const firstEpNum = await getFirstEpisodeNumber(id, last_page);
|
||||
const offset = firstEpNum - 1;
|
||||
@@ -213,6 +471,13 @@ async function fetchEpisode(id, page = 1) {
|
||||
currentPage: page,
|
||||
};
|
||||
} catch (err) {
|
||||
if (
|
||||
err?.response?.status === 404 ||
|
||||
err?.status === 404 ||
|
||||
err?.message?.includes("404")
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
return { episodes: [], totalPages: 0, total: 0, currentPage: page };
|
||||
}
|
||||
}
|
||||
@@ -220,7 +485,7 @@ async function fetchEpisode(id, page = 1) {
|
||||
// fetching Episodes Download Links
|
||||
async function fetchEpisodeSources(episodeId, category = null) {
|
||||
try {
|
||||
const { data } = await global.axios.get(`${baseUrl}/play/${episodeId}`, {
|
||||
const { data } = await safeGet(`${baseUrl}/play/${episodeId}`, {
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
@@ -346,7 +611,7 @@ async function extract(videoUrl, retries = 2, delay = 1000) {
|
||||
|
||||
module.exports = {
|
||||
name: "pahe",
|
||||
version: "4.0.1",
|
||||
version: "5.0.1",
|
||||
SearchAnime,
|
||||
AnimeInfo,
|
||||
fetchEpisodeSources,
|
||||
|
||||
Reference in New Issue
Block a user