From 3ba2551ba17f686db7beb4a2acf15583b5725a38 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 17 Feb 2026 15:55:38 +0200 Subject: [PATCH] Fix: Handle database merge and redirect during consolidation conflicts --- client/layouts/default.vue | 9 +- server/controllers/LibraryItemController.js | 94 ++++++++++++++++++--- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/client/layouts/default.vue b/client/layouts/default.vue index fbd08284d..416b0f05d 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -627,9 +627,16 @@ export default { this.processingConsolidationConflict = true const axios = this.$axios || this.$nuxt.$axios try { - await axios.$post(`/api/items/${this.consolidationConflictItem.id}/consolidate`, payload) + const data = await axios.$post(`/api/items/${this.consolidationConflictItem.id}/consolidate`, payload) this.$toast.success(this.$strings.ToastConsolidateSuccess || 'Consolidation successful') this.showConsolidationConflictModal = false + + if (data.mergedInto) { + const id = data.mergedInto + if (this.$route.name.startsWith('item') && this.$route.params.id === this.consolidationConflictItem.id) { + this.$router.push(`/item/${id}`) + } + } } catch (error) { console.error('Failed to resolve consolidation conflict', error) this.$toast.error(error.response?.data?.error || error.response?.data || 'Failed to resolve conflict') diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index c8549fa4f..8230023c7 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -1728,25 +1728,61 @@ class LibraryItemController { const expectedPath = Path.join(targetFolder.path, targetFolderName) const isSamePath = req.libraryItem.path === expectedPath - if (!isSamePath && !merge && (await fs.pathExists(expectedPath))) { + let existingItem = null + if (!isSamePath && (await fs.pathExists(expectedPath))) { // Find existing library item at this path if any - const existingItem = await Database.libraryItemModel.findOne({ + existingItem = await Database.libraryItemModel.findOne({ where: { path: expectedPath } }) - return res.status(409).json({ - error: 'Destination already exists', - path: expectedPath, - existingLibraryItemId: existingItem?.id || null - }) + + if (!merge) { + return res.status(409).json({ + error: 'Destination already exists', + path: expectedPath, + existingLibraryItemId: existingItem?.id || null + }) + } } try { + const oldPath = req.libraryItem.path await handleMoveLibraryItem(req.libraryItem, library, targetFolder, targetFolderName, !!merge) - // Recursively remove empty parent directories - let parentDir = Path.dirname(req.libraryItem.path) + if (merge && existingItem) { + Logger.info(`[LibraryItemController] Consolidated item "${req.libraryItem.id}" was merged into existing item "${existingItem.id}"`) + + const authorIds = req.libraryItem.media.authors?.map((au) => au.id) || [] + const seriesIds = req.libraryItem.media.series?.map((se) => se.id) || [] + + // Cleanup associations for the item being absorbed + await this.handleDeleteLibraryItem(req.libraryItem.id, [req.libraryItem.media.id]) + + // Delete the redundant database record + await req.libraryItem.destroy() + + if (authorIds.length) { + await this.checkRemoveAuthorsWithNoBooks(authorIds) + } + if (seriesIds.length) { + await this.checkRemoveEmptySeries(seriesIds) + } + + // Rescan target item to pick up merged files + await LibraryItemScanner.scanLibraryItem(existingItem.id) + + const updatedExistingItem = await Database.libraryItemModel.findByPkExpanded(existingItem.id) + + return res.json({ + success: true, + mergedInto: existingItem.id, + libraryItem: updatedExistingItem.toOldJSONExpanded() + }) + } + + // Recursively remove empty parent directories from original location + let parentDir = Path.dirname(oldPath) while (parentDir && parentDir !== targetFolder.path && parentDir !== Path.dirname(parentDir)) { try { const files = await fs.readdir(parentDir) @@ -1805,10 +1841,48 @@ class LibraryItemController { const library = await Database.libraryModel.findByIdWithFolders(libraryItem.libraryId) const currentLibraryFolder = library.libraryFolders.find((lf) => libraryItem.path.startsWith(lf.path)) || library.libraryFolders[0] + const targetPath = Path.join(currentLibraryFolder.path, sanitizedFolderName) + const isSamePath = libraryItem.path === targetPath + + let existingItem = null + if (!isSamePath && (await fs.pathExists(targetPath))) { + existingItem = await Database.libraryItemModel.findOne({ + where: { path: targetPath } + }) + + if (!merge) { + results.push({ id: libraryItem.id, success: false, error: 'Destination already exists' }) + continue + } + } + const oldPath = libraryItem.path await handleMoveLibraryItem(libraryItem, library, currentLibraryFolder, sanitizedFolderName, !!merge) - // Recursively remove empty parent directories + if (merge && existingItem) { + Logger.info(`[LibraryItemController] Batch Consolidate: Merging item "${libraryItem.id}" into existing item "${existingItem.id}"`) + + const authorIds = libraryItem.media.authors?.map((au) => au.id) || [] + const seriesIds = libraryItem.media.series?.map((se) => se.id) || [] + + await this.handleDeleteLibraryItem(libraryItem.id, [libraryItem.media.id]) + await libraryItem.destroy() + + if (authorIds.length) { + await this.checkRemoveAuthorsWithNoBooks(authorIds) + } + if (seriesIds.length) { + await this.checkRemoveEmptySeries(seriesIds) + } + + await LibraryItemScanner.scanLibraryItem(existingItem.id) + + results.push({ id: libraryItem.id, success: true, mergedInto: existingItem.id }) + numSuccess++ + continue + } + + // Recursively remove empty parent directories from original location let parentDir = Path.dirname(oldPath) while (parentDir && parentDir !== currentLibraryFolder.path && parentDir !== Path.dirname(parentDir)) { try {