Files
WaifuBoard-Extensions/book/mangadex.js
2026-01-13 17:26:06 +01:00

415 lines
18 KiB
JavaScript

class MangaDex {
constructor() {
this.baseUrl = "https://mangadex.org";
this.apiUrl = "https://api.mangadex.org";
this.type = "book-board";
this.mediaType = "manga";
this.version = "1.1";
}
getHeaders() {
return {
'User-Agent': 'MangaDex-Client-Adapter/1.0',
'Content-Type': 'application/json'
};
}
getFilters() {
return {
sort: {
label: "Sort By",
type: "select",
options: [
{ value: "relevance", label: "Relevance" },
{ value: "latestUploadedChapter", label: "Latest Upload" },
{ value: "followedCount", label: "Most Follows" },
{ value: "createdAt", label: "Created At" },
{ value: "updatedAt", label: "Updated At" },
{ value: "title", label: "Title" },
{ value: "year", label: "Year" },
{ value: "rating", label: "Rating" }
],
default: "relevance"
},
status: {
label: "Status",
type: "multiselect",
options: [
{ value: "ongoing", label: "Ongoing" },
{ value: "completed", label: "Completed" },
{ value: "hiatus", label: "Hiatus" },
{ value: "cancelled", label: "Cancelled" }
]
},
demographic: {
label: "Demographic",
type: "multiselect",
options: [
{ value: "shounen", label: "Shounen" },
{ value: "shoujo", label: "Shoujo" },
{ value: "seinen", label: "Seinen" },
{ value: "josei", label: "Josei" },
{ value: "none", label: "None" }
]
},
content_rating: {
label: "Content Rating",
type: "multiselect",
options: [
{ value: "safe", label: "Safe" },
{ value: "suggestive", label: "Suggestive" },
{ value: "erotica", label: "Erotica" },
{ value: "pornographic", label: "Pornographic" }
],
default: "safe,suggestive"
},
original_language: {
label: "Original Language",
type: "multiselect",
options: [
{ value: "ja", label: "Japanese" },
{ value: "zh", label: "Chinese" },
{ value: "ko", label: "Korean" }
]
},
tags_mode: {
label: "Tags Mode",
type: "select",
options: [
{ value: "AND", label: "AND (Match All)" },
{ value: "OR", label: "OR (Match Any)" }
],
default: "AND"
},
tags: {
label: "Tags",
type: "multiselect",
options: [
// Genres
{ value: "391b0423-d847-456f-aff0-8b0cfc03066b", label: "Action" },
{ value: "87cc87cd-a395-47af-b27a-93258283bbc6", label: "Adventure" },
{ value: "5920b825-4181-4a17-beeb-9918b0ff7a30", label: "Boys Love" },
{ value: "4d32cc48-9f00-4cca-9b5a-a839f0764984", label: "Comedy" },
{ value: "5ca48985-9a9d-4bd8-be29-80dc0303db72", label: "Crime" },
{ value: "b9af3a63-f058-46de-a9a0-e0c13906197a", label: "Drama" },
{ value: "cdc58593-87dd-415e-bbc0-2ec27bf404cc", label: "Fantasy" },
{ value: "a3c67850-4684-404e-9b7f-c69850ee5da6", label: "Girls Love" },
{ value: "33771934-028e-4cb3-8744-691e866a923e", label: "Historical" },
{ value: "cdad7e68-1419-41dd-bdce-27753074a640", label: "Horror" },
{ value: "ace04997-f6bd-436e-b261-779182193d3d", label: "Isekai" },
{ value: "81c836c9-914a-4eca-981a-560dad663e73", label: "Magical Girls" },
{ value: "50880a9d-5440-4732-9afb-8f457127e836", label: "Mecha" },
{ value: "c8cbe35b-1b2b-4a3f-9c37-db84c4514856", label: "Medical" },
{ value: "ee968100-4191-4968-93d3-f82d72be7e46", label: "Mystery" },
{ value: "b1e97889-25b4-4258-b28b-cd7f4d28ea9b", label: "Philosophical" },
{ value: "423e2eae-a7a2-4a8b-ac03-a8351462d71d", label: "Romance" },
{ value: "256c8bd9-4904-4360-bf4f-508a76d67183", label: "Sci-Fi" },
{ value: "e5301a23-ebd9-49dd-a0cb-2add944c7fe9", label: "Slice of Life" },
{ value: "69964a64-2f90-4d33-beeb-f3ed2875eb4c", label: "Sports" },
{ value: "7064a261-a137-4d3a-8848-2d385de3a99c", label: "Superhero" },
{ value: "07251805-a27e-4d59-b488-f0bfbec15168", label: "Thriller" },
{ value: "f8f62932-27da-4fe4-8ee1-6779a8c5edba", label: "Tragedy" },
{ value: "acc803a4-c95a-4c22-86fc-eb6b582d82a2", label: "Wuxia" },
// Themes
{ value: "e64f6742-c834-471d-8d72-dd51fc02b835", label: "Aliens" },
{ value: "3de8c75d-8ee3-48ff-98ee-e20a65c86451", label: "Animals" },
{ value: "ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", label: "Cooking" },
{ value: "9ab53f92-3eed-4e9b-903a-917c86035ee3", label: "Crossdressing" },
{ value: "da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", label: "Delinquents" },
{ value: "39730448-9a5f-48a2-85b0-a70db87b1233", label: "Demons" },
{ value: "2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", label: "Genderswap" },
{ value: "3bb26d85-09d5-4d2e-880c-c34b974339e9", label: "Ghosts" },
{ value: "fad12b5e-68ba-460e-b933-9ae8318f5b65", label: "Gyaru" },
{ value: "aafb99c1-7f60-43fa-b75f-fc9502ce29c7", label: "Harem" },
{ value: "5bd0e105-4481-44ca-b6e7-7544da56b1a3", label: "Incest" },
{ value: "2d1f5d56-a1e5-4d0d-a961-2193588b08ec", label: "Loli" },
{ value: "85daba54-a71c-4554-8a28-9901a8b0afad", label: "Mafia" },
{ value: "a1f53773-c69a-4ce5-8cab-fffcd90b1565", label: "Magic" },
{ value: "799c202e-7daa-44eb-9cf7-8a3c0441531e", label: "Martial Arts" },
{ value: "ac72833b-c4e9-4878-b9db-6c8a4a99444a", label: "Military" },
{ value: "dd1f77c5-dea9-4e2b-97ae-224af09caf99", label: "Monster Girls" },
{ value: "36fd93ea-e8b8-445e-b836-358f02b3d33d", label: "Monsters" },
{ value: "f42fbf9e-188a-447b-9fdc-f19dc1e4d685", label: "Music" },
{ value: "489dd859-9b61-4c37-af75-5b18e88daafc", label: "Ninja" },
{ value: "92d6d951-ca5e-429c-ac78-451071cbf064", label: "Office Workers" },
{ value: "df33b754-73a3-4c54-80e6-1a74a8058539", label: "Police" },
{ value: "9467335a-1b83-4497-9231-765337a00b96", label: "Post-Apocalyptic" },
{ value: "3b60b75c-a2d7-4860-ab56-05f391bb889c", label: "Psychological" },
{ value: "0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", label: "Reincarnation" },
{ value: "65761a2a-415e-47f3-bef2-a9dababba7a6", label: "Reverse Harem" },
{ value: "81183756-1453-4c81-aa9e-f6e1b63be016", label: "Samurai" },
{ value: "caaa44eb-cd40-4177-b930-79d3ef2afe87", label: "School Life" },
{ value: "ddefd648-5140-4e5f-ba18-4eca4071d19b", label: "Shota" },
{ value: "eabc5b4c-6aff-42f3-b657-3e90cbd00b75", label: "Supernatural" },
{ value: "5fff9cde-849c-4d78-aab0-0d52b2ee1d25", label: "Survival" },
{ value: "292e862b-2d17-4062-90a2-0356caa4ae27", label: "Time Travel" },
{ value: "d7d1730f-6eb0-4ba6-9437-602cac38664c", label: "Vampires" },
{ value: "9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", label: "Video Games" },
{ value: "d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", label: "Villainess" },
{ value: "631ef465-9aba-4afb-b0fc-ea10efe274a8", label: "Zombies" },
// Content
{ value: "b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", label: "Gore" },
{ value: "97893a4c-12af-4dac-b6be-0dffb353568e", label: "Sexual Violence" }
]
}
};
}
async search({ query = "", page = 1, filters = {} }) {
const limit = 25;
const offset = (page - 1) * limit;
const url = new URL(`${this.apiUrl}/manga`);
// --- 1. Query ---
if (query.trim()) {
url.searchParams.append("title", query.trim());
}
// --- 2. Parámetros Fijos ---
url.searchParams.append("limit", limit.toString());
url.searchParams.append("offset", offset.toString());
url.searchParams.append("includes[]", "cover_art");
url.searchParams.append("availableTranslatedLanguage[]", "en");
// --- 3. Filtros Dinámicos ---
// A) Content Rating (Si no se especifica, usa safe+suggestive por defecto)
if (filters.content_rating) {
const ratings = String(filters.content_rating).split(",");
ratings.forEach(r => {
if (r.trim()) url.searchParams.append("contentRating[]", r.trim());
});
} else {
// Default behavior if not set
url.searchParams.append("contentRating[]", "safe");
url.searchParams.append("contentRating[]", "suggestive");
}
// B) Tags (includedTags)
if (filters.tags) {
const tags = String(filters.tags).split(",");
tags.forEach(t => {
if (t.trim()) url.searchParams.append("includedTags[]", t.trim());
});
}
// C) Tag Inclusion Mode
if (filters.tags_mode) {
url.searchParams.append("includedTagsMode", filters.tags_mode);
}
// D) Status
if (filters.status) {
const stats = String(filters.status).split(",");
stats.forEach(s => {
if (s.trim()) url.searchParams.append("status[]", s.trim());
});
}
// E) Demographic
if (filters.demographic) {
const demos = String(filters.demographic).split(",");
demos.forEach(d => {
if (d.trim()) url.searchParams.append("publicationDemographic[]", d.trim());
});
}
// F) Original Language
if (filters.original_language) {
const langs = String(filters.original_language).split(",");
langs.forEach(l => {
if (l.trim()) url.searchParams.append("originalLanguage[]", l.trim());
});
}
// G) Sort
// MangaDex usa order[KEY]=asc/desc
const sortVal = filters.sort || "relevance";
const orderDir = (sortVal === "title") ? "asc" : "desc";
// Si hay búsqueda por texto, relevance es útil, sino latestUploaded
url.searchParams.append(`order[${sortVal}]`, orderDir);
try {
const response = await fetch(url.toString(), { headers: this.getHeaders() });
if (!response.ok) {
console.error(`MangaDex API Error: ${response.statusText}`);
return [];
}
const json = await response.json();
if (!json || !Array.isArray(json.data)) {
return [];
}
return json.data.map(manga => {
const attributes = manga.attributes;
const titleObject = attributes.title || {};
const title = titleObject.en || Object.values(titleObject)[0] || 'Unknown Title';
const coverRelationship = manga.relationships?.find(rel => rel.type === 'cover_art');
const coverFileName = coverRelationship?.attributes?.fileName;
const coverUrl = coverFileName
? `https://uploads.mangadex.org/covers/${manga.id}/${coverFileName}.256.jpg`
: '';
return {
id: manga.id,
image: coverUrl,
title: title,
rating: null,
type: 'book'
};
});
} catch (e) {
console.error("Error during MangaDex search:", e);
return [];
}
}
async getMetadata(id) {
try {
const res = await fetch(`https://api.mangadex.org/manga/${id}?includes[]=cover_art`);
if (!res.ok) throw new Error("MangaDex API error");
const json = await res.json();
const manga = json.data;
const attr = manga.attributes;
const title =
attr.title?.en ||
Object.values(attr.title || {})[0] ||
"";
const summary =
attr.description?.en ||
Object.values(attr.description || {})[0] ||
"";
const genres = manga.relationships
?.filter(r => r.type === "tag")
?.map(r =>
r.attributes?.name?.en ||
Object.values(r.attributes?.name || {})[0]
)
?.filter(Boolean) || [];
const coverRel = manga.relationships.find(r => r.type === "cover_art");
const coverFile = coverRel?.attributes?.fileName;
const image = coverFile
? `https://uploads.mangadex.org/covers/${id}/${coverFile}.512.jpg`
: "";
const statusMap = {
ongoing: "Ongoing",
completed: "Completed",
hiatus: "Hiatus",
cancelled: "Cancelled"
};
return {
id,
title,
format: "Manga",
score: 0,
genres,
status: statusMap[attr.status] || "",
published: attr.year ? String(attr.year) : "???",
summary,
chapters: attr.lastChapter ? Number(attr.lastChapter) || 0 : 0,
image
};
} catch (e) {
console.error("MangaDex getMetadata error:", e);
return {
id,
title: "",
format: "Manga",
score: 0,
genres: [],
status: "",
published: "???",
summary: "",
chapters: 0,
image: ""
};
}
}
async findChapters(mangaId) {
if (!mangaId) return [];
const url = `${this.apiUrl}/manga/${mangaId}/feed?translatedLanguage[]=en&order[chapter]=asc&limit=500&includes[]=scanlation_group`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
let chapters = [];
if (response.ok) {
const json = await response.json();
if (json && Array.isArray(json.data)) {
const allChapters = json.data
.filter(ch => ch.attributes.chapter && !ch.attributes.externalUrl)
.map((ch, index) => ({
id: ch.id,
title: ch.attributes.title || `Chapter ${ch.attributes.chapter}`,
number: ch.attributes.chapter,
index: index,
language: ch.attributes.translatedLanguage
}));
const seenChapters = new Set();
allChapters.forEach(ch => {
if (!seenChapters.has(ch.chapter)) {
seenChapters.add(ch.chapter);
chapters.push(ch);
}
});
chapters.sort((a, b) => parseFloat(a.chapter) - parseFloat(b.chapter));
}
}
return chapters;
} catch (e) {
console.error("Error finding MangaDex chapters:", e);
return [];
}
}
async findChapterPages(chapterId) {
if (!chapterId) return [];
const url = `${this.apiUrl}/at-home/server/${chapterId}`;
try {
const response = await fetch(url, { headers: this.getHeaders() });
if (!response.ok) throw new Error(`Failed to fetch pages: ${response.statusText}`);
const json = await response.json();
if (!json || !json.baseUrl || !json.chapter) return [];
const baseUrl = json.baseUrl;
const chapterHash = json.chapter.hash;
const imageFilenames = json.chapter.data;
return imageFilenames.map((filename, index) => ({
url: `${baseUrl}/data/${chapterHash}/${filename}`,
index: index,
headers: {
'Referer': `https://mangadex.org/chapter/${chapterId}`
}
}));
} catch (e) {
console.error("Error finding MangaDex pages:", e);
return [];
}
}
}
module.exports = MangaDex;