mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			577 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			577 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const { Request, Response, NextFunction } = require('express')
 | 
						|
const Logger = require('../Logger')
 | 
						|
const SocketAuthority = require('../SocketAuthority')
 | 
						|
const Database = require('../Database')
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef RequestUserObject
 | 
						|
 * @property {import('../models/User')} user
 | 
						|
 *
 | 
						|
 * @typedef {Request & RequestUserObject} RequestWithUser
 | 
						|
 *
 | 
						|
 * @typedef RequestEntityObject
 | 
						|
 * @property {import('../models/Playlist')} playlist
 | 
						|
 *
 | 
						|
 * @typedef {RequestWithUser & RequestEntityObject} PlaylistControllerRequest
 | 
						|
 */
 | 
						|
 | 
						|
class PlaylistController {
 | 
						|
  constructor() {}
 | 
						|
 | 
						|
  /**
 | 
						|
   * POST: /api/playlists
 | 
						|
   * Create playlist
 | 
						|
   *
 | 
						|
   * @param {RequestWithUser} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async create(req, res) {
 | 
						|
    const reqBody = req.body || {}
 | 
						|
 | 
						|
    // Validation
 | 
						|
    if (!reqBody.name || !reqBody.libraryId) {
 | 
						|
      return res.status(400).send('Invalid playlist data')
 | 
						|
    }
 | 
						|
    if (reqBody.description && typeof reqBody.description !== 'string') {
 | 
						|
      return res.status(400).send('Invalid playlist description')
 | 
						|
    }
 | 
						|
    const items = reqBody.items || []
 | 
						|
    const isPodcast = items.some((i) => i.episodeId)
 | 
						|
    const libraryItemIds = new Set()
 | 
						|
    for (const item of items) {
 | 
						|
      if (!item.libraryItemId || typeof item.libraryItemId !== 'string') {
 | 
						|
        return res.status(400).send('Invalid playlist item')
 | 
						|
      }
 | 
						|
      if (isPodcast && (!item.episodeId || typeof item.episodeId !== 'string')) {
 | 
						|
        return res.status(400).send('Invalid playlist item episodeId')
 | 
						|
      } else if (!isPodcast && item.episodeId) {
 | 
						|
        return res.status(400).send('Invalid playlist item episodeId')
 | 
						|
      }
 | 
						|
      libraryItemIds.add(item.libraryItemId)
 | 
						|
    }
 | 
						|
 | 
						|
    // Load library items
 | 
						|
    const libraryItems = await Database.libraryItemModel.findAll({
 | 
						|
      attributes: ['id', 'mediaId', 'mediaType', 'libraryId'],
 | 
						|
      where: {
 | 
						|
        id: Array.from(libraryItemIds),
 | 
						|
        libraryId: reqBody.libraryId,
 | 
						|
        mediaType: isPodcast ? 'podcast' : 'book'
 | 
						|
      }
 | 
						|
    })
 | 
						|
    if (libraryItems.length !== libraryItemIds.size) {
 | 
						|
      return res.status(400).send('Invalid playlist data. Invalid items')
 | 
						|
    }
 | 
						|
 | 
						|
    // Validate podcast episodes
 | 
						|
    if (isPodcast) {
 | 
						|
      const podcastEpisodeIds = items.map((i) => i.episodeId)
 | 
						|
      const podcastEpisodes = await Database.podcastEpisodeModel.findAll({
 | 
						|
        attributes: ['id'],
 | 
						|
        where: {
 | 
						|
          id: podcastEpisodeIds
 | 
						|
        }
 | 
						|
      })
 | 
						|
      if (podcastEpisodes.length !== podcastEpisodeIds.length) {
 | 
						|
        return res.status(400).send('Invalid playlist data. Invalid podcast episodes')
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const transaction = await Database.sequelize.transaction()
 | 
						|
    try {
 | 
						|
      // Create playlist
 | 
						|
      const newPlaylist = await Database.playlistModel.create(
 | 
						|
        {
 | 
						|
          libraryId: reqBody.libraryId,
 | 
						|
          userId: req.user.id,
 | 
						|
          name: reqBody.name,
 | 
						|
          description: reqBody.description || null
 | 
						|
        },
 | 
						|
        { transaction }
 | 
						|
      )
 | 
						|
 | 
						|
      // Create playlistMediaItems
 | 
						|
      const playlistItemPayloads = []
 | 
						|
      for (const [index, item] of items.entries()) {
 | 
						|
        const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId)
 | 
						|
        playlistItemPayloads.push({
 | 
						|
          playlistId: newPlaylist.id,
 | 
						|
          mediaItemId: item.episodeId || libraryItem.mediaId,
 | 
						|
          mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
 | 
						|
          order: index + 1
 | 
						|
        })
 | 
						|
      }
 | 
						|
 | 
						|
      await Database.playlistMediaItemModel.bulkCreate(playlistItemPayloads, { transaction })
 | 
						|
 | 
						|
      await transaction.commit()
 | 
						|
 | 
						|
      newPlaylist.playlistMediaItems = await newPlaylist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
 | 
						|
      const jsonExpanded = newPlaylist.toOldJSONExpanded()
 | 
						|
      SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
 | 
						|
      res.json(jsonExpanded)
 | 
						|
    } catch (error) {
 | 
						|
      await transaction.rollback()
 | 
						|
      Logger.error('[PlaylistController] create:', error)
 | 
						|
      res.status(500).send('Failed to create playlist')
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @deprecated - Use /api/libraries/:libraryId/playlists
 | 
						|
   * This is not used by Abs web client or mobile apps
 | 
						|
   * TODO: Remove this endpoint or make it the primary
 | 
						|
   *
 | 
						|
   * GET: /api/playlists
 | 
						|
   * Get all playlists for user
 | 
						|
   *
 | 
						|
   * @param {RequestWithUser} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async findAllForUser(req, res) {
 | 
						|
    const playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id)
 | 
						|
    res.json({
 | 
						|
      playlists: playlistsForUser
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * GET: /api/playlists/:id
 | 
						|
   *
 | 
						|
   * @param {PlaylistControllerRequest} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async findOne(req, res) {
 | 
						|
    req.playlist.playlistMediaItems = await req.playlist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
    res.json(req.playlist.toOldJSONExpanded())
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * PATCH: /api/playlists/:id
 | 
						|
   * Update playlist
 | 
						|
   *
 | 
						|
   * Used for updating name and description or reordering items
 | 
						|
   *
 | 
						|
   * @param {PlaylistControllerRequest} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async update(req, res) {
 | 
						|
    // Validation
 | 
						|
    const reqBody = req.body || {}
 | 
						|
    if (reqBody.libraryId || reqBody.userId) {
 | 
						|
      // Could allow support for this if needed with additional validation
 | 
						|
      return res.status(400).send('Invalid playlist data. Cannot update libraryId or userId')
 | 
						|
    }
 | 
						|
    if (reqBody.name && typeof reqBody.name !== 'string') {
 | 
						|
      return res.status(400).send('Invalid playlist name')
 | 
						|
    }
 | 
						|
    if (reqBody.description && typeof reqBody.description !== 'string') {
 | 
						|
      return res.status(400).send('Invalid playlist description')
 | 
						|
    }
 | 
						|
    if (reqBody.items && (!Array.isArray(reqBody.items) || reqBody.items.some((i) => !i.libraryItemId || typeof i.libraryItemId !== 'string' || (i.episodeId && typeof i.episodeId !== 'string')))) {
 | 
						|
      return res.status(400).send('Invalid playlist items')
 | 
						|
    }
 | 
						|
 | 
						|
    const playlistUpdatePayload = {}
 | 
						|
    if (reqBody.name) playlistUpdatePayload.name = reqBody.name
 | 
						|
    if (reqBody.description) playlistUpdatePayload.description = reqBody.description
 | 
						|
 | 
						|
    // Update name and description
 | 
						|
    let wasUpdated = false
 | 
						|
    if (Object.keys(playlistUpdatePayload).length) {
 | 
						|
      req.playlist.set(playlistUpdatePayload)
 | 
						|
      const changed = req.playlist.changed()
 | 
						|
      if (changed?.length) {
 | 
						|
        await req.playlist.save()
 | 
						|
        Logger.debug(`[PlaylistController] Updated playlist ${req.playlist.id} keys [${changed.join(',')}]`)
 | 
						|
        wasUpdated = true
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // If array of items is set then update order of playlist media items
 | 
						|
    if (reqBody.items?.length) {
 | 
						|
      const libraryItemIds = Array.from(new Set(reqBody.items.map((i) => i.libraryItemId)))
 | 
						|
      const libraryItems = await Database.libraryItemModel.findAll({
 | 
						|
        attributes: ['id', 'mediaId', 'mediaType'],
 | 
						|
        where: {
 | 
						|
          id: libraryItemIds
 | 
						|
        }
 | 
						|
      })
 | 
						|
      if (libraryItems.length !== libraryItemIds.length) {
 | 
						|
        return res.status(400).send('Invalid playlist items. Items not found')
 | 
						|
      }
 | 
						|
      /** @type {import('../models/PlaylistMediaItem')[]} */
 | 
						|
      const existingPlaylistMediaItems = await req.playlist.getPlaylistMediaItems({
 | 
						|
        order: [['order', 'ASC']]
 | 
						|
      })
 | 
						|
      if (existingPlaylistMediaItems.length !== reqBody.items.length) {
 | 
						|
        return res.status(400).send('Invalid playlist items. Length mismatch')
 | 
						|
      }
 | 
						|
 | 
						|
      // Set an array of mediaItemId
 | 
						|
      const newMediaItemIdOrder = []
 | 
						|
      for (const item of reqBody.items) {
 | 
						|
        const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId)
 | 
						|
        const mediaItemId = item.episodeId || libraryItem.mediaId
 | 
						|
        newMediaItemIdOrder.push(mediaItemId)
 | 
						|
      }
 | 
						|
 | 
						|
      // Sort existing playlist media items into new order
 | 
						|
      existingPlaylistMediaItems.sort((a, b) => {
 | 
						|
        const aIndex = newMediaItemIdOrder.findIndex((i) => i === a.mediaItemId)
 | 
						|
        const bIndex = newMediaItemIdOrder.findIndex((i) => i === b.mediaItemId)
 | 
						|
        return aIndex - bIndex
 | 
						|
      })
 | 
						|
 | 
						|
      // Update order on playlistMediaItem records
 | 
						|
      for (const [index, playlistMediaItem] of existingPlaylistMediaItems.entries()) {
 | 
						|
        if (playlistMediaItem.order !== index + 1) {
 | 
						|
          await playlistMediaItem.update({
 | 
						|
            order: index + 1
 | 
						|
          })
 | 
						|
          wasUpdated = true
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    req.playlist.playlistMediaItems = await req.playlist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
 | 
						|
    const jsonExpanded = req.playlist.toOldJSONExpanded()
 | 
						|
    if (wasUpdated) {
 | 
						|
      SocketAuthority.clientEmitter(req.playlist.userId, 'playlist_updated', jsonExpanded)
 | 
						|
    }
 | 
						|
    res.json(jsonExpanded)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * DELETE: /api/playlists/:id
 | 
						|
   * Remove playlist
 | 
						|
   *
 | 
						|
   * @param {PlaylistControllerRequest} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async delete(req, res) {
 | 
						|
    req.playlist.playlistMediaItems = await req.playlist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
    const jsonExpanded = req.playlist.toOldJSONExpanded()
 | 
						|
 | 
						|
    await req.playlist.destroy()
 | 
						|
    SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
 | 
						|
    res.sendStatus(200)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * POST: /api/playlists/:id/item
 | 
						|
   * Add item to playlist
 | 
						|
   *
 | 
						|
   * This is not used by Abs web client or mobile apps. Only the batch endpoints are used.
 | 
						|
   *
 | 
						|
   * @param {PlaylistControllerRequest} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async addItem(req, res) {
 | 
						|
    const itemToAdd = req.body || {}
 | 
						|
 | 
						|
    if (!itemToAdd.libraryItemId) {
 | 
						|
      return res.status(400).send('Request body has no libraryItemId')
 | 
						|
    }
 | 
						|
 | 
						|
    const libraryItem = await Database.libraryItemModel.getExpandedById(itemToAdd.libraryItemId)
 | 
						|
    if (!libraryItem) {
 | 
						|
      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()
 | 
						|
 | 
						|
    if (req.playlist.checkHasMediaItem(itemToAdd.libraryItemId, itemToAdd.episodeId)) {
 | 
						|
      return res.status(400).send('Item already in playlist')
 | 
						|
    }
 | 
						|
 | 
						|
    const jsonExpanded = req.playlist.toOldJSONExpanded()
 | 
						|
 | 
						|
    const playlistMediaItem = {
 | 
						|
      playlistId: req.playlist.id,
 | 
						|
      mediaItemId: itemToAdd.episodeId || libraryItem.media.id,
 | 
						|
      mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book',
 | 
						|
      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()
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
 | 
						|
    res.json(jsonExpanded)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * DELETE: /api/playlists/:id/item/:libraryItemId/:episodeId?
 | 
						|
   * Remove item from playlist
 | 
						|
   *
 | 
						|
   * @param {PlaylistControllerRequest} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async removeItem(req, res) {
 | 
						|
    req.playlist.playlistMediaItems = await req.playlist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
 | 
						|
    let playlistMediaItem = null
 | 
						|
    if (req.params.episodeId) {
 | 
						|
      playlistMediaItem = req.playlist.playlistMediaItems.find((pmi) => pmi.mediaItemId === req.params.episodeId)
 | 
						|
    } else {
 | 
						|
      playlistMediaItem = req.playlist.playlistMediaItems.find((pmi) => pmi.mediaItem.libraryItem?.id === req.params.libraryItemId)
 | 
						|
    }
 | 
						|
    if (!playlistMediaItem) {
 | 
						|
      return res.status(404).send('Media item not found in playlist')
 | 
						|
    }
 | 
						|
 | 
						|
    // Remove record
 | 
						|
    await playlistMediaItem.destroy()
 | 
						|
    req.playlist.playlistMediaItems = req.playlist.playlistMediaItems.filter((pmi) => pmi.id !== playlistMediaItem.id)
 | 
						|
 | 
						|
    // Update playlist media items order
 | 
						|
    for (const [index, mediaItem] of req.playlist.playlistMediaItems.entries()) {
 | 
						|
      if (mediaItem.order !== index + 1) {
 | 
						|
        await mediaItem.update({
 | 
						|
          order: index + 1
 | 
						|
        })
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const jsonExpanded = req.playlist.toOldJSONExpanded()
 | 
						|
 | 
						|
    // Playlist is removed when there are no items
 | 
						|
    if (!jsonExpanded.items.length) {
 | 
						|
      Logger.info(`[PlaylistController] Playlist "${jsonExpanded.name}" has no more items - removing it`)
 | 
						|
      await req.playlist.destroy()
 | 
						|
      SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
 | 
						|
    } else {
 | 
						|
      SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
 | 
						|
    }
 | 
						|
 | 
						|
    res.json(jsonExpanded)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * POST: /api/playlists/:id/batch/add
 | 
						|
   * Batch add playlist items
 | 
						|
   *
 | 
						|
   * @param {PlaylistControllerRequest} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async addBatch(req, res) {
 | 
						|
    if (!req.body.items?.length || !Array.isArray(req.body.items) || req.body.items.some((i) => !i?.libraryItemId || typeof i.libraryItemId !== 'string' || (i.episodeId && typeof i.episodeId !== 'string'))) {
 | 
						|
      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) {
 | 
						|
      return res.status(400).send('Invalid request body items')
 | 
						|
    }
 | 
						|
 | 
						|
    req.playlist.playlistMediaItems = await req.playlist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
 | 
						|
    const mediaItemsToAdd = []
 | 
						|
    const jsonExpanded = req.playlist.toOldJSONExpanded()
 | 
						|
 | 
						|
    // 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
 | 
						|
      if (req.playlist.playlistMediaItems.some((pmi) => pmi.mediaItemId === mediaItemId)) {
 | 
						|
        // Already exists in playlist
 | 
						|
        continue
 | 
						|
      } else {
 | 
						|
        mediaItemsToAdd.push({
 | 
						|
          playlistId: req.playlist.id,
 | 
						|
          mediaItemId,
 | 
						|
          mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
 | 
						|
          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()
 | 
						|
          })
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (mediaItemsToAdd.length) {
 | 
						|
      await Database.playlistMediaItemModel.bulkCreate(mediaItemsToAdd)
 | 
						|
 | 
						|
      SocketAuthority.clientEmitter(req.playlist.userId, 'playlist_updated', jsonExpanded)
 | 
						|
    }
 | 
						|
 | 
						|
    res.json(jsonExpanded)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * POST: /api/playlists/:id/batch/remove
 | 
						|
   * Batch remove playlist items
 | 
						|
   *
 | 
						|
   * @param {PlaylistControllerRequest} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async removeBatch(req, res) {
 | 
						|
    if (!req.body.items?.length || !Array.isArray(req.body.items) || req.body.items.some((i) => !i?.libraryItemId || typeof i.libraryItemId !== 'string' || (i.episodeId && typeof i.episodeId !== 'string'))) {
 | 
						|
      return res.status(400).send('Invalid request body items')
 | 
						|
    }
 | 
						|
 | 
						|
    req.playlist.playlistMediaItems = await req.playlist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
 | 
						|
    // Remove playlist media items
 | 
						|
    let hasUpdated = false
 | 
						|
    for (const item of req.body.items) {
 | 
						|
      let playlistMediaItem = null
 | 
						|
      if (item.episodeId) {
 | 
						|
        playlistMediaItem = req.playlist.playlistMediaItems.find((pmi) => pmi.mediaItemId === item.episodeId)
 | 
						|
      } else {
 | 
						|
        playlistMediaItem = req.playlist.playlistMediaItems.find((pmi) => pmi.mediaItem.libraryItem?.id === item.libraryItemId)
 | 
						|
      }
 | 
						|
      if (!playlistMediaItem) {
 | 
						|
        Logger.warn(`[PlaylistController] Playlist item not found in playlist ${req.playlist.id}`, item)
 | 
						|
        continue
 | 
						|
      }
 | 
						|
 | 
						|
      await playlistMediaItem.destroy()
 | 
						|
      req.playlist.playlistMediaItems = req.playlist.playlistMediaItems.filter((pmi) => pmi.id !== playlistMediaItem.id)
 | 
						|
 | 
						|
      hasUpdated = true
 | 
						|
    }
 | 
						|
 | 
						|
    const jsonExpanded = req.playlist.toOldJSONExpanded()
 | 
						|
    if (hasUpdated) {
 | 
						|
      // Playlist is removed when there are no items
 | 
						|
      if (!req.playlist.playlistMediaItems.length) {
 | 
						|
        Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`)
 | 
						|
        await req.playlist.destroy()
 | 
						|
        SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
 | 
						|
      } else {
 | 
						|
        SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
 | 
						|
      }
 | 
						|
    }
 | 
						|
    res.json(jsonExpanded)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * POST: /api/playlists/collection/:collectionId
 | 
						|
   * Create a playlist from a collection
 | 
						|
   *
 | 
						|
   * @param {RequestWithUser} req
 | 
						|
   * @param {Response} res
 | 
						|
   */
 | 
						|
  async createFromCollection(req, res) {
 | 
						|
    const collection = await Database.collectionModel.findByPk(req.params.collectionId)
 | 
						|
    if (!collection) {
 | 
						|
      return res.status(404).send('Collection not found')
 | 
						|
    }
 | 
						|
    // Expand collection to get library items
 | 
						|
    const collectionExpanded = await collection.getOldJsonExpanded(req.user)
 | 
						|
    if (!collectionExpanded) {
 | 
						|
      // This can happen if the user has no access to all items in collection
 | 
						|
      return res.status(404).send('Collection not found')
 | 
						|
    }
 | 
						|
 | 
						|
    // Playlists cannot be empty
 | 
						|
    if (!collectionExpanded.books.length) {
 | 
						|
      return res.status(400).send('Collection has no books')
 | 
						|
    }
 | 
						|
 | 
						|
    const transaction = await Database.sequelize.transaction()
 | 
						|
    try {
 | 
						|
      const playlist = await Database.playlistModel.create(
 | 
						|
        {
 | 
						|
          userId: req.user.id,
 | 
						|
          libraryId: collection.libraryId,
 | 
						|
          name: collection.name,
 | 
						|
          description: collection.description || null
 | 
						|
        },
 | 
						|
        { transaction }
 | 
						|
      )
 | 
						|
 | 
						|
      const mediaItemsToAdd = []
 | 
						|
      for (const [index, libraryItem] of collectionExpanded.books.entries()) {
 | 
						|
        mediaItemsToAdd.push({
 | 
						|
          playlistId: playlist.id,
 | 
						|
          mediaItemId: libraryItem.media.id,
 | 
						|
          mediaItemType: 'book',
 | 
						|
          order: index + 1
 | 
						|
        })
 | 
						|
      }
 | 
						|
      await Database.playlistMediaItemModel.bulkCreate(mediaItemsToAdd, { transaction })
 | 
						|
 | 
						|
      await transaction.commit()
 | 
						|
 | 
						|
      playlist.playlistMediaItems = await playlist.getMediaItemsExpandedWithLibraryItem()
 | 
						|
 | 
						|
      const jsonExpanded = playlist.toOldJSONExpanded()
 | 
						|
      SocketAuthority.clientEmitter(playlist.userId, 'playlist_added', jsonExpanded)
 | 
						|
      res.json(jsonExpanded)
 | 
						|
    } catch (error) {
 | 
						|
      await transaction.rollback()
 | 
						|
      Logger.error('[PlaylistController] createFromCollection:', error)
 | 
						|
      res.status(500).send('Failed to create playlist')
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   *
 | 
						|
   * @param {RequestWithUser} req
 | 
						|
   * @param {Response} res
 | 
						|
   * @param {NextFunction} next
 | 
						|
   */
 | 
						|
  async middleware(req, res, next) {
 | 
						|
    if (req.params.id) {
 | 
						|
      const playlist = await Database.playlistModel.findByPk(req.params.id)
 | 
						|
      if (!playlist) {
 | 
						|
        return res.status(404).send('Playlist not found')
 | 
						|
      }
 | 
						|
      if (playlist.userId !== req.user.id) {
 | 
						|
        Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`)
 | 
						|
        return res.sendStatus(403)
 | 
						|
      }
 | 
						|
      req.playlist = playlist
 | 
						|
    }
 | 
						|
 | 
						|
    next()
 | 
						|
  }
 | 
						|
}
 | 
						|
module.exports = new PlaylistController()
 |