2024-08-11 15:15:34 -05:00
const { Request , Response , NextFunction } = require ( 'express' )
2022-11-26 15:14:45 -06:00
const Logger = require ( '../Logger' )
const SocketAuthority = require ( '../SocketAuthority' )
2023-07-04 18:14:44 -05:00
const Database = require ( '../Database' )
2026-03-19 16:57:22 -05:00
const htmlSanitizer = require ( '../utils/htmlSanitizer' )
2022-11-26 15:14:45 -06:00
2024-08-11 15:15:34 -05:00
/ * *
2024-08-11 17:01:25 -05:00
* @ typedef RequestUserObject
2024-08-11 16:07:29 -05:00
* @ property { import ( '../models/User' ) } user
2024-08-11 15:15:34 -05:00
*
2024-08-11 17:01:25 -05:00
* @ typedef { Request & RequestUserObject } RequestWithUser
2024-12-31 17:01:42 -06:00
*
* @ typedef RequestEntityObject
* @ property { import ( '../models/Playlist' ) } playlist
*
* @ typedef { RequestWithUser & RequestEntityObject } PlaylistControllerRequest
2024-08-11 15:15:34 -05:00
* /
2022-11-26 15:14:45 -06:00
class PlaylistController {
2024-08-10 17:15:21 -05:00
constructor ( ) { }
2022-11-26 15:14:45 -06:00
2023-08-13 11:22:38 -05:00
/ * *
* POST : / a p i / p l a y l i s t s
* Create playlist
2024-08-11 15:15:34 -05:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-11-26 15:14:45 -06:00
async create ( req , res ) {
2024-12-31 17:01:42 -06:00
const reqBody = req . body || { }
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
// Validation
2026-03-19 16:57:22 -05:00
const nameCleaned = htmlSanitizer . stripAllTags ( reqBody . name )
if ( ! nameCleaned || ! reqBody . libraryId ) {
2024-12-31 17:01:42 -06:00
return res . status ( 400 ) . send ( 'Invalid playlist data' )
}
if ( reqBody . description && typeof reqBody . description !== 'string' ) {
return res . status ( 400 ) . send ( 'Invalid playlist description' )
}
2026-04-22 16:42:58 -05:00
if ( ! req . user . checkCanAccessLibrary ( reqBody . libraryId ) ) {
Logger . warn ( ` [PlaylistController] User " ${ req . user . username } " attempted to create playlist in inaccessible library ${ reqBody . libraryId } ` )
return res . sendStatus ( 403 )
}
2024-12-31 17:01:42 -06:00
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 )
}
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
// Load library items
const libraryItems = await Database . libraryItemModel . findAll ( {
attributes : [ 'id' , 'mediaId' , 'mediaType' , 'libraryId' ] ,
2023-08-13 11:22:38 -05:00
where : {
2024-12-31 17:01:42 -06:00
id : Array . from ( libraryItemIds ) ,
libraryId : reqBody . libraryId ,
mediaType : isPodcast ? 'podcast' : 'book'
2023-08-13 11:22:38 -05:00
}
} )
2024-12-31 17:01:42 -06:00
if ( libraryItems . length !== libraryItemIds . size ) {
return res . status ( 400 ) . send ( 'Invalid playlist data. Invalid items' )
}
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
// Validate podcast episodes
if ( isPodcast ) {
const podcastEpisodeIds = items . map ( ( i ) => i . episodeId )
const podcastEpisodes = await Database . podcastEpisodeModel . findAll ( {
attributes : [ 'id' ] ,
where : {
id : podcastEpisodeIds
}
2023-08-13 11:22:38 -05:00
} )
2024-12-31 17:01:42 -06:00
if ( podcastEpisodes . length !== podcastEpisodeIds . length ) {
return res . status ( 400 ) . send ( 'Invalid playlist data. Invalid podcast episodes' )
}
2023-08-13 11:22:38 -05:00
}
2024-12-31 17:01:42 -06:00
const transaction = await Database . sequelize . transaction ( )
try {
// Create playlist
const newPlaylist = await Database . playlistModel . create (
{
libraryId : reqBody . libraryId ,
userId : req . user . id ,
2026-03-19 16:57:22 -05:00
name : nameCleaned ,
2024-12-31 17:01:42 -06:00
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' )
}
2022-11-26 15:14:45 -06:00
}
2023-08-13 11:22:38 -05:00
/ * *
2024-12-31 17:01:42 -06:00
* @ deprecated - Use / api / libraries / : libraryId / playlists
* This is not used by Abs web client or mobile apps
2024-12-31 17:11:31 -06:00
* TODO : Remove this endpoint or make it the primary
2024-12-31 17:01:42 -06:00
*
2023-08-13 11:22:38 -05:00
* GET : / a p i / p l a y l i s t s
* Get all playlists for user
2024-08-11 15:15:34 -05:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2023-07-23 09:42:57 -05:00
async findAllForUser ( req , res ) {
2024-12-31 17:11:31 -06:00
const playlistsForUser = await Database . playlistModel . getOldPlaylistsForUserAndLibrary ( req . user . id )
2026-04-22 16:42:58 -05:00
const accessiblePlaylists = playlistsForUser . filter ( ( p ) => req . user . checkCanAccessLibrary ( p . libraryId ) )
2022-11-26 15:14:45 -06:00
res . json ( {
2026-04-22 16:42:58 -05:00
playlists : accessiblePlaylists
2022-11-26 15:14:45 -06:00
} )
}
2023-08-13 11:22:38 -05:00
/ * *
* GET : / a p i / p l a y l i s t s / : i d
2024-08-11 15:15:34 -05:00
*
2024-12-31 17:01:42 -06:00
* @ param { PlaylistControllerRequest } req
2024-08-11 15:15:34 -05:00
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
async findOne ( req , res ) {
2024-12-31 17:01:42 -06:00
req . playlist . playlistMediaItems = await req . playlist . getMediaItemsExpandedWithLibraryItem ( )
res . json ( req . playlist . toOldJSONExpanded ( ) )
2022-11-26 15:14:45 -06:00
}
2023-08-13 11:22:38 -05:00
/ * *
* PATCH : / a p i / p l a y l i s t s / : i d
* Update playlist
2024-08-11 15:15:34 -05:00
*
2024-12-31 17:01:42 -06:00
* Used for updating name and description or reordering items
*
* @ param { PlaylistControllerRequest } req
2024-08-11 15:15:34 -05:00
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-11-26 15:14:45 -06:00
async update ( req , res ) {
2024-12-31 17:01:42 -06:00
// 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 = { }
2026-03-19 16:57:22 -05:00
const nameCleaned = htmlSanitizer . stripAllTags ( reqBody . name )
if ( nameCleaned ) {
playlistUpdatePayload . name = nameCleaned
}
2024-12-31 17:01:42 -06:00
if ( reqBody . description ) playlistUpdatePayload . description = reqBody . description
// Update name and description
2023-08-13 11:22:38 -05:00
let wasUpdated = false
2024-12-31 17:01:42 -06:00
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
}
2023-08-13 11:22:38 -05:00
}
2024-12-31 17:01:42 -06:00
// 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 ) ) )
2023-08-20 13:34:03 -05:00
const libraryItems = await Database . libraryItemModel . findAll ( {
2024-12-31 17:01:42 -06:00
attributes : [ 'id' , 'mediaId' , 'mediaType' ] ,
2023-08-13 11:22:38 -05:00
where : {
id : libraryItemIds
}
} )
2024-12-31 17:01:42 -06:00
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 ( {
2023-08-13 11:22:38 -05:00
order : [ [ 'order' , 'ASC' ] ]
} )
2024-12-31 17:01:42 -06:00
if ( existingPlaylistMediaItems . length !== reqBody . items . length ) {
return res . status ( 400 ) . send ( 'Invalid playlist items. Length mismatch' )
}
2023-08-13 11:22:38 -05:00
// Set an array of mediaItemId
const newMediaItemIdOrder = [ ]
2024-12-31 17:01:42 -06:00
for ( const item of reqBody . items ) {
2024-08-10 17:15:21 -05:00
const libraryItem = libraryItems . find ( ( li ) => li . id === item . libraryItemId )
2023-08-13 11:22:38 -05:00
const mediaItemId = item . episodeId || libraryItem . mediaId
newMediaItemIdOrder . push ( mediaItemId )
}
// Sort existing playlist media items into new order
existingPlaylistMediaItems . sort ( ( a , b ) => {
2024-08-10 17:15:21 -05:00
const aIndex = newMediaItemIdOrder . findIndex ( ( i ) => i === a . mediaItemId )
const bIndex = newMediaItemIdOrder . findIndex ( ( i ) => i === b . mediaItemId )
2023-08-13 11:22:38 -05:00
return aIndex - bIndex
} )
// Update order on playlistMediaItem records
2024-12-31 17:01:42 -06:00
for ( const [ index , playlistMediaItem ] of existingPlaylistMediaItems . entries ( ) ) {
if ( playlistMediaItem . order !== index + 1 ) {
2023-08-13 11:22:38 -05:00
await playlistMediaItem . update ( {
2024-12-31 17:01:42 -06:00
order : index + 1
2023-08-13 11:22:38 -05:00
} )
wasUpdated = true
}
}
}
2024-12-31 17:01:42 -06:00
req . playlist . playlistMediaItems = await req . playlist . getMediaItemsExpandedWithLibraryItem ( )
const jsonExpanded = req . playlist . toOldJSONExpanded ( )
2022-11-26 15:14:45 -06:00
if ( wasUpdated ) {
2024-12-31 17:01:42 -06:00
SocketAuthority . clientEmitter ( req . playlist . userId , 'playlist_updated' , jsonExpanded )
2022-11-26 15:14:45 -06:00
}
res . json ( jsonExpanded )
}
2023-08-13 11:22:38 -05:00
/ * *
* DELETE : / a p i / p l a y l i s t s / : i d
* Remove playlist
2024-08-11 15:15:34 -05:00
*
2024-12-31 17:01:42 -06:00
* @ param { PlaylistControllerRequest } req
2024-08-11 15:15:34 -05:00
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-11-26 15:14:45 -06:00
async delete ( req , res ) {
2024-12-31 17:01:42 -06:00
req . playlist . playlistMediaItems = await req . playlist . getMediaItemsExpandedWithLibraryItem ( )
const jsonExpanded = req . playlist . toOldJSONExpanded ( )
2023-08-13 11:22:38 -05:00
await req . playlist . destroy ( )
SocketAuthority . clientEmitter ( jsonExpanded . userId , 'playlist_removed' , jsonExpanded )
2022-11-26 15:14:45 -06:00
res . sendStatus ( 200 )
}
2023-08-13 11:22:38 -05:00
/ * *
* POST : / a p i / p l a y l i s t s / : i d / i t e m
* Add item to playlist
2024-08-11 15:15:34 -05:00
*
2024-12-31 17:01:42 -06:00
* This is not used by Abs web client or mobile apps . Only the batch endpoints are used .
*
* @ param { PlaylistControllerRequest } req
2024-08-11 15:15:34 -05:00
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-11-26 15:14:45 -06:00
async addItem ( req , res ) {
2024-12-31 17:01:42 -06:00
const itemToAdd = req . body || { }
2022-11-26 15:14:45 -06:00
if ( ! itemToAdd . libraryItemId ) {
return res . status ( 400 ) . send ( 'Request body has no libraryItemId' )
}
2025-01-04 15:20:41 -06:00
const libraryItem = await Database . libraryItemModel . getExpandedById ( itemToAdd . libraryItemId )
2022-11-26 15:14:45 -06:00
if ( ! libraryItem ) {
return res . status ( 400 ) . send ( 'Library item not found' )
}
2024-12-31 17:01:42 -06:00
if ( libraryItem . libraryId !== req . playlist . libraryId ) {
2022-11-26 15:14:45 -06:00
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' )
}
2025-01-04 15:20:41 -06:00
if ( itemToAdd . episodeId && ! libraryItem . media . podcastEpisodes . some ( ( pe ) => pe . id === itemToAdd . episodeId ) ) {
2022-11-26 15:14:45 -06:00
return res . status ( 400 ) . send ( 'Episode not found in library item' )
}
2024-12-31 17:01:42 -06:00
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 ( )
2023-07-04 18:14:44 -05:00
const playlistMediaItem = {
2024-12-31 17:01:42 -06:00
playlistId : req . playlist . id ,
2023-07-04 18:14:44 -05:00
mediaItemId : itemToAdd . episodeId || libraryItem . media . id ,
mediaItemType : itemToAdd . episodeId ? 'podcastEpisode' : 'book' ,
2024-12-31 17:01:42 -06:00
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 ) {
2025-01-04 15:20:41 -06:00
const episode = libraryItem . media . podcastEpisodes . find ( ( ep ) => ep . id === itemToAdd . episodeId )
2024-12-31 17:01:42 -06:00
jsonExpanded . items . push ( {
episodeId : itemToAdd . episodeId ,
2025-01-04 15:20:41 -06:00
episode : episode . toOldJSONExpanded ( libraryItem . id ) ,
2024-12-31 17:01:42 -06:00
libraryItemId : libraryItem . id ,
2025-01-04 15:20:41 -06:00
libraryItem : libraryItem . toOldJSONMinified ( )
2024-12-31 17:01:42 -06:00
} )
} else {
jsonExpanded . items . push ( {
libraryItemId : libraryItem . id ,
2025-01-04 15:20:41 -06:00
libraryItem : libraryItem . toOldJSONExpanded ( )
2024-12-31 17:01:42 -06:00
} )
2023-07-04 18:14:44 -05:00
}
2023-09-17 12:40:13 -05:00
SocketAuthority . clientEmitter ( jsonExpanded . userId , 'playlist_updated' , jsonExpanded )
2022-11-26 15:14:45 -06:00
res . json ( jsonExpanded )
}
2023-08-13 11:22:38 -05:00
/ * *
* DELETE : / a p i / p l a y l i s t s / : i d / i t e m / : l i b r a r y I t e m I d / : e p i s o d e I d ?
* Remove item from playlist
2024-08-11 15:15:34 -05:00
*
2024-12-31 17:01:42 -06:00
* @ param { PlaylistControllerRequest } req
2024-08-11 15:15:34 -05:00
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-11-26 15:14:45 -06:00
async removeItem ( req , res ) {
2024-12-31 17:01:42 -06:00
req . playlist . playlistMediaItems = await req . playlist . getMediaItemsExpandedWithLibraryItem ( )
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
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 ) {
2023-08-13 11:22:38 -05:00
return res . status ( 404 ) . send ( 'Media item not found in playlist' )
2022-11-26 15:14:45 -06:00
}
2023-08-13 11:22:38 -05:00
// Remove record
2024-12-31 17:01:42 -06:00
await playlistMediaItem . destroy ( )
req . playlist . playlistMediaItems = req . playlist . playlistMediaItems . filter ( ( pmi ) => pmi . id !== playlistMediaItem . id )
2022-11-26 15:14:45 -06:00
2023-08-13 11:22:38 -05:00
// Update playlist media items order
2024-12-31 17:01:42 -06:00
for ( const [ index , mediaItem ] of req . playlist . playlistMediaItems . entries ( ) ) {
if ( mediaItem . order !== index + 1 ) {
2023-08-13 11:22:38 -05:00
await mediaItem . update ( {
2024-12-31 17:01:42 -06:00
order : index + 1
2023-08-13 11:22:38 -05:00
} )
}
}
2024-12-31 17:01:42 -06:00
const jsonExpanded = req . playlist . toOldJSONExpanded ( )
2022-11-27 12:04:49 -06:00
// Playlist is removed when there are no items
2023-08-13 11:22:38 -05:00
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 )
2022-11-27 12:04:49 -06:00
} else {
2023-08-13 11:22:38 -05:00
SocketAuthority . clientEmitter ( jsonExpanded . userId , 'playlist_updated' , jsonExpanded )
2022-11-27 12:04:49 -06:00
}
res . json ( jsonExpanded )
2022-11-26 15:14:45 -06:00
}
2023-08-13 11:22:38 -05:00
/ * *
* POST : / a p i / p l a y l i s t s / : i d / b a t c h / a d d
* Batch add playlist items
2024-08-11 15:15:34 -05:00
*
2024-12-31 17:01:42 -06:00
* @ param { PlaylistControllerRequest } req
2024-08-11 15:15:34 -05:00
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-11-26 15:14:45 -06:00
async addBatch ( req , res ) {
2024-12-31 17:01:42 -06:00
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' )
2023-08-13 11:22:38 -05:00
}
2022-11-26 15:14:45 -06:00
2023-08-13 11:22:38 -05:00
// Find all library items
2024-12-31 17:01:42 -06:00
const libraryItemIds = new Set ( req . body . items . map ( ( i ) => i . libraryItemId ) . filter ( ( i ) => i ) )
2023-07-04 18:14:44 -05:00
2025-01-04 15:20:41 -06:00
const libraryItems = await Database . libraryItemModel . findAllExpandedWhere ( { id : Array . from ( libraryItemIds ) } )
if ( libraryItems . length !== libraryItemIds . size ) {
2024-12-31 17:01:42 -06:00
return res . status ( 400 ) . send ( 'Invalid request body items' )
}
req . playlist . playlistMediaItems = await req . playlist . getMediaItemsExpandedWithLibraryItem ( )
2023-08-13 11:22:38 -05:00
const mediaItemsToAdd = [ ]
2024-12-31 17:01:42 -06:00
const jsonExpanded = req . playlist . toOldJSONExpanded ( )
2023-08-13 11:22:38 -05:00
// Setup array of playlistMediaItem records to add
2024-12-31 17:01:42 -06:00
let order = req . playlist . playlistMediaItems . length + 1
for ( const item of req . body . items ) {
2025-01-04 15:20:41 -06:00
const libraryItem = libraryItems . find ( ( li ) => li . id === item . libraryItemId )
2024-12-31 17:01:42 -06:00
const mediaItemId = item . episodeId || libraryItem . media . id
if ( req . playlist . playlistMediaItems . some ( ( pmi ) => pmi . mediaItemId === mediaItemId ) ) {
// Already exists in playlist
continue
2023-08-13 11:22:38 -05:00
} else {
2024-12-31 17:01:42 -06:00
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 ) {
2025-01-04 15:20:41 -06:00
const episode = libraryItem . media . podcastEpisodes . find ( ( ep ) => ep . id === item . episodeId )
2024-12-31 17:01:42 -06:00
jsonExpanded . items . push ( {
episodeId : item . episodeId ,
2025-01-04 15:20:41 -06:00
episode : episode . toOldJSONExpanded ( libraryItem . id ) ,
2024-12-31 17:01:42 -06:00
libraryItemId : libraryItem . id ,
2025-01-04 15:20:41 -06:00
libraryItem : libraryItem . toOldJSONMinified ( )
2024-12-31 17:01:42 -06:00
} )
2023-08-13 11:22:38 -05:00
} else {
2024-12-31 17:01:42 -06:00
jsonExpanded . items . push ( {
libraryItemId : libraryItem . id ,
2025-01-04 15:20:41 -06:00
libraryItem : libraryItem . toOldJSONExpanded ( )
2023-08-13 11:22:38 -05:00
} )
}
2022-11-26 15:14:45 -06:00
}
}
2023-08-13 11:22:38 -05:00
if ( mediaItemsToAdd . length ) {
2024-12-31 17:01:42 -06:00
await Database . playlistMediaItemModel . bulkCreate ( mediaItemsToAdd )
2023-08-13 11:22:38 -05:00
SocketAuthority . clientEmitter ( req . playlist . userId , 'playlist_updated' , jsonExpanded )
2022-11-26 15:14:45 -06:00
}
2024-12-31 17:01:42 -06:00
2022-11-26 15:14:45 -06:00
res . json ( jsonExpanded )
}
2023-08-13 11:22:38 -05:00
/ * *
* POST : / a p i / p l a y l i s t s / : i d / b a t c h / r e m o v e
* Batch remove playlist items
2024-08-11 15:15:34 -05:00
*
2024-12-31 17:01:42 -06:00
* @ param { PlaylistControllerRequest } req
2024-08-11 15:15:34 -05:00
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-11-26 15:14:45 -06:00
async removeBatch ( req , res ) {
2024-12-31 17:01:42 -06:00
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' )
2022-11-26 15:14:45 -06:00
}
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
req . playlist . playlistMediaItems = await req . playlist . getMediaItemsExpandedWithLibraryItem ( )
2023-07-04 18:14:44 -05:00
2024-12-31 17:01:42 -06:00
// 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
2022-11-26 15:14:45 -06:00
}
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
await playlistMediaItem . destroy ( )
req . playlist . playlistMediaItems = req . playlist . playlistMediaItems . filter ( ( pmi ) => pmi . id !== playlistMediaItem . id )
2023-08-13 11:22:38 -05:00
hasUpdated = true
2022-11-26 15:14:45 -06:00
}
2024-12-31 17:01:42 -06:00
const jsonExpanded = req . playlist . toOldJSONExpanded ( )
2022-11-26 15:14:45 -06:00
if ( hasUpdated ) {
2022-11-27 12:04:49 -06:00
// Playlist is removed when there are no items
2024-12-31 17:01:42 -06:00
if ( ! req . playlist . playlistMediaItems . length ) {
2023-08-13 11:22:38 -05:00
Logger . info ( ` [PlaylistController] Playlist " ${ req . playlist . name } " has no more items - removing it ` )
await req . playlist . destroy ( )
2023-09-17 12:40:13 -05:00
SocketAuthority . clientEmitter ( jsonExpanded . userId , 'playlist_removed' , jsonExpanded )
2022-11-27 12:04:49 -06:00
} else {
2023-09-17 12:40:13 -05:00
SocketAuthority . clientEmitter ( jsonExpanded . userId , 'playlist_updated' , jsonExpanded )
2022-11-27 12:04:49 -06:00
}
2022-11-26 15:14:45 -06:00
}
res . json ( jsonExpanded )
}
2023-08-13 11:22:38 -05:00
/ * *
* POST : / a p i / p l a y l i s t s / c o l l e c t i o n / : c o l l e c t i o n I d
* Create a playlist from a collection
2024-08-11 15:15:34 -05:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 11:22:38 -05:00
* /
2022-12-17 17:31:19 -06:00
async createFromCollection ( req , res ) {
2023-08-20 13:34:03 -05:00
const collection = await Database . collectionModel . findByPk ( req . params . collectionId )
2022-12-17 17:31:19 -06:00
if ( ! collection ) {
return res . status ( 404 ) . send ( 'Collection not found' )
}
2026-04-22 16:42:58 -05:00
if ( ! req . user . checkCanAccessLibrary ( collection . libraryId ) ) {
Logger . warn ( ` [PlaylistController] User " ${ req . user . username } " attempted to create playlist from collection ${ collection . id } in inaccessible library ${ collection . libraryId } ` )
return res . status ( 404 ) . send ( 'Collection not found' )
}
2022-12-17 17:31:19 -06:00
// Expand collection to get library items
2024-08-11 16:07:29 -05:00
const collectionExpanded = await collection . getOldJsonExpanded ( req . user )
2023-08-13 11:22:38 -05:00
if ( ! collectionExpanded ) {
// This can happen if the user has no access to all items in collection
return res . status ( 404 ) . send ( 'Collection not found' )
2022-12-17 17:31:19 -06:00
}
2023-08-13 11:22:38 -05:00
// Playlists cannot be empty
if ( ! collectionExpanded . books . length ) {
return res . status ( 400 ) . send ( 'Collection has no books' )
}
2022-12-17 17:31:19 -06:00
2024-12-31 17:01:42 -06:00
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 } )
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
await transaction . commit ( )
2023-08-13 11:22:38 -05:00
2024-12-31 17:01:42 -06:00
playlist . playlistMediaItems = await playlist . getMediaItemsExpandedWithLibraryItem ( )
2022-12-17 17:31:19 -06:00
2024-12-31 17:01:42 -06:00
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' )
}
2022-12-17 17:31:19 -06:00
}
2024-08-11 15:15:34 -05:00
/ * *
*
* @ param { RequestWithUser } req
* @ param { Response } res
* @ param { NextFunction } next
* /
2023-07-23 09:42:57 -05:00
async middleware ( req , res , next ) {
2022-11-26 15:14:45 -06:00
if ( req . params . id ) {
2023-08-20 13:34:03 -05:00
const playlist = await Database . playlistModel . findByPk ( req . params . id )
2022-11-26 15:14:45 -06:00
if ( ! playlist ) {
return res . status ( 404 ) . send ( 'Playlist not found' )
}
2024-08-11 16:07:29 -05:00
if ( playlist . userId !== req . user . id ) {
Logger . warn ( ` [PlaylistController] Playlist ${ req . params . id } requested by user ${ req . user . id } that is not the owner ` )
2022-11-26 15:14:45 -06:00
return res . sendStatus ( 403 )
}
2026-04-22 16:42:58 -05:00
if ( ! req . user . checkCanAccessLibrary ( playlist . libraryId ) ) {
Logger . warn ( ` [PlaylistController] User " ${ req . user . username } " attempted to access playlist ${ playlist . id } in inaccessible library ${ playlist . libraryId } ` )
return res . status ( 404 ) . send ( 'Playlist not found' )
}
2022-11-26 15:14:45 -06:00
req . playlist = playlist
}
next ( )
}
}
2024-08-10 17:15:21 -05:00
module . exports = new PlaylistController ( )