Add rescan feature

This commit is contained in:
Tiberiu Ichim 2026-02-06 14:25:20 +02:00
parent d358b82b17
commit e433cf9c05
4 changed files with 55 additions and 11 deletions

View file

@ -56,11 +56,27 @@ POST /api/items/:id/move
### Localization Strings Added ### Localization Strings Added
- `ButtonMove`, `ButtonMoveToLibrary` - `ButtonMove`, `ButtonMoveToLibrary`, `ButtonReScan`
- `LabelMoveToLibrary`, `LabelMovingItem` - `LabelMoveToLibrary`, `LabelMovingItem`
- `LabelSelectTargetLibrary`, `LabelSelectTargetFolder` - `LabelSelectTargetLibrary`, `LabelSelectTargetFolder`
- `MessageNoCompatibleLibraries` - `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).
--- ---

View file

@ -566,7 +566,7 @@ export default {
text: this.$strings.HeaderMatch text: this.$strings.HeaderMatch
}) })
} }
if (this.userIsAdminOrUp && !this.isFile) { if ((this.userIsAdminOrUp || this.userCanDelete) && !this.isFile) {
items.push({ items.push({
func: 'rescan', func: 'rescan',
text: this.$strings.ButtonReScan text: this.$strings.ButtonReScan

View file

@ -424,6 +424,10 @@ export default {
} }
if (this.userCanDelete) { if (this.userCanDelete) {
items.push({
text: this.$strings.ButtonReScan,
action: 'rescan'
})
items.push({ items.push({
text: this.$strings.ButtonMoveToLibrary, text: this.$strings.ButtonMoveToLibrary,
action: 'move' action: 'move'
@ -760,6 +764,28 @@ export default {
} }
this.$store.commit('globals/setConfirmPrompt', payload) 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 }) { contextMenuAction({ action, data }) {
if (action === 'collections') { if (action === 'collections') {
this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('setSelectedLibraryItem', this.libraryItem)
@ -775,6 +801,8 @@ export default {
this.downloadLibraryItem() this.downloadLibraryItem()
} else if (action === 'delete') { } else if (action === 'delete') {
this.deleteLibraryItem() this.deleteLibraryItem()
} else if (action === 'rescan') {
this.rescan()
} else if (action === 'move') { } else if (action === 'move') {
this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setShowMoveToLibraryModal', true) this.$store.commit('globals/setShowMoveToLibraryModal', true)

View file

@ -220,7 +220,7 @@ class BookScanner {
if (key === 'authors') { if (key === 'authors') {
// Check for authors added // Check for authors added
for (const authorName of bookMetadata.authors) { 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) const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
if (existingAuthorId) { if (existingAuthorId) {
await Database.bookAuthorModel.create({ 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) { 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() 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 authorsUpdated = true
bookAuthorsRemoved.push(author.id) bookAuthorsRemoved.push(author.id)
} }
@ -254,7 +254,7 @@ class BookScanner {
} else if (key === 'series') { } else if (key === 'series') {
// Check for series added // Check for series added
for (const seriesObj of bookMetadata.series) { 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) { if (!existingBookSeries) {
const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name) const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name)
if (existingSeriesId) { if (existingSeriesId) {
@ -283,11 +283,11 @@ class BookScanner {
await existingBookSeries.bookSeries.save() await existingBookSeries.bookSeries.save()
} }
} }
// Check for series removed // Check for series removed (including those from wrong library)
for (const series of media.series) { 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() 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 seriesUpdated = true
bookSeriesRemoved.push(series.id) bookSeriesRemoved.push(series.id)
} }