Add first-party MangaPill source and distinct volume releases
This commit is contained in:
1 parent
a8df57f165
commit
a08e847932
20 files changed
+364
-34
No files matched your search
@@ -22,6 +22,10 @@ function publicAddress(address) {
|
||||
}
|
||||
|
||||
function validateWeebRequest(value, options = {}) {
|
||||
return validateHostRequest(value, 'weebcentral.com', options);
|
||||
}
|
||||
|
||||
function validateHostRequest(value, sourceHost, options = {}) {
|
||||
const url = new URL(value);
|
||||
const host = url.hostname.toLowerCase();
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.port
|
||||
@@ -29,8 +33,8 @@ function validateWeebRequest(value, options = {}) {
|
||||
|| host.endsWith('.local') || !host.includes('.')) {
|
||||
throw new Error('Source request URL is not public HTTPS');
|
||||
}
|
||||
if (options.accept === 'text/html' && host !== 'weebcentral.com') {
|
||||
throw new Error('WeebCentral catalog request left its source host');
|
||||
if (options.accept === 'text/html' && host !== sourceHost) {
|
||||
throw new Error('Source catalog request left its source host');
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@@ -88,4 +92,9 @@ function createWeebHttp(options = {}) {
|
||||
return new SlowHttp({ ...options, transport: pinnedFetch, validateUrl: validateWeebRequest });
|
||||
}
|
||||
|
||||
module.exports = { publicAddress, validateWeebRequest, pinnedLookup, pinnedFetch, createWeebHttp };
|
||||
function createHostHttp(sourceHost, options = {}) {
|
||||
return new SlowHttp({ ...options, transport: pinnedFetch,
|
||||
validateUrl: (value, requestOptions) => validateHostRequest(value, sourceHost, requestOptions) });
|
||||
}
|
||||
|
||||
module.exports = { publicAddress, validateWeebRequest, validateHostRequest, pinnedLookup, pinnedFetch, createWeebHttp, createHostHttp };
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
// First-party parser; inputs are fetched by the paced source broker, not here.
|
||||
const decode = value => String(value).replace(/&/g, '&').replace(/'|'/gi, "'")
|
||||
.replace(/"/g, '"').replace(/<[^>]+>/g, '').trim();
|
||||
|
||||
function catalog({ html, url }) {
|
||||
const sourceId = new URL(url).pathname.split('/')[2];
|
||||
const title = decode(html.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1] || 'Untitled');
|
||||
const chapters = [];
|
||||
const re = /<a\b[^>]*href="(\/chapters\/(\d+-\d+)\/[^"?#]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
for (const match of html.matchAll(re)) {
|
||||
if (match[2].split('-')[0] !== sourceId) continue;
|
||||
const label = decode(match[3]);
|
||||
const number = label.match(/^Chapter\s+([0-9]+(?:\.[0-9]+)?)(?:\b|$)/i)?.[1];
|
||||
if (!number) continue;
|
||||
chapters.push({ id: match[2], number, title: '', url: new URL(match[1], url).href, raw: label });
|
||||
}
|
||||
return { title, chapters: [...new Map(chapters.map(ref => [ref.id, ref])).values()],
|
||||
chapterLinks: [...html.matchAll(/href="\/chapters\/\d+-\d+\//gi)].length };
|
||||
}
|
||||
|
||||
function pages({ html }) {
|
||||
const images = [];
|
||||
for (const match of html.matchAll(/<img\b[^>]*class="[^"]*\bjs-page\b[^"]*"[^>]*>/gi)) {
|
||||
const url = match[0].match(/\bdata-src="([^"]+)"/i)?.[1];
|
||||
if (url) images.push(url.replace(/&/g, '&'));
|
||||
}
|
||||
return { pages: [...new Set(images)] };
|
||||
}
|
||||
|
||||
module.exports = { catalog, pages };
|
||||
@@ -17,6 +17,12 @@ const manifests = Object.freeze({
|
||||
provenance: 'MangaCircus existing MangaDex adapter; no Uchiyomi code copied',
|
||||
operations: Object.freeze(['metadata', 'feed', 'catalog', 'atHome']),
|
||||
}),
|
||||
mangapill: Object.freeze({
|
||||
id: 'mangapill', version: 1, label: 'MangaPill', status: 'in-process',
|
||||
sourceHosts: Object.freeze(['mangapill.com']), implementation: 'first-party',
|
||||
provenance: 'MangaCircus implementation; upstream extension reviewed, no code copied',
|
||||
operations: Object.freeze(['catalog', 'pages']),
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = { manifests };
|
||||
@@ -6,6 +6,7 @@ const { validate } = require('./validation');
|
||||
const plugins = Object.freeze({
|
||||
weebcentral: require('./weebcentral'),
|
||||
mangadex: require('./mangadex'),
|
||||
mangapill: require('./mangapill'),
|
||||
});
|
||||
|
||||
function parse(plugin, operation, input) {
|
||||
|
||||
@@ -33,8 +33,20 @@ function mangaDexUrl(value, kind) {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
function mangaPillUrl(value, kind) {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.port) throw new Error('Invalid MangaPill plugin URL');
|
||||
if (kind === 'chapter' && (url.hostname !== 'mangapill.com' || !/^\/chapters\/\d+-\d+\//.test(url.pathname))) {
|
||||
throw new Error('Unexpected MangaPill chapter URL');
|
||||
}
|
||||
if (kind === 'page' && (url.hostname !== 'cdn.readdetectiveconan.com' || !url.pathname.startsWith('/file/mangap/'))) {
|
||||
throw new Error('Unexpected MangaPill page host');
|
||||
}
|
||||
return url.href;
|
||||
}
|
||||
|
||||
function validate(plugin, operation, output) {
|
||||
if (!['weebcentral', 'mangadex'].includes(plugin) || !output || typeof output !== 'object' || Array.isArray(output)) {
|
||||
if (!['weebcentral', 'mangadex', 'mangapill'].includes(plugin) || !output || typeof output !== 'object' || Array.isArray(output)) {
|
||||
throw new Error('Invalid source plugin output');
|
||||
}
|
||||
if (plugin === 'mangadex') {
|
||||
@@ -51,6 +63,21 @@ function validate(plugin, operation, output) {
|
||||
} else throw new Error('Unknown source plugin operation');
|
||||
return output;
|
||||
}
|
||||
if (plugin === 'mangapill') {
|
||||
if (operation === 'catalog') {
|
||||
if (typeof output.title !== 'string' || !Array.isArray(output.chapters)
|
||||
|| !Number.isInteger(output.chapterLinks) || output.chapterLinks < 0) throw new Error('Invalid MangaPill catalog');
|
||||
for (const chapter of output.chapters) {
|
||||
if (typeof chapter?.id !== 'string' || !/^\d+-\d+$/.test(chapter.id)
|
||||
|| !/^\d+(?:\.\d+)?$/.test(chapter.number)) throw new Error('Invalid MangaPill chapter');
|
||||
mangaPillUrl(chapter.url, 'chapter');
|
||||
}
|
||||
} else if (operation === 'pages') {
|
||||
if (!Array.isArray(output.pages)) throw new Error('Invalid MangaPill pages');
|
||||
for (const page of output.pages) mangaPillUrl(page, 'page');
|
||||
} else throw new Error('Unknown MangaPill operation');
|
||||
return output;
|
||||
}
|
||||
if (operation === 'series') {
|
||||
if (typeof output.title !== 'string' || typeof output.listUrl !== 'string' && output.listUrl !== null) throw new Error('Invalid series parse');
|
||||
if (output.listUrl !== null) sourceUrl(output.listUrl, '/series/');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use strict';
|
||||
const { volumeKey } = require('../../chapter-identity');
|
||||
|
||||
// First-party, pure parsing functions. This module never fetches remote content.
|
||||
const decode = value => value.replace(/&/g, '&').replace(/'/g, "'").replace(/"/g, '"').replace(/<[^>]+>/g, '').trim();
|
||||
@@ -17,6 +18,7 @@ function catalog({ html, url }) {
|
||||
const text = decode(match[3]).replace(/\s+/g, ' ');
|
||||
const season = Number(text.match(/^S(\d+)\s*-/i)?.[1] || 0) || null;
|
||||
const regular = text.match(/\b(?:Chapter|Rating|Episode|Night|Log|Act|Scene|Kapitel|File|Battle)\s*[.:]?\s*([0-9]+(?:\.[0-9]+)?)/i)?.[1];
|
||||
const volume = !regular ? text.match(/^Volume\s+([0-9]+(?:\.[0-9]+)?)(?!\d)/i)?.[1] : null;
|
||||
const prologue = text.match(/Prologue(?:\s+([0-9]+))?/i)?.[1];
|
||||
const number = regular || (prologue ? `0.${prologue.padStart(2, '0')}` : /Prologue/i.test(text) ? '0' : null);
|
||||
if (number) found.push({
|
||||
@@ -24,6 +26,12 @@ function catalog({ html, url }) {
|
||||
...(season ? { book: { number: season, title: `Season ${season}` } } : {}),
|
||||
url: absolute(url, match[1]), raw: text,
|
||||
});
|
||||
else if (volume) found.push({
|
||||
id: match[2], number: volumeKey(match[2]), title: '', special: true,
|
||||
label: `Volume ${volume}`, releaseKind: 'volume', volume,
|
||||
book: { number: Number(volume), title: `Volume ${volume}` },
|
||||
url: absolute(url, match[1]), raw: text,
|
||||
});
|
||||
}
|
||||
const chapters = [...new Map(found.map(chapter => [chapter.id, chapter])).values()];
|
||||
const chapterLinks = [...html.matchAll(/<a\b[^>]*href="[^"]*\/chapters\/[A-Z0-9]+[^"]*"/gi)].length;
|
||||
|
||||
Reference in new issue
Block a user