Implement Kaizoku provider downloader
This commit is contained in:
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
|
||||
const PROVIDERS = {
|
||||
anikoto: "./extensions/Anime/anikoto.js",
|
||||
anineko: "./extensions/Anime/anineko.js",
|
||||
pahe: "./extensions/Anime/pahe.js",
|
||||
};
|
||||
|
||||
const dynamicReferers = new Map();
|
||||
let fallbackReferer = "";
|
||||
|
||||
global.axios = axios.create({
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
||||
Accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
},
|
||||
});
|
||||
global.setDynamicReferer = (domain, referer) => {
|
||||
if (domain && referer) dynamicReferers.set(String(domain), String(referer));
|
||||
};
|
||||
global.setFallbackReferer = (referer) => {
|
||||
if (referer) fallbackReferer = String(referer);
|
||||
};
|
||||
|
||||
function fail(message, code = 1) {
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function providerName(raw) {
|
||||
const value = String(raw || "").trim().toLowerCase();
|
||||
if (!value || !PROVIDERS[value]) fail(`Unknown provider: ${raw || ""}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function loadProvider(name) {
|
||||
return require(path.resolve(__dirname, PROVIDERS[name]));
|
||||
}
|
||||
|
||||
function parseId(value) {
|
||||
const text = String(value || "").trim();
|
||||
const match = text.match(/^([a-z0-9_-]+):(.+)$/i);
|
||||
if (match && PROVIDERS[match[1].toLowerCase()]) {
|
||||
return { provider: match[1].toLowerCase(), id: match[2] };
|
||||
}
|
||||
return { provider: "", id: text };
|
||||
}
|
||||
|
||||
async function collectEpisodes(provider, id) {
|
||||
const first = await provider.fetchEpisode(id, 1);
|
||||
const episodes = Array.isArray(first?.episodes) ? [...first.episodes] : [];
|
||||
const totalPages = Number(first?.totalPages || 1);
|
||||
for (let page = 2; page <= totalPages; page += 1) {
|
||||
const payload = await provider.fetchEpisode(id, page);
|
||||
if (Array.isArray(payload?.episodes)) episodes.push(...payload.episodes);
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
||||
function qualityScore(source, wanted) {
|
||||
const text = `${source.quality || ""} ${source.name || ""}`;
|
||||
const match = text.match(/(\d{3,4})p?/);
|
||||
const value = match ? Number(match[1]) : 0;
|
||||
const normalized = String(wanted || "best").replace(/p$/i, "").toLowerCase();
|
||||
if (normalized === "worst") return value ? -value : 0;
|
||||
if (normalized === "best" || !/^\d+$/.test(normalized)) return value;
|
||||
const target = Number(normalized);
|
||||
if (!value) return -9999;
|
||||
return -Math.abs(target - value);
|
||||
}
|
||||
|
||||
async function resolve(provider, episodeId, mode, quality) {
|
||||
const sourcesPayload = await provider.fetchEpisodeSources(episodeId, mode);
|
||||
let sources = Array.isArray(sourcesPayload?.sources) ? sourcesPayload.sources : [];
|
||||
if (!sources.length && mode === "dub") {
|
||||
sourcesPayload = await provider.fetchEpisodeSources(episodeId, "sub");
|
||||
sources = Array.isArray(sourcesPayload?.sources) ? sourcesPayload.sources : [];
|
||||
}
|
||||
if (!sources.length) throw new Error("No episode sources were returned.");
|
||||
sources.sort((a, b) => qualityScore(b, quality) - qualityScore(a, quality));
|
||||
for (const source of sources) {
|
||||
const resolved = source.isUnresolved && provider.processServer
|
||||
? await provider.processServer(source.rawServer || source)
|
||||
: source;
|
||||
if (resolved?.url) {
|
||||
const headers = Object.assign({}, resolved.headers || {});
|
||||
try {
|
||||
const host = new URL(resolved.url).hostname;
|
||||
if (!headers.Referer && dynamicReferers.has(host)) {
|
||||
headers.Referer = dynamicReferers.get(host);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (!headers.Referer && fallbackReferer) headers.Referer = fallbackReferer;
|
||||
return {
|
||||
url: resolved.url,
|
||||
isM3U8: Boolean(resolved.isM3U8 || String(resolved.url).includes(".m3u8")),
|
||||
quality: resolved.quality || source.quality || source.name || "auto",
|
||||
type: resolved.type || source.type || mode,
|
||||
headers,
|
||||
subtitles: resolved.subtitles || sourcesPayload.subtitles || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error("Could not resolve a playable source.");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [command, rawProvider, ...rest] = process.argv.slice(2);
|
||||
if (!command) fail("Missing command.");
|
||||
|
||||
if (command === "providers") {
|
||||
console.log(JSON.stringify(Object.keys(PROVIDERS)));
|
||||
return;
|
||||
}
|
||||
|
||||
const name = providerName(rawProvider);
|
||||
const provider = loadProvider(name);
|
||||
|
||||
if (command === "search") {
|
||||
const query = rest[0] || "";
|
||||
const page = Number(rest[1] || 1);
|
||||
const data = await provider.SearchAnime(query, { page });
|
||||
const results = (data.results || []).map((item, index) => ({
|
||||
id: `${name}:${item.id}`,
|
||||
provider: name,
|
||||
provider_id: item.id,
|
||||
title: item.title || item.name || item.id,
|
||||
image: item.image || null,
|
||||
index: index + 1,
|
||||
}));
|
||||
console.log(JSON.stringify({ ...data, results }));
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseId(rest[0]);
|
||||
if (parsed.provider && parsed.provider !== name) {
|
||||
fail(`Show id provider ${parsed.provider} does not match requested ${name}.`);
|
||||
}
|
||||
const id = parsed.id;
|
||||
if (!id) fail("Missing id.");
|
||||
|
||||
if (command === "info") {
|
||||
const data = await provider.AnimeInfo(id);
|
||||
console.log(JSON.stringify({ ...data, id: `${name}:${data.id || id}`, provider: name, provider_id: data.id || id }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "episodes") {
|
||||
const episodes = await collectEpisodes(provider, id);
|
||||
console.log(JSON.stringify({ episodes }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "resolve") {
|
||||
const mode = rest[1] || "sub";
|
||||
const quality = rest[2] || "best";
|
||||
const data = await resolve(provider, id, mode, quality);
|
||||
console.log(JSON.stringify(data));
|
||||
return;
|
||||
}
|
||||
|
||||
fail(`Unknown command: ${command}`);
|
||||
}
|
||||
|
||||
main().catch((err) => fail(err?.stack || err?.message || String(err)));
|
||||
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* StrawVerse Extension - Anikoto Scraper
|
||||
* Copyright (C) 2026 TheYogMehta
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* DISCLAIMER: This extension is intended for research, educational,
|
||||
* and developer testing purposes only. It functions as a client-side parser
|
||||
* of publicly available web pages. The developers do not host or distribute
|
||||
* any copyrighted media. Users are responsible for compliance with the terms of
|
||||
* service of the target website.
|
||||
*/
|
||||
|
||||
const cheerio = require("cheerio");
|
||||
|
||||
const baseUrl = "https://anikototv.to";
|
||||
|
||||
function parsePagination($, defaultPage) {
|
||||
let totalPages = 1;
|
||||
$(".pagination a").each((i, el) => {
|
||||
const href = $(el).attr("href");
|
||||
if (href) {
|
||||
const match = href.match(/page=(\d+)/);
|
||||
if (match) {
|
||||
const pageNum = parseInt(match[1]);
|
||||
if (pageNum > totalPages) {
|
||||
totalPages = pageNum;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let hasNextPage = false;
|
||||
$(".pagination a").each((i, el) => {
|
||||
const rel = $(el).attr("rel");
|
||||
if (rel === "next") {
|
||||
hasNextPage = true;
|
||||
}
|
||||
});
|
||||
|
||||
const activePageText = $(
|
||||
".pagination li.active, .pagination li.page-item.active",
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
const currentPage = parseInt(activePageText) || defaultPage || 1;
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
hasNextPage,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
async function SearchAnime(query, filters = {}) {
|
||||
try {
|
||||
const page = filters?.page || 1;
|
||||
const { data: html } = await global.axios.get(
|
||||
`${baseUrl}/search?keyword=${encodeURIComponent(query)}&page=${page}`,
|
||||
);
|
||||
const $ = cheerio.load(html);
|
||||
const results = [];
|
||||
|
||||
$("div.item").each((i, el) => {
|
||||
const aTag = $(el).find(".name.d-title");
|
||||
const title =
|
||||
aTag.text().trim() ||
|
||||
aTag.attr("data-jp") ||
|
||||
aTag.attr("title") ||
|
||||
$(el).find(".name.d-title").text().trim() ||
|
||||
$(el).find(".title").text().trim();
|
||||
let href = aTag.attr("href");
|
||||
if (!href) return;
|
||||
const match = href.match(/\/watch\/([^\/]+)/);
|
||||
if (!match) return;
|
||||
const id = match[1];
|
||||
|
||||
const image =
|
||||
$(el).find(".ani.poster img").attr("src") ||
|
||||
$(el).find("img").attr("src");
|
||||
|
||||
results.push({
|
||||
id: id,
|
||||
title: title,
|
||||
image: image || null,
|
||||
});
|
||||
});
|
||||
|
||||
const pagination = parsePagination($, page);
|
||||
|
||||
return {
|
||||
currentPage: pagination.currentPage,
|
||||
hasNextPage: pagination.hasNextPage,
|
||||
totalPages: pagination.totalPages,
|
||||
results: results,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRecentEpisodes(filters = {}) {
|
||||
try {
|
||||
const page = filters?.page || 1;
|
||||
const { data: html } = await global.axios.get(
|
||||
`${baseUrl}/latest-updated?page=${page}`,
|
||||
);
|
||||
const $ = cheerio.load(html);
|
||||
const results = [];
|
||||
|
||||
$(".item").each((i, el) => {
|
||||
const aTag = $(el).find(".name.d-title").length
|
||||
? $(el).find(".name.d-title").first()
|
||||
: $(el).find("a").last();
|
||||
const imgTag = $(el).find("img");
|
||||
|
||||
const title =
|
||||
$(el).find(".name.d-title").text().trim() ||
|
||||
$(el).find(".title").text().trim() ||
|
||||
imgTag.attr("title") ||
|
||||
imgTag.attr("alt") ||
|
||||
aTag.attr("title") ||
|
||||
aTag.attr("data-jp") ||
|
||||
aTag
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/TV\s*Sub\s*Dub/i, "")
|
||||
.trim();
|
||||
|
||||
let href = $(el).find("a").first().attr("href") || aTag.attr("href");
|
||||
|
||||
if (!href) return;
|
||||
const match = href.match(/\/watch\/([^\/]+)/);
|
||||
if (!match) return;
|
||||
const id = match[1];
|
||||
|
||||
const image =
|
||||
$(el).find(".ani.poster img").attr("src") ||
|
||||
$(el).find("img").attr("src");
|
||||
|
||||
results.push({
|
||||
id: id,
|
||||
title: title,
|
||||
image: image || null,
|
||||
});
|
||||
});
|
||||
|
||||
const pagination = parsePagination($, page);
|
||||
|
||||
return {
|
||||
currentPage: pagination.currentPage,
|
||||
hasNextPage: pagination.hasNextPage,
|
||||
totalPages: pagination.totalPages,
|
||||
results: results,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function AnimeInfo(id) {
|
||||
const animeInfo = {
|
||||
id: id,
|
||||
title: "",
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: html } = await global.axios.get(`${baseUrl}/watch/${id}`);
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const dataId = $("#watch-main").attr("data-id");
|
||||
|
||||
animeInfo.title =
|
||||
$('h1[itemprop="name"]').text().trim() ||
|
||||
$(".title").first().text().trim() ||
|
||||
id;
|
||||
animeInfo.image =
|
||||
$('img[itemprop="image"]').attr("src") ||
|
||||
$(".ani.poster img").attr("src") ||
|
||||
null;
|
||||
animeInfo.description =
|
||||
$(".synopsis").text().trim() || $(".description").text().trim() || "";
|
||||
|
||||
const genres = [];
|
||||
$(".genre a").each((i, el) => {
|
||||
genres.push($(el).text().trim());
|
||||
});
|
||||
animeInfo.genres = genres;
|
||||
animeInfo.status = "Unknown";
|
||||
|
||||
$(".info .item").each((i, el) => {
|
||||
const text = $(el).text();
|
||||
if (text.includes("Status:")) {
|
||||
const status = $(el).find(".name").text().trim();
|
||||
if (status.includes("Currently Airing")) animeInfo.status = "Ongoing";
|
||||
else if (status.includes("Finished Airing"))
|
||||
animeInfo.status = "Completed";
|
||||
}
|
||||
});
|
||||
animeInfo.dataId = dataId;
|
||||
|
||||
return animeInfo;
|
||||
} catch (error) {
|
||||
console.error("Error fetching data from AnikotoTV:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEpisode(dataId, page = 1) {
|
||||
try {
|
||||
let numericId = dataId;
|
||||
if (!/^\d+$/.test(String(dataId))) {
|
||||
try {
|
||||
const { data: watchHtml } = await global.axios.get(
|
||||
`${baseUrl}/watch/${dataId}`,
|
||||
);
|
||||
const $w = cheerio.load(watchHtml);
|
||||
numericId =
|
||||
$w("#watch-main").attr("data-id") ||
|
||||
$w("[data-id]").attr("data-id") ||
|
||||
$w("#wrapper").attr("data-id");
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!numericId) {
|
||||
return { episodes: [], totalPages: 0, total: 0, currentPage: page };
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/ajax/episode/list/${numericId}`;
|
||||
const { data } = await global.axios.get(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
});
|
||||
|
||||
const $ = cheerio.load(data.result || "");
|
||||
let episodes = [];
|
||||
|
||||
$("a[data-id][data-ids], .ep-item, li a, .ssl-item, a.item").each(
|
||||
(i, el) => {
|
||||
const epNum =
|
||||
$(el).attr("data-num") || $(el).attr("data-number") || String(i + 1);
|
||||
const epId = $(el).attr("data-id");
|
||||
const dataIds = $(el).attr("data-ids");
|
||||
const title =
|
||||
$(el).attr("title") ||
|
||||
$(el).find(".d-title").text().trim() ||
|
||||
`Episode ${epNum}`;
|
||||
|
||||
if (epId && dataIds) {
|
||||
const langs = [];
|
||||
if ($(el).attr("data-sub") === "1") langs.push("sub");
|
||||
if ($(el).attr("data-dub") === "1") langs.push("dub");
|
||||
|
||||
episodes.push({
|
||||
id: `${epId}|${dataIds}`,
|
||||
number: parseFloat(epNum),
|
||||
title: title,
|
||||
duration: "Unknown",
|
||||
langs,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
episodes: episodes,
|
||||
totalPages: 1,
|
||||
total: episodes.length,
|
||||
currentPage: 1,
|
||||
};
|
||||
} catch (err) {
|
||||
return { episodes: [], totalPages: 0, total: 0, currentPage: page };
|
||||
}
|
||||
}
|
||||
|
||||
async function processServer(server) {
|
||||
if (!server || !server.linkId) return null;
|
||||
try {
|
||||
const linkRes = await global.axios.get(
|
||||
`${baseUrl}/ajax/server?get=${server.linkId}`,
|
||||
{
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let iframeUrl =
|
||||
linkRes.data?.result?.url ||
|
||||
linkRes.data?.url ||
|
||||
(typeof linkRes.data?.result === "string" ? linkRes.data.result : null);
|
||||
if (!iframeUrl && typeof linkRes.data?.result === "string") {
|
||||
const match = linkRes.data.result.match(/src=["']([^"']+)["']/);
|
||||
if (match) iframeUrl = match[1];
|
||||
}
|
||||
if (!iframeUrl || typeof iframeUrl !== "string") return null;
|
||||
if (iframeUrl.startsWith("//")) iframeUrl = "https:" + iframeUrl;
|
||||
|
||||
const iframeRes = await global.axios.get(iframeUrl, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Referer: baseUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const $iframe = cheerio.load(iframeRes.data);
|
||||
let playerDbId =
|
||||
$iframe("#megaplay-player").attr("data-id") ||
|
||||
$iframe("[data-id]").attr("data-id");
|
||||
if (!playerDbId) {
|
||||
const match = iframeRes.data.match(/data-id=["']([^"']+)["']/);
|
||||
if (match) playerDbId = match[1];
|
||||
}
|
||||
if (!playerDbId) return null;
|
||||
|
||||
const typeMatch =
|
||||
iframeRes.data.match(/type\s*:\s*'([^']+)'/) ||
|
||||
iframeUrl.match(/\/stream\/[^\/]+\/([^\/\?]+)/);
|
||||
const type = typeMatch ? typeMatch[1] : "";
|
||||
|
||||
const ciduMatch = iframeRes.data.match(/cidu\s*:\s*'([^']+)'/);
|
||||
const cidu = ciduMatch ? ciduMatch[1] : "";
|
||||
|
||||
const domainName = new URL(iframeUrl).origin;
|
||||
const playerReferer = domainName + "/";
|
||||
const sourcesRes = await global.axios.get(
|
||||
`${domainName}/stream/getSources?id=${playerDbId}${type ? `&type=${encodeURIComponent(type)}` : ""}${cidu ? `&cidu=${encodeURIComponent(cidu)}` : ""}`,
|
||||
{
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Referer: playerReferer,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (sourcesRes.data && sourcesRes.data.sources) {
|
||||
const rawSrc = sourcesRes.data.sources;
|
||||
const m3u8Url =
|
||||
typeof rawSrc === "string"
|
||||
? rawSrc
|
||||
: rawSrc.file ||
|
||||
rawSrc.url ||
|
||||
(Array.isArray(rawSrc)
|
||||
? rawSrc[0]?.file || rawSrc[0]?.url || (typeof rawSrc[0] === "string" ? rawSrc[0] : null)
|
||||
: null);
|
||||
if (m3u8Url) {
|
||||
try {
|
||||
const cdnDomain = new URL(m3u8Url).hostname;
|
||||
global.setDynamicReferer(cdnDomain, playerReferer);
|
||||
global.setFallbackReferer(playerReferer);
|
||||
} catch (e) {}
|
||||
|
||||
const subtitles = (sourcesRes.data.tracks || [])
|
||||
.filter(
|
||||
(t) => t.file && (!t.kind || t.kind.toLowerCase() !== "thumbnails"),
|
||||
)
|
||||
.map((t) => {
|
||||
let sUrl = t.file;
|
||||
if (sUrl.startsWith("//")) {
|
||||
sUrl = "https:" + sUrl;
|
||||
} else if (
|
||||
!sUrl.startsWith("http://") &&
|
||||
!sUrl.startsWith("https://")
|
||||
) {
|
||||
try {
|
||||
sUrl = new URL(sUrl, iframeUrl).href;
|
||||
} catch (e) {}
|
||||
}
|
||||
return {
|
||||
url: sUrl,
|
||||
lang: t.label || t.language || "English",
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
url: m3u8Url,
|
||||
isM3U8: true,
|
||||
quality: server.name || "auto",
|
||||
isDub: server.type === "dub",
|
||||
isHsub: server.type === "hsub",
|
||||
type: server.type,
|
||||
headers: { Referer: playerReferer },
|
||||
subtitles: subtitles,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to process server ${server.name}:`, err.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchEpisodeSources(episodeIdStr, category = null) {
|
||||
try {
|
||||
const parts = episodeIdStr.split("|");
|
||||
const epId = parts[0];
|
||||
const dataIds = parts[1];
|
||||
|
||||
const serverUrl = `${baseUrl}/ajax/server/list?servers=${dataIds}`;
|
||||
const serverRes = await global.axios.get(serverUrl, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
});
|
||||
|
||||
const $ = cheerio.load(serverRes.data.result);
|
||||
|
||||
let selector =
|
||||
".type[data-type='sub'] li, .type[data-type='dub'] li, .type[data-type='hsub'] li";
|
||||
|
||||
const servers = [];
|
||||
$(selector).each((i, el) => {
|
||||
const type = $(el).closest(".type").attr("data-type");
|
||||
servers.push({
|
||||
type: type,
|
||||
linkId: $(el).attr("data-link-id"),
|
||||
name: $(el).text().trim(),
|
||||
});
|
||||
});
|
||||
|
||||
let targetServers = servers;
|
||||
if (category) {
|
||||
const catLower = category.toLowerCase();
|
||||
targetServers = servers.filter((s) => {
|
||||
const typeLower = (s.type || "").toLowerCase();
|
||||
if (catLower === "hsub" || catLower === "hardsub") {
|
||||
return typeLower === "hsub" || typeLower === "hardsub";
|
||||
} else if (catLower === "sub" || catLower === "softsub") {
|
||||
return typeLower === "sub" || typeLower === "softsub";
|
||||
} else if (catLower === "dub") {
|
||||
return typeLower === "dub";
|
||||
}
|
||||
return typeLower === catLower;
|
||||
});
|
||||
}
|
||||
|
||||
const requestedCategory = (category || "sub").toLowerCase();
|
||||
if (targetServers.length === 0) {
|
||||
return { sources: [], subtitles: [] };
|
||||
}
|
||||
|
||||
const sources = targetServers.map((s) => ({
|
||||
quality: s.name,
|
||||
name: s.name,
|
||||
linkId: s.linkId,
|
||||
lang: s.type || requestedCategory,
|
||||
type: s.type || requestedCategory,
|
||||
isUnresolved: true,
|
||||
rawServer: s,
|
||||
}));
|
||||
|
||||
return {
|
||||
sources,
|
||||
subtitles: [],
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Error fetching data from AnikotoTV:", err);
|
||||
return { sources: [], subtitles: [] };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: "anikoto",
|
||||
version: "5.0.1",
|
||||
SearchAnime,
|
||||
AnimeInfo,
|
||||
fetchEpisodeSources,
|
||||
processServer,
|
||||
fetchRecentEpisodes,
|
||||
fetchEpisode,
|
||||
};
|
||||
@@ -0,0 +1,628 @@
|
||||
/**
|
||||
* StrawVerse Extension - AniNeko Scraper
|
||||
* Copyright (C) 2026 TheYogMehta
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* DISCLAIMER: This extension is intended for research, educational,
|
||||
* and developer testing purposes only. It functions as a client-side parser
|
||||
* of publicly available web pages. The developers do not host or distribute
|
||||
* any copyrighted media. Users are responsible for compliance with the terms of
|
||||
* service of the target website.
|
||||
*/
|
||||
|
||||
const cheerio = require("cheerio");
|
||||
|
||||
const baseUrl = "https://anineko.to";
|
||||
|
||||
function parsePagination($, defaultPage) {
|
||||
let totalPages = 1;
|
||||
$(".pagination a.page-link").each((i, el) => {
|
||||
const href = $(el).attr("href");
|
||||
if (href) {
|
||||
const match = href.match(/page=(\d+)/);
|
||||
if (match) {
|
||||
const pageNum = parseInt(match[1]);
|
||||
if (pageNum > totalPages) {
|
||||
totalPages = pageNum;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let hasNextPage = false;
|
||||
$(".pagination li.next, .pagination li.page-item.next").each((i, el) => {
|
||||
hasNextPage = true;
|
||||
});
|
||||
|
||||
const activeText = $(".pagination li.active a.page-link").text().trim();
|
||||
const currentPage = parseInt(activeText) || defaultPage || 1;
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
hasNextPage,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
// Anime Search
|
||||
async function SearchAnime(query, filters = {}) {
|
||||
try {
|
||||
const page = filters?.page || 1;
|
||||
const { data: html } = await global.axios.get(
|
||||
`${baseUrl}/browser?keyword=${encodeURIComponent(query)}&page=${page}`,
|
||||
);
|
||||
const $ = cheerio.load(html);
|
||||
const results = [];
|
||||
|
||||
$("article.nv-anime-card").each((i, el) => {
|
||||
const titleEl = $(el).find("h3.nv-anime-title a");
|
||||
const title = titleEl.text().trim();
|
||||
let href = titleEl.attr("href") || $(el).find("a").first().attr("href");
|
||||
if (!href) return;
|
||||
|
||||
const match = href.match(/\/watch\/([^\/]+)/);
|
||||
if (!match) return;
|
||||
const id = match[1];
|
||||
|
||||
const image =
|
||||
$(el).find(".nv-anime-thumb img").attr("src") ||
|
||||
$(el).find("img").attr("src") ||
|
||||
null;
|
||||
|
||||
results.push({
|
||||
id,
|
||||
title,
|
||||
image,
|
||||
});
|
||||
});
|
||||
|
||||
const pagination = parsePagination($, page);
|
||||
|
||||
return {
|
||||
currentPage: pagination.currentPage,
|
||||
hasNextPage: pagination.hasNextPage,
|
||||
totalPages: pagination.totalPages,
|
||||
results,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Recent Episodes
|
||||
async function fetchRecentEpisodes(filters = {}) {
|
||||
try {
|
||||
const page = filters?.page || 1;
|
||||
const { data: html } = await global.axios.get(
|
||||
`${baseUrl}/updates?page=${page}`,
|
||||
);
|
||||
const $ = cheerio.load(html);
|
||||
const results = [];
|
||||
|
||||
$("article.nv-anime-card").each((i, el) => {
|
||||
const titleEl = $(el).find("h3.nv-anime-title a");
|
||||
const title = titleEl.text().trim();
|
||||
let href = titleEl.attr("href") || $(el).find("a").first().attr("href");
|
||||
if (!href) return;
|
||||
|
||||
const match = href.match(/\/watch\/([^\/]+)/);
|
||||
if (!match) return;
|
||||
const id = match[1];
|
||||
|
||||
const image =
|
||||
$(el).find(".nv-anime-thumb img").attr("src") ||
|
||||
$(el).find("img").attr("src") ||
|
||||
null;
|
||||
|
||||
results.push({
|
||||
id,
|
||||
title,
|
||||
image,
|
||||
});
|
||||
});
|
||||
|
||||
const pagination = parsePagination($, page);
|
||||
|
||||
return {
|
||||
currentPage: pagination.currentPage,
|
||||
hasNextPage: pagination.hasNextPage,
|
||||
totalPages: pagination.totalPages,
|
||||
results,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Anime Info
|
||||
async function AnimeInfo(id) {
|
||||
const animeInfo = {
|
||||
id: id,
|
||||
title: "",
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: html } = await global.axios.get(`${baseUrl}/watch/${id}`);
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
animeInfo.title = $("h1").first().text().trim() || id;
|
||||
animeInfo.image = $(".nv-info-poster img").attr("src") || null;
|
||||
animeInfo.description =
|
||||
$(".nv-info-desc").text().trim() ||
|
||||
$(".nv-info-synopsis").text().trim() ||
|
||||
"";
|
||||
|
||||
const genres = [];
|
||||
$(".nv-info-tags span").each((i, el) => {
|
||||
const text = $(el).text().trim();
|
||||
if (
|
||||
text &&
|
||||
!["SUB", "DUB", "Hardsub", "HD", "HSUB"].includes(text) &&
|
||||
!text.match(/^\d{4}$/) &&
|
||||
![
|
||||
"Currently Airing",
|
||||
"Finished Airing",
|
||||
"Not yet aired",
|
||||
"TV",
|
||||
"Movie",
|
||||
"OVA",
|
||||
"ONA",
|
||||
"Special",
|
||||
"Music",
|
||||
"TV_SHORT",
|
||||
].includes(text)
|
||||
) {
|
||||
genres.push(text);
|
||||
}
|
||||
});
|
||||
animeInfo.genres = genres;
|
||||
animeInfo.status = "Unknown";
|
||||
const statusText = $(".nv-info-tags span, .nv-pill").text();
|
||||
if (statusText.includes("Currently Airing")) {
|
||||
animeInfo.status = "Ongoing";
|
||||
} else if (statusText.includes("Finished Airing")) {
|
||||
animeInfo.status = "Completed";
|
||||
}
|
||||
const typeEl = $(".nv-info-stats div").first().find("strong").text().trim();
|
||||
animeInfo.type = typeEl || "TV";
|
||||
|
||||
animeInfo.dataId = id;
|
||||
|
||||
return animeInfo;
|
||||
} catch (error) {
|
||||
console.error("Error fetching data from AniNeko:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Episodes
|
||||
async function fetchEpisode(id, page = 1) {
|
||||
try {
|
||||
const { data: html } = await global.axios.get(`${baseUrl}/watch/${id}`);
|
||||
const $ = cheerio.load(html);
|
||||
let episodes = [];
|
||||
|
||||
$("article.nv-info-episode-item").each((i, el) => {
|
||||
const mainLink = $(el).find("a.nv-info-episode-main");
|
||||
const href = mainLink.attr("href") || "";
|
||||
const epMatch = href.match(/\/ep-(\d+)/);
|
||||
const epNum = epMatch ? parseInt(epMatch[1]) : i + 1;
|
||||
|
||||
const titleStrong = mainLink.find("strong").text().trim();
|
||||
const titleSpan = mainLink.find("span").text().trim();
|
||||
const title = titleSpan || titleStrong || `Episode ${epNum}`;
|
||||
|
||||
const badges = $(el)
|
||||
.find(".nv-info-episode-badges span")
|
||||
.map((j, badge) => $(badge).text().trim().toUpperCase())
|
||||
.get();
|
||||
|
||||
const langs = [];
|
||||
if (badges.includes("SUB")) langs.push("sub");
|
||||
if (badges.includes("HSUB") || badges.includes("HARDSUB"))
|
||||
langs.push("hsub");
|
||||
if (badges.includes("DUB")) langs.push("dub");
|
||||
if (
|
||||
badges.includes("SOFTSUB") ||
|
||||
badges.includes("SOFT SUB") ||
|
||||
badges.includes("SOFT-SUB")
|
||||
)
|
||||
langs.push("sub");
|
||||
if (
|
||||
badges.includes("SOFTDUB") ||
|
||||
badges.includes("SOFT DUB") ||
|
||||
badges.includes("SOFT-DUB")
|
||||
)
|
||||
langs.push("dub");
|
||||
|
||||
const watchPathMatch = href.match(/\/watch\/(.+)$/);
|
||||
const epSlug = watchPathMatch ? watchPathMatch[1] : `${id}/ep-${epNum}`;
|
||||
|
||||
episodes.push({
|
||||
id: epSlug,
|
||||
number: epNum,
|
||||
title,
|
||||
duration: "Unknown",
|
||||
langs,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
episodes,
|
||||
totalPages: 1,
|
||||
total: episodes.length,
|
||||
currentPage: 1,
|
||||
};
|
||||
} catch (err) {
|
||||
return { episodes: [], totalPages: 0, total: 0, currentPage: page };
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Episode Sources
|
||||
async function fetchEpisodeSources(episodeId, category = null) {
|
||||
try {
|
||||
const { data: html } = await global.axios.get(
|
||||
`${baseUrl}/watch/${episodeId}`,
|
||||
);
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const servers = [];
|
||||
|
||||
$(".lang-group, .server-group, [data-lang]").each((i, panel) => {
|
||||
let rawType = (
|
||||
$(panel).attr("data-lang") ||
|
||||
$(panel).attr("data-type") ||
|
||||
$(panel).attr("data-id") ||
|
||||
""
|
||||
).toLowerCase();
|
||||
|
||||
let panelType = "";
|
||||
if (
|
||||
rawType.includes("hsub") ||
|
||||
rawType.includes("hardsub") ||
|
||||
rawType.includes("hard-sub")
|
||||
) {
|
||||
panelType = "hsub";
|
||||
} else if (rawType.includes("dub")) {
|
||||
panelType = "dub";
|
||||
} else if (rawType.includes("sub")) {
|
||||
panelType = "sub";
|
||||
}
|
||||
|
||||
$(panel)
|
||||
.find("button.server-video, a.server-video, .server-item")
|
||||
.each((j, btn) => {
|
||||
const videoUrl =
|
||||
$(btn).attr("data-video") ||
|
||||
$(btn).attr("data-url") ||
|
||||
$(btn).attr("href");
|
||||
if (
|
||||
!videoUrl ||
|
||||
videoUrl === "#" ||
|
||||
videoUrl.startsWith("javascript:")
|
||||
)
|
||||
return;
|
||||
|
||||
const cloned = $(btn).clone();
|
||||
cloned.find("span").remove();
|
||||
const serverName = cloned.text().trim() || "Server";
|
||||
|
||||
let btnRaw = (
|
||||
$(btn).attr("data-lang") ||
|
||||
$(btn).attr("data-type") ||
|
||||
""
|
||||
).toLowerCase();
|
||||
|
||||
let btnType = panelType;
|
||||
if (btnRaw.includes("hsub") || btnRaw.includes("hardsub")) {
|
||||
btnType = "hsub";
|
||||
} else if (btnRaw.includes("dub")) {
|
||||
btnType = "dub";
|
||||
} else if (btnRaw.includes("sub") && !btnRaw.includes("hsub")) {
|
||||
btnType = "sub";
|
||||
}
|
||||
|
||||
servers.push({
|
||||
url: videoUrl,
|
||||
name: serverName,
|
||||
type: btnType || "sub",
|
||||
isDefault: $(btn).hasClass("default") || $(btn).hasClass("active"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let targetServers = servers;
|
||||
if (category) {
|
||||
const catLower = category.toLowerCase();
|
||||
targetServers = servers.filter((s) => {
|
||||
const typeLower = (s.type || "").toLowerCase();
|
||||
if (catLower === "hsub" || catLower === "hardsub") {
|
||||
return typeLower === "hsub" || typeLower === "hardsub";
|
||||
} else if (catLower === "sub" || catLower === "softsub") {
|
||||
return typeLower === "sub" || typeLower === "softsub";
|
||||
} else if (catLower === "dub") {
|
||||
return typeLower === "dub";
|
||||
}
|
||||
return typeLower === catLower;
|
||||
});
|
||||
}
|
||||
|
||||
const requestedCategory = (category || "sub").toLowerCase();
|
||||
if (targetServers.length === 0) {
|
||||
return { sources: [], subtitles: [] };
|
||||
}
|
||||
|
||||
const sources = targetServers.map((s) => ({
|
||||
quality: s.name,
|
||||
name: s.name,
|
||||
url: s.url,
|
||||
lang: s.type || requestedCategory,
|
||||
type: s.type || requestedCategory,
|
||||
isUnresolved: true,
|
||||
rawServer: s,
|
||||
}));
|
||||
|
||||
return {
|
||||
sources,
|
||||
subtitles: [],
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Error fetching data from AniNeko:", err);
|
||||
return { sources: [], subtitles: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Process an embed server URL to extract the actual video source
|
||||
async function processEmbedServer(server) {
|
||||
try {
|
||||
const embedUrl = server.url;
|
||||
let typeLabel = server.type ? server.type.toUpperCase() : "";
|
||||
if (server.type === "sub") typeLabel = "Sub";
|
||||
if (server.type === "hsub") typeLabel = "HSub";
|
||||
if (server.type === "dub") typeLabel = "Dub";
|
||||
const qualityLabel = `${server.name} ${typeLabel}`;
|
||||
let subtitles = [];
|
||||
try {
|
||||
const urlObj = new URL(embedUrl);
|
||||
const subParam = urlObj.searchParams.get("sub");
|
||||
const captionParam = urlObj.searchParams.get("caption_1");
|
||||
const c1FileParam = urlObj.searchParams.get("c1_file");
|
||||
let subUrl = subParam || captionParam || c1FileParam;
|
||||
if (subUrl) {
|
||||
if (subUrl.startsWith("//")) {
|
||||
subUrl = "https:" + subUrl;
|
||||
} else if (
|
||||
!subUrl.startsWith("http://") &&
|
||||
!subUrl.startsWith("https://")
|
||||
) {
|
||||
try {
|
||||
subUrl = new URL(subUrl, embedUrl).href;
|
||||
} catch (e) {}
|
||||
}
|
||||
subtitles.push({
|
||||
url: subUrl,
|
||||
lang: "English",
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const { data: embedHtml } = await global.axios.get(embedUrl, {
|
||||
headers: {
|
||||
Referer: `${baseUrl}/`,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const tracksRegex = /tracks\s*[:=]\s*(\[[^\]]+\])/i;
|
||||
const tracksMatch = embedHtml.match(tracksRegex);
|
||||
if (tracksMatch) {
|
||||
const arrayStr = tracksMatch[1];
|
||||
const objRegex = /\{([^}]+)\}/g;
|
||||
let objMatch;
|
||||
while ((objMatch = objRegex.exec(arrayStr)) !== null) {
|
||||
const objContent = objMatch[1];
|
||||
const fileM = objContent.match(
|
||||
/['"]?file['"]?\s*:\s*['"]([^'"]+)['"]/,
|
||||
);
|
||||
if (fileM) {
|
||||
let sUrl = fileM[1];
|
||||
const labelM = objContent.match(
|
||||
/['"]?label['"]?\s*:\s*['"]([^'"]+)['"]/,
|
||||
);
|
||||
const kindM = objContent.match(
|
||||
/['"]?kind['"]?\s*:\s*['"]([^'"]+)['"]/,
|
||||
);
|
||||
const langM = objContent.match(
|
||||
/['"]?(?:language|lang)['"]?\s*:\s*['"]([^'"]+)['"]/,
|
||||
);
|
||||
|
||||
const kind = kindM ? kindM[1].toLowerCase() : "";
|
||||
if (!kind || kind !== "thumbnails") {
|
||||
if (sUrl.startsWith("//")) {
|
||||
sUrl = "https:" + sUrl;
|
||||
} else if (
|
||||
!sUrl.startsWith("http://") &&
|
||||
!sUrl.startsWith("https://")
|
||||
) {
|
||||
try {
|
||||
sUrl = new URL(sUrl, embedUrl).href;
|
||||
} catch (e) {}
|
||||
}
|
||||
subtitles.push({
|
||||
url: sUrl,
|
||||
lang: labelM ? labelM[1] : langM ? langM[1] : "English",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subtitles.length === 0) {
|
||||
const subFileRegex =
|
||||
/["']?file["']?\s*:\s*["']([^"']+\.(?:vtt|srt)[^"']*)["']/gi;
|
||||
let subMatch;
|
||||
while ((subMatch = subFileRegex.exec(embedHtml)) !== null) {
|
||||
let sUrl = subMatch[1];
|
||||
if (sUrl.startsWith("//")) {
|
||||
sUrl = "https:" + sUrl;
|
||||
} else if (
|
||||
!sUrl.startsWith("http://") &&
|
||||
!sUrl.startsWith("https://")
|
||||
) {
|
||||
try {
|
||||
sUrl = new URL(sUrl, embedUrl).href;
|
||||
} catch (e) {}
|
||||
}
|
||||
subtitles.push({ url: sUrl, lang: "English" });
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
let m3u8Match = embedHtml.match(/https?:\/\/[^\s"']+\.m3u8[^\s"']*/);
|
||||
if (!m3u8Match) {
|
||||
const srcMatch = embedHtml.match(
|
||||
/src\s*=\s*["']([^"']*\.m3u8[^"']*)["']/,
|
||||
);
|
||||
if (srcMatch) {
|
||||
m3u8Match = [srcMatch[1]];
|
||||
}
|
||||
}
|
||||
if (!m3u8Match) {
|
||||
const evalMatch = /(eval)(\(f.*?)(<\/script>)/s.exec(embedHtml);
|
||||
if (evalMatch) {
|
||||
try {
|
||||
const unpacked = eval(evalMatch[2].replace("eval", ""));
|
||||
m3u8Match = unpacked.match(/https?:\/\/[^\s"']+\.m3u8[^\s"']*/);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
if (!m3u8Match) {
|
||||
const fileMatch = embedHtml.match(
|
||||
/["']?file["']?\s*:\s*["']([^"']+\.m3u8[^"']*)["']/,
|
||||
);
|
||||
if (fileMatch) {
|
||||
m3u8Match = [fileMatch[1]];
|
||||
}
|
||||
}
|
||||
if (!m3u8Match) {
|
||||
const sourcesMatch = embedHtml.match(
|
||||
/sources\s*[:=]\s*\[\s*\{[^}]*["']?file["']?\s*:\s*["']([^"']+)["']/,
|
||||
);
|
||||
if (sourcesMatch) {
|
||||
m3u8Match = [sourcesMatch[1]];
|
||||
}
|
||||
}
|
||||
|
||||
if (m3u8Match) {
|
||||
let m3u8Url = m3u8Match[0].replace(/["'\\]/g, "");
|
||||
if (m3u8Url.startsWith("//")) {
|
||||
m3u8Url = "https:" + m3u8Url;
|
||||
} else if (
|
||||
!m3u8Url.startsWith("http://") &&
|
||||
!m3u8Url.startsWith("https://")
|
||||
) {
|
||||
try {
|
||||
m3u8Url = new URL(m3u8Url, embedUrl).href;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
try {
|
||||
const cdnDomain = new URL(m3u8Url).hostname;
|
||||
const embedDomain = new URL(embedUrl).origin + "/";
|
||||
if (global.setDynamicReferer) {
|
||||
global.setDynamicReferer(cdnDomain, embedDomain);
|
||||
global.setFallbackReferer(embedDomain);
|
||||
}
|
||||
for (const sub of subtitles) {
|
||||
try {
|
||||
const subDomain = new URL(sub.url).hostname;
|
||||
if (subDomain !== cdnDomain && global.setDynamicReferer) {
|
||||
global.setDynamicReferer(subDomain, embedDomain);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return {
|
||||
url: m3u8Url,
|
||||
isM3U8: true,
|
||||
quality: qualityLabel,
|
||||
isDub: server.type === "dub",
|
||||
isHsub: server.type === "hsub",
|
||||
type: server.type,
|
||||
headers: { Referer: new URL(embedUrl).origin + "/" },
|
||||
subtitles: subtitles.length > 0 ? subtitles : undefined,
|
||||
};
|
||||
}
|
||||
const mp4Match = embedHtml.match(
|
||||
/["']?file["']?\s*:\s*["']([^"']+\.mp4[^"']*)["']/,
|
||||
);
|
||||
if (mp4Match) {
|
||||
let mp4Url = mp4Match[1];
|
||||
if (mp4Url.startsWith("//")) {
|
||||
mp4Url = "https:" + mp4Url;
|
||||
} else if (
|
||||
!mp4Url.startsWith("http://") &&
|
||||
!mp4Url.startsWith("https://")
|
||||
) {
|
||||
try {
|
||||
mp4Url = new URL(mp4Url, embedUrl).href;
|
||||
} catch (e) {}
|
||||
}
|
||||
try {
|
||||
const cdnDomain = new URL(mp4Url).hostname;
|
||||
const embedDomain = new URL(embedUrl).origin + "/";
|
||||
if (global.setDynamicReferer) {
|
||||
global.setDynamicReferer(cdnDomain, embedDomain);
|
||||
global.setFallbackReferer(embedDomain);
|
||||
}
|
||||
for (const sub of subtitles) {
|
||||
try {
|
||||
const subDomain = new URL(sub.url).hostname;
|
||||
if (subDomain !== cdnDomain && global.setDynamicReferer) {
|
||||
global.setDynamicReferer(subDomain, embedDomain);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return {
|
||||
url: mp4Url,
|
||||
isM3U8: false,
|
||||
quality: qualityLabel,
|
||||
isDub: server.type === "dub",
|
||||
isHsub: server.type === "hsub",
|
||||
type: server.type,
|
||||
headers: { Referer: new URL(embedUrl).origin + "/" },
|
||||
subtitles: subtitles.length > 0 ? subtitles : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error(`Failed to process embed ${server.url}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: "anineko",
|
||||
version: "3.0.2",
|
||||
SearchAnime,
|
||||
AnimeInfo,
|
||||
fetchEpisodeSources,
|
||||
processServer: processEmbedServer,
|
||||
fetchRecentEpisodes,
|
||||
fetchEpisode,
|
||||
};
|
||||
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* StrawVerse Extension - AnimePahe Scraper
|
||||
* Copyright (C) 2026 TheYogMehta
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* DISCLAIMER: This extension is intended for research, educational,
|
||||
* and developer testing purposes only. It functions as a client-side parser
|
||||
* of publicly available web pages. The developers do not host or distribute
|
||||
* any copyrighted media. Users are responsible for compliance with the terms of
|
||||
* service of the target website.
|
||||
*/
|
||||
|
||||
// imports
|
||||
const cheerio = require("cheerio");
|
||||
|
||||
// variables
|
||||
const baseUrl = "https://animepahe.pw";
|
||||
|
||||
// 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Animeinfo
|
||||
async function AnimeInfo(id) {
|
||||
const animeInfo = {
|
||||
id: id,
|
||||
title: "",
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await global.axios.get(`${baseUrl}/anime/${id}`, {
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
});
|
||||
const $ = (0, cheerio.load)(data);
|
||||
|
||||
let MalId =
|
||||
parseInt($('meta[name="myanimelist"]').attr("content") ?? null) ?? null;
|
||||
|
||||
animeInfo.malid = MalId;
|
||||
animeInfo.title = $("div.title-wrapper > h1 > span").first().text();
|
||||
let image = $("div.anime-poster a").attr("href") ?? null;
|
||||
animeInfo.image = image;
|
||||
animeInfo.description = $("div.anime-summary").text();
|
||||
animeInfo.genres = $("div.anime-genre ul li")
|
||||
.map((i, el) => $(el).find("a").attr("title"))
|
||||
.get();
|
||||
switch (
|
||||
$('div.col-sm-4.anime-info p:icontains("Status:") a').text().trim()
|
||||
) {
|
||||
case "Currently Airing":
|
||||
animeInfo.status = "Ongoing";
|
||||
break;
|
||||
case "Finished Airing":
|
||||
animeInfo.status = "Completed";
|
||||
break;
|
||||
default:
|
||||
animeInfo.status = "Unknown";
|
||||
}
|
||||
|
||||
animeInfo.type = $('div.col-sm-4.anime-info p:icontains("Type") a')
|
||||
.text()
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
|
||||
animeInfo.aired = $('div.col-sm-4.anime-info p:icontains("Aired")')
|
||||
.text()
|
||||
.replace("Aired:", "")
|
||||
.replaceAll("\n", " ")
|
||||
.replaceAll(" ", "")
|
||||
.trim();
|
||||
|
||||
animeInfo.dataId = id;
|
||||
|
||||
return animeInfo;
|
||||
} catch (error) {
|
||||
console.error("Error fetching data from AnimePahe:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const firstEpCache = {};
|
||||
|
||||
async function getFirstEpisodeNumber(id, lastPage) {
|
||||
if (firstEpCache[id] !== undefined) {
|
||||
return firstEpCache[id];
|
||||
}
|
||||
try {
|
||||
const { data } = await global.axios.get(
|
||||
`${baseUrl}/api?m=release&id=${id}&sort=episode_desc&page=${lastPage}`,
|
||||
{
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (data?.data && data.data.length > 0) {
|
||||
const firstEp = data.data[data.data.length - 1].episode;
|
||||
firstEpCache[id] = firstEp;
|
||||
return firstEp;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch first episode number:", err);
|
||||
}
|
||||
firstEpCache[id] = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fetching Episodes Pages
|
||||
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,
|
||||
},
|
||||
},
|
||||
)
|
||||
).data;
|
||||
|
||||
const firstEpNum = await getFirstEpisodeNumber(id, last_page);
|
||||
const offset = firstEpNum - 1;
|
||||
|
||||
data.forEach((item) => {
|
||||
let hasEngAudio = item?.audio && item?.audio?.toLowerCase() === "eng";
|
||||
let mappedNumber = item.episode - offset;
|
||||
if (mappedNumber < 1) mappedNumber = item.episode;
|
||||
|
||||
const langs = ["sub"];
|
||||
if (hasEngAudio) langs.push("dub");
|
||||
|
||||
episodes.push({
|
||||
id: `${id}/${item.session}`,
|
||||
number: mappedNumber,
|
||||
title: item.title,
|
||||
duration: item.duration,
|
||||
langs,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
episodes: episodes,
|
||||
totalPages: last_page,
|
||||
total: total,
|
||||
currentPage: page,
|
||||
};
|
||||
} catch (err) {
|
||||
return { episodes: [], totalPages: 0, total: 0, currentPage: page };
|
||||
}
|
||||
}
|
||||
|
||||
// fetching Episodes Download Links
|
||||
async function fetchEpisodeSources(episodeId, category = null) {
|
||||
try {
|
||||
const { data } = await global.axios.get(`${baseUrl}/play/${episodeId}`, {
|
||||
headers: {
|
||||
Referer: baseUrl,
|
||||
},
|
||||
});
|
||||
const $ = (0, cheerio.load)(data);
|
||||
|
||||
let linksArray = $("div#resolutionMenu > button")
|
||||
.map((i, el) => ({
|
||||
url: $(el).attr("data-src"),
|
||||
quality: extractQualityNumber($(el).text()),
|
||||
audio: $(el).attr("data-audio"),
|
||||
}))
|
||||
.get();
|
||||
|
||||
if (category) {
|
||||
const catLower = category.toLowerCase();
|
||||
if (catLower === "dub") {
|
||||
const filtered = linksArray.filter((l) => l.audio === "eng");
|
||||
if (filtered.length > 0) linksArray = filtered;
|
||||
} else if (catLower === "sub") {
|
||||
const filtered = linksArray.filter((l) => l.audio !== "eng");
|
||||
if (filtered.length > 0) linksArray = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
if (linksArray.length === 0) {
|
||||
return { sources: [], subtitles: [] };
|
||||
}
|
||||
|
||||
const sources = linksArray.map((l) => ({
|
||||
quality: l.quality || "auto",
|
||||
name: `Server ${l.quality}`,
|
||||
url: l.url,
|
||||
lang: l.audio === "eng" ? "dub" : "sub",
|
||||
type: l.audio === "eng" ? "dub" : "sub",
|
||||
isUnresolved: true,
|
||||
rawServer: l,
|
||||
}));
|
||||
|
||||
return {
|
||||
sources,
|
||||
subtitles: [],
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Error fetching data from AnimePahe:", err);
|
||||
return { sources: [], subtitles: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async function processServer(server) {
|
||||
if (!server?.url) return null;
|
||||
try {
|
||||
const embedUrlObj = new URL(server.url);
|
||||
const playerReferer = embedUrlObj.origin + "/";
|
||||
const res = await extract(embedUrlObj);
|
||||
if (res && res[0]) {
|
||||
const streamUrl = res[0].url;
|
||||
try {
|
||||
const streamDomain = new URL(streamUrl).hostname;
|
||||
if (global.setDynamicReferer) {
|
||||
global.setDynamicReferer(streamDomain, playerReferer);
|
||||
global.setFallbackReferer(playerReferer);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
quality: server.quality || "auto",
|
||||
isM3U8: res[0].isM3U8 || streamUrl.includes(".m3u8"),
|
||||
headers: { Referer: playerReferer },
|
||||
lang: server.audio === "eng" ? "dub" : "sub",
|
||||
type: server.audio === "eng" ? "dub" : "sub",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to extract server:", err.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: "pahe",
|
||||
version: "3.1.4",
|
||||
SearchAnime,
|
||||
AnimeInfo,
|
||||
fetchEpisodeSources,
|
||||
processServer,
|
||||
fetchRecentEpisodes,
|
||||
fetchEpisode,
|
||||
};
|
||||
|
||||
// helpers for extracting video links
|
||||
function extractQualityNumber(qualityString) {
|
||||
const match = qualityString.match(/\d+p/);
|
||||
return match ? match[0] : "";
|
||||
}
|
||||
|
||||
// helpers for extracting video links
|
||||
async function extract(videoUrl, retries = 2, delay = 1000) {
|
||||
let sources = [];
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const { data } = await global.axios.get(videoUrl.href, {
|
||||
headers: {
|
||||
Referer: "https://animepahe.pw/",
|
||||
},
|
||||
});
|
||||
const match = /(eval)(\(f.*?)(<\/script>)/s.exec(data);
|
||||
if (!match) {
|
||||
throw new Error("Failed to find video source packer block");
|
||||
}
|
||||
const source = eval(match[2].replace("eval", "")).match(/https.*?m3u8/);
|
||||
sources.push({
|
||||
url: source[0],
|
||||
isM3U8: source[0].includes(".m3u8"),
|
||||
});
|
||||
return sources;
|
||||
} catch (err) {
|
||||
if (
|
||||
(err.response?.status === 429 || err.message.includes("429")) &&
|
||||
attempt < retries
|
||||
) {
|
||||
console.warn(
|
||||
`Request to ${videoUrl.href} returned 429. Retrying in ${delay}ms (attempt ${attempt}/${retries})...`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
delay *= 2;
|
||||
continue;
|
||||
}
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: "pahe",
|
||||
version: "4.0.0",
|
||||
SearchAnime,
|
||||
AnimeInfo,
|
||||
fetchEpisodeSources,
|
||||
processServer,
|
||||
fetchRecentEpisodes,
|
||||
fetchEpisode,
|
||||
};
|
||||
Reference in New Issue
Block a user