diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 6ad7cff9..20f510b6 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -3,6 +3,7 @@ const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const htmlSanitizer = require('../utils/htmlSanitizer') +const { resolvePlaylistRequestItems } = require('../utils/playlistHelpers') /** * @typedef RequestUserObject @@ -287,19 +288,10 @@ class PlaylistController { return res.status(400).send('Request body has no libraryItemId') } - const libraryItem = await Database.libraryItemModel.getExpandedById(itemToAdd.libraryItemId) - if (!libraryItem) { + const [resolvedItem] = (await resolvePlaylistRequestItems([itemToAdd], req.playlist.libraryId)) || [] + if (!resolvedItem) { return res.status(400).send('Library item not found') } - if (libraryItem.libraryId !== req.playlist.libraryId) { - return res.status(400).send('Library item in different library') - } - if ((itemToAdd.episodeId && !libraryItem.isPodcast) || (libraryItem.isPodcast && !itemToAdd.episodeId)) { - return res.status(400).send('Invalid item to add for this library type') - } - if (itemToAdd.episodeId && !libraryItem.media.podcastEpisodes.some((pe) => pe.id === itemToAdd.episodeId)) { - return res.status(400).send('Episode not found in library item') - } req.playlist.playlistMediaItems = await req.playlist.getMediaItemsExpandedWithLibraryItem() @@ -311,27 +303,13 @@ class PlaylistController { const playlistMediaItem = { playlistId: req.playlist.id, - mediaItemId: itemToAdd.episodeId || libraryItem.media.id, - mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book', + mediaItemId: resolvedItem.mediaItemId, + mediaItemType: resolvedItem.mediaItemType, order: req.playlist.playlistMediaItems.length + 1 } await Database.playlistMediaItemModel.create(playlistMediaItem) - // Add the new item to to the old json expanded to prevent having to fully reload the playlist media items - if (itemToAdd.episodeId) { - const episode = libraryItem.media.podcastEpisodes.find((ep) => ep.id === itemToAdd.episodeId) - jsonExpanded.items.push({ - episodeId: itemToAdd.episodeId, - episode: episode.toOldJSONExpanded(libraryItem.id), - libraryItemId: libraryItem.id, - libraryItem: libraryItem.toOldJSONMinified() - }) - } else { - jsonExpanded.items.push({ - libraryItemId: libraryItem.id, - libraryItem: libraryItem.toOldJSONExpanded() - }) - } + jsonExpanded.items.push(resolvedItem.jsonItem) SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded) res.json(jsonExpanded) @@ -396,11 +374,8 @@ class PlaylistController { return res.status(400).send('Invalid request body items') } - // Find all library items - const libraryItemIds = new Set(req.body.items.map((i) => i.libraryItemId).filter((i) => i)) - - const libraryItems = await Database.libraryItemModel.findAllExpandedWhere({ id: Array.from(libraryItemIds) }) - if (libraryItems.length !== libraryItemIds.size) { + const resolvedItems = await resolvePlaylistRequestItems(req.body.items, req.playlist.libraryId) + if (!resolvedItems || resolvedItems.some((item) => !item)) { return res.status(400).send('Invalid request body items') } @@ -411,10 +386,8 @@ class PlaylistController { // Setup array of playlistMediaItem records to add let order = req.playlist.playlistMediaItems.length + 1 - for (const item of req.body.items) { - const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId) - - const mediaItemId = item.episodeId || libraryItem.media.id + for (const resolvedItem of resolvedItems) { + const { mediaItemId, mediaItemType, jsonItem } = resolvedItem if (req.playlist.playlistMediaItems.some((pmi) => pmi.mediaItemId === mediaItemId)) { // Already exists in playlist continue @@ -422,25 +395,10 @@ class PlaylistController { mediaItemsToAdd.push({ playlistId: req.playlist.id, mediaItemId, - mediaItemType: item.episodeId ? 'podcastEpisode' : 'book', + mediaItemType, order: order++ }) - - // Add the new item to to the old json expanded to prevent having to fully reload the playlist media items - if (item.episodeId) { - const episode = libraryItem.media.podcastEpisodes.find((ep) => ep.id === item.episodeId) - jsonExpanded.items.push({ - episodeId: item.episodeId, - episode: episode.toOldJSONExpanded(libraryItem.id), - libraryItemId: libraryItem.id, - libraryItem: libraryItem.toOldJSONMinified() - }) - } else { - jsonExpanded.items.push({ - libraryItemId: libraryItem.id, - libraryItem: libraryItem.toOldJSONExpanded() - }) - } + jsonExpanded.items.push(jsonItem) } } diff --git a/server/utils/playlistHelpers.js b/server/utils/playlistHelpers.js new file mode 100644 index 00000000..b2ac1fde --- /dev/null +++ b/server/utils/playlistHelpers.js @@ -0,0 +1,100 @@ +const Database = require('../Database') + +async function resolvePlaylistRequestItems(items, libraryId) { + // Get lists of items, episodes, and books to simplify later operations + const libraryItemIds = Array.from(new Set(items.map((i) => i.libraryItemId).filter((i) => i))) + const episodeIds = Array.from(new Set(items.map((i) => i.episodeId).filter((i) => i))) + const bookLibraryItemIds = Array.from(new Set(items.filter((i) => !i.episodeId).map((i) => i.libraryItemId))) + + // Load library items for later mapping to episodes and books. + const libraryItems = await Database.libraryItemModel.findAll({ + attributes: ['id', 'mediaId', 'mediaType', 'libraryId'], + where: { + id: libraryItemIds, + libraryId + } + }) + if (libraryItems.length !== libraryItemIds.length) { + return null + } + + const libraryItemsById = new Map(libraryItems.map((libraryItem) => [libraryItem.id, libraryItem])) + + // Books still need their fully expanded library item because the playlist response embeds the whole book object. + const bookLibraryItems = bookLibraryItemIds.length + ? await Database.libraryItemModel.findAllExpandedWhere({ + id: bookLibraryItemIds, + libraryId, + mediaType: 'book' + }) + : [] + const bookLibraryItemsById = new Map(bookLibraryItems.map((libraryItem) => [libraryItem.id, libraryItem])) + + // For podcast adds, load only the requested episodes plus their owning podcast/library item. + const podcastEpisodes = episodeIds.length + ? await Database.podcastEpisodeModel.findAll({ + where: { + id: episodeIds + }, + include: [ + { + model: Database.podcastModel, + include: [ + { + model: Database.libraryItemModel + } + ] + } + ] + }) + : [] + const podcastEpisodesById = new Map(podcastEpisodes.map((episode) => [episode.id, episode])) + + return items.map((item) => { + const libraryItem = libraryItemsById.get(item.libraryItemId) + if (!libraryItem) return null + + // If this is an episode item, create the object with owning library item + if (item.episodeId) { + const episode = podcastEpisodesById.get(item.episodeId) + if (libraryItem.mediaType !== 'podcast' || !episode?.podcast?.libraryItem || episode.podcast.libraryItem.id !== item.libraryItemId) { + return null + } + + const episodeLibraryItem = episode.podcast.libraryItem + episodeLibraryItem.media = episode.podcast + + return { + item, + mediaItemId: item.episodeId, + mediaItemType: 'podcastEpisode', + jsonItem: { + episodeId: item.episodeId, + episode: episode.toOldJSONExpanded(episodeLibraryItem.id), + libraryItemId: episodeLibraryItem.id, + libraryItem: episodeLibraryItem.toOldJSONMinified() + } + } + } + + // If not a podcast, this is a book + const expandedBookLibraryItem = bookLibraryItemsById.get(item.libraryItemId) + if (libraryItem.mediaType !== 'book' || !expandedBookLibraryItem) { + return null + } + + return { + item, + mediaItemId: expandedBookLibraryItem.media.id, + mediaItemType: 'book', + jsonItem: { + libraryItemId: expandedBookLibraryItem.id, + libraryItem: expandedBookLibraryItem.toOldJSONExpanded() + } + } + }) +} + +module.exports = { + resolvePlaylistRequestItems +}