diff --git a/server/Database.js b/server/Database.js index d3966e922b..a3959ccd4f 100644 --- a/server/Database.js +++ b/server/Database.js @@ -384,11 +384,6 @@ class Database { return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook))) } - updateLibrary(oldLibrary) { - if (!this.sequelize) return false - return this.models.library.updateFromOld(oldLibrary) - } - removeLibrary(libraryId) { if (!this.sequelize) return false return this.models.library.removeById(libraryId) diff --git a/server/Server.js b/server/Server.js index 0110ab6a70..ac30ba5270 100644 --- a/server/Server.js +++ b/server/Server.js @@ -142,7 +142,7 @@ class Server { await this.backupManager.init() await this.rssFeedManager.init() - const libraries = await Database.libraryModel.getAllOldLibraries() + const libraries = await Database.libraryModel.getAllWithFolders() await this.cronManager.init(libraries) this.apiCacheManager.init() diff --git a/server/Watcher.js b/server/Watcher.js index cb8b030f4a..999437d805 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -45,6 +45,10 @@ class FolderWatcher extends EventEmitter { return this.pendingFileUpdates.map((f) => f.path) } + /** + * + * @param {import('./models/Library')} library + */ buildLibraryWatcher(library) { if (this.libraryWatchers.find((w) => w.id === library.id)) { Logger.warn('[Watcher] Already watching library', library.name) @@ -52,7 +56,7 @@ class FolderWatcher extends EventEmitter { } Logger.info(`[Watcher] Initializing watcher for "${library.name}".`) - const folderPaths = library.folderPaths + const folderPaths = library.libraryFolders.map((f) => f.path) folderPaths.forEach((fp) => { Logger.debug(`[Watcher] Init watcher for library folder path "${fp}"`) }) @@ -90,12 +94,16 @@ class FolderWatcher extends EventEmitter { this.libraryWatchers.push({ id: library.id, name: library.name, - folders: library.folders, - paths: library.folderPaths, + libraryFolders: library.libraryFolders, + paths: folderPaths, watcher }) } + /** + * + * @param {import('./models/Library')[]} libraries + */ initWatcher(libraries) { libraries.forEach((lib) => { if (!lib.settings.disableWatcher) { @@ -104,12 +112,17 @@ class FolderWatcher extends EventEmitter { }) } + /** + * + * @param {import('./models/Library')} library + */ addLibrary(library) { if (this.disabled || library.settings.disableWatcher) return this.buildLibraryWatcher(library) } /** + * TODO: Update to new library model * * @param {import('./objects/Library')} library */ @@ -129,8 +142,9 @@ class FolderWatcher extends EventEmitter { libwatcher.name = library.name // If any folder paths were added or removed then re-init watcher - const pathsToAdd = library.folderPaths.filter((path) => !libwatcher.paths.includes(path)) - const pathsRemoved = libwatcher.paths.filter((path) => !library.folderPaths.includes(path)) + const folderPaths = library.libraryFolders.map((f) => f.path) + const pathsToAdd = folderPaths.filter((path) => !libwatcher.paths.includes(path)) + const pathsRemoved = libwatcher.paths.filter((path) => !folderPaths.includes(path)) if (pathsToAdd.length || pathsRemoved.length) { Logger.info(`[Watcher] Re-Initializing watcher for "${library.name}".`) @@ -145,6 +159,10 @@ class FolderWatcher extends EventEmitter { } } + /** + * + * @param {import('./models/Library')} library + */ removeLibrary(library) { if (this.disabled) return var libwatcher = this.libraryWatchers.find((lib) => lib.id === library.id) @@ -255,15 +273,15 @@ class FolderWatcher extends EventEmitter { } // Get file folder - const folder = libwatcher.folders.find((fold) => isSameOrSubPath(fold.fullPath, path)) + const folder = libwatcher.libraryFolders.find((fold) => isSameOrSubPath(fold.path, path)) if (!folder) { Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`) return } - const folderFullPath = filePathToPOSIX(folder.fullPath) + const folderPath = filePathToPOSIX(folder.path) - const relPath = path.replace(folderFullPath, '') + const relPath = path.replace(folderPath, '') if (Path.extname(relPath).toLowerCase() === '.part') { Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 31e6e2da1c..13b82ad402 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -4,7 +4,6 @@ const Path = require('path') const fs = require('../libs/fsExtra') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') -const Library = require('../objects/Library') const libraryHelpers = require('../utils/libraryHelpers') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') const libraryItemFilters = require('../utils/queries/libraryItemFilters') @@ -158,7 +157,7 @@ class LibraryController { SocketAuthority.emitter('library_added', oldLibrary.toJSON(), userFilter) // Add library watcher - this.watcher.addLibrary(oldLibrary) + this.watcher.addLibrary(library) res.json(oldLibrary) } @@ -190,15 +189,16 @@ class LibraryController { const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id) const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType) + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) return res.json({ filterdata, issues: filterdata.numIssues, numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), customMetadataProviders, - library: req.library + library: oldLibrary }) } - res.json(req.library) + res.json(oldLibrary) } /** @@ -220,7 +220,7 @@ class LibraryController { */ async update(req, res) { /** @type {import('../objects/Library')} */ - const library = req.library + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) // Validate that the custom provider exists if given any if (req.body.provider?.startsWith('custom-')) { @@ -259,7 +259,7 @@ class LibraryController { } // Handle removing folders - for (const folder of library.folders) { + for (const folder of oldLibrary.folders) { if (!req.body.folders.some((f) => f.id === folder.id)) { // Remove library items in folder const libraryItemsInFolder = await Database.libraryItemModel.findAll({ @@ -278,10 +278,10 @@ class LibraryController { } ] }) - Logger.info(`[LibraryController] Removed folder "${folder.fullPath}" from library "${library.name}" with ${libraryItemsInFolder.length} library items`) + Logger.info(`[LibraryController] Removed folder "${folder.fullPath}" from library "${oldLibrary.name}" with ${libraryItemsInFolder.length} library items`) for (const libraryItem of libraryItemsInFolder) { let mediaItemIds = [] - if (library.isPodcast) { + if (oldLibrary.isPodcast) { mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) } else { mediaItemIds.push(libraryItem.mediaId) @@ -293,26 +293,27 @@ class LibraryController { } } - const hasUpdates = library.update(req.body) + const hasUpdates = oldLibrary.update(req.body) // TODO: Should check if this is an update to folder paths or name only if (hasUpdates) { - // Update watcher - this.watcher.updateLibrary(library) - // Update auto scan cron - this.cronManager.updateLibraryScanCron(library) + this.cronManager.updateLibraryScanCron(oldLibrary) - await Database.updateLibrary(library) + const updatedLibrary = await Database.libraryModel.updateFromOld(oldLibrary) + updatedLibrary.libraryFolders = await updatedLibrary.getLibraryFolders() + + // Update watcher + this.watcher.updateLibrary(updatedLibrary) // Only emit to users with access to library const userFilter = (user) => { - return user.checkCanAccessLibrary?.(library.id) + return user.checkCanAccessLibrary?.(oldLibrary.id) } - SocketAuthority.emitter('library_updated', library.toJSON(), userFilter) + SocketAuthority.emitter('library_updated', oldLibrary.toJSON(), userFilter) - await Database.resetLibraryIssuesFilterData(library.id) + await Database.resetLibraryIssuesFilterData(oldLibrary.id) } - return res.json(library.toJSON()) + return res.json(oldLibrary.toJSON()) } /** @@ -323,10 +324,10 @@ class LibraryController { * @param {Response} res */ async delete(req, res) { - const library = req.library + const library = Database.libraryModel.getOldLibrary(req.library) // Remove library watcher - this.watcher.removeLibrary(library) + this.watcher.removeLibrary(req.library) // Remove collections for library const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(library.id) @@ -386,6 +387,8 @@ class LibraryController { * @param {Response} res */ async getLibraryItems(req, res) { + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) + const include = (req.query.include || '') .split(',') .map((v) => v.trim().toLowerCase()) @@ -411,9 +414,9 @@ class LibraryController { const filterByValue = filterByGroup ? libraryFilters.decode(payload.filterBy.replace(`${filterByGroup}.`, '')) : null if (filterByGroup === 'series' && filterByValue !== 'no-series' && payload.collapseseries) { const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1]) - payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, req.library) + payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, oldLibrary) } else { - const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.user, payload) + const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(oldLibrary, req.user, payload) payload.results = libraryItems payload.total = count } @@ -461,7 +464,7 @@ class LibraryController { Logger.info(`[LibraryController] Removing ${libraryItemsWithIssues.length} items with issues`) for (const libraryItem of libraryItemsWithIssues) { let mediaItemIds = [] - if (req.library.isPodcast) { + if (req.library.mediaType === 'podcast') { mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) } else { mediaItemIds.push(libraryItem.mediaId) @@ -486,6 +489,8 @@ class LibraryController { * @param {Response} res */ async getAllSeriesForLibrary(req, res) { + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) + const include = (req.query.include || '') .split(',') .map((v) => v.trim().toLowerCase()) @@ -504,7 +509,7 @@ class LibraryController { } const offset = payload.page * payload.limit - const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.user, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset) + const { series, count } = await seriesFilters.getFilteredSeries(oldLibrary, req.user, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset) payload.total = count payload.results = series @@ -635,12 +640,13 @@ class LibraryController { * @param {Response} res */ async getUserPersonalizedShelves(req, res) { + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10 const include = (req.query.include || '') .split(',') .map((v) => v.trim().toLowerCase()) .filter((v) => !!v) - const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.user, include, limitPerShelf) + const shelves = await Database.libraryItemModel.getPersonalizedShelves(oldLibrary, req.user, include, limitPerShelf) res.json(shelves) } @@ -668,7 +674,7 @@ class LibraryController { } if (library.update({ displayOrder: orderdata[i].newOrder })) { hasUpdates = true - await Database.updateLibrary(library) + await Database.libraryModel.updateFromOld(library) } } @@ -696,10 +702,11 @@ class LibraryController { if (!req.query.q || typeof req.query.q !== 'string') { return res.status(400).send('Invalid request. Query param "q" must be a string') } + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 const query = asciiOnlyToLowerCase(req.query.q.trim()) - const matches = await libraryItemFilters.search(req.user, req.library, query, limit) + const matches = await libraryItemFilters.search(req.user, oldLibrary, query, limit) res.json(matches) } @@ -715,7 +722,7 @@ class LibraryController { largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10) } - if (req.library.isBook) { + if (req.library.mediaType === 'book') { const authors = await authorFilters.getAuthorsWithCount(req.library.id, 10) const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id) const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id) @@ -938,7 +945,8 @@ class LibraryController { Logger.error(`[LibraryController] Non-root user "${req.user.username}" attempted to match library items`) return res.sendStatus(403) } - Scanner.matchLibraryItems(req.library) + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) + Scanner.matchLibraryItems(oldLibrary) res.sendStatus(200) } @@ -956,9 +964,9 @@ class LibraryController { return res.sendStatus(403) } res.sendStatus(200) - + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const forceRescan = req.query.force === '1' - await LibraryScanner.scan(req.library, forceRescan) + await LibraryScanner.scan(oldLibrary, forceRescan) await Database.resetLibraryIssuesFilterData(req.library.id) Logger.info('[LibraryController] Scan complete') @@ -972,10 +980,10 @@ class LibraryController { * @param {Response} res */ async getRecentEpisodes(req, res) { - if (!req.library.isPodcast) { + if (req.library.mediaType !== 'podcast') { return res.sendStatus(404) } - + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const payload = { episodes: [], limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, @@ -983,7 +991,7 @@ class LibraryController { } const offset = payload.page * payload.limit - payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.user, req.library, payload.limit, offset) + payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.user, oldLibrary, payload.limit, offset) res.json(payload) } @@ -1076,7 +1084,8 @@ class LibraryController { return res.sendStatus(403) } - const library = await Database.libraryModel.getOldById(req.params.id) + // const library = await Database.libraryModel.getOldById(req.params.id) + const library = await Database.libraryModel.findByIdWithFolders(req.params.id) if (!library) { return res.status(404).send('Library not found') } diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index b35cf804a8..911d6480c1 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -21,7 +21,8 @@ class CronManager { /** * Initialize library scan crons & podcast download crons - * @param {import('../objects/Library')[]} libraries + * + * @param {import('../models/Library')[]} libraries */ async init(libraries) { this.initOpenSessionCleanupCron() @@ -46,7 +47,7 @@ class CronManager { /** * Initialize library scan crons - * @param {import('../objects/Library')[]} libraries + * @param {import('../models/Library')[]} libraries */ initLibraryScanCrons(libraries) { for (const library of libraries) { @@ -59,17 +60,17 @@ class CronManager { /** * Start cron schedule for library * - * @param {import('../objects/Library')} _library + * @param {import('../models/Library')} _library */ startCronForLibrary(_library) { Logger.debug(`[CronManager] Init library scan cron for ${_library.name} on schedule ${_library.settings.autoScanCronExpression}`) const libScanCron = cron.schedule(_library.settings.autoScanCronExpression, async () => { - const library = await Database.libraryModel.getOldById(_library.id) - if (!library) { + const oldLibrary = await Database.libraryModel.getOldById(_library.id) + if (!oldLibrary) { Logger.error(`[CronManager] Library not found for scan cron ${_library.id}`) } else { - Logger.debug(`[CronManager] Library scan cron executing for ${library.name}`) - LibraryScanner.scan(library) + Logger.debug(`[CronManager] Library scan cron executing for ${oldLibrary.name}`) + LibraryScanner.scan(oldLibrary) } }) this.libraryScanCrons.push({ @@ -79,11 +80,21 @@ class CronManager { }) } + /** + * TODO: Update to new library model + * + * @param {*} library + */ removeCronForLibrary(library) { Logger.debug(`[CronManager] Removing library scan cron for ${library.name}`) this.libraryScanCrons = this.libraryScanCrons.filter((lsc) => lsc.libraryId !== library.id) } + /** + * TODO: Update to new library model + * + * @param {*} library + */ updateLibraryScanCron(library) { const expression = library.settings.autoScanCronExpression const existingCron = this.libraryScanCrons.find((lsc) => lsc.libraryId === library.id) diff --git a/server/models/Library.js b/server/models/Library.js index 9b42adea8f..9aa6016bf6 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -43,6 +43,8 @@ class Library extends Model { this.createdAt /** @type {Date} */ this.updatedAt + /** @type {import('./LibraryFolder')[]|undefined} */ + this.libraryFolders } /** @@ -74,6 +76,28 @@ class Library extends Model { } } + /** + * + * @returns {Promise} + */ + static getAllWithFolders() { + return this.findAll({ + include: this.sequelize.models.libraryFolder, + order: [['displayOrder', 'ASC']] + }) + } + + /** + * + * @param {string} libraryId + * @returns {Promise} + */ + static findByIdWithFolders(libraryId) { + return this.findByPk(libraryId, { + include: this.sequelize.models.libraryFolder + }) + } + /** * Get all old libraries * @returns {Promise} @@ -121,7 +145,7 @@ class Library extends Model { /** * Update library and library folders * @param {object} oldLibrary - * @returns + * @returns {Promise} */ static async updateFromOld(oldLibrary) { const existingLibrary = await this.findByPk(oldLibrary.id, {