From e629c63b2cb26e63f3c6d543d786f67fa50c9607 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 5 Apr 2025 15:53:07 -0400 Subject: [PATCH] Added random and individual queuing from playlists. --- src/app/channel/media/playlistHandler.js | 165 ++++++++++++++++-- src/app/channel/media/queue.js | 1 - src/schemas/channel/channelSchema.js | 25 --- src/schemas/channel/media/playlistSchema.js | 27 +++ www/css/panel/queue.css | 12 +- .../panels/queuePanel/playlistManager.js | 71 ++++++-- 6 files changed, 244 insertions(+), 57 deletions(-) diff --git a/src/app/channel/media/playlistHandler.js b/src/app/channel/media/playlistHandler.js index ab20170..3ad64c6 100644 --- a/src/app/channel/media/playlistHandler.js +++ b/src/app/channel/media/playlistHandler.js @@ -38,6 +38,8 @@ module.exports = class{ socket.on("deleteChannelPlaylistMedia", (data) => {this.deleteChannelPlaylistMedia(socket, data)}); socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)}); socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)}); + socket.on("queueRandomFromChannelPlaylist", (data) => {this.queueRandomFromChannelPlaylist(socket, data)}); + socket.on("queueFromChannelPlaylist", (data) => {this.queueFromChannelPlaylist(socket, data)}); socket.on("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)}); socket.on("changeDefaultTitlesChannelPlaylist", (data) => {this.changeDefaultTitlesChannelPlaylist(socket, data)}); } @@ -108,7 +110,7 @@ module.exports = class{ await chanDB.save(); //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); + this.getChannelPlaylists(socket, chanDB); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); @@ -128,7 +130,7 @@ module.exports = class{ await chanDB.deletePlaylistByName(data.playlist); //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); + this.getChannelPlaylists(socket, chanDB); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); @@ -171,11 +173,25 @@ module.exports = class{ return; } - //Add media object to the given playlist - await chanDB.addToPlaylist(data.playlist, mediaList[0]); + //Find the playlist + let playlist = chanDB.getPlaylistByName(data.playlist); + + //If we didn't find a real playlist + if(playlist == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist not found!", "validation"); + //and ignore it! + return; + } + + //delete media from playlist + chanDB.media.playlists[playlist.listIndex].addMedia(mediaList); + + //save the channel document + await chanDB.save(); //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); + this.getChannelPlaylists(socket, chanDB); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); @@ -198,6 +214,14 @@ module.exports = class{ //Grab playlist from the DB let playlist = chanDB.getPlaylistByName(data.playlist); + //If we didn't find a real playlist + if(playlist == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist not found!", "validation"); + //and ignore it! + return; + } + //Create an empty array to hold our media list const mediaList = []; @@ -207,7 +231,7 @@ module.exports = class{ let mediaObj = item.rehydrate(); //Set media title from default titles - mediaObj.title = playlist.defaultTitles[Math.floor(Math.random() * playlist.defaultTitles.length)]; + mediaObj.title = playlist.pickDefaultTitle(); //Push rehydrated item on to the mediaList mediaList.push(mediaObj); @@ -221,6 +245,95 @@ module.exports = class{ } } + async queueFromChannelPlaylist(socket, data, chanDB){ + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } + + //Permcheck the user + if((!this.channel.queue.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ + //Pull a valid start time from input, or make one up if we can't + let start = this.channel.queue.getStart(data.start); + + //Grab playlist + const playlist = chanDB.getPlaylistByName(data.playlist); + + //If we didn't find a real playlist + if(playlist == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist not found!", "validation"); + //and ignore it! + return; + } + + //If we don't have a valid UUID + if(!validator.isUUID(data.uuid)){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, `'${data.uuid}' is not a valid UUID!`, "validation"); + //and ignore it! + return; + } + + //Pull and rehydrate media from playlist + const media = playlist.findMediaByUUID(data.uuid).rehydrate(); + + //Set title from default titles + media.title = playlist.pickDefaultTitle(); + + //Queue found media + this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray([media], start), socket, chanDB); + } + + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); + } + } + + async queueRandomFromChannelPlaylist(socket, data, chanDB){ + try{ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } + + //Permcheck the user + if((!this.channel.queue.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ + //Pull a valid start time from input, or make one up if we can't + let start = this.channel.queue.getStart(data.start); + + //Grab playlist + const playlist = chanDB.getPlaylistByName(data.playlist); + + //If we didn't find a real playlist + if(playlist == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist not found!", "validation"); + //and ignore it! + return; + } + + //Pick a random video ID based on the playlist's length + const foundID = Math.round(Math.random() * (playlist.media.length - 1)); + + //Pull and rehydrate media from playlist + const foundMedia = playlist.media[foundID].rehydrate(); + + //Set title from default titles + foundMedia.title = playlist.pickDefaultTitle(); + + //Queue found media + this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray([foundMedia], start), socket, chanDB); + } + + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); + } + } + async renameChannelPlaylist(socket, data, chanDB){ try{ //if we wherent handed a channel document @@ -252,6 +365,14 @@ module.exports = class{ //Find playlist let playlist = chanDB.getPlaylistByName(data.playlist); + //If we didn't find a real playlist + if(playlist == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist not found!", "validation"); + //and ignore it! + return; + } + //Change playlist name chanDB.media.playlists[playlist.listIndex].name = name; @@ -259,7 +380,7 @@ module.exports = class{ await chanDB.save(); //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); + this.getChannelPlaylists(socket, chanDB); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); @@ -278,6 +399,14 @@ module.exports = class{ //Find playlist let playlist = chanDB.getPlaylistByName(data.playlist); + //If we didn't find a real playlist + if(playlist == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist not found!", "validation"); + //and ignore it! + return; + } + //Create empty array to hold titles const safeTitles = []; @@ -297,7 +426,7 @@ module.exports = class{ await chanDB.save(); //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); + this.getChannelPlaylists(socket, chanDB); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); @@ -321,11 +450,25 @@ module.exports = class{ return; } - //Delete media from channel playlist - chanDB.deletePlaylistMediaByUUID(data.playlist, data.uuid); + //Find the playlist + let playlist = chanDB.getPlaylistByName(data.playlist); + + //If we didn't find a real playlist + if(playlist == null){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Playlist not found!", "validation"); + //and ignore it! + return; + } + + //delete media from playlist + chanDB.media.playlists[playlist.listIndex].deleteMedia(data.uuid); + + //save the channel document + await chanDB.save(); //Return playlists from channel doc - socket.emit('chanPlaylists', chanDB.getPlaylists()); + this.getChannelPlaylists(socket, chanDB); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index eda6eb4..83690a8 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -125,7 +125,6 @@ module.exports = class{ //Get the current channel from the database const chanDB = await channelModel.findOne({name: socket.chan}); - console.log(!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin'); //Permcheck to make sure the user can fuck w/ the queue if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 6c346f7..8ab4925 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -606,31 +606,6 @@ channelSchema.methods.deletePlaylistByName = async function(name){ await this.save(); } -channelSchema.methods.deletePlaylistMediaByUUID = async function(name, uuid){ - //Find the playlist - let playlist = this.getPlaylistByName(name); - - //splice out the given playlist - this.media.playlists[playlist.listIndex].deleteMedia(uuid); - - //save the channel document - await this.save(); -} - -channelSchema.methods.addToPlaylist = async function(name, media){ - //Find the playlist - let playlist = this.getPlaylistByName(name); - - //Set media status schema discriminator - media.status = 'saved'; - - //Add the media to the playlist - this.media.playlists[playlist.listIndex].media.push(media); - - //Save the changes made to the chan doc - await this.save(); -} - channelSchema.methods.getChanBans = async function(){ //Create an empty list to hold our found bans var banList = []; diff --git a/src/schemas/channel/media/playlistSchema.js b/src/schemas/channel/media/playlistSchema.js index 22ea1db..274633a 100644 --- a/src/schemas/channel/media/playlistSchema.js +++ b/src/schemas/channel/media/playlistSchema.js @@ -52,6 +52,28 @@ playlistSchema.methods.dehydrate = function(){ } } +playlistSchema.methods.addMedia = function(mediaList){ + //For every piece of media in the list + for(let media of mediaList){ + //Set media status schema discriminator + media.status = 'saved'; + + //Add the media to the playlist + this.media.push(media); + } +} + +playlistSchema.methods.findMediaByUUID = function(uuid){ + //For every piece of media in the current playlist + for(let media of this.media){ + //If we found our match + if(media.uuid.toString() == uuid){ + //return it + return media; + } + } +} + playlistSchema.methods.deleteMedia = function(uuid){ //Create new array to hold list of media to be kept const keptMedia = []; @@ -69,4 +91,9 @@ playlistSchema.methods.deleteMedia = function(uuid){ this.media = keptMedia; } +playlistSchema.methods.pickDefaultTitle = function(){ + //Grab a random default title and return it + return this.defaultTitles[Math.floor(Math.random() * this.defaultTitles.length)]; +} + module.exports = playlistSchema; \ No newline at end of file diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css index dbfd851..d3907ca 100644 --- a/www/css/panel/queue.css +++ b/www/css/panel/queue.css @@ -160,6 +160,7 @@ div.dragging-queue-entry{ flex-direction: row; align-items: baseline; text-wrap: nowrap; + overflow: hidden; } .queue-playlist-span{ @@ -234,8 +235,6 @@ div.dragging-queue-entry{ padding: 0 0.15em; } - - .queue-playlist-add-url-button i.bi-link-45deg{ margin-right: 0.5em; } @@ -251,9 +250,16 @@ div.dragging-queue-entry{ display: flex; flex-direction: row; justify-content: space-between; + text-wrap: nowrap; } -.queue-playlist-media-delete-icon{ +.queue-playlist-media-title{ + overflow: hidden; +} + +.queue-playlist-media-control-span{ + display: flex; + flex-direction: row; height: 0.8em; } diff --git a/www/js/channel/panels/queuePanel/playlistManager.js b/www/js/channel/panels/queuePanel/playlistManager.js index a1ff59b..2bceb96 100644 --- a/www/js/channel/panels/queuePanel/playlistManager.js +++ b/www/js/channel/panels/queuePanel/playlistManager.js @@ -151,7 +151,6 @@ class playlistManager{ //Create playlist title caret this.playlistTitleCaret = document.createElement('i'); - //If this is supposed to be open if(this.openMap.get(this.playlist.name)){ //Set class accordingly @@ -268,6 +267,7 @@ class playlistManager{ this.playlistAddURLButton.addEventListener('click', this.addURL.bind(this)); this.playlistDefaultTitlesButton.addEventListener('click', this.editDefaultTitles.bind(this)); this.playlistRenameButton.addEventListener('click', this.renamePlaylist.bind(this)); + this.playlistQueueRandomButton.addEventListener('click', this.queueRandom.bind(this)); this.playlistQueueAllButton.addEventListener('click', this.queueAll.bind(this)); this.playlistDeleteButton.addEventListener('click', this.deletePlaylist.bind(this)); } @@ -289,42 +289,38 @@ class playlistManager{ for(let mediaIndex in this.playlist.media){ //Grab media object from playlist - const media = this.playlist.media[mediaIndex]; + this.media = this.playlist.media[mediaIndex]; + + //Sanatize title text + const title = utils.unescapeEntities(this.media.title); //Create media div const mediaDiv = document.createElement('div'); //Set class mediaDiv.classList.add('queue-playlist-media-div'); + //Inject title + mediaDiv.title = title; //If this isn't our first rodeo if(mediaIndex != 0){ mediaDiv.classList.add('not-first'); } + //Create media title const mediaTitle = document.createElement('p'); //Set class mediaTitle.classList.add('queue-playlist-media-title'); //Inject text content - mediaTitle.innerText = utils.unescapeEntities(media.title); + mediaTitle.innerText = title; - const deleteMediaIcon = document.createElement('i'); - //set class - deleteMediaIcon.classList.add('queue-playlist-control', 'queue-playlist-media-delete-icon', 'danger-text', 'bi-trash-fill'); - //Set title - deleteMediaIcon.title = 'Delete media from playlist'; - //Set dataset - //It's probably more effecient to set this at mediaContainer level, but I don't want to crawl multiple parents later on - deleteMediaIcon.dataset['playlist'] = this.playlist.name; - deleteMediaIcon.dataset['uuid'] = media.uuid; + //Render out media controls + this.renderMediaControls(); //Append items to media div mediaDiv.appendChild(mediaTitle); - mediaDiv.appendChild(deleteMediaIcon); + mediaDiv.appendChild(this.mediaControlSpan); - - //Handle input event listeners - deleteMediaIcon.addEventListener('click', this.deleteMedia.bind(this)); //Append media div to media container this.mediaContainer.appendChild(mediaDiv); @@ -334,6 +330,39 @@ class playlistManager{ this.mediaContainer; } + renderMediaControls(){ + //Create media control span + this.mediaControlSpan = document.createElement('span'); + //Set it's class + this.mediaControlSpan.classList.add('queue-playlist-media-control-span'); + //Set dataset + this.mediaControlSpan.dataset['playlist'] = this.playlist.name; + this.mediaControlSpan.dataset['uuid'] = this.media.uuid; + + + //Create Queue Media icon + const queueMediaIcon = document.createElement('i'); + //set class + queueMediaIcon.classList.add('queue-playlist-control', 'queue-playlist-media-queue-icon', 'bi-play-circle'); + //Set title + queueMediaIcon.title = (`Queue '${this.media.title}'`); + + //Create delete media icon + const deleteMediaIcon = document.createElement('i'); + //set class + deleteMediaIcon.classList.add('queue-playlist-control', 'queue-playlist-media-delete-icon', 'danger-text', 'bi-trash-fill'); + //Set title + deleteMediaIcon.title = `Delete '${this.media.title}' from playlist '${this.playlist.name}'`; + + //Append items to media control span + this.mediaControlSpan.appendChild(queueMediaIcon); + this.mediaControlSpan.appendChild(deleteMediaIcon); + + //Handle input event listeners + queueMediaIcon.addEventListener('click', this.queueMedia.bind(this)); + deleteMediaIcon.addEventListener('click', this.deleteMedia.bind(this)); + } + //I'd rather make this a class function but it's probably cleaner to not have to parent crawl toggleMedia(event){ //Grab playlist title caret @@ -404,12 +433,20 @@ class playlistManager{ this.client.socket.emit('queueChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']}); } + queueMedia(event){ + this.client.socket.emit('queueFromChannelPlaylist',{playlist: event.target.parentNode.dataset['playlist'], uuid: event.target.parentNode.dataset['uuid']}); + } + + queueRandom(event){ + this.client.socket.emit('queueRandomFromChannelPlaylist',{playlist: event.target.parentNode.dataset['playlist']}); + } + deletePlaylist(event){ this.client.socket.emit('deleteChannelPlaylist', {playlist: event.target.parentNode.dataset['playlist']}); } deleteMedia(event){ - this.client.socket.emit('deleteChannelPlaylistMedia', {playlist: event.target.dataset['playlist'], uuid: event.target.dataset['uuid']}); + this.client.socket.emit('deleteChannelPlaylistMedia', {playlist: event.target.parentNode.dataset['playlist'], uuid: event.target.parentNode.dataset['uuid']}); } }