mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-12 20:31:36 +00:00
Merge 9d9ab49664 into 82aec5f60c
This commit is contained in:
commit
567caa0c66
6 changed files with 81 additions and 7 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model, fn, col } = require('sequelize')
|
||||||
|
|
||||||
class BookAuthor extends Model {
|
class BookAuthor extends Model {
|
||||||
constructor(values, options) {
|
constructor(values, options) {
|
||||||
|
|
@ -37,6 +37,32 @@ class BookAuthor extends Model {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get number of books for each author
|
||||||
|
*
|
||||||
|
* @param {string[]} authorIds
|
||||||
|
* @returns {Promise<Record<string, number>>}
|
||||||
|
*/
|
||||||
|
static async getCountsForAuthors(authorIds) {
|
||||||
|
if (!authorIds.length) return {}
|
||||||
|
|
||||||
|
const rows = await this.findAll({
|
||||||
|
attributes: ['authorId', [fn('COUNT', col('id')), 'count']],
|
||||||
|
where: {
|
||||||
|
authorId: authorIds
|
||||||
|
},
|
||||||
|
group: ['authorId'],
|
||||||
|
raw: true
|
||||||
|
})
|
||||||
|
|
||||||
|
/** @type {Record<string, number>} */
|
||||||
|
const counts = {}
|
||||||
|
for (const row of rows) {
|
||||||
|
counts[row.authorId] = Number(row.count)
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize model
|
* Initialize model
|
||||||
* @param {import('../Database').sequelize} sequelize
|
* @param {import('../Database').sequelize} sequelize
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,7 @@ class BookScanner {
|
||||||
bookId: media.id,
|
bookId: media.id,
|
||||||
authorId: existingAuthorId
|
authorId: existingAuthorId
|
||||||
})
|
})
|
||||||
|
libraryScan.authorsNumBooksChangedIds.add(existingAuthorId)
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added author "${authorName}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added author "${authorName}"`)
|
||||||
authorsUpdated = true
|
authorsUpdated = true
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -238,6 +239,7 @@ class BookScanner {
|
||||||
})
|
})
|
||||||
await media.addAuthor(newAuthor)
|
await media.addAuthor(newAuthor)
|
||||||
Database.addAuthorToFilterData(libraryItemData.libraryId, newAuthor.name, newAuthor.id)
|
Database.addAuthorToFilterData(libraryItemData.libraryId, newAuthor.name, newAuthor.id)
|
||||||
|
SocketAuthority.emitter('author_added', newAuthor.toOldJSON())
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new author "${authorName}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new author "${authorName}"`)
|
||||||
authorsUpdated = true
|
authorsUpdated = true
|
||||||
}
|
}
|
||||||
|
|
@ -247,6 +249,7 @@ class BookScanner {
|
||||||
for (const author of media.authors) {
|
for (const author of media.authors) {
|
||||||
if (!bookMetadata.authors.includes(author.name)) {
|
if (!bookMetadata.authors.includes(author.name)) {
|
||||||
await author.bookAuthor.destroy()
|
await author.bookAuthor.destroy()
|
||||||
|
libraryScan.authorsNumBooksChangedIds.add(author.id)
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed author "${author.name}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed author "${author.name}"`)
|
||||||
authorsUpdated = true
|
authorsUpdated = true
|
||||||
bookAuthorsRemoved.push(author.id)
|
bookAuthorsRemoved.push(author.id)
|
||||||
|
|
@ -478,6 +481,7 @@ class BookScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdAtTimestamp = new Date().getTime()
|
const createdAtTimestamp = new Date().getTime()
|
||||||
|
const newAuthorNames = new Set()
|
||||||
if (bookMetadata.authors.length) {
|
if (bookMetadata.authors.length) {
|
||||||
for (const authorName of bookMetadata.authors) {
|
for (const authorName of bookMetadata.authors) {
|
||||||
const matchingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
|
const matchingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
|
||||||
|
|
@ -485,6 +489,8 @@ class BookScanner {
|
||||||
bookObject.bookAuthors.push({
|
bookObject.bookAuthors.push({
|
||||||
authorId: matchingAuthorId
|
authorId: matchingAuthorId
|
||||||
})
|
})
|
||||||
|
// Existing author — only numBooks changes; batch emit at scan end
|
||||||
|
libraryScan.authorsNumBooksChangedIds.add(matchingAuthorId)
|
||||||
} else {
|
} else {
|
||||||
// New author
|
// New author
|
||||||
bookObject.bookAuthors.push({
|
bookObject.bookAuthors.push({
|
||||||
|
|
@ -496,6 +502,7 @@ class BookScanner {
|
||||||
lastFirst: Database.authorModel.getLastFirst(authorName)
|
lastFirst: Database.authorModel.getLastFirst(authorName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
newAuthorNames.add(authorName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -593,6 +600,9 @@ class BookScanner {
|
||||||
for (const ba of libraryItem.book.bookAuthors) {
|
for (const ba of libraryItem.book.bookAuthors) {
|
||||||
if (ba.author) {
|
if (ba.author) {
|
||||||
Database.addAuthorToFilterData(libraryItemData.libraryId, ba.author.name, ba.author.id)
|
Database.addAuthorToFilterData(libraryItemData.libraryId, ba.author.name, ba.author.id)
|
||||||
|
if (newAuthorNames.has(ba.author.name)) {
|
||||||
|
SocketAuthority.emitter('author_added', ba.author.toOldJSON())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -891,6 +901,34 @@ class BookScanner {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit authors_num_books_updated for authors whose book count changed during a scan (linked or unlinked).
|
||||||
|
* Authors with no book links are omitted — orphans are handled by author_removed.
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @param {import('./LibraryScan')|import('./ScanLogger')} scanLogger
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
async emitAuthorsNumBooksUpdated(libraryId, scanLogger) {
|
||||||
|
if (!scanLogger.authorsNumBooksChangedIds?.size) return
|
||||||
|
|
||||||
|
const authorIds = [...scanLogger.authorsNumBooksChangedIds]
|
||||||
|
scanLogger.authorsNumBooksChangedIds.clear()
|
||||||
|
|
||||||
|
const countsByAuthorId = await Database.bookAuthorModel.getCountsForAuthors(authorIds)
|
||||||
|
|
||||||
|
const authors = Object.entries(countsByAuthorId).map(([id, numBooks]) => ({
|
||||||
|
id,
|
||||||
|
numBooks
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!authors.length) return
|
||||||
|
|
||||||
|
SocketAuthority.emitter('authors_num_books_updated', {
|
||||||
|
libraryId,
|
||||||
|
authors
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check authors that were removed from a book and remove them if they no longer have any books
|
* Check authors that were removed from a book and remove them if they no longer have any books
|
||||||
* keep authors without books that have a asin, description or imagePath
|
* keep authors without books that have a asin, description or imagePath
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class LibraryItemScanner {
|
||||||
if (libraryItemDataUpdated || wasUpdated) {
|
if (libraryItemDataUpdated || wasUpdated) {
|
||||||
SocketAuthority.libraryItemEmitter('item_updated', expandedLibraryItem)
|
SocketAuthority.libraryItemEmitter('item_updated', expandedLibraryItem)
|
||||||
|
|
||||||
await this.checkAuthorsAndSeriesRemovedFromBooks(library.id, scanLogger)
|
await this.finalizeAuthorsAndSeriesFromScan(library.id, scanLogger)
|
||||||
|
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
}
|
}
|
||||||
|
|
@ -76,12 +76,13 @@ class LibraryItemScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove empty authors and series
|
* Emit author book count updates, then remove empty authors/series left after the scan
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
* @param {ScanLogger} scanLogger
|
* @param {ScanLogger} scanLogger
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async checkAuthorsAndSeriesRemovedFromBooks(libraryId, scanLogger) {
|
async finalizeAuthorsAndSeriesFromScan(libraryId, scanLogger) {
|
||||||
|
await BookScanner.emitAuthorsNumBooksUpdated(libraryId, scanLogger)
|
||||||
if (scanLogger.authorsRemovedFromBooks.length) {
|
if (scanLogger.authorsRemovedFromBooks.length) {
|
||||||
await BookScanner.checkAuthorsRemovedFromBooks(libraryId, scanLogger)
|
await BookScanner.checkAuthorsRemovedFromBooks(libraryId, scanLogger)
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +216,10 @@ class LibraryItemScanner {
|
||||||
scanLogger.verbose = true
|
scanLogger.verbose = true
|
||||||
scanLogger.setData('libraryItem', libraryItemScanData.relPath)
|
scanLogger.setData('libraryItem', libraryItemScanData.relPath)
|
||||||
|
|
||||||
return this.scanNewLibraryItem(libraryItemScanData, library.settings, scanLogger)
|
const newLibraryItem = await this.scanNewLibraryItem(libraryItemScanData, library.settings, scanLogger)
|
||||||
|
// Watcher path does not go through full-library scan cleanup — emit author book count updates here
|
||||||
|
await BookScanner.emitAuthorsNumBooksUpdated(library.id, scanLogger)
|
||||||
|
return newLibraryItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new LibraryItemScanner()
|
module.exports = new LibraryItemScanner()
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ class LibraryScan {
|
||||||
|
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
this.authorsRemovedFromBooks = []
|
this.authorsRemovedFromBooks = []
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
this.authorsNumBooksChangedIds = new Set()
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
this.seriesRemovedFromBooks = []
|
this.seriesRemovedFromBooks = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,8 +234,7 @@ class LibraryScanner {
|
||||||
SocketAuthority.libraryItemsEmitter('items_updated', libraryItemsUpdated)
|
SocketAuthority.libraryItemsEmitter('items_updated', libraryItemsUpdated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authors and series that were removed from books should be removed if they are now empty
|
if (this.shouldCancelScan(libraryScan)) return true
|
||||||
await LibraryItemScanner.checkAuthorsAndSeriesRemovedFromBooks(libraryScan.libraryId, libraryScan)
|
|
||||||
|
|
||||||
// Update missing library items
|
// Update missing library items
|
||||||
if (libraryItemIdsMissing.length) {
|
if (libraryItemIdsMissing.length) {
|
||||||
|
|
@ -281,6 +280,9 @@ class LibraryScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Author book count updates and empty authors/series cleanup after all items are processed
|
||||||
|
await LibraryItemScanner.finalizeAuthorsAndSeriesFromScan(libraryScan.libraryId, libraryScan)
|
||||||
|
|
||||||
libraryScan.addLog(LogLevel.INFO, `Scan completed. ${libraryScan.resultStats}`)
|
libraryScan.addLog(LogLevel.INFO, `Scan completed. ${libraryScan.resultStats}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ class ScanLogger {
|
||||||
|
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
this.authorsRemovedFromBooks = []
|
this.authorsRemovedFromBooks = []
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
this.authorsNumBooksChangedIds = new Set()
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
this.seriesRemovedFromBooks = []
|
this.seriesRemovedFromBooks = []
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue