Official release dates from MangaUpdates; peers lower ecosystem dates (BL-0048, BL-0049)

- scraper/metadata/release-dates.js fills x-dates.release from MangaUpdates
  publisher releases. MangaUpdates also lists English back-catalog re-releases,
  so a date is kept only inside the AniList original run, when no scanlation or
  source upload is more than 4 days earlier, and when it fits the release order
  (no lower chapter dated after a higher one). Seasons that restart chapter
  numbers are skipped; dates this job wrote and no longer accepts are withdrawn.
- The scraper refreshes ongoing series nightly before its update checks, only
  while no download runs, and bumps the publish revision so the reader reconciles.
- tools/release-dates.js for a whole library (dry run default). Production dry
  run: 728 chapters dated (Kagurabachi, JJK, Chainsaw Man, One Piece from 928).
- Peer sync: chapters this library already has adopt a peer's earlier ecosystem
  date (job result ecosystemLowered); the peer listing now carries dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MaashkinoozheandClaude Opus 5.5 committed 2026-10-24 08:47:36 +02:00
1 parent e0dacd81c6
commit 7b52da2f9c
8 files changed
+402 -7

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env node
'use strict';
// Official chapter release dates from MangaUpdates (BL-0048) for a whole library or one manga.
// Dry run by default. Apply only with the scraper stopped (--apply --writers-stopped), then let a
// full reconciliation (or admin Rescan) pick up the edited chapter.json files.
const fs = require('node:fs').promises;
const path = require('node:path');
const { ReleaseDateSources, refreshMangaReleaseDates } = require('../scraper/metadata/release-dates');
const args = process.argv.slice(2);
const apply = args.includes('--apply');
const only = args.find(arg => arg.startsWith('--manga='))?.slice('--manga='.length);
const cacheDir = args.find(arg => arg.startsWith('--cache='))?.slice('--cache='.length) || path.join(require('node:os').tmpdir(), 'mangacircus-release-dates');
const root = args.find(arg => !arg.startsWith('--'));
if (!root || args.some(arg => arg.startsWith('--') && !['--apply', '--writers-stopped'].includes(arg) && !/^--(manga|cache)=/.test(arg))
|| (apply && !args.includes('--writers-stopped'))) {
console.error('Usage: node tools/release-dates.js <library> [--manga=<folder>] [--cache=<dir>] [--apply --writers-stopped]');
process.exit(2);
}
async function main() {
const sources = new ReleaseDateSources({ cacheDir });
const totals = { manga: 0, matched: 0, chapters: 0, accepted: 0, written: 0, cleared: 0, outsideRun: 0, earlier: 0, outOfOrder: 0, noRun: 0 };
const names = (await fs.readdir(root, { withFileTypes: true })).filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name).sort();
for (const name of names) {
if (only && name !== only) continue;
try {
const s = await refreshMangaReleaseDates(path.join(root, name), sources, { apply });
totals.manga++; totals.chapters += s.chapters;
if (s.ambiguous) { console.log(`${name.slice(0, 44).padEnd(44)} skipped: chapter numbers repeat across books`); continue; }
if (!s.series) { console.log(`${name.slice(0, 44).padEnd(44)} no MangaUpdates match`); continue; }
totals.matched++; totals.accepted += s.accepted; totals.written += s.written; totals.cleared += s.cleared;
for (const k of ['outsideRun', 'earlier', 'outOfOrder', 'noRun']) totals[k] += s.rejected[k];
console.log(`${name.slice(0, 44).padEnd(44)} ${String(s.chapters).padStart(5)} ch official ${String(s.official).padStart(4)} accepted ${String(s.accepted).padStart(4)}`
+ ` rejected: outside run ${s.rejected.outsideRun}, earlier evidence ${s.rejected.earlier}, out of order ${s.rejected.outOfOrder}`
+ `${s.rejected.noRun ? `, no AniList run ${s.rejected.noRun}` : ''} (${s.series.title})`);
} catch (error) { console.warn(`${name}: ${error.message}`); }
}
console.log(JSON.stringify({ apply, ...totals }));
}
main().catch(error => { console.error(error); process.exit(1); });