feat: report skipped duplicates in the upload tray

A book whose files were all already stored settles as skipped rather than done,
naming the book that holds them and offering to add it anyway.
This commit is contained in:
2026-08-13 17:23:57 -04:00
parent d78b21c27f
commit b124a65d6e
7 changed files with 302 additions and 58 deletions
+38 -18
View File
@@ -9,11 +9,27 @@ import {
editBookMetadataSchema,
updateBookProgressSchema,
type Book,
type BooksUploadResult,
bookFilesUpload
} from '$lib/schema/index';
import { stringCoerce, type PaginatedResponse } from '$lib/schema/common';
import { createQueryParams } from '$lib/utils';
/**
* The backend's own message for a failed response, rather than its JSON envelope.
*
* A refused duplicate answers 409 with a `detail` worth reading and the offending
* files in `extra`; passing the body through whole puts JSON in front of the reader.
*/
function detailOf(body: string): string {
try {
const parsed = JSON.parse(body);
return typeof parsed?.detail === 'string' ? parsed.detail : body;
} catch {
return body;
}
}
export const getBook = query(stringCoerce, async (id): Promise<Book> => {
const { locals } = getRequestEvent();
@@ -68,26 +84,29 @@ export const updateBookCover = form(bookCoverUpload, async ({ book_id, file }) =
return await response.json();
});
export const uploadBooks = form(booksUpload, async ({ library_id, files }) => {
const { locals } = getRequestEvent();
export const uploadBooks = form(
booksUpload,
async ({ library_id, files }): Promise<BooksUploadResult> => {
const { locals } = getRequestEvent();
const formData = new FormData();
files.forEach((file) => {
formData.append('files', file);
});
const formData = new FormData();
files.forEach((file) => {
formData.append('files', file);
});
const response = await locals.api.postMultipart(
`/books/fromFiles?library_id=${library_id}`,
formData
);
const response = await locals.api.postMultipart(
`/books/fromFiles?library_id=${library_id}`,
formData
);
if (!response.ok) {
const message = await response.text();
error(response.status, message);
if (!response.ok) {
const message = await response.text();
error(response.status, message);
}
return await response.json();
}
return await response.json();
});
);
export const uploadBookFiles = form(bookFilesUpload, async ({ book_id, files }) => {
const { locals } = getRequestEvent();
@@ -100,8 +119,9 @@ export const uploadBookFiles = form(bookFilesUpload, async ({ book_id, files })
const response = await locals.api.postMultipart(`/books/${book_id}/files`, formData);
if (!response.ok) {
const message = await response.text();
error(response.status, message);
// 409 here means the file is already stored under a different book, which is
// something the reader can act on — so the message has to survive the trip.
error(response.status, detailOf(await response.text()));
}
return await response.json();