updates and new extensions
This commit is contained in:
@@ -2,65 +2,185 @@ class WeebCentral {
|
||||
constructor() {
|
||||
this.baseUrl = "https://weebcentral.com";
|
||||
this.type = "book-board";
|
||||
this.version = "1.0";
|
||||
this.version = "1.1";
|
||||
this.mediaType = "manga";
|
||||
}
|
||||
|
||||
async fetch(url, options = {}) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
...(options.headers || {}),
|
||||
getFilters() {
|
||||
return {
|
||||
sort: {
|
||||
label: "Sort By",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "Best Match", label: "Best Match" },
|
||||
{ value: "Alphabet", label: "Alphabet" },
|
||||
{ value: "Popularity", label: "Popularity" },
|
||||
{ value: "Subscribers", label: "Subscribers" },
|
||||
{ value: "Recently Added", label: "Recently Added" },
|
||||
{ value: "Latest Updates", label: "Latest Updates" }
|
||||
],
|
||||
default: "Popularity"
|
||||
},
|
||||
});
|
||||
order: {
|
||||
label: "Sort Order",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "Descending", label: "Descending" },
|
||||
{ value: "Ascending", label: "Ascending" }
|
||||
],
|
||||
default: "Descending"
|
||||
},
|
||||
official: {
|
||||
label: "Official Translation",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "Any", label: "Any" },
|
||||
{ value: "True", label: "True" },
|
||||
{ value: "False", label: "False" }
|
||||
],
|
||||
default: "Any"
|
||||
},
|
||||
status: {
|
||||
label: "Status",
|
||||
type: "multiselect",
|
||||
options: [
|
||||
{ value: "Ongoing", label: "Ongoing" },
|
||||
{ value: "Complete", label: "Complete" },
|
||||
{ value: "Hiatus", label: "Hiatus" },
|
||||
{ value: "Canceled", label: "Canceled" }
|
||||
]
|
||||
},
|
||||
type: {
|
||||
label: "Type",
|
||||
type: "multiselect",
|
||||
options: [
|
||||
{ value: "Manga", label: "Manga" },
|
||||
{ value: "Manhwa", label: "Manhwa" },
|
||||
{ value: "Manhua", label: "Manhua" },
|
||||
{ value: "OEL", label: "OEL" }
|
||||
]
|
||||
},
|
||||
tags: {
|
||||
label: "Tags",
|
||||
type: "multiselect",
|
||||
options: [
|
||||
{ value: "Action", label: "Action" },
|
||||
{ value: "Adult", label: "Adult" },
|
||||
{ value: "Adventure", label: "Adventure" },
|
||||
{ value: "Comedy", label: "Comedy" },
|
||||
{ value: "Doujinshi", label: "Doujinshi" },
|
||||
{ value: "Drama", label: "Drama" },
|
||||
{ value: "Ecchi", label: "Ecchi" },
|
||||
{ value: "Fantasy", label: "Fantasy" },
|
||||
{ value: "Gender Bender", label: "Gender Bender" },
|
||||
{ value: "Harem", label: "Harem" },
|
||||
{ value: "Hentai", label: "Hentai" },
|
||||
{ value: "Historical", label: "Historical" },
|
||||
{ value: "Horror", label: "Horror" },
|
||||
{ value: "Isekai", label: "Isekai" },
|
||||
{ value: "Josei", label: "Josei" },
|
||||
{ value: "Lolicon", label: "Lolicon" },
|
||||
{ value: "Martial Arts", label: "Martial Arts" },
|
||||
{ value: "Mature", label: "Mature" },
|
||||
{ value: "Mecha", label: "Mecha" },
|
||||
{ value: "Mystery", label: "Mystery" },
|
||||
{ value: "Psychological", label: "Psychological" },
|
||||
{ value: "Romance", label: "Romance" },
|
||||
{ value: "School Life", label: "School Life" },
|
||||
{ value: "Sci-fi", label: "Sci-fi" },
|
||||
{ value: "Seinen", label: "Seinen" },
|
||||
{ value: "Shotacon", label: "Shotacon" },
|
||||
{ value: "Shoujo", label: "Shoujo" },
|
||||
{ value: "Shoujo Ai", label: "Shoujo Ai" },
|
||||
{ value: "Shounen", label: "Shounen" },
|
||||
{ value: "Shounen Ai", label: "Shounen Ai" },
|
||||
{ value: "Slice of Life", label: "Slice of Life" },
|
||||
{ value: "Smut", label: "Smut" },
|
||||
{ value: "Sports", label: "Sports" },
|
||||
{ value: "Supernatural", label: "Supernatural" },
|
||||
{ value: "Tragedy", label: "Tragedy" },
|
||||
{ value: "Yaoi", label: "Yaoi" },
|
||||
{ value: "Yuri", label: "Yuri" },
|
||||
{ value: "Other", label: "Other" }
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const query = queryObj.query || "";
|
||||
const form = new URLSearchParams();
|
||||
form.set("text", query);
|
||||
get headers() {
|
||||
return {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Referer": this.baseUrl + "/"
|
||||
};
|
||||
}
|
||||
|
||||
const res = await this.fetch(
|
||||
`${this.baseUrl}/search/simple?location=main`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"HX-Request": "true",
|
||||
"HX-Trigger": "quick-search-input",
|
||||
"HX-Target": "quick-search-result",
|
||||
"HX-Current-URL": `${this.baseUrl}/`,
|
||||
},
|
||||
body: form.toString(),
|
||||
async search({ query, page = 1, filters }) {
|
||||
const limit = 32;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const url = new URL(`${this.baseUrl}/search/data`);
|
||||
|
||||
// Parámetros básicos
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
url.searchParams.set("offset", offset.toString());
|
||||
url.searchParams.set("display_mode", "Full Display");
|
||||
|
||||
// Texto de búsqueda
|
||||
if (query && query.trim().length > 0) {
|
||||
url.searchParams.set("text", query.trim());
|
||||
}
|
||||
|
||||
// Filtros
|
||||
if (filters) {
|
||||
if (filters.sort) url.searchParams.set("sort", filters.sort);
|
||||
if (filters.order) url.searchParams.set("order", filters.order);
|
||||
if (filters.official) url.searchParams.set("official", filters.official);
|
||||
|
||||
// Multiselects
|
||||
if (filters.status) {
|
||||
const vals = String(filters.status).split(',');
|
||||
vals.forEach(v => { if(v.trim()) url.searchParams.append("included_status", v.trim()); });
|
||||
}
|
||||
);
|
||||
if (filters.type) {
|
||||
const vals = String(filters.type).split(',');
|
||||
vals.forEach(v => { if(v.trim()) url.searchParams.append("included_type", v.trim()); });
|
||||
}
|
||||
if (filters.tags) {
|
||||
const vals = String(filters.tags).split(',');
|
||||
vals.forEach(v => { if(v.trim()) url.searchParams.append("included_tag", v.trim()); });
|
||||
}
|
||||
} else if (!query) {
|
||||
// Default para "Latest" si no hay query ni filtros
|
||||
url.searchParams.set("sort", "Popularity");
|
||||
}
|
||||
|
||||
const res = await fetch(url.toString(), { headers: this.headers });
|
||||
if (!res.ok) return [];
|
||||
|
||||
const html = await res.text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const results = [];
|
||||
|
||||
$("#quick-search-result > div > a").each((_, el) => {
|
||||
const link = $(el).attr("href");
|
||||
if (!link) return;
|
||||
$("article > section > a").each((_, el) => {
|
||||
const href = $(el).attr("href");
|
||||
if (!href) return;
|
||||
|
||||
const idMatch = link.match(/\/series\/([^/]+)/);
|
||||
const idMatch = href.match(/\/series\/([^/]+)/);
|
||||
if (!idMatch) return;
|
||||
|
||||
const title = $(el).find(".flex-1").text().trim();
|
||||
// Título es el último div sin clase
|
||||
const title = $(el).find("div:not([class]):last-child").text().trim();
|
||||
|
||||
let image =
|
||||
$(el).find("source").attr("srcset") ||
|
||||
$(el).find("img").attr("src") ||
|
||||
null;
|
||||
// Imagen
|
||||
let image = $(el).find("source").attr("srcset") || $(el).find("img").attr("src");
|
||||
if (image) image = image.replace("small", "normal");
|
||||
|
||||
results.push({
|
||||
id: idMatch[1],
|
||||
title,
|
||||
image,
|
||||
rating: null,
|
||||
type: "book",
|
||||
image: image || "",
|
||||
type: "book"
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,83 +188,76 @@ class WeebCentral {
|
||||
}
|
||||
|
||||
async getMetadata(id) {
|
||||
const res = await this.fetch(`${this.baseUrl}/series/${id}`, {
|
||||
headers: { Referer: `${this.baseUrl}/series/${id}` },
|
||||
});
|
||||
const res = await fetch(`${this.baseUrl}/series/${id}`, { headers: this.headers });
|
||||
if (!res.ok) throw new Error("Metadata failed");
|
||||
|
||||
const html = await res.text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const title =
|
||||
$("section.md\\:w-8\\/12 h1").first().text().trim() || "";
|
||||
// Sección 1: Info (Imagen, Autor, Tags, Status)
|
||||
const section1 = $("section[x-data] > section").eq(0);
|
||||
|
||||
let image = section1.find("source").attr("srcset") || section1.find("img").attr("src");
|
||||
if(image) image = image.replace("small", "normal");
|
||||
|
||||
const authors = [];
|
||||
section1.find("li:has(strong:contains('Author')) > span > a").each((_, el) => {
|
||||
authors.push($(el).text().trim());
|
||||
});
|
||||
|
||||
const genres = [];
|
||||
$("li strong")
|
||||
.filter((_, el) => $(el).text().includes("Tags"))
|
||||
.parent()
|
||||
.find("a")
|
||||
.each((_, a) => {
|
||||
genres.push($(a).text().trim());
|
||||
section1.find("li:has(strong:contains('Tag'), strong:contains('Type')) a").each((_, el) => {
|
||||
genres.push($(el).text().trim());
|
||||
});
|
||||
|
||||
const statusRaw = section1.find("li:has(strong:contains('Status')) > a").text().trim().toLowerCase();
|
||||
let status = "unknown";
|
||||
if (statusRaw.includes("ongoing")) status = "ongoing";
|
||||
else if (statusRaw.includes("complete")) status = "completed";
|
||||
else if (statusRaw.includes("hiatus")) status = "hiatus";
|
||||
else if (statusRaw.includes("canceled")) status = "cancelled";
|
||||
|
||||
// Sección 2: Título y Descripción
|
||||
const section2 = $("section[x-data] > section").eq(1);
|
||||
const title = section2.find("h1").first().text().trim();
|
||||
|
||||
let descText = section2.find("li:has(strong:contains('Description')) > p").text().trim();
|
||||
descText = descText.replace("NOTE:", "\n\nNOTE:");
|
||||
|
||||
// Añadir series relacionadas y nombres alternativos a la descripción (como en la app)
|
||||
const related = section2.find("li:has(strong:contains('Related Series')) li");
|
||||
if (related.length > 0) {
|
||||
descText += "\n\nRelated Series:";
|
||||
related.each((_, el) => {
|
||||
descText += `\n• ${$(el).text().trim()}`;
|
||||
});
|
||||
}
|
||||
|
||||
const status =
|
||||
$("li strong")
|
||||
.filter((_, el) => $(el).text().includes("Status"))
|
||||
.parent()
|
||||
.find("a")
|
||||
.first()
|
||||
.text()
|
||||
.trim() || "unknown";
|
||||
|
||||
const published =
|
||||
$("li strong")
|
||||
.filter((_, el) => $(el).text().includes("Released"))
|
||||
.parent()
|
||||
.find("span")
|
||||
.first()
|
||||
.text()
|
||||
.trim() || "";
|
||||
|
||||
const summary =
|
||||
$("li strong")
|
||||
.filter((_, el) => $(el).text().includes("Description"))
|
||||
.parent()
|
||||
.find("p")
|
||||
.text()
|
||||
.trim() || "";
|
||||
|
||||
const image =
|
||||
$("section.flex picture source").attr("srcset") ||
|
||||
$("section.flex picture img").attr("src") ||
|
||||
null;
|
||||
const alts = section2.find("li:has(strong:contains('Associated Name')) li");
|
||||
if (alts.length > 0) {
|
||||
descText += "\n\nAssociated Names:";
|
||||
alts.each((_, el) => {
|
||||
descText += `\n• ${$(el).text().trim()}`;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
format: "MANGA",
|
||||
score: 0,
|
||||
genres: genres.join(", "),
|
||||
genres,
|
||||
status,
|
||||
published,
|
||||
summary,
|
||||
chapters: "???",
|
||||
image,
|
||||
summary: descText,
|
||||
chapters: 0,
|
||||
author: authors.join(", "),
|
||||
image: image || ""
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(mangaId) {
|
||||
const res = await this.fetch(
|
||||
`${this.baseUrl}/series/${mangaId}/full-chapter-list`,
|
||||
{
|
||||
headers: {
|
||||
"HX-Request": "true",
|
||||
"HX-Target": "chapter-list",
|
||||
"HX-Current-URL": `${this.baseUrl}/series/${mangaId}`,
|
||||
Referer: `${this.baseUrl}/series/${mangaId}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await fetch(`${this.baseUrl}/series/${mangaId}/full-chapter-list`, { headers: this.headers });
|
||||
if (!res.ok) return [];
|
||||
|
||||
const html = await res.text();
|
||||
const $ = this.cheerio.load(html);
|
||||
@@ -152,25 +265,29 @@ class WeebCentral {
|
||||
const chapters = [];
|
||||
const numRegex = /(\d+(?:\.\d+)?)/;
|
||||
|
||||
$("div.flex.items-center").each((_, el) => {
|
||||
const a = $(el).find("a");
|
||||
if (!a.length) return;
|
||||
|
||||
const href = a.attr("href");
|
||||
$("div[x-data] > a").each((_, el) => {
|
||||
const href = $(el).attr("href");
|
||||
if (!href) return;
|
||||
|
||||
const idMatch = href.match(/\/chapters\/([^/]+)/);
|
||||
if (!idMatch) return;
|
||||
|
||||
const title = a.find("span.grow > span").first().text().trim();
|
||||
const titleElement = $(el).find("span.flex > span").first();
|
||||
const title = titleElement.text().trim();
|
||||
const numMatch = title.match(numRegex);
|
||||
|
||||
// Intentar detectar scanlator por el color del stroke del SVG (imitando la lógica de Kotlin)
|
||||
let scanlator = null;
|
||||
const svgStroke = $(el).find("svg").attr("stroke");
|
||||
if (svgStroke === "#d8b4fe") scanlator = "Official"; // Color morado en WeebCentral indica oficial
|
||||
|
||||
chapters.push({
|
||||
id: idMatch[1],
|
||||
title,
|
||||
number: numMatch ? Number(numMatch[1]) : 0,
|
||||
releaseDate: null,
|
||||
index: 0,
|
||||
scanlator,
|
||||
releaseDate: null, // La fecha requiere parseo complejo, lo omitimos por ahora
|
||||
index: 0
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,23 +297,19 @@ class WeebCentral {
|
||||
}
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
const res = await this.fetch(
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/chapters/${chapterId}/images?is_prev=False&reading_style=long_strip`,
|
||||
{
|
||||
headers: {
|
||||
"HX-Request": "true",
|
||||
"HX-Current-URL": `${this.baseUrl}/chapters/${chapterId}`,
|
||||
Referer: `${this.baseUrl}/chapters/${chapterId}`,
|
||||
},
|
||||
}
|
||||
{ headers: this.headers }
|
||||
);
|
||||
|
||||
if (!res.ok) return [];
|
||||
|
||||
const html = await res.text();
|
||||
const $ = this.cheerio.load(html);
|
||||
|
||||
const pages = [];
|
||||
|
||||
$("section.flex-1 img").each((i, el) => {
|
||||
$("section[x-data~='scroll'] > img").each((i, el) => {
|
||||
const src = $(el).attr("src");
|
||||
if (src) {
|
||||
pages.push({
|
||||
@@ -207,19 +320,6 @@ class WeebCentral {
|
||||
}
|
||||
});
|
||||
|
||||
if (pages.length === 0) {
|
||||
$("img").each((i, el) => {
|
||||
const src = $(el).attr("src");
|
||||
if (src) {
|
||||
pages.push({
|
||||
url: src,
|
||||
index: i,
|
||||
headers: { Referer: this.baseUrl },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user