class MangaPill { constructor() { this.baseUrl = "https://mangapill.com"; this.type = "book-board"; this.version = "1.1"; this.mediaType = "manga"; } getFilters() { return { type: { label: "Type", type: "select", options: [ { value: "", label: "All" }, { value: "manga", label: "Manga" }, { value: "novel", label: "Novel" }, { value: "one-shot", label: "One-Shot" }, { value: "doujinshi", label: "Doujinshi" }, { value: "manhwa", label: "Manhwa" }, { value: "manhua", label: "Manhua" }, { value: "oel", label: "OEL" } ] }, status: { label: "Status", type: "select", options: [ { value: "", label: "All" }, { value: "publishing", label: "Publishing" }, { value: "finished", label: "Finished" }, { value: "on hiatus", label: "On Hiatus" }, { value: "discontinued", label: "Discontinued" }, { value: "not yet published", label: "Not Yet Published" } ] }, genre: { label: "Genres", type: "multiselect", options: [ { value: "Action", label: "Action" }, { value: "Adventure", label: "Adventure" }, { value: "Cars", label: "Cars" }, { value: "Comedy", label: "Comedy" }, { value: "Dementia", label: "Dementia" }, { value: "Demons", label: "Demons" }, { value: "Drama", label: "Drama" }, { value: "Ecchi", label: "Ecchi" }, { value: "Fantasy", label: "Fantasy" }, { value: "Game", label: "Game" }, { value: "Harem", label: "Harem" }, { value: "Hentai", label: "Hentai" }, { value: "Historical", label: "Historical" }, { value: "Horror", label: "Horror" }, { value: "Josei", label: "Josei" }, { value: "Kids", label: "Kids" }, { value: "Magic", label: "Magic" }, { value: "Martial Arts", label: "Martial Arts" }, { value: "Mecha", label: "Mecha" }, { value: "Military", label: "Military" }, { value: "Music", label: "Music" }, { value: "Mystery", label: "Mystery" }, { value: "Parody", label: "Parody" }, { value: "Police", label: "Police" }, { value: "Psychological", label: "Psychological" }, { value: "Romance", label: "Romance" }, { value: "Samurai", label: "Samurai" }, { value: "School", label: "School" }, { value: "Sci-Fi", label: "Sci-Fi" }, { value: "Seinen", label: "Seinen" }, { 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: "Space", label: "Space" }, { value: "Sports", label: "Sports" }, { value: "Super Power", label: "Super Power" }, { value: "Supernatural", label: "Supernatural" }, { value: "Thriller", label: "Thriller" }, { value: "Vampire", label: "Vampire" }, { value: "Yaoi", label: "Yaoi" }, { value: "Yuri", label: "Yuri" } ] } }; } 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 url = new URL(`${this.baseUrl}/search`); if (query) url.searchParams.set("q", query.trim()); url.searchParams.set("page", page.toString()); if (filters) { if (filters.type) url.searchParams.set("type", filters.type); if (filters.status) url.searchParams.set("status", filters.status); if (filters.genre) { // MangaPill espera ?genre=Action&genre=Comedy const genres = String(filters.genre).split(','); genres.forEach(g => { if (g.trim()) url.searchParams.append("genre", g.trim()); }); } } const res = await fetch(url.toString(), { headers: this.headers }); if (!res.ok) throw new Error(`Search failed: ${res.status}`); const html = await res.text(); const $ = this.cheerio.load(html); const results = []; // Selector actualizado basado en la extensión de Kotlin (.grid > div:not([class])) // Buscamos dentro del grid de resultados $(".grid > div:not([class]), div.container div.my-3.justify-end > div").each((_, el) => { const a = $(el).find("a").first(); const href = a.attr("href"); if (!href) return; // Extraer ID (manga/123/title -> 123$title) const parts = href.split("/manga/"); if (parts.length < 2) return; const id = parts[1].replace(/\//g, "$"); // Título: A veces es un div hermano, a veces dentro del anchor let title = $(el).find("div > a > div").text().trim(); // Selector antiguo if (!title) title = $(el).find("a:not(:first-child) > div").text().trim(); // Selector nuevo if (!title) title = $(el).find(".font-bold, div[class*='font-bold']").text().trim(); // Fallback const img = $(el).find("img").attr("data-src") || $(el).find("img").attr("src"); results.push({ id, title, image: img || "", type: "book" }); }); // Eliminar duplicados si el selector doble atrapó cosas repetidas const uniqueResults = []; const seen = new Set(); for (const r of results) { if (!seen.has(r.id)) { seen.add(r.id); uniqueResults.push(r); } } return uniqueResults; } async getMetadata(id) { const uriId = id.replace(/\$/g, "/"); let res = await fetch(`${this.baseUrl}/manga/${uriId}`, { headers: this.headers, redirect: "manual", }); if (res.status === 301 || res.status === 302) { const loc = res.headers.get("location"); if (loc) { res = await fetch(`${this.baseUrl}${loc}`, { headers: this.headers }); } } if (!res.ok) throw new Error("Failed to fetch metadata"); const html = await res.text(); const $ = this.cheerio.load(html); const title = $("h1.font-bold").first().text().trim(); const summary = $("div.mb-3 p.text-sm").first().text().trim() || ""; // Status y Published suelen estar en labels const status = $("label:contains('Status')").next("div").text().trim().toLowerCase() || "unknown"; const published = $("label:contains('Year')").next("div").text().trim() || ""; const genres = []; $("a[href*='genre']").each((_, el) => { genres.push($(el).text().trim()); }); const image = $("img[data-src]").first().attr("data-src") || ""; return { id, title, format: "MANGA", score: 0, genres: genres, // Array de strings status, published, summary, chapters: 0, // Se calcula dinámicamente si es necesario image }; } async findChapters(mangaId) { const uriId = mangaId.replace(/\$/g, "/"); const res = await fetch(`${this.baseUrl}/manga/${uriId}`, { headers: this.headers }); const html = await res.text(); const $ = this.cheerio.load(html); const chapters = []; $("#chapters > div > a").each((_, el) => { const href = $(el).attr("href"); if (!href) return; const id = href.split("/chapters/")[1].replace(/\//g, "$"); const title = $(el).text().trim(); const match = title.match(/Chapter\s+([\d.]+)/); const number = match ? Number(match[1]) : 0; chapters.push({ id, title, number, releaseDate: null, index: 0, }); }); chapters.reverse(); chapters.forEach((c, i) => (c.index = i)); return chapters; } async findChapterPages(chapterId) { const uriId = chapterId.replace(/\$/g, "/"); const res = await fetch(`${this.baseUrl}/chapters/${uriId}`, { headers: this.headers }); const html = await res.text(); const $ = this.cheerio.load(html); const pages = []; $("picture img").each((i, el) => { const img = $(el).attr("data-src"); if (!img) return; pages.push({ url: img, index: i, headers: { "Referer": this.baseUrl + "/", "User-Agent": this.headers["User-Agent"] }, }); }); return pages; } } module.exports = MangaPill;