From db86bfd63d7011fdd1323d5a9c8325861f1b65b2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 1 Sep 2024 15:08:56 -0500 Subject: [PATCH] Fix:New authors not setting lastFirst column, updates for new Series model --- server/Database.js | 17 +- server/controllers/AuthorController.js | 8 + server/controllers/LibraryController.js | 5 +- server/controllers/RSSFeedController.js | 4 +- server/controllers/SeriesController.js | 23 +- server/managers/RssFeedManager.js | 6 +- server/models/Author.js | 11 + server/models/Series.js | 80 +++--- server/routers/ApiRouter.js | 21 +- server/scanner/BookScanner.js | 258 +++++++++--------- server/scanner/Scanner.js | 14 +- server/utils/queries/libraryFilters.js | 2 +- .../utils/queries/libraryItemsBookFilters.js | 8 +- server/utils/queries/seriesFilters.js | 2 +- 14 files changed, 234 insertions(+), 225 deletions(-) diff --git a/server/Database.js b/server/Database.js index 6628eb05..465ae9f7 100644 --- a/server/Database.js +++ b/server/Database.js @@ -447,21 +447,6 @@ class Database { return this.models.series.updateFromOld(oldSeries) } - async createSeries(oldSeries) { - if (!this.sequelize) return false - await this.models.series.createFromOld(oldSeries) - } - - async createBulkSeries(oldSeriesObjs) { - if (!this.sequelize) return false - await this.models.series.createBulkFromOld(oldSeriesObjs) - } - - async removeSeries(seriesId) { - if (!this.sequelize) return false - await this.models.series.removeById(seriesId) - } - async createBulkBookAuthors(bookAuthors) { if (!this.sequelize) return false await this.models.bookAuthor.bulkCreate(bookAuthors) @@ -678,7 +663,7 @@ class Database { */ async getSeriesIdByName(libraryId, seriesName) { if (!this.libraryFilterData[libraryId]) { - return (await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId))?.id || null + return (await this.seriesModel.getByNameAndLibrary(seriesName, libraryId))?.id || null } return this.libraryFilterData[libraryId].series.find((se) => se.name === seriesName)?.id || null } diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 7a46b9b8..54a64185 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -104,6 +104,9 @@ class AuthorController { let hasUpdated = false const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name + if (authorNameUpdate) { + payload.lastFirst = Database.authorModel.getLastFirst(payload.name) + } // Check if author name matches another author and merge the authors let existingAuthor = null @@ -169,6 +172,11 @@ class AuthorController { return } + // If lastFirst is not set, get it from the name + if (!authorNameUpdate && !req.author.lastFirst) { + payload.lastFirst = Database.authorModel.getLastFirst(req.author.name) + } + // Regular author update req.author.set(payload) if (req.author.changed()) { diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 9d6be80e..65243acc 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -629,11 +629,10 @@ class LibraryController { const series = await Database.seriesModel.findByPk(req.params.seriesId) if (!series) return res.sendStatus(404) - const oldSeries = series.getOldSeries() - const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.user) + const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user) - const seriesJson = oldSeries.toJSON() + const seriesJson = series.toOldJSON() if (include.includes('progress')) { const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.media.id)?.isFinished) seriesJson.progress = { diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index 5c7cc2a0..2e07c10e 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -125,7 +125,7 @@ class RSSFeedController { async openRSSFeedForSeries(req, res) { const options = req.body || {} - const series = await Database.seriesModel.getOldById(req.params.seriesId) + const series = await Database.seriesModel.findByPk(req.params.seriesId) if (!series) return res.sendStatus(404) // Check request body options exist @@ -140,7 +140,7 @@ class RSSFeedController { return res.status(400).send('Slug already in use') } - const seriesJson = series.toJSON() + const seriesJson = series.toOldJSON() // Get books in series that have audio tracks seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index 54b04538..1b5b6708 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -9,6 +9,11 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter * @property {import('../models/User')} user * * @typedef {Request & RequestUserObject} RequestWithUser + * + * @typedef RequestEntityObject + * @property {import('../models/Series')} series + * + * @typedef {RequestWithUser & RequestEntityObject} SeriesControllerRequest */ class SeriesController { @@ -21,7 +26,7 @@ class SeriesController { * TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead * Series are not library specific so we need to know what the library id is * - * @param {RequestWithUser} req + * @param {SeriesControllerRequest} req * @param {Response} res */ async findOne(req, res) { @@ -30,7 +35,7 @@ class SeriesController { .map((v) => v.trim()) .filter((v) => !!v) - const seriesJson = req.series.toJSON() + const seriesJson = req.series.toOldJSON() // Add progress map with isFinished flag if (include.includes('progress')) { @@ -54,17 +59,19 @@ class SeriesController { } /** + * TODO: Update to use new model * - * @param {RequestWithUser} req + * @param {SeriesControllerRequest} req * @param {Response} res */ async update(req, res) { - const hasUpdated = req.series.update(req.body) + const oldSeries = req.series.getOldSeries() + const hasUpdated = oldSeries.update(req.body) if (hasUpdated) { - await Database.updateSeries(req.series) - SocketAuthority.emitter('series_updated', req.series.toJSON()) + await Database.updateSeries(oldSeries) + SocketAuthority.emitter('series_updated', oldSeries.toJSON()) } - res.json(req.series.toJSON()) + res.json(oldSeries.toJSON()) } /** @@ -74,7 +81,7 @@ class SeriesController { * @param {NextFunction} next */ async middleware(req, res, next) { - const series = await Database.seriesModel.getOldById(req.params.id) + const series = await Database.seriesModel.findByPk(req.params.id) if (!series) return res.sendStatus(404) /** diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 35ce4e1f..7716440d 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -25,7 +25,7 @@ class RssFeedManager { return false } } else if (feedObj.entityType === 'series') { - const series = await Database.seriesModel.getOldById(feedObj.entityId) + const series = await Database.seriesModel.findByPk(feedObj.entityId) if (!series) { Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`) return false @@ -133,9 +133,9 @@ class RssFeedManager { } } } else if (feed.entityType === 'series') { - const series = await Database.seriesModel.getOldById(feed.entityId) + const series = await Database.seriesModel.findByPk(feed.entityId) if (series) { - const seriesJson = series.toJSON() + const seriesJson = series.toOldJSON() // Get books in series that have audio tracks seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) diff --git a/server/models/Author.js b/server/models/Author.js index 1668d9e7..f3bbba57 100644 --- a/server/models/Author.js +++ b/server/models/Author.js @@ -1,4 +1,5 @@ const { DataTypes, Model, where, fn, col } = require('sequelize') +const parseNameString = require('../utils/parsers/parseNameString') class Author extends Model { constructor(values, options) { @@ -24,6 +25,16 @@ class Author extends Model { this.createdAt } + /** + * + * @param {string} name + * @returns {string} + */ + static getLastFirst(name) { + if (!name) return null + return parseNameString.nameToLastFirst(name) + } + /** * Check if author exists * @param {string} authorId diff --git a/server/models/Series.js b/server/models/Series.js index 9f8f1c56..493de150 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -1,6 +1,7 @@ const { DataTypes, Model, where, fn, col } = require('sequelize') const oldSeries = require('../objects/entities/Series') +const { getTitlePrefixAtEnd } = require('../utils/index') class Series extends Model { constructor(values, options) { @@ -22,11 +23,6 @@ class Series extends Model { this.updatedAt } - static async getAllOldSeries() { - const series = await this.findAll() - return series.map((se) => se.getOldSeries()) - } - getOldSeries() { return new oldSeries({ id: this.id, @@ -47,16 +43,6 @@ class Series extends Model { }) } - static createFromOld(oldSeries) { - const series = this.getFromOld(oldSeries) - return this.create(series) - } - - static createBulkFromOld(oldSeriesObjs) { - const series = oldSeriesObjs.map(this.getFromOld) - return this.bulkCreate(series) - } - static getFromOld(oldSeries) { return { id: oldSeries.id, @@ -67,25 +53,6 @@ class Series extends Model { } } - static removeById(seriesId) { - return this.destroy({ - where: { - id: seriesId - } - }) - } - - /** - * Get oldSeries by id - * @param {string} seriesId - * @returns {Promise} - */ - static async getOldById(seriesId) { - const series = await this.findByPk(seriesId) - if (!series) return null - return series.getOldSeries() - } - /** * Check if series exists * @param {string} seriesId @@ -96,24 +63,21 @@ class Series extends Model { } /** - * Get old series by name and libraryId. name case insensitive + * Get series by name and libraryId. name case insensitive * * @param {string} seriesName * @param {string} libraryId - * @returns {Promise} + * @returns {Promise} */ - static async getOldByNameAndLibrary(seriesName, libraryId) { - const series = ( - await this.findOne({ - where: [ - where(fn('lower', col('name')), seriesName.toLowerCase()), - { - libraryId - } - ] - }) - )?.getOldSeries() - return series + static async getByNameAndLibrary(seriesName, libraryId) { + return this.findOne({ + where: [ + where(fn('lower', col('name')), seriesName.toLowerCase()), + { + libraryId + } + ] + }) } /** @@ -163,6 +127,26 @@ class Series extends Model { }) Series.belongsTo(library) } + + toOldJSON() { + return { + id: this.id, + name: this.name, + nameIgnorePrefix: getTitlePrefixAtEnd(this.name), + description: this.description, + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf(), + libraryId: this.libraryId + } + } + + toJSONMinimal(sequence) { + return { + id: this.id, + name: this.name, + sequence + } + } } module.exports = Series diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 484377e0..f44fedb4 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -33,7 +33,7 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP const MiscController = require('../controllers/MiscController') const ShareController = require('../controllers/ShareController') -const Series = require('../objects/entities/Series') +const { getTitleIgnorePrefix } = require('../utils/index') class ApiRouter { constructor(Server) { @@ -524,13 +524,15 @@ class ApiRouter { async removeEmptySeries(series) { await this.rssFeedManager.closeFeedForEntityId(series.id) Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`) - await Database.removeSeries(series.id) + // Remove series from library filter data Database.removeSeriesFromFilterData(series.libraryId, series.id) SocketAuthority.emitter('series_removed', { id: series.id, libraryId: series.libraryId }) + + await series.destroy() } async getUserListeningSessionsHelper(userId) { @@ -619,6 +621,7 @@ class ApiRouter { if (!author) { author = await Database.authorModel.create({ name: authorName, + lastFirst: Database.authorModel.getLastFirst(authorName), libraryId }) Logger.debug(`[ApiRouter] Creating new author "${author.name}"`) @@ -663,11 +666,14 @@ class ApiRouter { } if (!mediaMetadata.series[i].id) { - let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId) + let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesName, libraryId) if (!seriesItem) { - seriesItem = new Series() - seriesItem.setData(mediaMetadata.series[i], libraryId) - Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`) + seriesItem = await Database.seriesModel.create({ + name: seriesName, + nameIgnorePrefix: getTitleIgnorePrefix(seriesName), + libraryId + }) + Logger.debug(`[ApiRouter] Creating new series "${seriesItem.name}"`) newSeries.push(seriesItem) // Update filter data Database.addSeriesToFilterData(libraryId, seriesItem.name, seriesItem.id) @@ -680,10 +686,9 @@ class ApiRouter { // Remove series without an id mediaMetadata.series = mediaMetadata.series.filter((se) => se.id) if (newSeries.length) { - await Database.createBulkSeries(newSeries) SocketAuthority.emitter( 'multiple_series_added', - newSeries.map((se) => se.toJSON()) + newSeries.map((se) => se.toOldJSON()) ) } } diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index c12441b2..07f817a8 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -1,4 +1,4 @@ -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 const Path = require('path') const sequelize = require('sequelize') const { LogLevel } = require('../utils/constants') @@ -13,14 +13,14 @@ const AudioFile = require('../objects/files/AudioFile') const CoverManager = require('../managers/CoverManager') const LibraryFile = require('../objects/files/LibraryFile') const SocketAuthority = require('../SocketAuthority') -const fsExtra = require("../libs/fsExtra") +const fsExtra = require('../libs/fsExtra') const BookFinder = require('../finders/BookFinder') -const LibraryScan = require("./LibraryScan") +const LibraryScan = require('./LibraryScan') const OpfFileScanner = require('./OpfFileScanner') const NfoFileScanner = require('./NfoFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') -const EBookFile = require("../objects/files/EBookFile") +const EBookFile = require('../objects/files/EBookFile') /** * Metadata for books pulled from files @@ -46,13 +46,13 @@ const EBookFile = require("../objects/files/EBookFile") */ class BookScanner { - constructor() { } + constructor() {} /** - * @param {import('../models/LibraryItem')} existingLibraryItem - * @param {import('./LibraryItemScanData')} libraryItemData + * @param {import('../models/LibraryItem')} existingLibraryItem + * @param {import('./LibraryItemScanData')} libraryItemData * @param {import('../models/Library').LibrarySettingsObject} librarySettings - * @param {LibraryScan} libraryScan + * @param {LibraryScan} libraryScan * @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>} */ async rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) { @@ -81,19 +81,23 @@ class BookScanner { let hasMediaChanges = libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== media.audioFiles.length if (hasMediaChanges) { // Filter out audio files that were removed - media.audioFiles = media.audioFiles.filter(af => !libraryItemData.checkAudioFileRemoved(af)) + media.audioFiles = media.audioFiles.filter((af) => !libraryItemData.checkAudioFileRemoved(af)) // Update audio files that were modified if (libraryItemData.audioLibraryFilesModified.length) { - let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans( + existingLibraryItem.mediaType, + libraryItemData, + libraryItemData.audioLibraryFilesModified.map((lf) => lf.new) + ) media.audioFiles = media.audioFiles.map((audioFileObj) => { - let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path) + let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === audioFileObj.metadata.path) if (!matchedScannedAudioFile) { - matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === audioFileObj.ino) + matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === audioFileObj.ino) } if (matchedScannedAudioFile) { - scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile) + scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile) const audioFile = new AudioFile(audioFileObj) audioFile.updateFromScan(matchedScannedAudioFile) return audioFile.toJSON() @@ -115,7 +119,7 @@ class BookScanner { // Add audio library files that are not already set on the book (safety check) let audioLibraryFilesToAdd = [] for (const audioLibraryFile of libraryItemData.audioLibraryFiles) { - if (!media.audioFiles.some(af => af.ino === audioLibraryFile.ino)) { + if (!media.audioFiles.some((af) => af.ino === audioLibraryFile.ino)) { libraryScan.addLog(LogLevel.DEBUG, `Existing audio library file "${audioLibraryFile.metadata.relPath}" was not set on book "${media.title}" so setting it now`) audioLibraryFilesToAdd.push(audioLibraryFile) @@ -139,14 +143,14 @@ class BookScanner { } // Check if cover was removed - if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { + if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some((lf) => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { media.coverPath = null hasMediaChanges = true } // Update cover if it was modified if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { - let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) + let coverMatch = libraryItemData.imageLibraryFilesModified.find((iFile) => iFile.old.metadata.path === media.coverPath) if (coverMatch) { const coverPath = coverMatch.new.metadata.path if (coverPath !== media.coverPath) { @@ -161,7 +165,7 @@ class BookScanner { // Check if cover is not set and image files were found if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { // Prefer using a cover image with the name "cover" otherwise use the first image - const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) + const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path hasMediaChanges = true } @@ -174,7 +178,7 @@ class BookScanner { // Update ebook if it was modified if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) { - let ebookMatch = libraryItemData.ebookLibraryFilesModified.find(eFile => eFile.old.metadata.path === media.ebookFile.metadata.path) + let ebookMatch = libraryItemData.ebookLibraryFilesModified.find((eFile) => eFile.old.metadata.path === media.ebookFile.metadata.path) if (ebookMatch) { const ebookFile = new EBookFile(ebookMatch.new) ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase() @@ -188,7 +192,7 @@ class BookScanner { // Check if ebook is not set and ebooks were found if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) { // Prefer to use an epub ebook then fallback to the first ebook found - let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') + let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub') if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0] ebookLibraryFile = ebookLibraryFile.toJSON() // Ebook file is the same as library file except for additional `ebookFormat` @@ -213,7 +217,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)) { const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName) if (existingAuthorId) { await Database.bookAuthorModel.create({ @@ -225,7 +229,7 @@ class BookScanner { } else { const newAuthor = await Database.authorModel.create({ name: authorName, - lastFirst: parseNameString.nameToLastFirst(authorName), + lastFirst: Database.authorModel.getLastFirst(authorName), libraryId: libraryItemData.libraryId }) await media.addAuthor(newAuthor) @@ -247,7 +251,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) if (!existingBookSeries) { const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name) if (existingSeriesId) { @@ -278,7 +282,7 @@ class BookScanner { } // Check for series removed for (const series of media.series) { - if (!bookMetadata.series.some(se => se.name === series.name)) { + if (!bookMetadata.series.some((se) => se.name === series.name)) { await series.bookSeries.destroy() libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"`) seriesUpdated = true @@ -287,21 +291,21 @@ class BookScanner { } } else if (key === 'genres') { const existingGenres = media.genres || [] - if (bookMetadata.genres.some(g => !existingGenres.includes(g)) || existingGenres.some(g => !bookMetadata.genres.includes(g))) { + if (bookMetadata.genres.some((g) => !existingGenres.includes(g)) || existingGenres.some((g) => !bookMetadata.genres.includes(g))) { libraryScan.addLog(LogLevel.DEBUG, `Updating book genres "${existingGenres.join(',')}" => "${bookMetadata.genres.join(',')}" for book "${bookMetadata.title}"`) media.genres = bookMetadata.genres hasMediaChanges = true } } else if (key === 'tags') { const existingTags = media.tags || [] - if (bookMetadata.tags.some(t => !existingTags.includes(t)) || existingTags.some(t => !bookMetadata.tags.includes(t))) { + if (bookMetadata.tags.some((t) => !existingTags.includes(t)) || existingTags.some((t) => !bookMetadata.tags.includes(t))) { libraryScan.addLog(LogLevel.DEBUG, `Updating book tags "${existingTags.join(',')}" => "${bookMetadata.tags.join(',')}" for book "${bookMetadata.title}"`) media.tags = bookMetadata.tags hasMediaChanges = true } } else if (key === 'narrators') { const existingNarrators = media.narrators || [] - if (bookMetadata.narrators.some(t => !existingNarrators.includes(t)) || existingNarrators.some(t => !bookMetadata.narrators.includes(t))) { + if (bookMetadata.narrators.some((t) => !existingNarrators.includes(t)) || existingNarrators.some((t) => !bookMetadata.narrators.includes(t))) { libraryScan.addLog(LogLevel.DEBUG, `Updating book narrators "${existingNarrators.join(',')}" => "${bookMetadata.narrators.join(',')}" for book "${bookMetadata.title}"`) media.narrators = bookMetadata.narrators hasMediaChanges = true @@ -333,17 +337,13 @@ class BookScanner { if (authorsUpdated) { media.authors = await media.getAuthors({ joinTableAttributes: ['createdAt'], - order: [ - sequelize.literal(`bookAuthor.createdAt ASC`) - ] + order: [sequelize.literal(`bookAuthor.createdAt ASC`)] }) } if (seriesUpdated) { media.series = await media.getSeries({ joinTableAttributes: ['sequence', 'createdAt'], - order: [ - sequelize.literal(`bookSeries.createdAt ASC`) - ] + order: [sequelize.literal(`bookSeries.createdAt ASC`)] }) } @@ -367,7 +367,10 @@ class BookScanner { // If no cover then search for cover if enabled in server settings if (!media.coverPath && Database.serverSettings.scannerFindCovers) { - const authorName = media.authors.map(au => au.name).filter(au => au).join(', ') + const authorName = media.authors + .map((au) => au.name) + .filter((au) => au) + .join(', ') const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan) if (coverPath) { media.coverPath = coverPath @@ -428,10 +431,10 @@ class BookScanner { } /** - * - * @param {import('./LibraryItemScanData')} libraryItemData + * + * @param {import('./LibraryItemScanData')} libraryItemData * @param {import('../models/Library').LibrarySettingsObject} librarySettings - * @param {LibraryScan} libraryScan + * @param {LibraryScan} libraryScan * @returns {Promise} */ async scanNewBookLibraryItem(libraryItemData, librarySettings, libraryScan) { @@ -440,7 +443,7 @@ class BookScanner { scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles) // Find ebook file (prefer epub) - let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0] + let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0] // Do not add library items that have no valid audio files and no ebook file if (!ebookLibraryFile && !scannedAudioFiles.length) { @@ -460,7 +463,7 @@ class BookScanner { bookMetadata.abridged = !!bookMetadata.abridged // Ensure boolean let duration = 0 - scannedAudioFiles.forEach((af) => duration += (!isNaN(af.duration) ? Number(af.duration) : 0)) + scannedAudioFiles.forEach((af) => (duration += !isNaN(af.duration) ? Number(af.duration) : 0)) const bookObject = { ...bookMetadata, audioFiles: scannedAudioFiles, @@ -482,7 +485,7 @@ class BookScanner { author: { libraryId: libraryItemData.libraryId, name: authorName, - lastFirst: parseNameString.nameToLastFirst(authorName) + lastFirst: Database.authorModel.getLastFirst(authorName) } }) } @@ -619,11 +622,11 @@ class BookScanner { } /** - * - * @param {import('../models/Book').AudioFileObject[]} audioFiles + * + * @param {import('../models/Book').AudioFileObject[]} audioFiles * @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData - * @param {import('./LibraryItemScanData')} libraryItemData - * @param {LibraryScan} libraryScan + * @param {import('./LibraryItemScanData')} libraryItemData + * @param {LibraryScan} libraryScan * @param {import('../models/Library').LibrarySettingsObject} librarySettings * @param {string} [existingLibraryItemId] * @returns {Promise} @@ -664,7 +667,7 @@ class BookScanner { // Set cover from library file if one is found otherwise check audiofile if (libraryItemData.imageLibraryFiles.length) { - const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) + const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) bookMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path } @@ -673,16 +676,15 @@ class BookScanner { return bookMetadata } - static BookMetadataSourceHandler = class { /** - * - * @param {Object} bookMetadata - * @param {import('../models/Book').AudioFileObject[]} audioFiles + * + * @param {Object} bookMetadata + * @param {import('../models/Book').AudioFileObject[]} audioFiles * @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData - * @param {import('./LibraryItemScanData')} libraryItemData - * @param {LibraryScan} libraryScan - * @param {string} existingLibraryItemId + * @param {import('./LibraryItemScanData')} libraryItemData + * @param {LibraryScan} libraryScan + * @param {string} existingLibraryItemId */ constructor(bookMetadata, audioFiles, ebookFileScanData, libraryItemData, libraryScan, existingLibraryItemId) { this.bookMetadata = bookMetadata @@ -785,8 +787,8 @@ class BookScanner { } /** - * - * @param {import('../models/LibraryItem')} libraryItem + * + * @param {import('../models/LibraryItem')} libraryItem * @param {LibraryScan} libraryScan * @returns {Promise} */ @@ -805,12 +807,12 @@ class BookScanner { const jsonObject = { tags: libraryItem.media.tags || [], - chapters: libraryItem.media.chapters?.map(c => ({ ...c })) || [], + chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [], title: libraryItem.media.title, subtitle: libraryItem.media.subtitle, - authors: libraryItem.media.authors.map(a => a.name), + authors: libraryItem.media.authors.map((a) => a.name), narrators: libraryItem.media.narrators, - series: libraryItem.media.series.map(se => { + series: libraryItem.media.series.map((se) => { const sequence = se.bookSeries?.sequence || '' if (!sequence) return se.name return `${se.name} #${sequence}` @@ -826,70 +828,75 @@ class BookScanner { explicit: !!libraryItem.media.explicit, abridged: !!libraryItem.media.abridged } - return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { - // Add metadata.json to libraryFiles array if it is new - let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - const newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) - metadataLibraryFile = newLibraryFile.toJSON() - libraryItem.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino + return fsExtra + .writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)) + .then(async () => { + // Add metadata.json to libraryFiles array if it is new + let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath)) + if (storeMetadataWithItem) { + if (!metadataLibraryFile) { + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + metadataLibraryFile = newLibraryFile.toJSON() + libraryItem.libraryFiles.push(metadataLibraryFile) + } else { + const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) + if (fileTimestamps) { + metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs + metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs + metadataLibraryFile.metadata.size = fileTimestamps.size + metadataLibraryFile.ino = fileTimestamps.ino + } + } + const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) + if (libraryItemDirTimestamps) { + libraryItem.mtime = libraryItemDirTimestamps.mtimeMs + libraryItem.ctime = libraryItemDirTimestamps.ctimeMs + let size = 0 + libraryItem.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + libraryItem.size = size } } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) - if (libraryItemDirTimestamps) { - libraryItem.mtime = libraryItemDirTimestamps.mtimeMs - libraryItem.ctime = libraryItemDirTimestamps.ctimeMs - let size = 0 - libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) - libraryItem.size = size - } - } - libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) + libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) - return metadataLibraryFile - }).catch((error) => { - libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) - return null - }) + return metadataLibraryFile + }) + .catch((error) => { + libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) + return null + }) } /** * 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 - * @param {string} libraryId - * @param {import('./ScanLogger')} scanLogger + * @param {string} libraryId + * @param {import('./ScanLogger')} scanLogger * @returns {Promise} */ async checkAuthorsRemovedFromBooks(libraryId, scanLogger) { - const bookAuthorsToRemove = (await Database.authorModel.findAll({ - where: [ - { - id: scanLogger.authorsRemovedFromBooks, - asin: { - [sequelize.Op.or]: [null, ""] + const bookAuthorsToRemove = ( + await Database.authorModel.findAll({ + where: [ + { + id: scanLogger.authorsRemovedFromBooks, + asin: { + [sequelize.Op.or]: [null, ''] + }, + description: { + [sequelize.Op.or]: [null, ''] + }, + imagePath: { + [sequelize.Op.or]: [null, ''] + } }, - description: { - [sequelize.Op.or]: [null, ""] - }, - imagePath: { - [sequelize.Op.or]: [null, ""] - } - }, - sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0) - ], - attributes: ['id'], - raw: true - })).map(au => au.id) + sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0) + ], + attributes: ['id'], + raw: true + }) + ).map((au) => au.id) if (bookAuthorsToRemove.length) { await Database.authorModel.destroy({ where: { @@ -907,21 +914,23 @@ class BookScanner { /** * Check series that were removed from books and remove them if they no longer have any books - * @param {string} libraryId - * @param {import('./ScanLogger')} scanLogger + * @param {string} libraryId + * @param {import('./ScanLogger')} scanLogger * @returns {Promise} */ async checkSeriesRemovedFromBooks(libraryId, scanLogger) { - const bookSeriesToRemove = (await Database.seriesModel.findAll({ - where: [ - { - id: scanLogger.seriesRemovedFromBooks - }, - sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0) - ], - attributes: ['id'], - raw: true - })).map(se => se.id) + const bookSeriesToRemove = ( + await Database.seriesModel.findAll({ + where: [ + { + id: scanLogger.seriesRemovedFromBooks + }, + sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0) + ], + attributes: ['id'], + raw: true + }) + ).map((se) => se.id) if (bookSeriesToRemove.length) { await Database.seriesModel.destroy({ where: { @@ -938,11 +947,11 @@ class BookScanner { /** * Search cover provider for matching cover - * @param {string} libraryItemId + * @param {string} libraryItemId * @param {string} libraryItemPath null if book isFile - * @param {string} title - * @param {string} author - * @param {LibraryScan} libraryScan + * @param {string} title + * @param {string} author + * @param {LibraryScan} libraryScan * @returns {Promise} path to downloaded cover or null if no cover found */ async searchForCover(libraryItemId, libraryItemPath, title, author, libraryScan) { @@ -956,7 +965,6 @@ class BookScanner { // If the first cover result fails, attempt to download the second for (let i = 0; i < results.length && i < 2; i++) { - // Downloads and updates the book cover const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath) @@ -970,4 +978,4 @@ class BookScanner { return null } } -module.exports = new BookScanner() \ No newline at end of file +module.exports = new BookScanner() diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 5508ff18..69230dae 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -8,7 +8,6 @@ const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcast const BookFinder = require('../finders/BookFinder') const PodcastFinder = require('../finders/PodcastFinder') const LibraryScan = require('./LibraryScan') -const Series = require('../objects/entities/Series') const LibraryScanner = require('./LibraryScanner') const CoverManager = require('../managers/CoverManager') const TaskManager = require('../managers/TaskManager') @@ -209,6 +208,7 @@ class Scanner { if (!author) { author = await Database.authorModel.create({ name: authorName, + lastFirst: Database.authorModel.getLastFirst(authorName), libraryId: libraryItem.libraryId }) SocketAuthority.emitter('author_added', author.toOldJSON()) @@ -225,14 +225,16 @@ class Scanner { if (!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, sequence: matchData.sequence }] const seriesPayload = [] for (const seriesMatchItem of matchData.series) { - let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId) + let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId) if (!seriesItem) { - seriesItem = new Series() - seriesItem.setData({ name: seriesMatchItem.series }, libraryItem.libraryId) - await Database.createSeries(seriesItem) + seriesItem = await Database.seriesModel.create({ + name: seriesMatchItem.series, + nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series), + libraryId + }) // Update filter data Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id) - SocketAuthority.emitter('series_added', seriesItem.toJSON()) + SocketAuthority.emitter('series_added', seriesItem.toOldJSON()) } seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence)) } diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 39c50856..a8b0cc46 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -276,7 +276,7 @@ module.exports = { const allOldSeries = [] for (const s of series) { - const oldSeries = s.getOldSeries().toJSON() + const oldSeries = s.toOldJSON() if (s.feeds?.length) { oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified() diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 094095ce..ae1ccc03 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -954,12 +954,12 @@ module.exports = { /** * Get library items for series - * @param {import('../../objects/entities/Series')} oldSeries + * @param {import('../../models/Series')} series * @param {import('../../models/User')} [user] * @returns {Promise} */ - async getLibraryItemsForSeries(oldSeries, user) { - const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, user, 'series', oldSeries.id, null, null, false, [], null, null) + async getLibraryItemsForSeries(series, user) { + const { libraryItems } = await this.getFilteredLibraryItems(series.libraryId, user, 'series', series.id, null, null, false, [], null, null) return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li)) }, @@ -1130,7 +1130,7 @@ module.exports = { return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON() }) seriesMatches.push({ - series: series.getOldSeries().toJSON(), + series: series.toOldJSON(), books }) } diff --git a/server/utils/queries/seriesFilters.js b/server/utils/queries/seriesFilters.js index b7afcf85..06ca2547 100644 --- a/server/utils/queries/seriesFilters.js +++ b/server/utils/queries/seriesFilters.js @@ -171,7 +171,7 @@ module.exports = { // Map series to old series const allOldSeries = [] for (const s of series) { - const oldSeries = s.getOldSeries().toJSON() + const oldSeries = s.toOldJSON() if (s.dataValues.totalDuration) { oldSeries.totalDuration = s.dataValues.totalDuration