fix: upload retry reuses the created manga instead of duplicating it
After a partial upload failure (manga created, a chapter failed), re-submitting re-ran createManga and spawned a duplicate manga. Track the created manga id: a retry skips creation, re-sends only the not-yet-done chapters, disables the now-saved title, relabels the button "Retry failed chapters", and shows a note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,11 +44,18 @@
|
||||
let mangaError = $state<string | null>(null);
|
||||
let success = $state<string | null>(null);
|
||||
|
||||
// Set once the manga row is created. A retry after a partial chapter
|
||||
// failure reuses this id instead of creating a duplicate manga, and the
|
||||
// form switches into "finish uploading chapters" mode.
|
||||
let createdMangaId = $state<string | null>(null);
|
||||
let createdMangaTitle = $state<string | null>(null);
|
||||
const mangaCreated = $derived(createdMangaId != null);
|
||||
|
||||
// Warn before navigating away with an in-progress upload. Suppressed while
|
||||
// submitting and after a successful save (which redirects).
|
||||
// submitting (the post-save redirect). After a partial failure the created
|
||||
// manga's failed chapters are still unsaved staged work, so we keep warning.
|
||||
guardUnsavedChanges(
|
||||
() =>
|
||||
!success &&
|
||||
!submitting &&
|
||||
(mangaTitle.trim() !== '' ||
|
||||
mangaDescription.trim() !== '' ||
|
||||
@@ -148,39 +155,46 @@
|
||||
submitting = true;
|
||||
mangaError = null;
|
||||
success = null;
|
||||
let manga;
|
||||
try {
|
||||
manga = await createManga(
|
||||
{
|
||||
title: mangaTitle.trim(),
|
||||
status: mangaStatus,
|
||||
authors: mangaAuthors,
|
||||
alt_titles: mangaAltTitles,
|
||||
genre_ids: mangaGenreIds,
|
||||
description: mangaDescription.trim() || null
|
||||
},
|
||||
coverFile ?? undefined
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 401) {
|
||||
await goto('/login?next=/upload');
|
||||
|
||||
// Create the manga only once. On a retry after a partial failure the
|
||||
// row already exists (createdMangaId set), so we skip creation and go
|
||||
// straight to re-uploading the not-yet-done chapters — creating it again
|
||||
// would spawn a duplicate manga.
|
||||
if (createdMangaId == null) {
|
||||
try {
|
||||
const manga = await createManga(
|
||||
{
|
||||
title: mangaTitle.trim(),
|
||||
status: mangaStatus,
|
||||
authors: mangaAuthors,
|
||||
alt_titles: mangaAltTitles,
|
||||
genre_ids: mangaGenreIds,
|
||||
description: mangaDescription.trim() || null
|
||||
},
|
||||
coverFile ?? undefined
|
||||
);
|
||||
createdMangaId = manga.id;
|
||||
createdMangaTitle = manga.title;
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 401) {
|
||||
await goto('/login?next=/upload');
|
||||
return;
|
||||
}
|
||||
mangaError = e instanceof Error ? e.message : String(e);
|
||||
submitting = false;
|
||||
return;
|
||||
}
|
||||
mangaError = e instanceof Error ? e.message : String(e);
|
||||
submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Manga is created; ship chapters one at a time and surface
|
||||
// per-row status. Failures don't roll back the manga — the
|
||||
// user can retry just the failed chapters from the manga
|
||||
// page's Upload-chapter button.
|
||||
// Ship the outstanding chapters one at a time, surfacing per-row status.
|
||||
// Already-succeeded rows are skipped so a retry only re-sends failures.
|
||||
for (const c of stagedChapters) {
|
||||
if (c.status === 'done') continue;
|
||||
c.status = 'uploading';
|
||||
c.error = null;
|
||||
try {
|
||||
await createChapter(
|
||||
manga.id,
|
||||
createdMangaId,
|
||||
{ number: c.number, title: c.title.trim() || null },
|
||||
c.pages.map((p) => p.file)
|
||||
);
|
||||
@@ -193,12 +207,11 @@
|
||||
|
||||
const failed = stagedChapters.filter((c) => c.status === 'failed');
|
||||
if (failed.length === 0) {
|
||||
// All-good — land the user on the manga page where they
|
||||
// can confirm and continue uploading.
|
||||
await goto(`/manga/${manga.id}`);
|
||||
// All-good — land the user on the manga page.
|
||||
await goto(`/manga/${createdMangaId}`);
|
||||
return;
|
||||
}
|
||||
success = `"${manga.title}" was created, but ${failed.length} of ${stagedChapters.length} chapters failed. Fix them and retry from the manga page.`;
|
||||
success = `"${createdMangaTitle}" was created, but ${failed.length} of ${stagedChapters.length} chapters still failed. Fix them and retry — the manga won't be duplicated.`;
|
||||
submitting = false;
|
||||
}
|
||||
</script>
|
||||
@@ -215,6 +228,12 @@
|
||||
<form onsubmit={submit} action="javascript:void(0)" data-testid="manga-form">
|
||||
<section class="card">
|
||||
<h2>Manga details</h2>
|
||||
{#if mangaCreated}
|
||||
<p class="status" data-testid="manga-created-note">
|
||||
Manga created. Its details are saved — fix any failed chapters
|
||||
below and submit again to finish; the manga won't be duplicated.
|
||||
</p>
|
||||
{/if}
|
||||
<label class="form-field">
|
||||
<span>Title <span aria-hidden="true">*</span></span>
|
||||
<input
|
||||
@@ -222,6 +241,7 @@
|
||||
bind:value={mangaTitle}
|
||||
required
|
||||
maxlength="200"
|
||||
disabled={mangaCreated}
|
||||
data-testid="manga-title"
|
||||
/>
|
||||
</label>
|
||||
@@ -435,7 +455,13 @@
|
||||
disabled={!canSubmit}
|
||||
data-testid="manga-submit"
|
||||
>
|
||||
{submitting ? 'Submitting…' : 'Create manga'}
|
||||
{#if submitting}
|
||||
Submitting…
|
||||
{:else if mangaCreated}
|
||||
Retry failed chapters
|
||||
{:else}
|
||||
Create manga
|
||||
{/if}
|
||||
</button>
|
||||
{#if success}
|
||||
<p class="success" data-testid="manga-success">{success}</p>
|
||||
|
||||
Reference in New Issue
Block a user