fix(stats): fix data loss and error handling in anime merge/move

- flush pending telemetry before merge/move so in-progress session time isn't dropped from lifetime totals
- repoint subtitle lines by video_id instead of anime_id so lines recorded before the async title parse assigns a link aren't stranded
- stop absorbing metadata into the target when moving an episode out of an emptied entry (a move isn't a same-show claim)
- return 404 only for missing episode/target, not storage failures; return 404 when a merge folds nothing
- keep AnimeMergeDialog open while a merge is in flight instead of letting dismiss race the request
- surface library load failures in LibraryEntryPicker instead of showing an empty list
This commit is contained in:
2026-08-10 23:06:00 -07:00
parent 5938095d92
commit d6e6e29b5e
9 changed files with 164 additions and 17 deletions
@@ -360,9 +360,11 @@ export function resolveAnimeAnilistConflict(
const summary = emptySummary(1);
summary.movedVideos = merge.movedVideos;
summary.deletedAnimeRows = merge.mergedAnimeIds.length;
summary.survivingAnimeId = survivingAnimeId;
if (merge.mergedAnimeIds.length > 0) {
summary.repaired = 1;
// Only reported once a row really absorbed the other, so callers never
// follow this to an anime id that was never written.
summary.survivingAnimeId = survivingAnimeId;
}
// Lifetime summaries are rebuilt by the caller off this summary, the same
// as the redistribution path below.
@@ -383,11 +385,16 @@ function canMergeAnilistConflict(
anilistId: number,
options: AnimeAnilistConflictOptions,
): boolean {
const targetRow = getAnimeRow(db, targetAnimeId);
if (!targetRow) {
// Nothing to merge with a row that no longer exists (a stale id from the
// caller); fall through to the redistribution path.
return false;
}
if (options.survivor !== 'target') {
// The target is the row about to disappear here, so an existing link of its
// own means this is a mis-resolution rather than a duplicate: leave it be.
const targetRow = getAnimeRow(db, targetAnimeId);
if (targetRow?.anilist_id != null && targetRow.anilist_id !== anilistId) {
if (targetRow.anilist_id != null && targetRow.anilist_id !== anilistId) {
return false;
}
}