'use strict'; const API = 'https://api.mangadex.org'; const { parse } = require('../plugins/registry'); const { localizedTitle, chapterNumber, selectChapterReleases, availabilitySummary, pageUrls } = require('../plugins/mangadex'); const { sourceDate } = require('../../chapter-dates'); function mangaDexInfo(value) { let url; try { url = new URL(String(value)); } catch { return null; } if (url.protocol !== 'https:' || !['mangadex.org', 'www.mangadex.org'].includes(url.hostname)) return null; const match = /^\/title\/([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})(?:\/([^/?#]+))?\/?$/i.exec(url.pathname); if (!match) return null; const id = match[1].toLowerCase(); const title = decodeURIComponent(match[2] || id).replace(/[-_]+/g, ' ').trim(); return { source: 'mangadex', id, title, url: `https://mangadex.org/title/${id}${match[2] ? `/${match[2]}` : ''}` }; } async function jsonGet(http, url) { const response = await http.get(url, { accept: 'application/json' }); const body = await response.json(); if (body.result && body.result !== 'ok') throw new Error(`MangaDex API result: ${body.result}`); return body; } async function discoverMangaDexCatalog(seriesUrl, http) { const info = mangaDexInfo(seriesUrl); if (!info) throw new Error('Invalid MangaDex title URL'); const manga = (await jsonGet(http, `${API}/manga/${info.id}`)).data; if (!manga || manga.type !== 'manga') throw new Error('MangaDex title was not found'); const metadata = parse('mangadex', 'metadata', { data: manga, url: `${API}/manga/${info.id}` }); const releases = []; let offset = 0; for (;;) { const params = new URLSearchParams({ limit: '100', offset: String(offset), includeFutureUpdates: '0', includeExternalUrl: '0' }); params.append('translatedLanguage[]', 'en'); params.append('includes[]', 'scanlation_group'); params.append('order[chapter]', 'asc'); const page = await jsonGet(http, `${API}/manga/${info.id}/feed?${params}`); releases.push(...parse('mangadex', 'feed', { data: page.data || [], url: `${API}/manga/${info.id}/feed` }).releases); offset += page.data?.length || 0; if (!page.data?.length || offset >= Number(page.total || 0)) break; } const { title, specialLabel } = metadata; const parseCatalog = externalReleases => parse('mangadex', 'catalog', { data: { attributes: metadata.availabilityAttributes, releases, specialLabel, externalReleases }, url: `${API}/manga/${info.id}`, }); let parsed = parseCatalog(null); const chapters = parsed.chapters; // External-only is a separate feed mode, not a combined hosted+external catalog. let externalReleases = null; if (!chapters.length) { const params = new URLSearchParams({ limit: '1', includeFutureUpdates: '0', includeExternalUrl: '1' }); params.append('translatedLanguage[]', 'en'); externalReleases = Number((await jsonGet(http, `${API}/manga/${info.id}/feed?${params}`)).total || 0); parsed = parseCatalog(externalReleases); } return { id: info.id, title, originalLanguage: metadata.originalLanguage, links: metadata.links, availability: parsed.availability, chapters, }; } async function discoverMangaDexChapter(ref, http) { const atHome = await jsonGet(http, `${API}/at-home/server/${ref.id}?forcePort443=true`); const groupName = ref.group?.name ? ` ยท ${ref.group.name}` : ''; const parsed = parse('mangadex', 'atHome', { data: { response: atHome, chapterUrl: ref.url, expectedPages: ref.pages }, url: `${API}/at-home/server/${ref.id}` }); const source = sourceDate(ref.uploadedAt, `MangaDex${groupName}`, ref.uploadBatch); return { number: ref.number, title: ref.title || '', ...(source ? { dates: { source } } : {}), ...(ref.special ? { special: true, label: ref.label, sourceId: ref.id, sourceNumber: ref.sourceNumber ?? null } : {}), source: { name: `MangaDex${groupName}`, url: ref.url }, pages: parsed.pages, }; } module.exports = { API, mangaDexInfo, localizedTitle, chapterNumber, selectChapterReleases, availabilitySummary, discoverMangaDexCatalog, discoverMangaDexChapter, pageUrls, };