From e433cf9c054c3802d15b45c67b40a151dd2b768b Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 6 Feb 2026 14:25:20 +0200 Subject: [PATCH] Add rescan feature --- .../2026-02-06-move-to-library-feature.md | 20 +++++++++++-- client/components/cards/LazyBookCard.vue | 2 +- client/pages/item/_id/index.vue | 28 +++++++++++++++++++ server/scanner/BookScanner.js | 16 +++++------ 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/artifacts/2026-02-06-move-to-library-feature.md b/artifacts/2026-02-06-move-to-library-feature.md index a9ba8cf83..11402a901 100644 --- a/artifacts/2026-02-06-move-to-library-feature.md +++ b/artifacts/2026-02-06-move-to-library-feature.md @@ -56,11 +56,27 @@ POST /api/items/:id/move ### Localization Strings Added -- `ButtonMove`, `ButtonMoveToLibrary` +- `ButtonMove`, `ButtonMoveToLibrary`, `ButtonReScan` - `LabelMoveToLibrary`, `LabelMovingItem` - `LabelSelectTargetLibrary`, `LabelSelectTargetFolder` - `MessageNoCompatibleLibraries` -- `ToastItemMoved`, `ToastItemMoveFailed` +- `ToastItemMoved`, `ToastItemMoveFailed`, `ToastRescanUpdated`, `ToastRescanUpToDate`, `ToastRescanFailed` + +--- + +## Post-Move Rescan Feature + +In addition to automated handling during moves, a manual "Re-scan" feature has been enhanced and exposed to users with move permissions. + +### Why it's needed + +If a book was moved before the recent logic enhancements, it might still point to authors or series in its _old_ library. The "Re-scan" action fixes this. + +### Logic Improvements + +- During a rescan, the system now validates that all linked authors and series belong to the library the book is currently in. +- If a link to an author/series in a different library is found, it is removed. +- The system then re-evaluates the file metadata and links the book to the correct author/series in its _current_ library (creating them if they don't exist). --- diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 77809904f..a35cc0541 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -566,7 +566,7 @@ export default { text: this.$strings.HeaderMatch }) } - if (this.userIsAdminOrUp && !this.isFile) { + if ((this.userIsAdminOrUp || this.userCanDelete) && !this.isFile) { items.push({ func: 'rescan', text: this.$strings.ButtonReScan diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 27c1c11a3..0307b48cb 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -424,6 +424,10 @@ export default { } if (this.userCanDelete) { + items.push({ + text: this.$strings.ButtonReScan, + action: 'rescan' + }) items.push({ text: this.$strings.ButtonMoveToLibrary, action: 'move' @@ -760,6 +764,28 @@ export default { } this.$store.commit('globals/setConfirmPrompt', payload) }, + rescan() { + this.processing = true + this.$axios + .$post(`/api/items/${this.libraryItemId}/scan`) + .then((data) => { + var result = data.result + if (!result) { + this.$toast.error(this.$getString('ToastRescanFailed', [this.title])) + } else if (result === 'UPDATED') { + this.$toast.success(this.$strings.ToastRescanUpdated) + } else if (result === 'UPTODATE') { + this.$toast.success(this.$strings.ToastRescanUpToDate) + } + }) + .catch((error) => { + console.error('Failed to rescan', error) + this.$toast.error(this.$getString('ToastRescanFailed', [this.title])) + }) + .finally(() => { + this.processing = false + }) + }, contextMenuAction({ action, data }) { if (action === 'collections') { this.$store.commit('setSelectedLibraryItem', this.libraryItem) @@ -775,6 +801,8 @@ export default { this.downloadLibraryItem() } else if (action === 'delete') { this.deleteLibraryItem() + } else if (action === 'rescan') { + this.rescan() } else if (action === 'move') { this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('globals/setShowMoveToLibraryModal', true) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index a1e7ff507..4fa2528c5 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -220,7 +220,7 @@ class BookScanner { if (key === 'authors') { // Check for authors added for (const authorName of bookMetadata.authors) { - if (!media.authors.some((au) => au.name === authorName)) { + if (!media.authors.some((au) => au.name === authorName && au.libraryId === libraryItemData.libraryId)) { const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName) if (existingAuthorId) { await Database.bookAuthorModel.create({ @@ -242,11 +242,11 @@ class BookScanner { } } } - // Check for authors removed + // Check for authors removed (including those from wrong library) for (const author of media.authors) { - if (!bookMetadata.authors.includes(author.name)) { + if (!bookMetadata.authors.includes(author.name) || author.libraryId !== libraryItemData.libraryId) { await author.bookAuthor.destroy() - libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed author "${author.name}"`) + libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed author "${author.name}"${author.libraryId !== libraryItemData.libraryId ? ' (wrong library)' : ''}`) authorsUpdated = true bookAuthorsRemoved.push(author.id) } @@ -254,7 +254,7 @@ class BookScanner { } else if (key === 'series') { // Check for series added for (const seriesObj of bookMetadata.series) { - const existingBookSeries = media.series.find((se) => se.name === seriesObj.name) + const existingBookSeries = media.series.find((se) => se.name === seriesObj.name && se.libraryId === libraryItemData.libraryId) if (!existingBookSeries) { const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name) if (existingSeriesId) { @@ -283,11 +283,11 @@ class BookScanner { await existingBookSeries.bookSeries.save() } } - // Check for series removed + // Check for series removed (including those from wrong library) for (const series of media.series) { - if (!bookMetadata.series.some((se) => se.name === series.name)) { + if (!bookMetadata.series.some((se) => se.name === series.name) || series.libraryId !== libraryItemData.libraryId) { await series.bookSeries.destroy() - libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"`) + libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"${series.libraryId !== libraryItemData.libraryId ? ' (wrong library)' : ''}`) seriesUpdated = true bookSeriesRemoved.push(series.id) }