From 710a62c2aff9b082d4ad136e2dbcf77063a4d54d Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 23 Jul 2023 09:42:57 -0500 Subject: [PATCH] Update:Load playlists only when needed & remove podcast episode from playlist when deleted --- server/Database.js | 6 - server/controllers/LibraryController.js | 5 +- server/controllers/PlaylistController.js | 9 +- server/controllers/PodcastController.js | 26 ++++- server/controllers/UserController.js | 2 +- server/models/Playlist.js | 142 ++++++++++++++++++++++- server/routers/ApiRouter.js | 11 +- 7 files changed, 178 insertions(+), 23 deletions(-) diff --git a/server/Database.js b/server/Database.js index e5f3719e..65b85070 100644 --- a/server/Database.js +++ b/server/Database.js @@ -19,7 +19,6 @@ class Database { // TODO: below data should be loaded from the DB as needed this.libraryItems = [] this.settings = [] - this.playlists = [] this.authors = [] this.series = [] @@ -160,9 +159,6 @@ class Database { this.libraryItems = await this.models.libraryItem.loadAllLibraryItems() Logger.info(`[Database] Loaded ${this.libraryItems.length} library items`) - this.playlists = await this.models.playlist.getOldPlaylists() - Logger.info(`[Database] Loaded ${this.playlists.length} playlists`) - this.authors = await this.models.author.getOldAuthors() Logger.info(`[Database] Loaded ${this.authors.length} authors`) @@ -341,7 +337,6 @@ class Database { await this.createBulkPlaylistMediaItems(playlistMediaItems) } } - this.playlists.push(oldPlaylist) } updatePlaylist(oldPlaylist) { @@ -364,7 +359,6 @@ class Database { async removePlaylist(playlistId) { if (!this.sequelize) return false await this.models.playlist.removeById(playlistId) - this.playlists = this.playlists.filter(p => p.id !== playlistId) } createPlaylistMediaItem(playlistMediaItem) { diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 807f5a0b..55d0533b 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -83,7 +83,7 @@ class LibraryController { return res.json({ filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems), issues: req.libraryItems.filter(li => li.hasIssues).length, - numUserPlaylists: Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length, + numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), library: req.library }) } @@ -557,7 +557,8 @@ class LibraryController { // api/libraries/:id/playlists async getUserPlaylistsForLibrary(req, res) { - let playlistsForUser = Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(Database.libraryItems)) + let playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id) + playlistsForUser = playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems)) const payload = { results: [], diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index d30fdc42..8c351c78 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -22,9 +22,10 @@ class PlaylistController { } // GET: api/playlists - findAllForUser(req, res) { + async findAllForUser(req, res) { + const playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id) res.json({ - playlists: Database.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(Database.libraryItems)) + playlists: playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems)) }) } @@ -231,9 +232,9 @@ class PlaylistController { res.json(jsonExpanded) } - middleware(req, res, next) { + async middleware(req, res, next) { if (req.params.id) { - const playlist = Database.playlists.find(p => p.id === req.params.id) + const playlist = await Database.models.playlist.getById(req.params.id) if (!playlist) { return res.status(404).send('Playlist not found') } diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 938b5c0c..1d1da4f7 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -241,18 +241,18 @@ class PodcastController { // DELETE: api/podcasts/:id/episode/:episodeId async removeEpisode(req, res) { - var episodeId = req.params.episodeId - var libraryItem = req.libraryItem - var hardDelete = req.query.hard === '1' + const episodeId = req.params.episodeId + const libraryItem = req.libraryItem + const hardDelete = req.query.hard === '1' - var episode = libraryItem.media.episodes.find(ep => ep.id === episodeId) + const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId) if (!episode) { Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`) return res.sendStatus(404) } if (hardDelete) { - var audioFile = episode.audioFile + const audioFile = episode.audioFile // TODO: this will trigger the watcher. should maybe handle this gracefully await fs.remove(audioFile.metadata.path).then(() => { Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`) @@ -267,6 +267,22 @@ class PodcastController { libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino) } + // Update/remove playlists that had this podcast episode + const playlistsWithEpisode = await Database.models.playlist.getPlaylistsForMediaItemIds([episodeId]) + for (const playlist of playlistsWithEpisode) { + playlist.removeItem(libraryItem.id, episodeId) + + // If playlist is now empty then remove it + if (!playlist.items.length) { + Logger.info(`[PodcastController] Playlist "${playlist.name}" has no more items - removing it`) + await Database.removePlaylist(playlist.id) + SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems)) + } else { + await Database.updatePlaylist(playlist) + SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems)) + } + } + await Database.updateLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) res.json(libraryItem.toJSON()) diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index a92fdbe1..45780045 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -122,7 +122,7 @@ class UserController { // Todo: check if user is logged in and cancel streams // Remove user playlists - const userPlaylists = Database.playlists.filter(p => p.userId === user.id) + const userPlaylists = await Database.models.playlist.getPlaylistsForUserAndLibrary(user.id) for (const playlist of userPlaylists) { await Database.removePlaylist(playlist.id) } diff --git a/server/models/Playlist.js b/server/models/Playlist.js index 3ae07f5a..bb471ba2 100644 --- a/server/models/Playlist.js +++ b/server/models/Playlist.js @@ -1,4 +1,4 @@ -const { DataTypes, Model } = require('sequelize') +const { DataTypes, Model, Op } = require('sequelize') const Logger = require('../Logger') const oldPlaylist = require('../objects/Playlist') @@ -119,6 +119,146 @@ module.exports = (sequelize) => { } }) } + + /** + * Get playlist by id + * @param {string} playlistId + * @returns {Promise} returns null if not found + */ + static async getById(playlistId) { + if (!playlistId) return null + const playlist = await this.findByPk(playlistId, { + include: { + model: sequelize.models.playlistMediaItem, + include: [ + { + model: sequelize.models.book, + include: sequelize.models.libraryItem + }, + { + model: sequelize.models.podcastEpisode, + include: { + model: sequelize.models.podcast, + include: sequelize.models.libraryItem + } + } + ] + }, + order: [['playlistMediaItems', 'order', 'ASC']] + }) + if (!playlist) return null + return this.getOldPlaylist(playlist) + } + + /** + * Get playlists for user and optionally for library + * @param {string} userId + * @param {[string]} libraryId optional + * @returns {Promise} + */ + static async getPlaylistsForUserAndLibrary(userId, libraryId = null) { + if (!userId && !libraryId) return [] + const whereQuery = {} + if (userId) { + whereQuery.userId = userId + } + if (libraryId) { + whereQuery.libraryId = libraryId + } + const playlists = await this.findAll({ + where: whereQuery, + include: { + model: sequelize.models.playlistMediaItem, + include: [ + { + model: sequelize.models.book, + include: sequelize.models.libraryItem + }, + { + model: sequelize.models.podcastEpisode, + include: { + model: sequelize.models.podcast, + include: sequelize.models.libraryItem + } + } + ] + }, + order: [['playlistMediaItems', 'order', 'ASC']] + }) + return playlists.map(p => this.getOldPlaylist(p)) + } + + /** + * Get number of playlists for a user and library + * @param {string} userId + * @param {string} libraryId + * @returns + */ + static async getNumPlaylistsForUserAndLibrary(userId, libraryId) { + return this.count({ + where: { + userId, + libraryId + } + }) + } + + /** + * Get all playlists for mediaItemIds + * @param {string[]} mediaItemIds + * @returns {Promise} + */ + static async getPlaylistsForMediaItemIds(mediaItemIds) { + if (!mediaItemIds?.length) return [] + + const playlistMediaItemsExpanded = await sequelize.models.playlistMediaItem.findAll({ + where: { + mediaItemId: { + [Op.in]: mediaItemIds + } + }, + include: [ + { + model: sequelize.models.playlist, + include: { + model: sequelize.models.playlistMediaItem, + include: [ + { + model: sequelize.models.book, + include: sequelize.models.libraryItem + }, + { + model: sequelize.models.podcastEpisode, + include: { + model: sequelize.models.podcast, + include: sequelize.models.libraryItem + } + } + ] + } + } + ], + order: [['playlist', 'playlistMediaItems', 'order', 'ASC']] + }) + return playlistMediaItemsExpanded.map(pmie => { + pmie.playlist.playlistMediaItems = pmie.playlist.playlistMediaItems.map(pmi => { + if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { + pmi.mediaItem = pmi.book + pmi.dataValues.mediaItem = pmi.dataValues.book + } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { + pmi.mediaItem = pmi.podcastEpisode + pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode + } + delete pmi.book + delete pmi.dataValues.book + delete pmi.podcastEpisode + delete pmi.dataValues.podcastEpisode + return pmi + }) + + return this.getOldPlaylist(pmie.playlist) + }) + } } Playlist.init({ diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 84fce0e8..cfad5545 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -389,7 +389,7 @@ class ApiRouter { } // TODO: Remove open sessions for library item - + let mediaItemIds = [] if (libraryItem.isBook) { // remove book from collections const collectionsWithBook = await Database.models.collection.getAllForBook(libraryItem.media.id) @@ -401,12 +401,15 @@ class ApiRouter { // Check remove empty series await this.checkRemoveEmptySeries(libraryItem.media.metadata.series, libraryItem.id) + + mediaItemIds.push(libraryItem.media.id) + } else if (libraryItem.isPodcast) { + mediaItemIds.push(...libraryItem.media.episodes.map(ep => ep.id)) } // remove item from playlists - const playlistsWithItem = Database.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id)) - for (let i = 0; i < playlistsWithItem.length; i++) { - const playlist = playlistsWithItem[i] + const playlistsWithItem = await Database.models.playlist.getPlaylistsForMediaItemIds(mediaItemIds) + for (const playlist of playlistsWithItem) { playlist.removeItemsForLibraryItem(libraryItem.id) // If playlist is now empty then remove it