From f9b87b94bf71b7b9a37ef02e478d85fe3d71988a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 26 Nov 2022 15:14:45 -0600 Subject: [PATCH] Add:Playlist API endpoints --- server/controllers/PlaylistController.js | 185 +++++++++++++++++++++++ server/objects/Playlist.js | 8 +- server/routers/ApiRouter.js | 15 +- 3 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 server/controllers/PlaylistController.js diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js new file mode 100644 index 00000000..30629fc5 --- /dev/null +++ b/server/controllers/PlaylistController.js @@ -0,0 +1,185 @@ +const Logger = require('../Logger') +const SocketAuthority = require('../SocketAuthority') + +const Playlist = require('../objects/Playlist') + +class PlaylistController { + constructor() { } + + // POST: api/playlists + async create(req, res) { + const newPlaylist = new Playlist() + req.body.userId = req.user.id + const success = newPlaylist.setData(req.body) + if (!success) { + return res.status(400).send('Invalid playlist request data') + } + const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems) + await this.db.insertEntity('playlist', newPlaylist) + SocketAuthority.emitter('playlist_added', jsonExpanded) + res.json(jsonExpanded) + } + + // GET: api/playlists + findAllForUser(req, res) { + res.json({ + playlists: this.db.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(this.db.libraryItems)) + }) + } + + // GET: api/playlists/:id + findOne(req, res) { + res.json(req.playlist.toJSONExpanded(this.db.libraryItems)) + } + + // PATCH: api/playlists/:id + async update(req, res) { + const playlist = req.playlist + let wasUpdated = playlist.update(req.body) + const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) + if (wasUpdated) { + await this.db.updateEntity('playlist', playlist) + SocketAuthority.emitter('playlist_updated', jsonExpanded) + } + res.json(jsonExpanded) + } + + // DELETE: api/playlists/:id + async delete(req, res) { + const playlist = req.playlist + const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) + await this.db.removeEntity('playlist', playlist.id) + SocketAuthority.emitter('playlist_removed', jsonExpanded) + res.sendStatus(200) + } + + // POST: api/playlists/:id/item + async addItem(req, res) { + const playlist = req.playlist + const itemToAdd = req.body + + if (!itemToAdd.libraryItemId) { + return res.status(400).send('Request body has no libraryItemId') + } + + const libraryItem = this.db.libraryItems.find(li => li.id === itemToAdd.libraryItemId) + if (!libraryItem) { + return res.status(400).send('Library item not found') + } + if (libraryItem.libraryId !== playlist.libraryId) { + return res.status(400).send('Library item in different library') + } + if (playlist.containsItem(itemToAdd)) { + return res.status(400).send('Item already in playlist') + } + 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.checkHasEpisode(itemToAdd.episodeId)) { + return res.status(400).send('Episode not found in library item') + } + + playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId) + const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) + await this.db.updateEntity('playlist', playlist) + SocketAuthority.emitter('playlist_updated', jsonExpanded) + res.json(jsonExpanded) + } + + // DELETE: api/playlists/:id/item/:libraryItemId/:episodeId? + async removeItem(req, res) { + const playlist = req.playlist + const itemToRemove = { + libraryItemId: req.params.libraryItemId, + episodeId: req.params.episodeId || null + } + if (!playlist.containsItem(itemToRemove)) { + return res.sendStatus(404) + } + + playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId) + + const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) + await this.db.updateEntity('playlist', playlist) + SocketAuthority.emitter('playlist_updated', jsonExpanded) + res.json(playlist.toJSONExpanded(this.db.libraryItems)) + } + + // POST: api/playlists/:id/batch/add + async addBatch(req, res) { + const playlist = req.playlist + if (!req.body.items || !req.body.items.length) { + return res.status(500).send('Invalid request body') + } + const itemsToAdd = req.body.items + let hasUpdated = false + for (const item of itemsToAdd) { + if (!item.libraryItemId) { + return res.status(400).send('Item does not have libraryItemId') + } + + if (!playlist.containsItem(item)) { + playlist.addItem(item.libraryItemId, item.episodeId) + hasUpdated = true + } + } + + const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) + if (hasUpdated) { + await this.db.updateEntity('playlist', playlist) + SocketAuthority.emitter('playlist_updated', jsonExpanded) + } + res.json(jsonExpanded) + } + + // POST: api/playlists/:id/batch/remove + async removeBatch(req, res) { + const playlist = req.playlist + if (!req.body.items || !req.body.items.length) { + return res.status(500).send('Invalid request body') + } + const itemsToRemove = req.body.items + let hasUpdated = false + for (const item of itemsToRemove) { + if (!item.libraryItemId) { + return res.status(400).send('Item does not have libraryItemId') + } + if (playlist.containsItem(item)) { + playlist.removeItem(item) + hasUpdated = true + } + } + + const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) + if (hasUpdated) { + await this.db.updateEntity('playlist', playlist) + SocketAuthority.emitter('playlist_updated', jsonExpanded) + } + res.json(jsonExpanded) + } + + middleware(req, res, next) { + if (req.params.id) { + var playlist = this.db.playlists.find(p => p.id === 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 + } + + if (req.method == 'DELETE' && !req.user.canDelete) { + Logger.warn(`[PlaylistController] User attempted to delete without permission`, req.user.username) + return res.sendStatus(403) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + Logger.warn('[PlaylistController] User attempted to update without permission', req.user.username) + return res.sendStatus(403) + } + + next() + } +} +module.exports = new PlaylistController() \ No newline at end of file diff --git a/server/objects/Playlist.js b/server/objects/Playlist.js index 6436b122..38594102 100644 --- a/server/objects/Playlist.js +++ b/server/objects/Playlist.js @@ -12,8 +12,7 @@ class Playlist { this.coverPath = null - // Array of objects like { libraryItemId: "", episodeId: "" } - // episodeId optional + // Array of objects like { libraryItemId: "", episodeId: "" } (episodeId optional) this.items = [] this.lastUpdate = null @@ -128,5 +127,10 @@ class Playlist { } return hasUpdates } + + containsItem(item) { + if (item.episodeId) return this.items.some(i => i.libraryItemId === item.libraryItemId && i.episodeId === item.episodeId) + return this.items.some(i => i.libraryItemId === item.libraryItemId) + } } module.exports = Playlist \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index febcb4f3..f67bb768 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -10,6 +10,7 @@ const date = require('../libs/dateAndTime') const LibraryController = require('../controllers/LibraryController') const UserController = require('../controllers/UserController') const CollectionController = require('../controllers/CollectionController') +const PlaylistController = require('../controllers/PlaylistController') const MeController = require('../controllers/MeController') const BackupController = require('../controllers/BackupController') const LibraryItemController = require('../controllers/LibraryItemController') @@ -133,12 +134,24 @@ class ApiRouter { this.router.get('/collections/:id', CollectionController.middleware.bind(this), CollectionController.findOne.bind(this)) this.router.patch('/collections/:id', CollectionController.middleware.bind(this), CollectionController.update.bind(this)) this.router.delete('/collections/:id', CollectionController.middleware.bind(this), CollectionController.delete.bind(this)) - this.router.post('/collections/:id/book', CollectionController.middleware.bind(this), CollectionController.addBook.bind(this)) this.router.delete('/collections/:id/book/:bookId', CollectionController.middleware.bind(this), CollectionController.removeBook.bind(this)) this.router.post('/collections/:id/batch/add', CollectionController.middleware.bind(this), CollectionController.addBatch.bind(this)) this.router.post('/collections/:id/batch/remove', CollectionController.middleware.bind(this), CollectionController.removeBatch.bind(this)) + // + // Playlist Routes + // + this.router.post('/playlists', PlaylistController.middleware.bind(this), PlaylistController.create.bind(this)) + this.router.get('/playlists', PlaylistController.findAllForUser.bind(this)) + this.router.get('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.findOne.bind(this)) + this.router.patch('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.update.bind(this)) + this.router.delete('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.delete.bind(this)) + this.router.post('/playlists/:id/item', PlaylistController.middleware.bind(this), PlaylistController.addItem.bind(this)) + this.router.delete('/playlists/:id/item/:libraryItemId/:episodeId?', PlaylistController.middleware.bind(this), PlaylistController.removeItem.bind(this)) + this.router.post('/playlists/:id/batch/add', PlaylistController.middleware.bind(this), PlaylistController.addBatch.bind(this)) + this.router.post('/playlists/:id/batch/remove', PlaylistController.middleware.bind(this), PlaylistController.removeBatch.bind(this)) + // // Current User Routes (Me) //