class Comix { constructor() { this.baseUrl = "https://comix.to"; this.apiUrl = "https://comix.to/api/v2"; this.type = "book-board"; this.version = "1.1"; this.mediaType = "manga"; } get headers() { return { "Referer": `${this.baseUrl}/`, "User-Agent": "Mozilla/5.0" }; } getFilters() { const currentYear = new Date().getFullYear(); const years = []; for (let i = currentYear; i >= 1990; i--) { years.push({ value: i.toString(), label: i.toString() }); } years.push({ value: "older", label: "Older" }); return { sort: { label: "Sort By", type: "select", options: [ { value: "relevance", label: "Best Match" }, { value: "views_30d", label: "Popular (30 days)" }, { value: "views_total", label: "Total Views" }, { value: "chapter_updated_at", label: "Latest Updates" }, { value: "created_at", label: "Created Date" }, { value: "title", label: "Title" }, { value: "year", label: "Year" }, { value: "follows_total", label: "Most Follows" } ], default: "views_30d" }, status: { label: "Status", type: "multiselect", options: [ { value: "finished", label: "Finished" }, { value: "releasing", label: "Releasing" }, { value: "on_hiatus", label: "On Hiatus" }, { value: "discontinued", label: "Discontinued" }, { value: "not_yet_released", label: "Not Yet Released" } ] }, type: { label: "Type", type: "multiselect", options: [ { value: "manga", label: "Manga" }, { value: "manhwa", label: "Manhwa" }, { value: "manhua", label: "Manhua" }, { value: "other", label: "Other" } ] }, demographic: { label: "Demographic", type: "multiselect", options: [ { value: "1", label: "Shoujo" }, { value: "2", label: "Shounen" }, { value: "3", label: "Josei" }, { value: "4", label: "Seinen" } ] }, genre: { label: "Genres", type: "multiselect", options: [ { value: "6", label: "Action" }, { value: "87264", label: "Adult" }, { value: "7", label: "Adventure" }, { value: "8", label: "Boys Love" }, { value: "9", label: "Comedy" }, { value: "10", label: "Crime" }, { value: "11", label: "Drama" }, { value: "87265", label: "Ecchi" }, { value: "12", label: "Fantasy" }, { value: "13", label: "Girls Love" }, { value: "87266", label: "Hentai" }, { value: "14", label: "Historical" }, { value: "15", label: "Horror" }, { value: "16", label: "Isekai" }, { value: "17", label: "Magical Girls" }, { value: "87267", label: "Mature" }, { value: "18", label: "Mecha" }, { value: "19", label: "Medical" }, { value: "20", label: "Mystery" }, { value: "21", label: "Philosophical" }, { value: "22", label: "Psychological" }, { value: "23", label: "Romance" }, { value: "24", label: "Sci-Fi" }, { value: "25", label: "Slice of Life" }, { value: "87268", label: "Smut" }, { value: "26", label: "Sports" }, { value: "27", label: "Superhero" }, { value: "28", label: "Thriller" }, { value: "29", label: "Tragedy" }, { value: "30", label: "Wuxia" }, { value: "31", label: "Aliens" }, { value: "32", label: "Animals" }, { value: "33", label: "Cooking" }, { value: "34", label: "Cross Dressing" }, { value: "35", label: "Delinquents" }, { value: "36", label: "Demons" }, { value: "37", label: "Genderswap" }, { value: "38", label: "Ghosts" }, { value: "39", label: "Gyaru" }, { value: "40", label: "Harem" }, { value: "41", label: "Incest" }, { value: "42", label: "Loli" }, { value: "43", label: "Mafia" }, { value: "44", label: "Magic" }, { value: "45", label: "Martial Arts" }, { value: "46", label: "Military" }, { value: "47", label: "Monster Girls" }, { value: "48", label: "Monsters" }, { value: "49", label: "Music" }, { value: "50", label: "Ninja" }, { value: "51", label: "Office Workers" }, { value: "52", label: "Police" }, { value: "53", label: "Post-Apocalyptic" }, { value: "54", label: "Reincarnation" }, { value: "55", label: "Reverse Harem" }, { value: "56", label: "Samurai" }, { value: "57", label: "School Life" }, { value: "58", label: "Shota" }, { value: "59", label: "Supernatural" }, { value: "60", label: "Survival" }, { value: "61", label: "Time Travel" }, { value: "62", label: "Traditional Games" }, { value: "63", label: "Vampires" }, { value: "64", label: "Video Games" }, { value: "65", label: "Villainess" }, { value: "66", label: "Virtual Reality" }, { value: "67", label: "Zombies" } ] }, year_from: { label: "Year From", type: "select", options: [{ value: "", label: "Any" }, ...years] }, year_to: { label: "Year To", type: "select", options: [{ value: "", label: "Any" }, ...years] }, min_chap: { label: "Min. Chapters", type: "number", placeholder: "e.g. 10" } }; } async search(queryObj) { const { query, filters, page } = queryObj; const q = (query || "").trim().replace(/\s+/g, "+"); const pageNum = page || 1; const url = new URL(`${this.apiUrl}/manga`); let sortMode = "views_30d"; if (filters) { if (filters.sort) { sortMode = filters.sort; } if (filters.genre) { const genres = String(filters.genre).split(','); genres.forEach(g => { if (g.trim()) url.searchParams.append("genres[]", g.trim()); }); } if (filters.status) { const statuses = String(filters.status).split(','); statuses.forEach(s => { if (s.trim()) url.searchParams.append("statuses[]", s.trim()); }); } if (filters.type) { const types = String(filters.type).split(','); types.forEach(t => { if (t.trim()) url.searchParams.append("types[]", t.trim()); }); } if (filters.demographic) { const demos = String(filters.demographic).split(','); demos.forEach(d => { if (d.trim()) url.searchParams.append("demographics[]", d.trim()); }); } if (filters.year_from) url.searchParams.set("release_year[from]", filters.year_from); if (filters.year_to) url.searchParams.set("release_year[to]", filters.year_to); if (filters.min_chap) url.searchParams.set("min_chap", filters.min_chap); } if (q) { url.searchParams.set("keyword", q); sortMode = "relevance"; } const orderDirection = (sortMode === "title") ? "asc" : "desc"; url.searchParams.set(`order[${sortMode}]`, orderDirection); url.searchParams.set("limit", "50"); url.searchParams.set("page", pageNum.toString()); const res = await fetch(url, { headers: this.headers }); if (!res.ok) throw new Error(`Search failed: ${res.status}`); const json = await res.json(); return json.result.items.map(m => ({ id: m.hash_id, title: m.title, image: m.poster?.large || null, rating: m.score ?? null, type: "book", year: m.year || null, status: m.status || null })); } async getMetadata(id) { const url = `${this.apiUrl}/manga/${id}?includes[]=genre&includes[]=author&includes[]=artist&includes[]=demographic`; const res = await fetch(url, { headers: this.headers }); if (!res.ok) throw new Error(`Metadata failed: ${res.status}`); const { result } = await res.json(); return { id: result.hash_id, title: result.title, format: "MANGA", score: result.score ?? 0, genres: result.genres?.map(g => g.name).join(", ") ?? "", status: result.status ?? "unknown", published: result.created_at ?? "", summary: result.description ?? "", chapters: result.chapters_count ?? 0, image: result.poster?.large || null }; } async getSlug(mangaId) { const res = await fetch(`${this.apiUrl}/manga/${mangaId}`, { headers: this.headers }); if (!res.ok) return ""; const { result } = await res.json(); return result?.slug || ""; } async findChapters(mangaId) { const slug = await this.getSlug(mangaId); if (!slug) return []; const baseUrl = `${this.apiUrl}/manga/${mangaId}/chapters?order[number]=desc&limit=100`; const res = await fetch(baseUrl, { headers: this.headers }); if (!res.ok) return []; const first = await res.json(); const totalPages = first.result.pagination?.last_page || 1; let all = [...first.result.items]; for (let p = 2; p <= totalPages; p++) { const r = await fetch(`${baseUrl}&page=${p}`, { headers: this.headers }); if (!r.ok) continue; const d = await r.json(); if (d?.result?.items) all.push(...d.result.items); } const map = new Map(); for (const ch of all) { if (ch.language !== "en") continue; const key = ch.number; if (!map.has(key) || ch.is_official === 1) { map.set(key, ch); } } const chapters = Array.from(map.values()) .sort((a, b) => a.number - b.number) .map((ch, i) => ({ id: `${mangaId}|${slug}|${ch.chapter_id}|${ch.number}`, title: ch.name ? `Chapter ${ch.number} — ${ch.name}` : `Chapter ${ch.number}`, number: Number(ch.number), releaseDate: ch.updated_at ?? null, index: i })); return chapters; } async findChapterPages(chapterId) { const parts = chapterId.split("|"); if (parts.length < 4) return []; const [hashId, slug, chapterRealId, number] = parts; const readerUrl = `${this.baseUrl}/title/${hashId}-${slug}/${chapterRealId}-chapter-${number}`; const apiUrl = `${this.apiUrl}/chapters/${chapterRealId}`; const res = await fetch(apiUrl, { headers: this.headers }); if (!res.ok) return []; const json = await res.json(); if (!json.result || !json.result.images) return []; return json.result.images.map((img, i) => ({ url: img.url, index: i, headers: { Referer: readerUrl, "User-Agent": "Mozilla/5.0" } })); } } module.exports = Comix;