347 lines
13 KiB
JavaScript
347 lines
13 KiB
JavaScript
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; |