class WeebCentral { constructor() { this.baseUrl = "https://weebcentral.com"; this.type = "book-board"; this.version = "1.1"; this.mediaType = "manga"; } 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" } ] } }; } 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 + "/" }; } 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 = []; $("article > section > a").each((_, el) => { const href = $(el).attr("href"); if (!href) return; const idMatch = href.match(/\/series\/([^/]+)/); if (!idMatch) return; // Título es el último div sin clase const title = $(el).find("div:not([class]):last-child").text().trim(); // 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: image || "", type: "book" }); }); return results; } async getMetadata(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); // 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 = []; 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 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, status, summary: descText, chapters: 0, author: authors.join(", "), image: image || "" }; } async findChapters(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); const chapters = []; const numRegex = /(\d+(?:\.\d+)?)/; $("div[x-data] > a").each((_, el) => { const href = $(el).attr("href"); if (!href) return; const idMatch = href.match(/\/chapters\/([^/]+)/); if (!idMatch) return; 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, scanlator, releaseDate: null, // La fecha requiere parseo complejo, lo omitimos por ahora index: 0 }); }); chapters.reverse(); chapters.forEach((c, i) => (c.index = i)); return chapters; } async findChapterPages(chapterId) { const res = await fetch( `${this.baseUrl}/chapters/${chapterId}/images?is_prev=False&reading_style=long_strip`, { headers: this.headers } ); if (!res.ok) return []; const html = await res.text(); const $ = this.cheerio.load(html); const pages = []; $("section[x-data~='scroll'] > img").each((i, el) => { const src = $(el).attr("src"); if (src) { pages.push({ url: src, index: i, headers: { Referer: this.baseUrl }, }); } }); return pages; } } module.exports = WeebCentral;