203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
class Anizone {
|
|
constructor() {
|
|
this.type = "anime-board";
|
|
this.version = "1.1";
|
|
this.api = "https://anizone.to";
|
|
}
|
|
|
|
getSettings() {
|
|
return {
|
|
episodeServers: ["HLS"],
|
|
supportsDub: true,
|
|
};
|
|
}
|
|
|
|
async search(queryObj) {
|
|
const query = queryObj.query ?? "";
|
|
|
|
const res = await fetch(
|
|
`${this.api}/anime?search=${encodeURIComponent(query)}`,
|
|
{
|
|
headers: {
|
|
accept: "*/*",
|
|
referer: "https://anizone.to/",
|
|
"user-agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
},
|
|
}
|
|
);
|
|
|
|
const html = await res.text();
|
|
|
|
const itemRegex =
|
|
/<div[^>]*class="relative overflow-hidden h-26 rounded-lg[\s\S]*?<img[^>]*src="([^"]+)"[^>]*alt="([^"]+)"[\s\S]*?<a[^>]*href="([^"]+)"[^>]*title="([^"]+)"/g;
|
|
|
|
const results = [];
|
|
let match;
|
|
|
|
while ((match = itemRegex.exec(html)) !== null) {
|
|
const [, image, altTitle, href, title] = match;
|
|
const animeId = href.split("/").pop();
|
|
|
|
// detectar sub / dub desde el episodio 1
|
|
let subOrDub = "sub";
|
|
|
|
try {
|
|
const epHtml = await fetch(`${this.api}/anime/${animeId}/1`, {
|
|
headers: {
|
|
referer: "https://anizone.to/",
|
|
"user-agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
},
|
|
}).then(r => r.text());
|
|
|
|
const audioMatch = epHtml.match(
|
|
/<div class="text-xs flex flex-wrap gap-1">([\s\S]*?)<\/div>/
|
|
);
|
|
|
|
if (audioMatch) {
|
|
const block = audioMatch[1];
|
|
const hasJP = /Japanese/i.test(block);
|
|
const hasOther = /(English|Spanish|French|German|Italian)/i.test(block);
|
|
|
|
if (hasJP && hasOther) subOrDub = "both";
|
|
else if (hasOther) subOrDub = "dub";
|
|
}
|
|
} catch {}
|
|
|
|
results.push({
|
|
id: animeId,
|
|
title: title || altTitle,
|
|
image,
|
|
url: href,
|
|
subOrDub,
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async getMetadata(id) {
|
|
const url = `https://anizone.to/anime/${id}`;
|
|
const res = await fetch(url);
|
|
const html = await res.text();
|
|
const $ = this.cheerio.load(html);
|
|
|
|
const title = $('div.flex.flex-col.items-center.lg\\:items-start h1').first().text().trim();
|
|
|
|
const summary = $('div.text-slate-100.text-center.lg\\:text-start.text-sm.md\\:text-base.xl\\:text-lg div').text().trim();
|
|
|
|
// Episodios
|
|
let episodes = 0;
|
|
$('span.flex.items-center.gap-1').each((i, el) => {
|
|
const text = $(el).text().trim();
|
|
const match = text.match(/(\d+)\s+Episodes?/i);
|
|
if (match) {
|
|
episodes = parseInt(match[1], 10);
|
|
return false; // rompe el each cuando lo encuentra
|
|
}
|
|
});
|
|
|
|
let status = "unknown";
|
|
$('span.flex.items-center.gap-1.5').each((i, el) => {
|
|
const text = $(el).text().trim().toLowerCase();
|
|
if (text.includes("completed")) status = "completed";
|
|
else if (text.includes("ongoing")) status = "ongoing";
|
|
});
|
|
|
|
const yearText = $('span.flex.items-center.gap-1 span.inline-block').text().trim();
|
|
const year = yearText ? parseInt(yearText, 10) : null;
|
|
|
|
const genres = [];
|
|
$('div.flex.flex-wrap.gap-2.justify-center.lg\\:justify-start a').each((i, el) => {
|
|
const genre = $(el).attr('title')?.trim();
|
|
if (genre) genres.push(genre);
|
|
});
|
|
|
|
const image = $('div.mx-auto.lg\\:mx-0 img').attr('src') || null;
|
|
|
|
return {
|
|
id,
|
|
title: title || "Unknown",
|
|
summary: summary || "",
|
|
episodes,
|
|
status,
|
|
season: null,
|
|
year,
|
|
genres,
|
|
score: 0,
|
|
image,
|
|
};
|
|
}
|
|
|
|
|
|
async findEpisodes(id) {
|
|
const html = await fetch(`${this.api}/anime/${id}/1`).then(r => r.text());
|
|
|
|
const regex =
|
|
/<a[^>]*href="([^"]*\/anime\/[^"]+?)"[^>]*>\s*<div[^>]*>\s*<div[^>]*class='[^']*min-w-10[^']*'[^>]*>(\d+)<\/div>\s*<div[^>]*class="[^"]*line-clamp-1[^"]*"[^>]*>([^<]+)<\/div>/g;
|
|
|
|
const episodes = [];
|
|
let match;
|
|
|
|
while ((match = regex.exec(html)) !== null) {
|
|
const [, href, num, title] = match;
|
|
|
|
episodes.push({
|
|
id: href.split("/").pop(),
|
|
number: Number(num),
|
|
title: title.trim(),
|
|
url: href,
|
|
});
|
|
}
|
|
|
|
return episodes;
|
|
}
|
|
|
|
async findEpisodeServer(episode, server) {
|
|
const html = await fetch(episode.url).then(r => r.text());
|
|
|
|
const srcMatch = html.match(
|
|
/<media-player[^>]+src="([^"]+\.m3u8)"/i
|
|
);
|
|
if (!srcMatch) throw new Error("No m3u8 found");
|
|
|
|
const masterUrl = srcMatch[1];
|
|
|
|
const subtitles = [];
|
|
const trackRegex =
|
|
/<track[^>]+src=([^ >]+)[^>]*label="([^"]+)"[^>]*srclang="([^"]+)"[^>]*(default)?/gi;
|
|
|
|
let match;
|
|
while ((match = trackRegex.exec(html)) !== null) {
|
|
const [, src, label, lang, def] = match;
|
|
subtitles.push({
|
|
id: lang,
|
|
url: src,
|
|
language: label.trim(),
|
|
isDefault: Boolean(def),
|
|
});
|
|
}
|
|
|
|
return {
|
|
server,
|
|
headers: {
|
|
Origin: "https://anizone.to",
|
|
Referer: "https://anizone.to/",
|
|
"User-Agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
},
|
|
videoSources: [
|
|
{
|
|
url: masterUrl,
|
|
type: "m3u8",
|
|
quality: "auto",
|
|
subtitles,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = Anizone;
|