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

+26 -1
View File
@@ -13,6 +13,7 @@ const { scanLibrary, findManga, shouldDownload, isNewerThanLibrary, summarizeInv
const { inspectLegacyWork, promoteLegacyWork } = require('./lib/legacy-publication');
const { classifyFailure, safeMessage, progressKey } = require('./lib/failures');
const { writeJsonAtomic } = require('./lib/json-file');
const { ReleaseDateSources, refreshMangaReleaseDates } = require('./metadata/release-dates');
const RATE_KEYS = ['minMs', 'maxMs', 'chapterMinMs', 'chapterMaxMs'];
const FAST_RATE_SETTINGS = {
@@ -353,12 +354,36 @@ class ScraperService {
const delay = Math.max(1000, next.getTime() - Date.now());
this.discoveryTimer = setTimeout(() => {
this.discoveryTimer = null;
this.enqueueDiscoveryChecks('schedule').catch(error => this.event('error', `Nightly discovery failed: ${error.message}`))
this.refreshReleaseDates('schedule').catch(error => this.event('error', `Nightly release dates failed: ${error.message}`))
.then(() => this.enqueueDiscoveryChecks('schedule'))
.catch(error => this.event('error', `Nightly discovery failed: ${error.message}`))
.finally(() => { if (!this.stopping) this.scheduleDiscovery(); });
}, delay);
this.discoveryTimer.unref();
}
// Nightly, before the update checks: official release dates of ongoing series from their
// newest MangaUpdates releases (BL-0048). Runs only while no download is active, so the
// scraper stays the single writer of chapter.json.
async refreshReleaseDates(reason = 'manual') {
if (this.active.size) { await this.event('info', 'Release dates skipped: a download is running', { reason }); return null; }
const inventory = await this.libraryInventory(true);
const sources = new ReleaseDateSources({ cacheDir: path.join(this.dataRoot, 'release-dates-cache') });
let checked = 0, written = 0;
for (const manga of inventory.mangas) {
if (this.stopping || this.active.size) break;
if (manga.meta?.status !== 'ongoing') continue;
try {
const result = await refreshMangaReleaseDates(manga.path, sources, { apply: true, pages: 1, maxAgeMs: 20 * 3600e3 });
checked++; written += result.written;
} catch (error) { await this.event('warning', `Release dates for ${manga.title} failed: ${error.message}`, { mangaId: manga.id }); }
}
// The reader reconciles fully when the revision rises without a visible folder change.
if (written) { this.state.publishedRevision = Number(this.state.publishedRevision || 0) + 1; await this.persist(); }
await this.event('info', `Release dates: ${written} chapter${written === 1 ? '' : 's'} dated in ${checked} ongoing series`, { reason, checked, written });
return { checked, written };
}
pendingSeries(source, seriesId) {
return this.state.queue.some(item => item.source === source && String(item.seriesId) === String(seriesId)
&& ['queued', 'downloading', 'discovering', 'ready', 'error', 'retry_wait', 'quarantined', 'unavailable'].includes(item.status));