Prevent loading full podcast when adding episodes to playlist

This commit is contained in:
Nicholas Wallace 2026-03-22 18:50:44 -07:00
parent 8b89b27654
commit 2bc6dcf576
2 changed files with 117 additions and 54 deletions

View file

@ -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
@ -282,19 +283,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()
@ -306,27 +298,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)
@ -391,11 +369,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')
}
@ -406,10 +381,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
@ -417,25 +390,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)
}
}

View file

@ -0,0 +1,105 @@
const Database = require('../Database')
async function getPodcastEpisodesWithLibraryItems(episodeIds) {
if (!episodeIds.length) return []
return Database.podcastEpisodeModel.findAll({
where: {
id: episodeIds
},
include: [
{
model: Database.podcastModel,
include: [
{
model: Database.libraryItemModel
}
]
}
]
})
}
async function resolvePlaylistRequestItems(items, libraryId) {
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)))
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.
// The old expanded library-item query pulled every episode for the podcast, which is what blew up memory.
const podcastEpisodes = await getPodcastEpisodesWithLibraryItems(episodeIds)
if (podcastEpisodes.length !== episodeIds.length) {
return null
}
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 (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()
}
}
}
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 = {
getPodcastEpisodesWithLibraryItems,
resolvePlaylistRequestItems
}