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;