- 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>
94 lines
5.6 KiB
JavaScript
94 lines
5.6 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs').promises;
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { officialDates, refreshMangaReleaseDates, chapterKey } = require('../metadata/release-dates');
|
|
|
|
const DAY = 864e5;
|
|
const run = (start, end = null) => ({ start: Date.parse(start), end: end ? Date.parse(end) : null });
|
|
const viz = (chapter, date) => ({ chapter, date, groups: ['Viz', 'MANGA Plus'] });
|
|
const scan = (chapter, date) => ({ chapter, date, groups: ['Some Scans'] });
|
|
|
|
test('chapter keys only match single numbered chapters', () => {
|
|
assert.equal(chapterKey('001'), '1');
|
|
assert.equal(chapterKey('10.50'), '10.5');
|
|
assert.equal(chapterKey('1-3'), null);
|
|
assert.equal(chapterKey('7a'), null);
|
|
});
|
|
|
|
test('simulpub dates are accepted, re-releases after the run are not', () => {
|
|
const ongoing = officialDates([viz('1', '2023-09-17'), viz('2', '2023-09-24'), scan('2', '2023-09-23')], run('2023-09-18'));
|
|
assert.deepEqual([...ongoing.accepted.keys()].sort(), ['1', '2'], 'scans a day early do not disqualify');
|
|
assert.equal(ongoing.accepted.get('1').at, '2023-09-17');
|
|
assert.equal(ongoing.accepted.get('1').source, 'MangaUpdates · Viz, MANGA Plus');
|
|
const ended = officialDates([viz('700', '2020-12-20')], run('1999-09-21', '2014-11-10'));
|
|
assert.equal(ended.accepted.size, 0);
|
|
assert.equal(ended.rejected.outsideRun, 1);
|
|
assert.equal(officialDates([viz('1', '2020-01-01')], null).rejected.noRun, 1);
|
|
});
|
|
|
|
test('an earlier scanlation or source upload means the official date is a re-release', () => {
|
|
const scanned = officialDates([viz('5', '2021-06-01'), scan('5', '2019-01-01')], run('2018-01-01'));
|
|
assert.equal(scanned.rejected.earlier, 1);
|
|
const uploaded = officialDates([viz('5', '2021-06-01')], run('2018-01-01'), new Map([['5', Date.parse('2021-05-01')]]));
|
|
assert.equal(uploaded.rejected.earlier, 1);
|
|
const leak = officialDates([viz('5', '2021-06-04')], run('2018-01-01'), new Map([['5', Date.parse('2021-06-04') - 2 * DAY]]));
|
|
assert.equal(leak.accepted.size, 1, 'an upload two days early is a leak, not an older release');
|
|
});
|
|
|
|
test('back-catalog chapters dated after newer chapters are out of order', () => {
|
|
const result = officialDates([
|
|
viz('8', '2019-02-06'), viz('33', '2019-07-31'), // weekly back-catalog re-release
|
|
viz('928', '2018-12-21'), viz('930', '2019-01-20'), viz('931', '2019-02-03'), viz('1196', '2026-10-18'),
|
|
viz('929', '2019-01-21'), // a day after 930: a double release is still in order
|
|
], run('1997-07-22'));
|
|
assert.deepEqual([...result.accepted.keys()].sort((a, b) => a - b), ['928', '929', '930', '931', '1196']);
|
|
assert.equal(result.rejected.outOfOrder, 2);
|
|
});
|
|
|
|
async function library(chapters) {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'release-dates-'));
|
|
const manga = path.join(root, 'Kagurabachi');
|
|
await fs.mkdir(manga, { recursive: true });
|
|
await fs.writeFile(path.join(manga, 'manga.json'), JSON.stringify({ title: 'Kagurabachi', year: 2023, 'x-anilist': { id: 169355 } }));
|
|
for (const [folder, meta] of Object.entries(chapters)) {
|
|
await fs.mkdir(path.join(manga, folder), { recursive: true });
|
|
await fs.writeFile(path.join(manga, folder, 'chapter.json'), JSON.stringify(meta));
|
|
}
|
|
return manga;
|
|
}
|
|
const fakeSources = releases => ({
|
|
async findSeries(meta) { return meta.title === 'Kagurabachi' ? { id: '60441951323', title: 'Kagurabachi' } : null; },
|
|
async originalRun() { return run('2023-09-18'); },
|
|
async releases() { return releases; },
|
|
});
|
|
|
|
test('refresh writes x-dates.release, keeps other dates and withdraws its own stale dates', async () => {
|
|
const manga = await library({
|
|
'v01/c0001 [WeebCentral]': { title: '', added: '2026-10-01T00:00:00Z', 'x-dates': { version: 1, source: { at: '2024-09-07T10:00:00Z', name: 'WeebCentral', batch: true } } },
|
|
'v01/c0002 [WeebCentral]': { title: '', 'x-dates': { version: 1, release: { at: '2020-01-01', precision: 'day', source: 'MangaUpdates · Viz' } } },
|
|
'v01/c0003 [WeebCentral]': { title: '', 'x-dates': { version: 1, release: { at: '2023-10-01', precision: 'day', source: 'Hand-entered' } } },
|
|
});
|
|
const dry = await refreshMangaReleaseDates(manga, fakeSources([viz('1', '2023-09-17')]));
|
|
assert.equal(dry.written, 1);
|
|
assert.equal(JSON.parse(await fs.readFile(path.join(manga, 'v01/c0001 [WeebCentral]/chapter.json'), 'utf8'))['x-dates'].release, undefined, 'dry run writes nothing');
|
|
const applied = await refreshMangaReleaseDates(manga, fakeSources([viz('1', '2023-09-17')]), { apply: true });
|
|
assert.deepEqual([applied.written, applied.cleared], [1, 1]);
|
|
const one = JSON.parse(await fs.readFile(path.join(manga, 'v01/c0001 [WeebCentral]/chapter.json'), 'utf8'));
|
|
assert.deepEqual(one['x-dates'].release, { at: '2023-09-17', precision: 'day', source: 'MangaUpdates · Viz, MANGA Plus' });
|
|
assert.equal(one['x-dates'].source.batch, true);
|
|
assert.equal(one.added, '2026-10-01T00:00:00Z');
|
|
assert.equal(JSON.parse(await fs.readFile(path.join(manga, 'v01/c0002 [WeebCentral]/chapter.json'), 'utf8'))['x-dates'].release, undefined);
|
|
assert.equal(JSON.parse(await fs.readFile(path.join(manga, 'v01/c0003 [WeebCentral]/chapter.json'), 'utf8'))['x-dates'].release.source, 'Hand-entered');
|
|
});
|
|
|
|
test('seasons that restart their numbering are skipped', async () => {
|
|
const manga = await library({ 'v01 Season 1/c0001': { title: '' }, 'v02 Season 2/c0001': { title: '' } });
|
|
const result = await refreshMangaReleaseDates(manga, fakeSources([viz('1', '2023-09-17')]), { apply: true });
|
|
assert.equal(result.ambiguous, true);
|
|
assert.equal(result.written, 0);
|
|
});
|