- Chapter rows show when you first finished a chapter (setting: date + hour, date, off) from a new durable progress doneAt; older reads fall back to the reading clock's earliest remembered completion. - chapter.json keeps added (arrival in this library, now kept on replace) plus x-dates.source (source upload, bulk imports flagged), x-dates.release (official, for a later metadata job) and x-dates.ecosystem (first entry into any Manga Circus library; carried over peering, earliest wins, clamped). - Rows show one release date by tier: official > source upload > ecosystem > library, with all known dates in the tooltip (setting to hide). - Fixes: WeebCentral dates came from the chapter page, which has none (every chapter got the download time); they now come from the full chapter list. MangaDex no longer writes a group's publishAt into added. Peering sends dates. - tools/backfill-chapter-dates.js migrates an existing library (dry run default). - Coverage dry run on the server: docs/chapter-dates-coverage-2026-10-24.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.9 KiB
JavaScript
75 lines
3.9 KiB
JavaScript
'use strict';
|
|
|
|
const { SlowHttp } = require('../lib/http');
|
|
const { fallbackDirection } = require('../../reading-direction');
|
|
const parser = require('../plugins/weebcentral');
|
|
const { parse } = require('../plugins/registry');
|
|
const { sourceDate } = require('../../chapter-dates');
|
|
|
|
function weebCentralInfo(value) {
|
|
let url;
|
|
try { url = new URL(String(value)); } catch { return null; }
|
|
if (url.protocol !== 'https:' || url.hostname !== 'weebcentral.com') return null;
|
|
const match = /^\/series\/([A-Z0-9]+)(?:\/([^/?#]+))?\/?$/i.exec(url.pathname);
|
|
if (!match) return null;
|
|
const title = decodeURIComponent(match[2] || match[1]).replace(/[-_]+/g, ' ').trim();
|
|
return { source: 'weebcentral', id: match[1].toUpperCase(), title, url: `https://weebcentral.com/series/${match[1]}${match[2] ? `/${match[2]}` : ''}` };
|
|
}
|
|
|
|
const parseChapters = (html, baseUrl) => parser.catalog({ html, url: baseUrl }).chapters;
|
|
const parsePageUrls = (html, baseUrl) => parser.pages({ html, url: baseUrl }).pages;
|
|
|
|
async function discoverWeebCatalog(seriesUrl, http) {
|
|
const seriesHtml = await (await http.get(seriesUrl, { accept: 'text/html' })).text();
|
|
const { title, listUrl } = parse('weebcentral', 'series', { html: seriesHtml, url: seriesUrl });
|
|
const listHtml = listUrl ? await (await http.get(listUrl, { accept: 'text/html', catalog: true })).text() : seriesHtml;
|
|
const parsed = parse('weebcentral', 'catalog', { html: listHtml, url: seriesUrl });
|
|
const chapterLinks = parsed.chapterLinks;
|
|
const chapters = parsed.chapters.sort((a, b) =>
|
|
Number(a.book?.number || 1) - Number(b.book?.number || 1)
|
|
|| Number(a.releaseKind === 'volume') - Number(b.releaseKind === 'volume')
|
|
|| Number(a.number) - Number(b.number));
|
|
return { title, chapters, chapterLinks,
|
|
...(chapterLinks && !chapters.length ? { availability: { reason: 'unrecognized-labels',
|
|
message: `WeebCentral lists ${chapterLinks} chapter links, but their numbering labels are not recognized by the parser.` } } : {}),
|
|
};
|
|
}
|
|
|
|
async function discoverWeebChapter(chapter, http) {
|
|
const pageHtml = await (await http.get(chapter.url, { accept: 'text/html' })).text();
|
|
const { manifestUrl } = parse('weebcentral', 'chapter', { html: pageHtml, url: chapter.url });
|
|
const source = sourceDate(chapter.uploadedAt, 'WeebCentral', chapter.uploadBatch);
|
|
const fragment = await (await http.get(`${manifestUrl}&reading_style=long_strip¤t_page=1`, { accept: 'text/html' })).text();
|
|
const parsed = parse('weebcentral', 'pages', { html: fragment, url: manifestUrl });
|
|
const pages = parsed.pages.map(url => ({ url, headers: { referer: chapter.url } }));
|
|
if (!pages.length) throw new Error(`No pages found for chapter ${chapter.number}`);
|
|
return {
|
|
number: chapter.number, title: chapter.title || '',
|
|
...(chapter.special ? { special: true, label: chapter.label, sourceId: chapter.id,
|
|
sourceNumber: chapter.volume, releaseKind: chapter.releaseKind, volume: chapter.volume } : {}),
|
|
...(chapter.book ? { book: chapter.book } : {}),
|
|
...(source ? { dates: { source } } : {}),
|
|
source: { name: 'WeebCentral', url: chapter.url }, pages,
|
|
};
|
|
}
|
|
|
|
async function discoverWeebCentral(seriesUrl, options = {}) {
|
|
const http = new SlowHttp(options.http || {});
|
|
const catalog = await discoverWeebCatalog(seriesUrl, http);
|
|
const chapters = catalog.chapters
|
|
.sort((a, b) => Number(b.number) - Number(a.number))
|
|
.slice(0, options.limit || 3)
|
|
.sort((a, b) => Number(a.number) - Number(b.number));
|
|
const output = [];
|
|
for (const chapter of chapters) {
|
|
output.push(await discoverWeebChapter(chapter, http));
|
|
}
|
|
return {
|
|
version: 1,
|
|
series: { title: catalog.title, ...fallbackDirection(), sources: [{ name: 'WeebCentral', url: seriesUrl }], scraper: { ids: { weebcentral: seriesUrl.split('/series/')[1]?.split('/')[0] } } },
|
|
http: options.http || {}, chapters: output,
|
|
};
|
|
}
|
|
|
|
module.exports = { weebCentralInfo, discoverWeebCentral, discoverWeebCatalog, discoverWeebChapter, parseChapters, parsePageUrls };
|