diff --git a/client/components/tables/library/LibrariesTable.vue b/client/components/tables/library/LibrariesTable.vue index faf8d69d27..62b1858efd 100644 --- a/client/components/tables/library/LibrariesTable.vue +++ b/client/components/tables/library/LibrariesTable.vue @@ -76,8 +76,7 @@ export default { var newOrder = libraryOrderData.map((lib) => lib.id).join(',') if (currOrder !== newOrder) { this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => { - if (response.libraries && response.libraries.length) { - this.$toast.success('Library order saved', { timeout: 1500 }) + if (response.libraries?.length) { this.$store.commit('libraries/set', response.libraries) } }) @@ -110,4 +109,4 @@ export default { this.$store.commit('libraries/removeListener', 'libraries-table') } } - \ No newline at end of file + diff --git a/server/Watcher.js b/server/Watcher.js index 999437d805..32788e27db 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -122,9 +122,8 @@ class FolderWatcher extends EventEmitter { } /** - * TODO: Update to new library model * - * @param {import('./objects/Library')} library + * @param {import('./models/Library')} library */ updateLibrary(library) { if (this.disabled) return diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 13b82ad402..cc9e6e2e55 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -27,6 +27,11 @@ const authorFilters = require('../utils/queries/authorFilters') * @property {import('../models/User')} user * * @typedef {Request & RequestUserObject} RequestWithUser + * + * @typedef RequestEntityObject + * @property {import('../models/Library')} library + * + * @typedef {RequestWithUser & RequestEntityObject} LibraryControllerRequest */ class LibraryController { @@ -147,21 +152,25 @@ class LibraryController { library.libraryFolders = await library.getLibraryFolders() - // TODO: Migrate to new library model - const oldLibrary = Database.libraryModel.getOldLibrary(library) - // Only emit to users with access to library const userFilter = (user) => { - return user.checkCanAccessLibrary?.(oldLibrary.id) + return user.checkCanAccessLibrary?.(library.id) } - SocketAuthority.emitter('library_added', oldLibrary.toJSON(), userFilter) + SocketAuthority.emitter('library_added', library.toOldJSON(), userFilter) // Add library watcher this.watcher.addLibrary(library) - res.json(oldLibrary) + res.json(library.toOldJSON()) } + /** + * GET: /api/libraries + * Get all libraries + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findAll(req, res) { const libraries = await Database.libraryModel.getAllOldLibraries() @@ -180,7 +189,7 @@ class LibraryController { /** * GET: /api/libraries/:id * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async findOne(req, res) { @@ -204,7 +213,8 @@ class LibraryController { /** * GET: /api/libraries/:id/episode-downloads * Get podcast episodes in download queue - * @param {RequestWithUser} req + * + * @param {LibraryControllerRequest} req * @param {Response} res */ async getEpisodeDownloadQueue(req, res) { @@ -215,12 +225,28 @@ class LibraryController { /** * PATCH: /api/libraries/:id * - * @param {RequestWithUser} req + * @this {import('../routers/ApiRouter')} + * + * @param {LibraryControllerRequest} req * @param {Response} res */ async update(req, res) { - /** @type {import('../objects/Library')} */ - const oldLibrary = Database.libraryModel.getOldLibrary(req.library) + // Validation + const updatePayload = {} + const keysToCheck = ['name', 'provider', 'mediaType', 'icon'] + for (const key of keysToCheck) { + if (!req.body[key]) continue + if (typeof req.body[key] !== 'string') { + return res.status(400).send(`Invalid request. ${key} must be a string`) + } + updatePayload[key] = req.body[key] + } + if (req.body.displayOrder !== undefined) { + if (isNaN(req.body.displayOrder)) { + return res.status(400).send('Invalid request. displayOrder must be a number') + } + updatePayload.displayOrder = req.body.displayOrder + } // Validate that the custom provider exists if given any if (req.body.provider?.startsWith('custom-')) { @@ -230,21 +256,72 @@ class LibraryController { } } + // Validate settings + const updatedSettings = { + ...(req.library.settings || Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType)) + } + let hasUpdates = false + let hasUpdatedDisableWatcher = false + let hasUpdatedScanCron = false + if (req.body.settings) { + for (const key in req.body.settings) { + if (updatedSettings[key] === undefined) continue + + if (key === 'metadataPrecedence') { + if (!Array.isArray(req.body.settings[key])) { + return res.status(400).send('Invalid request. Settings "metadataPrecedence" must be an array') + } + if (JSON.stringify(req.body.settings[key]) !== JSON.stringify(updatedSettings[key])) { + hasUpdates = true + updatedSettings[key] = [...req.body.settings[key]] + Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`) + } + } else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') { + if (req.body.settings[key] !== null && typeof req.body.settings[key] !== 'string') { + return res.status(400).send(`Invalid request. Settings "${key}" must be a string`) + } + if (req.body.settings[key] !== updatedSettings[key]) { + if (key === 'autoScanCronExpression') hasUpdatedScanCron = true + + hasUpdates = true + updatedSettings[key] = req.body.settings[key] + Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`) + } + } else { + if (typeof req.body.settings[key] !== typeof updatedSettings[key]) { + return res.status(400).send(`Invalid request. Setting "${key}" must be of type ${typeof updatedSettings[key]}`) + } + if (req.body.settings[key] !== updatedSettings[key]) { + if (key === 'disableWatcher') hasUpdatedDisableWatcher = true + + hasUpdates = true + updatedSettings[key] = req.body.settings[key] + Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`) + } + } + } + if (hasUpdates) { + updatePayload.settings = updatedSettings + req.library.changed('settings', true) + } + } + + let hasFolderUpdates = false // Validate new folder paths exist or can be created & resolve rel paths // returns 400 if a new folder fails to access - if (req.body.folders) { + if (Array.isArray(req.body.folders)) { const newFolderPaths = [] req.body.folders = req.body.folders.map((f) => { if (!f.id) { - f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath)) - newFolderPaths.push(f.fullPath) + const path = f.fullPath || f.path + f.path = fileUtils.filePathToPOSIX(Path.resolve(path)) + newFolderPaths.push(f.path) } return f }) for (const path of newFolderPaths) { const pathExists = await fs.pathExists(path) if (!pathExists) { - // Ensure dir will recursively create directories which might be preferred over mkdir const success = await fs .ensureDir(path) .then(() => true) @@ -256,10 +333,17 @@ class LibraryController { return res.status(400).send(`Invalid folder directory "${path}"`) } } + // Create folder + const libraryFolder = await Database.libraryFolderModel.create({ + path, + libraryId: req.library.id + }) + Logger.info(`[LibraryController] Created folder "${libraryFolder.path}" for library "${req.library.name}"`) + hasFolderUpdates = true } // Handle removing folders - for (const folder of oldLibrary.folders) { + for (const folder of req.library.libraryFolders) { if (!req.body.folders.some((f) => f.id === folder.id)) { // Remove library items in folder const libraryItemsInFolder = await Database.libraryItemModel.findAll({ @@ -278,67 +362,82 @@ class LibraryController { } ] }) - Logger.info(`[LibraryController] Removed folder "${folder.fullPath}" from library "${oldLibrary.name}" with ${libraryItemsInFolder.length} library items`) + Logger.info(`[LibraryController] Removed folder "${folder.path}" from library "${req.library.name}" with ${libraryItemsInFolder.length} library items`) for (const libraryItem of libraryItemsInFolder) { let mediaItemIds = [] - if (oldLibrary.isPodcast) { + if (req.library.isPodcast) { mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) } else { mediaItemIds.push(libraryItem.mediaId) } - Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.fullPath}"`) + Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`) await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds) } + + // Remove folder + await folder.destroy() + hasFolderUpdates = true } } } - const hasUpdates = oldLibrary.update(req.body) - // TODO: Should check if this is an update to folder paths or name only - if (hasUpdates) { + if (Object.keys(updatePayload).length) { + req.library.set(updatePayload) + if (req.library.changed()) { + Logger.debug(`[LibraryController] Updated library "${req.library.name}" with changed keys ${req.library.changed()}`) + hasUpdates = true + await req.library.save() + } + } + + if (hasUpdatedScanCron) { + Logger.debug(`[LibraryController] Updated library "${req.library.name}" auto scan cron`) // Update auto scan cron - this.cronManager.updateLibraryScanCron(oldLibrary) + this.cronManager.updateLibraryScanCron(req.library) + } - const updatedLibrary = await Database.libraryModel.updateFromOld(oldLibrary) - updatedLibrary.libraryFolders = await updatedLibrary.getLibraryFolders() + if (hasFolderUpdates || hasUpdatedDisableWatcher) { + req.library.libraryFolders = await req.library.getLibraryFolders() // Update watcher - this.watcher.updateLibrary(updatedLibrary) + this.watcher.updateLibrary(req.library) + hasUpdates = true + } + + if (hasUpdates) { // Only emit to users with access to library const userFilter = (user) => { - return user.checkCanAccessLibrary?.(oldLibrary.id) + return user.checkCanAccessLibrary?.(req.library.id) } - SocketAuthority.emitter('library_updated', oldLibrary.toJSON(), userFilter) + SocketAuthority.emitter('library_updated', req.library.toOldJSON(), userFilter) - await Database.resetLibraryIssuesFilterData(oldLibrary.id) + await Database.resetLibraryIssuesFilterData(req.library.id) } - return res.json(oldLibrary.toJSON()) + return res.json(req.library.toOldJSON()) } /** * DELETE: /api/libraries/:id * Delete a library * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async delete(req, res) { - const library = Database.libraryModel.getOldLibrary(req.library) - // Remove library watcher this.watcher.removeLibrary(req.library) // Remove collections for library - const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(library.id) + const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(req.library.id) if (numCollectionsRemoved) { - Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`) + Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${req.library.name}"`) } // Remove items in this library const libraryItemsInLibrary = await Database.libraryItemModel.findAll({ where: { - libraryId: library.id + libraryId: req.library.id }, attributes: ['id', 'mediaId', 'mediaType'], include: [ @@ -352,20 +451,20 @@ class LibraryController { } ] }) - Logger.info(`[LibraryController] Removing ${libraryItemsInLibrary.length} library items in library "${library.name}"`) + Logger.info(`[LibraryController] Removing ${libraryItemsInLibrary.length} library items in library "${req.library.name}"`) for (const libraryItem of libraryItemsInLibrary) { let mediaItemIds = [] - if (library.isPodcast) { + if (req.library.isPodcast) { mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) } else { mediaItemIds.push(libraryItem.mediaId) } - Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${library.name}"`) + Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`) await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds) } - const libraryJson = library.toJSON() - await Database.removeLibrary(library.id) + const libraryJson = req.library.toOldJSON() + await Database.removeLibrary(req.library.id) // Re-order libraries await Database.libraryModel.resetDisplayOrder() @@ -373,8 +472,8 @@ class LibraryController { SocketAuthority.emitter('library_removed', libraryJson) // Remove library filter data - if (Database.libraryFilterData[library.id]) { - delete Database.libraryFilterData[library.id] + if (Database.libraryFilterData[req.library.id]) { + delete Database.libraryFilterData[req.library.id] } return res.json(libraryJson) @@ -383,12 +482,10 @@ class LibraryController { /** * GET /api/libraries/:id/items * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @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()) @@ -410,6 +507,8 @@ class LibraryController { payload.offset = payload.page * payload.limit // TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries + // TODO: Update to new library model + const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const filterByGroup = payload.filterBy?.split('.').shift() const filterByValue = filterByGroup ? libraryFilters.decode(payload.filterBy.replace(`${filterByGroup}.`, '')) : null if (filterByGroup === 'series' && filterByValue !== 'no-series' && payload.collapseseries) { @@ -425,9 +524,10 @@ class LibraryController { } /** - * DELETE: /libraries/:id/issues + * DELETE: /api/libraries/:id/issues * Remove all library items missing or invalid - * @param {RequestWithUser} req + * + * @param {LibraryControllerRequest} req * @param {Response} res */ async removeLibraryItemsWithIssues(req, res) { @@ -464,7 +564,7 @@ class LibraryController { Logger.info(`[LibraryController] Removing ${libraryItemsWithIssues.length} items with issues`) for (const libraryItem of libraryItemsWithIssues) { let mediaItemIds = [] - if (req.library.mediaType === 'podcast') { + if (req.library.isPodcast) { mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) } else { mediaItemIds.push(libraryItem.mediaId) @@ -485,10 +585,11 @@ class LibraryController { * GET: /api/libraries/:id/series * Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getAllSeriesForLibrary(req, res) { + // TODO: Update to new library model const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const include = (req.query.include || '') @@ -523,7 +624,7 @@ class LibraryController { * rssfeed: adds `rssFeed` to series object if a feed is open * progress: adds `progress` to series object with { libraryItemIds:Array, libraryItemIdsFinished:Array, isFinished:boolean } * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res - Series */ async getSeriesForLibrary(req, res) { @@ -560,7 +661,7 @@ class LibraryController { * GET: /api/libraries/:id/collections * Get all collections for library * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getCollectionsForLibrary(req, res) { @@ -599,7 +700,7 @@ class LibraryController { * GET: /api/libraries/:id/playlists * Get playlists for user in library * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getUserPlaylistsForLibrary(req, res) { @@ -624,7 +725,7 @@ class LibraryController { /** * GET: /api/libraries/:id/filterdata * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getLibraryFilterData(req, res) { @@ -636,10 +737,11 @@ class LibraryController { * GET: /api/libraries/:id/personalized * Home page shelves * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getUserPersonalizedShelves(req, res) { + // TODO: Update to new library model 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 || '') @@ -654,7 +756,13 @@ class LibraryController { * POST: /api/libraries/order * Change the display order of libraries * - * @param {RequestWithUser} req + * @typedef LibraryReorderObj + * @property {string} id + * @property {number} newOrder + * + * @typedef {Request<{}, {}, LibraryReorderObj[], {}> & RequestUserObject} LibraryReorderRequest + * + * @param {LibraryReorderRequest} req * @param {Response} res */ async reorder(req, res) { @@ -662,20 +770,25 @@ class LibraryController { Logger.error(`[LibraryController] Non-admin user "${req.user}" attempted to reorder libraries`) return res.sendStatus(403) } - const libraries = await Database.libraryModel.getAllOldLibraries() + + const libraries = await Database.libraryModel.getAllWithFolders() const orderdata = req.body + if (!Array.isArray(orderdata) || orderdata.some((o) => typeof o?.id !== 'string' || typeof o?.newOrder !== 'number')) { + return res.status(400).send('Invalid request. Request body must be an array of objects') + } + let hasUpdates = false for (let i = 0; i < orderdata.length; i++) { 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) - } - if (library.update({ displayOrder: orderdata[i].newOrder })) { - hasUpdates = true - await Database.libraryModel.updateFromOld(library) + return res.status(400).send(`Library not found with id ${orderdata[i].id}`) } + if (library.displayOrder === orderdata[i].newOrder) continue + library.displayOrder = orderdata[i].newOrder + await library.save() + hasUpdates = true } if (hasUpdates) { @@ -686,7 +799,7 @@ class LibraryController { } res.json({ - libraries: libraries.map((lib) => lib.toJSON()) + libraries: libraries.map((lib) => lib.toOldJSON()) }) } @@ -695,18 +808,18 @@ class LibraryController { * Search library items with query * * ?q=search - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async search(req, res) { 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, oldLibrary, query, limit) + const matches = await libraryItemFilters.search(req.user, req.library, query, limit) res.json(matches) } @@ -714,7 +827,7 @@ class LibraryController { * GET: /api/libraries/:id/stats * Get stats for library * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async stats(req, res) { @@ -757,7 +870,7 @@ class LibraryController { * GET: /api/libraries/:id/authors * Get authors for library * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getAuthors(req, res) { @@ -796,7 +909,7 @@ class LibraryController { /** * GET: /api/libraries/:id/narrators * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getNarrators(req, res) { @@ -843,7 +956,7 @@ class LibraryController { * :narratorId is base64 encoded name * req.body { name } * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async updateNarrator(req, res) { @@ -894,7 +1007,7 @@ class LibraryController { * Remove narrator * :narratorId is base64 encoded name * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async removeNarrator(req, res) { @@ -937,7 +1050,7 @@ class LibraryController { * GET: /api/libraries/:id/matchall * Quick match all library items. Book libraries only. * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async matchAll(req, res) { @@ -945,6 +1058,7 @@ class LibraryController { Logger.error(`[LibraryController] Non-root user "${req.user.username}" attempted to match library items`) return res.sendStatus(403) } + // TODO: Update to new library model const oldLibrary = Database.libraryModel.getOldLibrary(req.library) Scanner.matchLibraryItems(oldLibrary) res.sendStatus(200) @@ -955,7 +1069,7 @@ class LibraryController { * Optional query: * ?force=1 * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async scan(req, res) { @@ -964,6 +1078,7 @@ class LibraryController { return res.sendStatus(403) } res.sendStatus(200) + // TODO: Update to new library model const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const forceRescan = req.query.force === '1' await LibraryScanner.scan(oldLibrary, forceRescan) @@ -976,13 +1091,14 @@ class LibraryController { * GET: /api/libraries/:id/recent-episodes * Used for latest page * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getRecentEpisodes(req, res) { if (req.library.mediaType !== 'podcast') { return res.sendStatus(404) } + // TODO: Update to new library model const oldLibrary = Database.libraryModel.getOldLibrary(req.library) const payload = { episodes: [], @@ -999,7 +1115,7 @@ class LibraryController { * GET: /api/libraries/:id/opml * Get OPML file for a podcast library * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async getOPMLFile(req, res) { @@ -1023,9 +1139,10 @@ class LibraryController { } /** + * POST: /api/libraries/:id/remove-metadata * Remove all metadata.json or metadata.abs files in library item folders * - * @param {RequestWithUser} req + * @param {LibraryControllerRequest} req * @param {Response} res */ async removeAllMetadataFiles(req, res) { @@ -1084,7 +1201,6 @@ class LibraryController { return res.sendStatus(403) } - // 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 911d6480c1..09ff4d95e3 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -81,9 +81,8 @@ class CronManager { } /** - * TODO: Update to new library model * - * @param {*} library + * @param {import('../models/Library')} library */ removeCronForLibrary(library) { Logger.debug(`[CronManager] Removing library scan cron for ${library.name}`) @@ -91,9 +90,8 @@ class CronManager { } /** - * TODO: Update to new library model * - * @param {*} library + * @param {import('../models/Library')} library */ updateLibraryScanCron(library) { const expression = library.settings.autoScanCronExpression diff --git a/server/models/Library.js b/server/models/Library.js index 9aa6016bf6..3ebd32df91 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -301,6 +301,35 @@ class Library extends Model { } ) } + + get isPodcast() { + return this.mediaType === 'podcast' + } + get isBook() { + return this.mediaType === 'book' + } + + /** + * TODO: Update to use new model + */ + toOldJSON() { + return { + id: this.id, + name: this.name, + folders: (this.libraryFolders || []).map((f) => f.toOldJSON()), + displayOrder: this.displayOrder, + icon: this.icon, + mediaType: this.mediaType, + provider: this.provider, + settings: { + ...this.settings + }, + lastScan: this.lastScan?.valueOf() || null, + lastScanVersion: this.lastScanVersion, + createdAt: this.createdAt.valueOf(), + lastUpdate: this.updatedAt.valueOf() + } + } } module.exports = Library diff --git a/server/models/LibraryFolder.js b/server/models/LibraryFolder.js index db607547d0..71d532176f 100644 --- a/server/models/LibraryFolder.js +++ b/server/models/LibraryFolder.js @@ -42,6 +42,18 @@ class LibraryFolder extends Model { }) LibraryFolder.belongsTo(library) } + + /** + * TODO: Update to use new model + */ + toOldJSON() { + return { + id: this.id, + fullPath: this.path, + libraryId: this.libraryId, + addedAt: this.createdAt.valueOf() + } + } } module.exports = LibraryFolder diff --git a/server/objects/Library.js b/server/objects/Library.js index 98b6ec393b..a16e7b01d1 100644 --- a/server/objects/Library.js +++ b/server/objects/Library.js @@ -1,4 +1,3 @@ -const uuidv4 = require('uuid').v4 const Folder = require('./Folder') const LibrarySettings = require('./settings/LibrarySettings') const { filePathToPOSIX } = require('../utils/fileUtils') @@ -28,15 +27,9 @@ class Library { } } - get folderPaths() { - return this.folders.map((f) => f.fullPath) - } get isPodcast() { return this.mediaType === 'podcast' } - get isMusic() { - return this.mediaType === 'music' - } get isBook() { return this.mediaType === 'book' } @@ -98,61 +91,5 @@ class Library { lastUpdate: this.lastUpdate } } - - update(payload) { - let hasUpdates = false - - const keysToCheck = ['name', 'provider', 'mediaType', 'icon'] - keysToCheck.forEach((key) => { - if (payload[key] && payload[key] !== this[key]) { - this[key] = payload[key] - hasUpdates = true - } - }) - - if (payload.settings && this.settings.update(payload.settings)) { - hasUpdates = true - } - - if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) { - this.displayOrder = Number(payload.displayOrder) - hasUpdates = true - } - if (payload.folders) { - const newFolders = payload.folders.filter((f) => !f.id) - const removedFolders = this.folders.filter((f) => !payload.folders.some((_f) => _f.id === f.id)) - - if (removedFolders.length) { - const removedFolderIds = removedFolders.map((f) => f.id) - this.folders = this.folders.filter((f) => !removedFolderIds.includes(f.id)) - } - - if (newFolders.length) { - newFolders.forEach((folderData) => { - folderData.libraryId = this.id - const newFolder = new Folder() - newFolder.setData(folderData) - this.folders.push(newFolder) - }) - } - - if (newFolders.length || removedFolders.length) { - hasUpdates = true - } - } - if (hasUpdates) { - this.lastUpdate = Date.now() - } - return hasUpdates - } - - checkFullPathInLibrary(fullPath) { - fullPath = filePathToPOSIX(fullPath) - return this.folders.find((folder) => fullPath.startsWith(filePathToPOSIX(folder.fullPath))) - } - - getFolderById(id) { - return this.folders.find((folder) => folder.id === id) - } } module.exports = Library diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 54cd97c094..e6e5a69427 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -53,6 +53,7 @@ class ApiRouter { this.audioMetadataManager = Server.audioMetadataManager /** @type {import('../managers/RssFeedManager')} */ this.rssFeedManager = Server.rssFeedManager + /** @type {import('../managers/CronManager')} */ this.cronManager = Server.cronManager /** @type {import('../managers/NotificationManager')} */ this.notificationManager = Server.notificationManager diff --git a/server/utils/migrations/dbMigration.js b/server/utils/migrations/dbMigration.js index 0a48ac60a3..cbb8edea01 100644 --- a/server/utils/migrations/dbMigration.js +++ b/server/utils/migrations/dbMigration.js @@ -1268,7 +1268,7 @@ async function handleOldLibraries(ctx) { return false } const folderPaths = ol.folders?.map((f) => f.fullPath) || [] - return folderPaths.join(',') === library.folderPaths.join(',') + return folderPaths.join(',') === library.folders.map((f) => f.fullPath).join(',') }) if (matchingOldLibrary) { diff --git a/server/utils/queries/libraryItemFilters.js b/server/utils/queries/libraryItemFilters.js index 128df6fde7..7f95d0ecc4 100644 --- a/server/utils/queries/libraryItemFilters.js +++ b/server/utils/queries/libraryItemFilters.js @@ -173,16 +173,16 @@ module.exports = { /** * Search library items * @param {import('../../models/User')} user - * @param {import('../../objects/Library')} oldLibrary + * @param {import('../../models/Library')} library * @param {string} query * @param {number} limit * @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[], podcast:object[]}} */ - search(user, oldLibrary, query, limit) { - if (oldLibrary.isBook) { - return libraryItemsBookFilters.search(user, oldLibrary, query, limit, 0) + search(user, library, query, limit) { + if (library.isBook) { + return libraryItemsBookFilters.search(user, library, query, limit, 0) } else { - return libraryItemsPodcastFilters.search(user, oldLibrary, query, limit, 0) + return libraryItemsPodcastFilters.search(user, library, query, limit, 0) } }, diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index da356b3ee6..4ea28b4df3 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -966,13 +966,13 @@ module.exports = { /** * Search books, authors, series * @param {import('../../models/User')} user - * @param {import('../../objects/Library')} oldLibrary + * @param {import('../../models/Library')} library * @param {string} query * @param {number} limit * @param {number} offset * @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}} */ - async search(user, oldLibrary, query, limit, offset) { + async search(user, library, query, limit, offset) { const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user) const normalizedQuery = query @@ -1006,7 +1006,7 @@ module.exports = { { model: Database.libraryItemModel, where: { - libraryId: oldLibrary.id + libraryId: library.id } }, { @@ -1047,7 +1047,7 @@ module.exports = { const narratorMatches = [] const [narratorResults] = await Database.sequelize.query(`SELECT value, count(*) AS numBooks FROM books b, libraryItems li, json_each(b.narrators) WHERE json_valid(b.narrators) AND ${matchJsonValue} AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, { replacements: { - libraryId: oldLibrary.id, + libraryId: library.id, limit, offset }, @@ -1064,7 +1064,7 @@ module.exports = { const tagMatches = [] const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.tags) WHERE json_valid(b.tags) AND ${matchJsonValue} AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, { replacements: { - libraryId: oldLibrary.id, + libraryId: library.id, limit, offset }, @@ -1081,7 +1081,7 @@ module.exports = { const genreMatches = [] const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.genres) WHERE json_valid(b.genres) AND ${matchJsonValue} AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, { replacements: { - libraryId: oldLibrary.id, + libraryId: library.id, limit, offset }, @@ -1101,7 +1101,7 @@ module.exports = { [Sequelize.Op.and]: [ Sequelize.literal(matchName), { - libraryId: oldLibrary.id + libraryId: library.id } ] }, @@ -1136,7 +1136,7 @@ module.exports = { } // Search authors - const authorMatches = await authorFilters.search(oldLibrary.id, normalizedQuery, limit, offset) + const authorMatches = await authorFilters.search(library.id, normalizedQuery, limit, offset) return { book: itemMatches, diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 1fe6dbc5cb..77eb50120b 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -306,13 +306,13 @@ module.exports = { /** * Search podcasts * @param {import('../../models/User')} user - * @param {import('../../objects/Library')} oldLibrary + * @param {import('../../models/Library')} library * @param {string} query * @param {number} limit * @param {number} offset * @returns {{podcast:object[], tags:object[]}} */ - async search(user, oldLibrary, query, limit, offset) { + async search(user, library, query, limit, offset) { const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user) const normalizedQuery = query @@ -345,7 +345,7 @@ module.exports = { { model: Database.libraryItemModel, where: { - libraryId: oldLibrary.id + libraryId: library.id } } ], @@ -372,7 +372,7 @@ module.exports = { const tagMatches = [] const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.tags) WHERE json_valid(p.tags) AND ${matchJsonValue} AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, { replacements: { - libraryId: oldLibrary.id, + libraryId: library.id, limit, offset }, @@ -389,7 +389,7 @@ module.exports = { const genreMatches = [] const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.genres) WHERE json_valid(p.genres) AND ${matchJsonValue} AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC LIMIT :limit OFFSET :offset;`, { replacements: { - libraryId: oldLibrary.id, + libraryId: library.id, limit, offset },