fix(stats): optimize lifetime summary maintenance

- Recompute only affected anime after metadata changes
- Normalize fractional metrics across apply, rebuild, and delete
- Preserve transactions and recover from silent worker exits
This commit is contained in:
2026-08-14 00:58:51 -07:00
parent de5b1b5d10
commit 7cc9f72fc9
8 changed files with 249 additions and 53 deletions
@@ -1,7 +1,12 @@
import { createHash } from 'node:crypto';
import type { DatabaseSync } from './sqlite';
import { buildCoverBlobReference, normalizeCoverBlobBytes } from './storage';
import { repairLifetimeSummariesFromMedia } from './lifetime';
import {
recomputeLifetimeAnimeFromMedia,
recomputeLifetimeGlobalFromSummaries,
repairLifetimeSummariesFromMedia,
shouldBackfillLifetimeSummaries,
} from './lifetime';
import { nowMs } from './time';
import { resolveAnimeAnilistConflict } from './anime-season-repair';
import { deleteMaintenanceBatch } from './query-delete-maintenance';
@@ -421,7 +426,7 @@ export function updateAnimeAnilistInfo(
} | null;
if (!row?.anime_id) return;
resolveAnimeAnilistConflict(db, row.anime_id, info.anilistId);
const conflictRepair = resolveAnimeAnilistConflict(db, row.anime_id, info.anilistId);
const targetRow = db
.prepare('SELECT anime_id FROM imm_videos WHERE video_id = ?')
.get(videoId) as {
@@ -450,10 +455,15 @@ export function updateAnimeAnilistInfo(
toDbTimestamp(nowMs()),
targetRow.anime_id,
);
// Moves change which anime owns the media rows, and an episodes_total change
// can flip anime_completed even without a move — both are derivable from the
// summary tables, so a full (retention-lossy) rebuild is never needed here.
repairLifetimeSummariesFromMedia(db);
if (shouldBackfillLifetimeSummaries(db)) {
repairLifetimeSummariesFromMedia(db);
} else {
const affectedAnimeIds = new Set(conflictRepair.affectedAnimeIds);
affectedAnimeIds.add(row.anime_id);
affectedAnimeIds.add(targetRow.anime_id);
recomputeLifetimeAnimeFromMedia(db, [...affectedAnimeIds]);
recomputeLifetimeGlobalFromSummaries(db);
}
}
export function markVideoWatched(db: DatabaseSync, videoId: number, watched: boolean): void {