diff --git a/server/Database.js b/server/Database.js index 1a7c533a..3f0fdf79 100644 --- a/server/Database.js +++ b/server/Database.js @@ -22,6 +22,9 @@ class Database { this.authors = [] this.series = [] + // Cached library filter data + this.libraryFilterData = {} + this.serverSettings = null this.notificationSettings = null this.emailSettings = null @@ -436,6 +439,34 @@ class Database { if (!this.sequelize) return false return this.models.device.createFromOld(oldDevice) } + + removeTagFromFilterData(tag) { + for (const libraryId in this.libraryFilterData) { + this.libraryFilterData[libraryId].tags = this.libraryFilterData[libraryId].tags.filter(t => t !== tag) + } + } + + addTagToFilterData(tag) { + for (const libraryId in this.libraryFilterData) { + if (!this.libraryFilterData[libraryId].tags.includes(tag)) { + this.libraryFilterData[libraryId].tags.push(tag) + } + } + } + + removeGenreFromFilterData(genre) { + for (const libraryId in this.libraryFilterData) { + this.libraryFilterData[libraryId].genres = this.libraryFilterData[libraryId].genres.filter(g => g !== genre) + } + } + + addGenreToFilterData(genre) { + for (const libraryId in this.libraryFilterData) { + if (!this.libraryFilterData[libraryId].genres.includes(genre)) { + this.libraryFilterData[libraryId].genres.push(genre) + } + } + } } module.exports = new Database() \ No newline at end of file diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 33e822c8..11f545ce 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -5,12 +5,14 @@ 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 { sort, createNewSortInstance } = require('../libs/fastSort') const naturalSort = createNewSortInstance({ comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare }) const Database = require('../Database') +const libraryFilters = require('../utils/queries/libraryFilters') class LibraryController { constructor() { } @@ -80,9 +82,11 @@ class LibraryController { async findOne(req, res) { const includeArray = (req.query.include || '').split(',') if (includeArray.includes('filterdata')) { + const filterdata = await libraryFilters.getFilterData(req.library) + return res.json({ - filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems), - issues: req.libraryItems.filter(li => li.hasIssues).length, + filterdata, + issues: filterdata.numIssues, numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), library: req.library }) @@ -90,9 +94,15 @@ class LibraryController { return res.json(req.library) } + /** + * GET: /api/libraries/:id/episode-downloads + * Get podcast episodes in download queue + * @param {*} req + * @param {*} res + */ async getEpisodeDownloadQueue(req, res) { const libraryDownloadQueueDetails = this.podcastManager.getDownloadQueueDetails(req.library.id) - return res.json(libraryDownloadQueueDetails) + res.json(libraryDownloadQueueDetails) } async update(req, res) { @@ -214,8 +224,12 @@ class LibraryController { res.json(payload) } - // api/libraries/:id/items - // TODO: Optimize this method, items are iterated through several times but can be combined + /** + * GET: /api/libraries/:id/items + * TODO: Remove after implementing getLibraryItemsNew + * @param {*} req + * @param {*} res + */ async getLibraryItems(req, res) { let libraryItems = req.libraryItems @@ -431,7 +445,7 @@ class LibraryController { } /** - * api/libraries/:id/series + * GET: /api/libraries/:id/series * Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open * * @param {*} req @@ -498,7 +512,7 @@ class LibraryController { } /** - * api/libraries/:id/series/:seriesId + * GET: /api/libraries/:id/series/:seriesId * * Optional includes (e.g. `?include=rssfeed,progress`) * rssfeed: adds `rssFeed` to series object if a feed is open @@ -513,7 +527,7 @@ class LibraryController { const series = Database.series.find(se => se.id === req.params.seriesId) if (!series) return res.sendStatus(404) - const libraryItemsInSeries = req.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id)) + const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user) const seriesJson = series.toJSON() if (include.includes('progress')) { @@ -533,7 +547,12 @@ class LibraryController { res.json(seriesJson) } - // api/libraries/:id/collections + /** + * GET: /api/libraries/:id/collections + * Get all collections for library + * @param {*} req + * @param {*} res + */ async getCollectionsForLibrary(req, res) { const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) @@ -563,10 +582,15 @@ class LibraryController { res.json(payload) } - // api/libraries/:id/playlists + /** + * GET: /api/libraries/:id/playlists + * Get playlists for user in library + * @param {*} req + * @param {*} res + */ async getUserPlaylistsForLibrary(req, res) { let playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id) - playlistsForUser = playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems)) + playlistsForUser = await Promise.all(playlistsForUser.map(async p => p.getOldJsonExpanded())) const payload = { results: [], @@ -584,8 +608,14 @@ class LibraryController { res.json(payload) } + /** + * GET: /api/libraries/:id/filterdata + * @param {*} req + * @param {*} res + */ async getLibraryFilterData(req, res) { - res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems)) + const filterData = await libraryFilters.getFilterData(req.library) + res.json(filterData) } /** diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 4442a5ce..e796cc03 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -229,6 +229,10 @@ class MiscController { let tagMerged = false let numItemsUpdated = 0 + // Update filter data + Database.removeTagFromFilterData(tag) + Database.addTagToFilterData(newTag) + const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag, newTag]) for (const libraryItem of libraryItemsWithTag) { let existingTags = libraryItem.media.tags @@ -275,6 +279,9 @@ class MiscController { // Get all items with tag const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag]) + // Update filterdata + Database.removeTagFromFilterData(tag) + let numItemsUpdated = 0 // Remove tag from items for (const libraryItem of libraryItemsWithTag) { @@ -356,6 +363,10 @@ class MiscController { let genreMerged = false let numItemsUpdated = 0 + // Update filter data + Database.removeGenreFromFilterData(genre) + Database.addGenreToFilterData(newGenre) + const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre, newGenre]) for (const libraryItem of libraryItemsWithGenre) { let existingGenres = libraryItem.media.genres @@ -399,6 +410,9 @@ class MiscController { const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString() + // Update filter data + Database.removeGenreFromFilterData(genre) + // Get all items with genre const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre]) diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index ad356fd5..51f818ef 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -1,5 +1,6 @@ const Logger = require('../Logger') const Database = require('../Database') +const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class RSSFeedController { constructor() { } @@ -8,7 +9,7 @@ class RSSFeedController { async openRSSFeedForItem(req, res) { const options = req.body || {} - const item = Database.libraryItems.find(li => li.id === req.params.itemId) + const item = await Database.models.libraryItem.getOldById(req.params.itemId) if (!item) return res.sendStatus(404) // Check user can access this library item @@ -45,7 +46,7 @@ class RSSFeedController { async openRSSFeedForCollection(req, res) { const options = req.body || {} - const collection = await Database.models.collection.getOldById(req.params.collectionId) + const collection = await Database.models.collection.findByPk(req.params.collectionId) if (!collection) return res.sendStatus(404) // Check request body options exist @@ -60,7 +61,7 @@ class RSSFeedController { return res.status(400).send('Slug already in use') } - const collectionExpanded = collection.toJSONExpanded(Database.libraryItems) + const collectionExpanded = await collection.getOldJsonExpanded() const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length) // Check collection has audio tracks @@ -95,8 +96,9 @@ class RSSFeedController { } const seriesJson = series.toJSON() + // Get books in series that have audio tracks - seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) + seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks) // Check series has audio tracks if (!seriesJson.books.length) { diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index 041c0716..61801fcf 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -1,6 +1,7 @@ const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class SeriesController { constructor() { } @@ -25,7 +26,7 @@ class SeriesController { const libraryItemsInSeries = req.libraryItemsInSeries const libraryItemsFinished = libraryItemsInSeries.filter(li => { const mediaProgress = req.user.getMediaProgress(li.id) - return mediaProgress && mediaProgress.isFinished + return mediaProgress?.isFinished }) seriesJson.progress = { libraryItemIds: libraryItemsInSeries.map(li => li.id), @@ -62,18 +63,17 @@ class SeriesController { res.json(req.series.toJSON()) } - middleware(req, res, next) { + async middleware(req, res, next) { const series = Database.series.find(se => se.id === req.params.id) if (!series) return res.sendStatus(404) /** * Filter out any library items not accessible to user */ - const libraryItems = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id)) - const libraryItemsAccessible = libraryItems.filter(li => req.user.checkCanAccessLibraryItem(li)) - if (libraryItems.length && !libraryItemsAccessible.length) { - Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to any of the books`, req.user) - return res.sendStatus(403) + const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user) + if (!libraryItems.length) { + Logger.warn(`[SeriesController] User attempted to access series "${series.id}" with no accessible books`, req.user) + return res.sendStatus(404) } if (req.method == 'DELETE' && !req.user.canDelete) { @@ -85,7 +85,7 @@ class SeriesController { } req.series = series - req.libraryItemsInSeries = libraryItemsAccessible + req.libraryItemsInSeries = libraryItems next() } } diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js index 6215dd43..9c28d618 100644 --- a/server/controllers/ToolsController.js +++ b/server/controllers/ToolsController.js @@ -99,15 +99,15 @@ class ToolsController { res.sendStatus(200) } - middleware(req, res, next) { + async middleware(req, res, next) { if (!req.user.isAdminOrUp) { Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user) return res.sendStatus(403) } if (req.params.id) { - const item = Database.libraryItems.find(li => li.id === req.params.id) - if (!item || !item.media) return res.sendStatus(404) + const item = await Database.models.libraryItem.getOldById(req.params.id) + if (!item?.media) return res.sendStatus(404) // Check user can access this library item if (!req.user.checkCanAccessLibraryItem(item)) { diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 98792941..06e50fdc 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -265,7 +265,7 @@ class PlaybackSessionManager { } async syncSession(user, session, syncData) { - const libraryItem = Database.libraryItems.find(li => li.id === session.libraryItemId) + const libraryItem = await Database.models.libraryItem.getOldById(session.libraryItemId) if (!libraryItem) { Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`) return null diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 9fe96793..743cb60b 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -150,7 +150,7 @@ class PodcastManager { return false } - const libraryItem = Database.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id) + const libraryItem = await Database.models.libraryItem.getOldById(this.currentDownload.libraryItem.id) if (!libraryItem) { Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`) return false diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index bf440b46..835f408a 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -6,6 +6,7 @@ const Database = require('../Database') const fs = require('../libs/fsExtra') const Feed = require('../objects/Feed') +const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class RssFeedManager { constructor() { } @@ -18,15 +19,15 @@ class RssFeedManager { return false } } else if (feedObj.entityType === 'libraryItem') { - if (!Database.libraryItems.some(li => li.id === feedObj.entityId)) { + const libraryItemExists = await Database.models.libraryItem.checkExistsById(feedObj.entityId) + if (!libraryItemExists) { Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`) return false } } else if (feedObj.entityType === 'series') { const series = Database.series.find(s => s.id === feedObj.entityId) - const hasSeriesBook = series ? Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) : false - if (!hasSeriesBook) { - Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`) + if (!series) { + Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`) return false } } else { @@ -102,9 +103,9 @@ class RssFeedManager { await Database.updateFeed(feed) } } else if (feed.entityType === 'collection') { - const collection = await Database.models.collection.getOldById(feed.entityId) + const collection = await Database.models.collection.findByPk(feed.entityId) if (collection) { - const collectionExpanded = collection.toJSONExpanded(Database.libraryItems) + const collectionExpanded = await collection.getOldJsonExpanded() // Find most recently updated item in collection let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate @@ -125,8 +126,9 @@ class RssFeedManager { const series = Database.series.find(s => s.id === feed.entityId) if (series) { const seriesJson = series.toJSON() + // Get books in series that have audio tracks - seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) + seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks) // Find most recently updated item in series let mostRecentlyUpdatedAt = seriesJson.updatedAt diff --git a/server/models/Playlist.js b/server/models/Playlist.js index dea94504..7e466289 100644 --- a/server/models/Playlist.js +++ b/server/models/Playlist.js @@ -1,8 +1,7 @@ -const { DataTypes, Model, Op } = require('sequelize') +const { DataTypes, Model, Op, literal } = require('sequelize') const Logger = require('../Logger') const oldPlaylist = require('../objects/Playlist') -const { areEquivalent } = require('../utils/index') module.exports = (sequelize) => { class Playlist extends Model { @@ -101,49 +100,6 @@ module.exports = (sequelize) => { return this.create(playlist) } - static async fullUpdateFromOld(oldPlaylist, playlistMediaItems) { - const existingPlaylist = await this.findByPk(oldPlaylist.id, { - include: sequelize.models.playlistMediaItem - }) - if (!existingPlaylist) return false - - let hasUpdates = false - const playlist = this.getFromOld(oldPlaylist) - - for (const pmi of playlistMediaItems) { - const existingPmi = existingPlaylist.playlistMediaItems.find(i => i.mediaItemId === pmi.mediaItemId) - if (!existingPmi) { - await sequelize.models.playlistMediaItem.create(pmi) - hasUpdates = true - } else if (existingPmi.order != pmi.order) { - await existingPmi.update({ order: pmi.order }) - hasUpdates = true - } - } - for (const pmi of existingPlaylist.playlistMediaItems) { - // Pmi was removed - if (!playlistMediaItems.some(i => i.mediaItemId === pmi.mediaItemId)) { - await pmi.destroy() - hasUpdates = true - } - } - - let hasPlaylistUpdates = false - for (const key in playlist) { - let existingValue = existingPlaylist[key] - if (existingValue instanceof Date) existingValue = existingValue.valueOf() - - if (!areEquivalent(playlist[key], existingValue)) { - hasPlaylistUpdates = true - } - } - if (hasPlaylistUpdates) { - existingPlaylist.update(playlist) - hasUpdates = true - } - return hasUpdates - } - static getFromOld(oldPlaylist) { return { id: oldPlaylist.id, @@ -196,7 +152,7 @@ module.exports = (sequelize) => { * Get playlists for user and optionally for library * @param {string} userId * @param {[string]} libraryId optional - * @returns {Promise} + * @returns {Promise} */ static async getPlaylistsForUserAndLibrary(userId, libraryId = null) { if (!userId && !libraryId) return [] @@ -225,9 +181,12 @@ module.exports = (sequelize) => { } ] }, - order: [['playlistMediaItems', 'order', 'ASC']] + order: [ + [literal('name COLLATE NOCASE'), 'ASC'], + ['playlistMediaItems', 'order', 'ASC'] + ] }) - return playlists.map(p => this.getOldPlaylist(p)) + return playlists } /** diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 260ea0cb..aa8c03a7 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -70,21 +70,21 @@ class ApiRouter { // this.router.post('/libraries', LibraryController.create.bind(this)) this.router.get('/libraries', LibraryController.findAll.bind(this)) - this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this)) + this.router.get('/libraries/:id', LibraryController.middlewareNew.bind(this), LibraryController.findOne.bind(this)) this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this)) this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this)) this.router.get('/libraries/:id/items2', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryItemsNew.bind(this)) this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this)) - this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) - this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) + this.router.delete('/libraries/:id/issues', LibraryController.middlewareNew.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) + this.router.get('/libraries/:id/episode-downloads', LibraryController.middlewareNew.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) - this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this)) + this.router.get('/libraries/:id/series/:seriesId', LibraryController.middlewareNew.bind(this), LibraryController.getSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/collections', LibraryController.middlewareNew.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) - this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) + this.router.get('/libraries/:id/playlists', LibraryController.middlewareNew.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) this.router.get('/libraries/:id/personalized2', LibraryController.middlewareNew.bind(this), LibraryController.getUserPersonalizedShelves.bind(this)) this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this)) - this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this)) + this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this)) this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this)) this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this)) @@ -447,11 +447,14 @@ class ApiRouter { } await Database.removeLibraryItem(libraryItem.id) - SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded()) + + SocketAuthority.emitter('item_removed', { + id: libraryItem.id + }) } async checkRemoveEmptySeries(seriesToCheck, excludeLibraryItemId = null) { - if (!seriesToCheck || !seriesToCheck.length) return + if (!seriesToCheck?.length) return for (const series of seriesToCheck) { const otherLibraryItemsInSeries = Database.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id)) diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index c0a93d15..492cbe76 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -1,7 +1,12 @@ const Sequelize = require('sequelize') +const Logger = require('../../Logger') const Database = require('../../Database') const libraryItemsBookFilters = require('./libraryItemsBookFilters') const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters') +const { createNewSortInstance } = require('../../libs/fastSort') +const naturalSort = createNewSortInstance({ + comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare +}) module.exports = { decode(text) { @@ -381,5 +386,109 @@ module.exports = { */ getLibraryItemsForCollection(collection) { return libraryItemsBookFilters.getLibraryItemsForCollection(collection) + }, + + /** + * Get filter data used in filter menus + * @param {oldLibrary} oldLibrary + * @returns {Promise} + */ + async getFilterData(oldLibrary) { + const cachedFilterData = Database.libraryFilterData[oldLibrary.id] + if (cachedFilterData) { + const cacheElapsed = Date.now() - cachedFilterData.loadedAt + // Cache library filters for 30 mins + // TODO: Keep cached filter data up-to-date on updates + if (cacheElapsed < 1000 * 60 * 30) { + return cachedFilterData + } + } + const start = Date.now() // Temp for checking load times + + const data = { + authors: [], + genres: new Set(), + tags: new Set(), + series: [], + narrators: new Set(), + languages: new Set(), + publishers: new Set(), + numIssues: 0 + } + + if (oldLibrary.isPodcast) { + const podcasts = await Database.models.podcast.findAll({ + include: { + model: Database.models.libraryItem, + attributes: [], + where: { + libraryId: oldLibrary.id + } + }, + attributes: ['tags', 'genres'] + }) + for (const podcast of podcasts) { + if (podcast.tags?.length) { + podcast.tags.forEach((tag) => data.tags.add(tag)) + } + if (podcast.genres?.length) { + podcast.genres.forEach((genre) => data.genres.add(genre)) + } + } + } else { + const books = await Database.models.book.findAll({ + include: { + model: Database.models.libraryItem, + attributes: ['isMissing', 'isInvalid'], + where: { + libraryId: oldLibrary.id + } + }, + attributes: ['tags', 'genres', 'publisher', 'narrators', 'language'] + }) + for (const book of books) { + if (book.libraryItem.isMissing || book.libraryItem.isInvalid) data.numIssues++ + if (book.tags?.length) { + book.tags.forEach((tag) => data.tags.add(tag)) + } + if (book.genres?.length) { + book.genres.forEach((genre) => data.genres.add(genre)) + } + if (book.narrators?.length) { + book.narrators.forEach((narrator) => data.narrators.add(narrator)) + } + if (book.publisher) data.publishers.add(book.publisher) + if (book.language) data.languages.add(book.language) + } + + const series = await Database.models.series.findAll({ + where: { + libraryId: oldLibrary.id + }, + attributes: ['id', 'name'] + }) + series.forEach((s) => data.series.push({ id: s.id, name: s.name })) + + const authors = await Database.models.author.findAll({ + where: { + libraryId: oldLibrary.id + }, + attributes: ['id', 'name'] + }) + authors.forEach((a) => data.authors.push({ id: a.id, name: a.name })) + } + + data.authors = naturalSort(data.authors).asc(au => au.name) + data.genres = naturalSort([...data.genres]).asc() + data.tags = naturalSort([...data.tags]).asc() + data.series = naturalSort(data.series).asc(se => se.name) + data.narrators = naturalSort([...data.narrators]).asc() + data.publishers = naturalSort([...data.publishers]).asc() + data.languages = naturalSort([...data.languages]).asc() + data.loadedAt = Date.now() + Database.libraryFilterData[oldLibrary.id] = data + + Logger.debug(`Loaded filterdata in ${((Date.now() - start) / 1000).toFixed(2)}s`) + return data } } \ No newline at end of file diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index a0abcc92..67a933b9 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -914,5 +914,16 @@ module.exports = { libraryItem.media = book return libraryItem }) + }, + + /** + * Get library items for series + * @param {oldSeries} oldSeries + * @param {[oldUser]} oldUser + * @returns {Promise} + */ + async getLibraryItemsForSeries(oldSeries, oldUser) { + const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null) + return libraryItems.map(li => Database.models.libraryItem.getOldLibraryItem(li)) } } \ No newline at end of file