207 lines
6.9 KiB
JavaScript
207 lines
6.9 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.0"
|
|
}
|
|
|
|
getHeaders() {
|
|
return {
|
|
'User-Agent': 'MangaDex-Client-Adapter/1.0',
|
|
'Content-Type': 'application/json'
|
|
};
|
|
}
|
|
|
|
async search(queryObj) {
|
|
const query = queryObj.query?.trim() || "";
|
|
const limit = 25;
|
|
const offset = (1 - 1) * limit;
|
|
|
|
const url = `${this.apiUrl}/manga?title=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&availableTranslatedLanguage[]=en`;
|
|
|
|
try {
|
|
const response = await fetch(url, { 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 score100 = 0;
|
|
|
|
const statusMap = {
|
|
ongoing: "Ongoing",
|
|
completed: "Completed",
|
|
hiatus: "Hiatus",
|
|
cancelled: "Cancelled"
|
|
};
|
|
|
|
return {
|
|
id,
|
|
title,
|
|
format: "Manga",
|
|
score: score100,
|
|
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 { chapters: [], cover: null };
|
|
}
|
|
}
|
|
|
|
|
|
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; |