perf: support concurrent book uploading, closes #2208 (#2229)

This commit is contained in:
Huang Xin
2025-10-14 20:00:31 +08:00
committed by GitHub
parent 96cc182a8c
commit 948b35f244
4 changed files with 58 additions and 24 deletions
@@ -31,11 +31,12 @@ interface BookshelfProps {
isSelectAll: boolean;
isSelectNone: boolean;
handleImportBooks: () => void;
handleBookUpload: (book: Book) => Promise<boolean>;
handleBookDownload: (book: Book) => Promise<boolean>;
handleBookDelete: (book: Book) => Promise<boolean>;
handleBookUpload: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
handlePushLibrary: () => Promise<void>;
booksTransferProgress: { [key: string]: number | null };
}
@@ -50,6 +51,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
handleBookDelete,
handleSetSelectMode,
handleShowDetailsBook,
handlePushLibrary,
booksTransferProgress,
}) => {
const _ = useTranslation();
@@ -217,9 +219,14 @@ const Bookshelf: React.FC<BookshelfProps> = ({
};
const confirmDelete = async () => {
for (const book of getBooksToDelete()) {
handleBookDelete(book);
const books = getBooksToDelete();
const concurrency = 4;
for (let i = 0; i < books.length; i += concurrency) {
const batch = books.slice(i, i + concurrency);
await Promise.all(batch.map((book) => handleBookDelete(book, false)));
}
handlePushLibrary();
setSelectedBooks([]);
setShowDeleteAlert(false);
setShowSelectModeActions(true);
@@ -77,9 +77,9 @@ interface BookshelfItemProps {
transferProgress: number | null;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
toggleSelection: (hash: string) => void;
handleBookUpload: (book: Book) => Promise<boolean>;
handleBookDownload: (book: Book) => Promise<boolean>;
handleBookDelete: (book: Book) => Promise<boolean>;
handleBookUpload: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
}
+44 -18
View File
@@ -419,41 +419,56 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const importBooks = async (files: SelectedFile[]) => {
setLoading(true);
const failedFiles = [];
const { library } = useLibraryStore.getState();
const failedImports: Array<{ filename: string; errorMessage: string }> = [];
const errorMap: [string, string][] = [
['No chapters detected.', _('No chapters detected.')],
['Failed to parse EPUB.', _('Failed to parse the EPUB file.')],
['Unsupported format.', _('This book format is not supported.')],
];
const { library } = useLibraryStore.getState();
for (const selectedFile of files) {
const processFile = async (selectedFile: SelectedFile) => {
const file = selectedFile.file || selectedFile.path;
if (!file) continue;
if (!file) return;
try {
const book = await appService?.importBook(file, library);
setLibrary([...library]);
if (user && book && !book.uploadedAt && settings.autoUpload) {
console.log('Uploading book:', book.title);
handleBookUpload(book);
handleBookUpload(book, false);
}
} catch (error) {
const filename = typeof file === 'string' ? file : file.name;
const baseFilename = getFilename(filename);
failedFiles.push(baseFilename);
const errorMessage =
error instanceof Error
? errorMap.find(([substring]) => error.message.includes(substring))?.[1] || ''
: '';
eventDispatcher.dispatch('toast', {
message:
_('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(failedFiles),
}) + (errorMessage ? `\n${errorMessage}` : ''),
type: 'error',
});
failedImports.push({ filename: baseFilename, errorMessage });
console.error('Failed to import book:', filename, error);
}
};
const concurrency = 4;
for (let i = 0; i < files.length; i += concurrency) {
const batch = files.slice(i, i + concurrency);
await Promise.all(batch.map(processFile));
}
pushLibrary();
if (failedImports.length > 0) {
const filenames = failedImports.map((f) => f.filename);
const errorMessage = failedImports.find((f) => f.errorMessage)?.errorMessage || '';
eventDispatcher.dispatch('toast', {
message:
_('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(filenames),
}) + (errorMessage ? `\n${errorMessage}` : ''),
type: 'error',
});
}
setLibrary([...library]);
appService?.saveLibraryBooks(library);
setLoading(false);
};
@@ -468,13 +483,18 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}, 500);
const handleBookUpload = useCallback(
async (book: Book) => {
async (book: Book, syncBooks = true) => {
try {
await appService?.uploadBook(book, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
setBooksTransferProgress((prev) => {
const updated = { ...prev };
delete updated[book.hash];
return updated;
});
await updateBook(envConfig, book);
pushLibrary();
if (syncBooks) pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
@@ -484,6 +504,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
});
return true;
} catch (err) {
setBooksTransferProgress((prev) => {
const updated = { ...prev };
delete updated[book.hash];
return updated;
});
if (err instanceof Error) {
if (err.message.includes('Not authenticated') && settings.keepLogin) {
settings.keepLogin = false;
@@ -541,7 +566,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
);
const handleBookDelete = (deleteAction: DeleteAction) => {
return async (book: Book) => {
return async (book: Book, syncBooks = true) => {
const deletionMessages = {
both: _('Book deleted: {{title}}', { title: book.title }),
cloud: _('Deleted cloud backup of the book: {{title}}', { title: book.title }),
@@ -555,7 +580,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
try {
await appService?.deleteBook(book, deleteAction);
await updateBook(envConfig, book);
pushLibrary();
if (syncBooks) pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
@@ -709,6 +734,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
handleSetSelectMode={handleSetSelectMode}
handleShowDetailsBook={handleShowDetailsBook}
booksTransferProgress={booksTransferProgress}
handlePushLibrary={pushLibrary}
/>
</div>
</OverlayScrollbarsComponent>
@@ -408,6 +408,7 @@ const FoliateViewer: React.FC<{
useEffect(() => {
if (!viewSettings) return;
applyBackgroundTexture(envConfig, viewSettings);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
viewSettings?.backgroundTextureId,
viewSettings?.backgroundOpacity,