diff --git a/server/Auth.js b/server/Auth.js index c9d67b20..44a9317e 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -104,10 +104,16 @@ class Auth { }) } - getUserLoginResponsePayload(user) { + /** + * Payload returned to a user after successful login + * @param {oldUser} user + * @returns {object} + */ + async getUserLoginResponsePayload(user) { + const libraryIds = await Database.models.library.getAllLibraryIds() return { user: user.toJSONForBrowser(), - userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries), + userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), serverSettings: Database.serverSettings.toJSONForBrowser(), ereaderDevices: Database.emailSettings.getEReaderDevices(user), Source: global.Source @@ -136,7 +142,8 @@ class Auth { return res.status(401).send('Invalid root password (hint: there is none)') } else { Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`) - return res.json(this.getUserLoginResponsePayload(user)) + const userLoginResponsePayload = await this.getUserLoginResponsePayload(user) + return res.json(userLoginResponsePayload) } } @@ -144,7 +151,8 @@ class Auth { const compare = await bcrypt.compare(password, user.pash) if (compare) { Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`) - res.json(this.getUserLoginResponsePayload(user)) + const userLoginResponsePayload = await this.getUserLoginResponsePayload(user) + res.json(userLoginResponsePayload) } else { Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) if (req.rateLimit.remaining <= 2) { diff --git a/server/Database.js b/server/Database.js index 99a77166..94ede654 100644 --- a/server/Database.js +++ b/server/Database.js @@ -17,7 +17,6 @@ class Database { // TODO: below data should be loaded from the DB as needed this.libraryItems = [] this.users = [] - this.libraries = [] this.settings = [] this.collections = [] this.playlists = [] @@ -168,9 +167,6 @@ class Database { this.users = await this.models.user.getOldUsers() Logger.info(`[Database] Loaded ${this.users.length} users`) - this.libraries = await this.models.library.getAllOldLibraries() - Logger.info(`[Database] Loaded ${this.libraries.length} libraries`) - this.collections = await this.models.collection.getOldCollections() Logger.info(`[Database] Loaded ${this.collections.length} collections`) @@ -190,6 +186,8 @@ class Database { this.serverSettings.version = packageJson.version await this.updateServerSettings() } + + this.models.library.getMaxDisplayOrder() } async createRootUser(username, pash, token) { @@ -254,7 +252,6 @@ class Database { async createLibrary(oldLibrary) { if (!this.sequelize) return false await this.models.library.createFromOld(oldLibrary) - this.libraries.push(oldLibrary) } updateLibrary(oldLibrary) { @@ -265,7 +262,6 @@ class Database { async removeLibrary(libraryId) { if (!this.sequelize) return false await this.models.library.removeById(libraryId) - this.libraries = this.libraries.filter(lib => lib.id !== libraryId) } async createCollection(oldCollection) { diff --git a/server/Server.js b/server/Server.js index a1fbdd2a..54e4a0b0 100644 --- a/server/Server.js +++ b/server/Server.js @@ -93,6 +93,10 @@ class Server { this.auth.authMiddleware(req, res, next) } + /** + * Initialize database, backups, logs, rss feeds, cron jobs & watcher + * Cleanup stale/invalid data + */ async init() { Logger.info('[Server] Init v' + version) await this.playbackSessionManager.removeOrphanStreams() @@ -111,13 +115,15 @@ class Server { await this.logManager.init() await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series await this.rssFeedManager.init() - this.cronManager.init() + + const libraries = await Database.models.library.getAllOldLibraries() + this.cronManager.init(libraries) if (Database.serverSettings.scannerDisableWatcher) { Logger.info(`[Server] Watcher is disabled`) this.watcher.disabled = true } else { - this.watcher.initWatcher(Database.libraries) + this.watcher.initWatcher(libraries) this.watcher.on('files', this.filesChanged.bind(this)) } } diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index edf9b736..8d538489 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -17,12 +17,11 @@ class FileSystemController { }) // Do not include existing mapped library paths in response - Database.libraries.forEach(lib => { - lib.folders.forEach((folder) => { - let dir = folder.fullPath - if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '') - excludedDirs.push(dir) - }) + const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths() + libraryFoldersPaths.forEach((path) => { + let dir = path || '' + if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '') + excludedDirs.push(dir) }) res.json({ diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 893bb5c7..bebbac34 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -44,7 +44,9 @@ class LibraryController { const library = new Library() - newLibraryPayload.displayOrder = Database.libraries.map(li => li.displayOrder).sort((a, b) => a - b).pop() + 1 + let currentLargestDisplayOrder = await Database.models.library.getMaxDisplayOrder() + if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0 + newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1 library.setData(newLibraryPayload) await Database.createLibrary(library) @@ -60,17 +62,18 @@ class LibraryController { res.json(library) } - findAll(req, res) { + async findAll(req, res) { + const libraries = await Database.models.library.getAllOldLibraries() + const librariesAccessible = req.user.librariesAccessible || [] if (librariesAccessible.length) { return res.json({ - libraries: Database.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON()) + libraries: libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON()) }) } res.json({ - libraries: Database.libraries.map(lib => lib.toJSON()) - // libraries: Database.libraries.map(lib => lib.toJSON()) + libraries: libraries.map(lib => lib.toJSON()) }) } @@ -151,6 +154,12 @@ class LibraryController { return res.json(library.toJSON()) } + /** + * DELETE: /api/libraries/:id + * Delete a library + * @param {*} req + * @param {*} res + */ async delete(req, res) { const library = req.library @@ -173,6 +182,10 @@ class LibraryController { const libraryJson = library.toJSON() await Database.removeLibrary(library.id) + + // Re-order libraries + await Database.models.library.resetDisplayOrder() + SocketAuthority.emitter('library_removed', libraryJson) return res.json(libraryJson) } @@ -601,17 +614,23 @@ class LibraryController { res.json(categories) } - // PATCH: Change the order of libraries + /** + * POST: /api/libraries/order + * Change the display order of libraries + * @param {*} req + * @param {*} res + */ async reorder(req, res) { if (!req.user.isAdminOrUp) { Logger.error('[LibraryController] ReorderLibraries invalid user', req.user) return res.sendStatus(403) } + const libraries = await Database.models.library.getAllOldLibraries() - var orderdata = req.body - var hasUpdates = false + const orderdata = req.body + let hasUpdates = false for (let i = 0; i < orderdata.length; i++) { - var library = Database.libraries.find(lib => lib.id === orderdata[i].id) + const library = libraries.find(lib => lib.id === orderdata[i].id) if (!library) { Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`) return res.sendStatus(500) @@ -623,14 +642,14 @@ class LibraryController { } if (hasUpdates) { - Database.libraries.sort((a, b) => a.displayOrder - b.displayOrder) + libraries.sort((a, b) => a.displayOrder - b.displayOrder) Logger.debug(`[LibraryController] Updated library display orders`) } else { Logger.debug(`[LibraryController] Library orders were up to date`) } res.json({ - libraries: Database.libraries.map(lib => lib.toJSON()) + libraries: libraries.map(lib => lib.toJSON()) }) } @@ -902,13 +921,13 @@ class LibraryController { res.send(opmlText) } - middleware(req, res, next) { + async middleware(req, res, next) { if (!req.user.checkCanAccessLibrary(req.params.id)) { Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`) return res.sendStatus(403) } - const library = Database.libraries.find(lib => lib.id === req.params.id) + const library = await Database.models.library.getOldById(req.params.id) if (!library) { return res.status(404).send('Library not found') } diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 0ed7ef8f..6bf889fb 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -24,18 +24,18 @@ class MiscController { Logger.error('Invalid request, no files') return res.sendStatus(400) } - var files = Object.values(req.files) - var title = req.body.title - var author = req.body.author - var series = req.body.series - var libraryId = req.body.library - var folderId = req.body.folder + const files = Object.values(req.files) + const title = req.body.title + const author = req.body.author + const series = req.body.series + const libraryId = req.body.library + const folderId = req.body.folder - var library = Database.libraries.find(lib => lib.id === libraryId) + const library = await Database.models.library.getOldById(libraryId) if (!library) { return res.status(404).send(`Library not found with id ${libraryId}`) } - var folder = library.folders.find(fold => fold.id === folderId) + const folder = library.folders.find(fold => fold.id === folderId) if (!folder) { return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`) } @@ -45,8 +45,8 @@ class MiscController { } // For setting permissions recursively - var outputDirectory = '' - var firstDirPath = '' + let outputDirectory = '' + let firstDirPath = '' if (library.isPodcast) { // Podcasts only in 1 folder outputDirectory = Path.join(folder.fullPath, title) @@ -62,8 +62,7 @@ class MiscController { } } - var exists = await fs.pathExists(outputDirectory) - if (exists) { + if (await fs.pathExists(outputDirectory)) { Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`) return res.status(500).send(`Directory "${outputDirectory}" already exists`) } @@ -132,12 +131,19 @@ class MiscController { }) } - authorize(req, res) { + /** + * POST: /api/authorize + * Used to authorize an API token + * + * @param {*} req + * @param {*} res + */ + async authorize(req, res) { if (!req.user) { Logger.error('Invalid user in authorize') return res.sendStatus(401) } - const userResponse = this.auth.getUserLoginResponsePayload(req.user) + const userResponse = await this.auth.getUserLoginResponsePayload(req.user) res.json(userResponse) } diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index fbcf007f..938b5c0c 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -19,7 +19,7 @@ class PodcastController { } const payload = req.body - const library = Database.libraries.find(lib => lib.id === payload.libraryId) + const library = await Database.models.library.getOldById(payload.libraryId) if (!library) { Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) return res.status(404).send('Library not found') diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index adbf87a5..b5d17cb1 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -13,13 +13,21 @@ class CronManager { this.podcastCronExpressionsExecuting = [] } - init() { - this.initLibraryScanCrons() + /** + * Initialize library scan crons & podcast download crons + * @param {oldLibrary[]} libraries + */ + init(libraries) { + this.initLibraryScanCrons(libraries) this.initPodcastCrons() } - initLibraryScanCrons() { - for (const library of Database.libraries) { + /** + * Initialize library scan crons + * @param {oldLibrary[]} libraries + */ + initLibraryScanCrons(libraries) { + for (const library of libraries) { if (library.settings.autoScanCronExpression) { this.startCronForLibrary(library) } diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js index bd62b880..5f3ab238 100644 --- a/server/managers/NotificationManager.js +++ b/server/managers/NotificationManager.js @@ -14,15 +14,15 @@ class NotificationManager { return notificationData } - onPodcastEpisodeDownloaded(libraryItem, episode) { + async onPodcastEpisodeDownloaded(libraryItem, episode) { if (!Database.notificationSettings.isUseable) return Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`) - const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId) + const library = await Database.models.library.getOldById(libraryItem.libraryId) const eventData = { libraryItemId: libraryItem.id, libraryId: libraryItem.libraryId, - libraryName: library ? library.name : 'Unknown', + libraryName: library?.name || 'Unknown', mediaTags: (libraryItem.media.tags || []).join(', '), podcastTitle: libraryItem.media.metadata.title, podcastAuthor: libraryItem.media.metadata.author || '', diff --git a/server/models/Library.js b/server/models/Library.js index 439b9a92..7f0f438d 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -4,6 +4,10 @@ const oldLibrary = require('../objects/Library') module.exports = (sequelize) => { class Library extends Model { + /** + * Get all old libraries + * @returns {Promise} + */ static async getAllOldLibraries() { const libraries = await this.findAll({ include: sequelize.models.libraryFolder, @@ -12,6 +16,11 @@ module.exports = (sequelize) => { return libraries.map(lib => this.getOldLibrary(lib)) } + /** + * Convert expanded Library to oldLibrary + * @param {Library} libraryExpanded + * @returns {Promise} + */ static getOldLibrary(libraryExpanded) { const folders = libraryExpanded.libraryFolders.map(folder => { return { @@ -58,6 +67,11 @@ module.exports = (sequelize) => { }) } + /** + * Update library and library folders + * @param {object} oldLibrary + * @returns + */ static async updateFromOld(oldLibrary) { const existingLibrary = await this.findByPk(oldLibrary.id, { include: sequelize.models.libraryFolder @@ -112,6 +126,11 @@ module.exports = (sequelize) => { } } + /** + * Destroy library by id + * @param {string} libraryId + * @returns + */ static removeById(libraryId) { return this.destroy({ where: { @@ -119,6 +138,59 @@ module.exports = (sequelize) => { } }) } + + /** + * Get all library ids + * @returns {Promise} array of library ids + */ + static async getAllLibraryIds() { + const libraries = await this.findAll({ + attributes: ['id'] + }) + return libraries.map(l => l.id) + } + + /** + * Find Library by primary key & return oldLibrary + * @param {string} libraryId + * @returns {Promise} Returns null if not found + */ + static async getOldById(libraryId) { + if (!libraryId) return null + const library = await this.findByPk(libraryId, { + include: sequelize.models.libraryFolder + }) + if (!library) return null + return this.getOldLibrary(library) + } + + /** + * Get the largest value in the displayOrder column + * Used for setting a new libraries display order + * @returns {Promise} + */ + static getMaxDisplayOrder() { + return this.max('displayOrder') || 0 + } + + /** + * Updates displayOrder to be sequential + * Used after removing a library + */ + static async resetDisplayOrder() { + const libraries = await this.findAll({ + order: [['displayOrder', 'ASC']] + }) + for (let i = 0; i < libraries.length; i++) { + const library = libraries[i] + if (library.displayOrder !== i + 1) { + Logger.dev(`[Library] Updating display order of library from ${library.displayOrder} to ${i + 1}`) + await library.update({ displayOrder: i + 1 }).catch((error) => { + Logger.error(`[Library] Failed to update library display order to ${i + 1}`, error) + }) + } + } + } } Library.init({ diff --git a/server/models/LibraryFolder.js b/server/models/LibraryFolder.js index 6578dcde..1ba240e7 100644 --- a/server/models/LibraryFolder.js +++ b/server/models/LibraryFolder.js @@ -1,7 +1,18 @@ const { DataTypes, Model } = require('sequelize') module.exports = (sequelize) => { - class LibraryFolder extends Model { } + class LibraryFolder extends Model { + /** + * Gets all library folder path strings + * @returns {Promise} array of library folder paths + */ + static async getAllLibraryFolderPaths() { + const libraryFolders = await this.findAll({ + attributes: ['path'] + }) + return libraryFolders.map(l => l.path) + } + } LibraryFolder.init({ id: { diff --git a/server/objects/user/User.js b/server/objects/user/User.js index fe1c79e2..8b26f518 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -258,11 +258,15 @@ class User { return hasUpdates } - getDefaultLibraryId(libraries) { + /** + * Get first available library id for user + * + * @param {string[]} libraryIds + * @returns {string|null} + */ + getDefaultLibraryId(libraryIds) { // Libraries should already be in ascending display order, find first accessible - var firstAccessibleLibrary = libraries.find(lib => this.checkCanAccessLibrary(lib.id)) - if (!firstAccessibleLibrary) return null - return firstAccessibleLibrary.id + return libraryIds.find(lid => this.checkCanAccessLibrary(lid)) || null } // Returns most recent media progress w/ `media` object and optionally an `episode` object diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 8d1a8ccf..5aa11ab9 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -66,7 +66,7 @@ class Scanner { } async scanLibraryItemByRequest(libraryItem) { - const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId) + const library = await Database.models.library.getOldById(libraryItem.libraryId) if (!library) { Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`) return ScanResult.NOTHING @@ -552,7 +552,7 @@ class Scanner { for (const folderId in folderGroups) { const libraryId = folderGroups[folderId].libraryId - const library = Database.libraries.find(lib => lib.id === libraryId) + const library = await Database.models.library.getOldById(libraryId) if (!library) { Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) continue;