'use strict'; const { volumeKey } = require('../../chapter-identity'); const { batchFlags } = require('../../chapter-dates'); // First-party, pure parsing functions. This module never fetches remote content. const decode = value => value.replace(/&/g, '&').replace(/'/g, "'").replace(/"/g, '"').replace(/<[^>]+>/g, '').trim(); const absolute = (base, value) => new URL(value, base).href; function series({ html, url }) { const title = decode(html.match(/]*>([\s\S]*?)<\/h1>/i)?.[1] || 'Untitled'); const listUrl = html.match(/hx-get="([^"]+\/full-chapter-list)"/i)?.[1]; return { title, listUrl: listUrl ? absolute(url, listUrl) : null }; } function catalog({ html, url }) { const found = []; const re = /]*href="([^"]*\/chapters\/([A-Z0-9]+)[^"]*)"[^>]*>([\s\S]*?)<\/a>/gi; for (const match of html.matchAll(re)) { 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); // The full chapter list carries each upload time; the chapter page itself has none. const uploaded = match[3].match(/datetime="([^"]+)"/i)?.[1]; const uploadedAt = uploaded && Number.isFinite(Date.parse(uploaded)) ? uploaded : undefined; if (number) found.push({ id: match[2], number, title: regular ? '' : `Prologue ${prologue || ''}`.trim(), ...(season ? { book: { number: season, title: `Season ${season}` } } : {}), url: absolute(url, match[1]), raw: text, ...(uploadedAt ? { uploadedAt } : {}), }); 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, ...(uploadedAt ? { uploadedAt } : {}), }); } const chapters = [...new Map(found.map(chapter => [chapter.id, chapter])).values()]; // Back-catalog chapters were imported together; their upload time is not a release date. batchFlags(chapters.map(chapter => chapter.uploadedAt)).forEach((batch, index) => { if (batch !== null) chapters[index].uploadBatch = batch; }); const chapterLinks = [...html.matchAll(/]*href="[^"]*\/chapters\/[A-Z0-9]+[^"]*"/gi)].length; return { chapters, chapterLinks }; } function chapter({ html, url }) { return { manifestUrl: html.match(/['"](https?:\/\/weebcentral\.com\/chapters\/[^'"]+\/images\?is_prev=[^'"]+)['"]/i)?.[1] || `${url}/images?is_prev=False`, }; } function pages({ html, url }) { const urls = []; for (const match of html.matchAll(/]*(?:src|data-src)="([^"]+)"/gi)) { const pageUrl = absolute(url, match[1].replace(/&/g, '&')); if (!pageUrl.startsWith('data:')) urls.push(pageUrl); } return { pages: [...new Set(urls)] }; } module.exports = { series, catalog, chapter, pages };