diff --git a/src/app/channel/media/playlistHandler.js b/src/app/channel/media/playlistHandler.js index 8749f50..9af4123 100644 --- a/src/app/channel/media/playlistHandler.js +++ b/src/app/channel/media/playlistHandler.js @@ -18,6 +18,7 @@ along with this program. If not, see .*/ const validator = require('validator'); //Local imports +const queuedMedia = require('./queuedMedia'); const loggerUtils = require('../../../utils/loggerUtils'); const yanker = require('../../../utils/media/yanker'); const channelModel = require('../../../schemas/channel/channelSchema'); @@ -35,8 +36,10 @@ module.exports = class{ socket.on("getChannelPlaylists", () => {this.getChannelPlaylists(socket)}); socket.on("createChannelPlaylist", (data) => {this.createChannelPlaylist(socket, data)}); socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)}); + socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)}); } + //--- USER-FACING PLAYLIST FUNCTIONS --- async getChannelPlaylists(socket, chanDB){ //if we wherent handed a channel document if(chanDB == null){ @@ -84,4 +87,30 @@ module.exports = class{ //Return playlists from channel doc socket.emit('chanPlaylists', chanDB.getPlaylists()); } + + async queueChannelPlaylist(socket, data, chanDB){ + //if we wherent handed a channel document + if(chanDB == null){ + //Pull it based on channel name + chanDB = await channelModel.findOne({name: this.channel.name}); + } + + //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 from the DB + let playlist = chanDB.getPlaylistByName(data.playlist); + + //Create an empty array to hold our media list + const mediaList = []; + + //Iterate through playlist media + for(let item of playlist.media){ + //Rehydrate playlist item and push it into the media list + mediaList.push(item.rehydrate()); + } + + //Convert array of standard media objects to queued media objects, and push to schedule + this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray(mediaList, start), socket, chanDB); + } } \ No newline at end of file diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 00fc770..75399fd 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -56,11 +56,12 @@ module.exports = class{ socket.on("queue", (data) => {this.queueURL(socket, data)}); socket.on("stop", (data) => {this.stopMedia(socket)}); socket.on("delete", (data) => {this.deleteMedia(socket, data)}); - socket.on("move", (data) => {this.moveMedia(socket, data)}); socket.on("clear", (data) => {this.deleteRange(socket, data)}); - socket.on("lock", (data) => {this.toggleLock(socket)}); + socket.on("move", (data) => {this.moveMedia(socket, data)}); + socket.on("lock", () => {this.toggleLock(socket)}); } + //--- USER FACING QUEUEING FUNCTIONS --- async queueURL(socket, data){ //Get the current channel from the database const chanDB = await channelModel.findOne({name: socket.chan}); @@ -68,7 +69,7 @@ module.exports = class{ if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ try{ //Set url - var url = data.url; + let url = data.url; //If we where given a bad URL if(!validator.isURL(url)){ @@ -94,14 +95,9 @@ module.exports = class{ //Set title const title = validator.escape(validator.trim(data.title)); - //set start - var start = data.start; - //If start time isn't an integer after the current epoch - if(start != null &&!validator.isInt(String(start), (new Date().getTime()))){ - //Null out time to tell the later parts of the function to start it now - start = null; - } + //set start + let start = this.getStart(data.start); //Pull media list const mediaList = await yanker.yankMedia(url, title); @@ -114,8 +110,56 @@ module.exports = class{ return; } - //Queue the first media object given - this.queueMedia(mediaList[0], start, socket); + //Convert media list + let queuedMediaList = queuedMedia.fromMediaArray(mediaList, start); + + //schedule the media + this.scheduleMedia(queuedMediaList, socket); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); + } + } + } + + stopMedia(socket){ + //If we're not currently playing anything + if(this.nowPlaying == null){ + //If an originating socket was provided for this request + if(socket != null){ + //Yell at the user for being an asshole + loggerUtils.socketErrorHandler(socket, "No media playing!", "queue"); + } + + //Ignore it + return false; + } + + //Stop playing + const stoppedMedia = this.nowPlaying; + + //Get difference between current time and start time and set as early end + stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000; + + //End the media + this.end(); + } + + async deleteMedia(socket, data){ + //Get the current channel from the database + const chanDB = await channelModel.findOne({name: socket.chan}); + + if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ + try{ + //If we don't have a valid UUID + if(!validator.isUUID(data.uuid)){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Bad UUID!", "queue"); + //and ignore it! + return; + } + + //Remove media by UUID + await this.removeMedia(data.uuid, socket); }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } @@ -150,28 +194,6 @@ module.exports = class{ } } - async deleteMedia(socket, data){ - //Get the current channel from the database - const chanDB = await channelModel.findOne({name: socket.chan}); - - if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){ - try{ - //If we don't have a valid UUID - if(!validator.isUUID(data.uuid)){ - //Bitch, moan, complain... - loggerUtils.socketErrorHandler(socket, "Bad UUID!", "queue"); - //and ignore it! - return; - } - - //Remove media by UUID - await this.removeMedia(data.uuid, socket); - }catch(err){ - return loggerUtils.socketExceptionHandler(socket, err); - } - } - } - async moveMedia(socket, data){ //Get the current channel from the database const chanDB = await channelModel.findOne({name: socket.chan}); @@ -214,36 +236,33 @@ module.exports = class{ } } - //Default start time to now + half a second to give everyone time to process shit - queueMedia(inputMedia, start, socket){ - //If we have an invalid time - if(start == null || start < (new Date).getTime()){ + //--- INTERNAL USE ONLY QUEUEING FUNCTIONS --- + getStart(start){ + //Pull current time + const now = new Date().getTime(); + + //If start time is null, or it isn't a valid integer after the current epoch + if(start == null || !validator.isInt(String(start), {min: now})){ //Get last item from schedule const lastItem = (Array.from(this.schedule)[this.schedule.size - 1]); - const now = new Date().getTime() - //if we have a last item if(lastItem != null){ //If the last item has ended if(lastItem[1].getEndTime() < now){ - start = now + 5; - //If it hasn't started yet + //Throw it on in five ms + return now; + //If it hasn't ended yet }else{ //Throw it on five ms after the last item - start = lastItem[1].getEndTime() + 5; + return lastItem[1].getEndTime() + 5; } + //If we don't have a last item }else{ - //Throw it on five ms after the last item - start = now + 5; + //Throw it on in five ms + return now; } } - - //Create a new media queued object, set start time to now - const mediaObj = queuedMedia.fromMedia(inputMedia, start, 0); - - //schedule the media - this.scheduleMedia(mediaObj, socket); } refreshNextTimer(volatile = false){ @@ -305,7 +324,7 @@ module.exports = class{ } } - async rescheduleMedia(uuid, start = new Date().getTime() + 5, socket){ + async rescheduleMedia(uuid, start = new Date().getTime(), socket){ //Find our media, don't remove it yet since we want to do some more testing first const media = this.getItemByUUID(uuid); @@ -355,7 +374,7 @@ module.exports = class{ //Attempt to schedule media at given time //Otherwise, if it returns false for fuckup - if(!(await this.scheduleMedia(media, socket))){ + if(!(await this.scheduleMedia([media], socket))){ //Reset start time media.startTime = oldStart; @@ -363,7 +382,7 @@ module.exports = class{ media.startTimeStamp = 0; //Schedule in old slot - this.scheduleMedia(media, socket, null, true); + this.scheduleMedia([media], socket, null, true); } } @@ -474,30 +493,7 @@ module.exports = class{ return media; } - stopMedia(socket){ - //If we're not currently playing anything - if(this.nowPlaying == null){ - //If an originating socket was provided for this request - if(socket != null){ - //Yell at the user for being an asshole - loggerUtils.socketErrorHandler(socket, "No media playing!", "queue"); - } - - //Ignore it - return false; - } - - //Stop playing - const stoppedMedia = this.nowPlaying; - - //Get difference between current time and start time and set as early end - stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000; - - //End the media - this.end(); - } - - async scheduleMedia(mediaObj, socket, chanDB, force = false, volatile = false, startVolatile = false){ + async scheduleMedia(media, socket, chanDB, force = false, volatile = false, startVolatile = false){ /* This is a fun method and I think it deserves it's own little explination... Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass. @@ -512,7 +508,7 @@ module.exports = class{ since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about. Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely. This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider - that, no matter what, re-ordering the schedule map would've required us to iterate through and convert it to an array and back anyways... + that, no matter what, re-ordering the schedule map would've required us to iterate through and rebuild the map anyways... Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them there as seconds. @@ -526,6 +522,8 @@ module.exports = class{ https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it */ + let mediaObj = media[0]; + //If someone is trying to schedule something that starts and ends in the past if((mediaObj.getEndTime() < new Date().getTime()) && !force){ //If an originating socket was provided for this request @@ -539,9 +537,16 @@ module.exports = class{ //If the item has already started if((mediaObj.startTime < new Date().getTime()) && !force){ //Set time stamp to existing timestamp plus the difference between the orginal start-date and now - mediaObj.startTimeStamp = mediaObj.startTimeStamp + ((new Date().getTime() - mediaObj.startTime) / 1000) - //Start the item now - mediaObj.startTime = new Date().getTime() + 5; + const calculatedTimeStamp = mediaObj.startTimeStamp + ((new Date().getTime() - mediaObj.startTime) / 1000) + + //If the calculated time stamp is more than negligible, and therefore not simply caused by serverside processing time + if(calculatedTimeStamp > 5){ + //Set the media timestamp + mediaObj.startTimeStamp = calculatedTimeStamp; + + //Start the item now + mediaObj.startTime = new Date().getTime(); + } } //If there's already something queued right now @@ -935,7 +940,7 @@ module.exports = class{ //If the media hasn't ended yet if(wasPlaying.getEndTime() > now){ //Re-Schedule it in RAM - await this.scheduleMedia(wasPlaying, null, chanDB, true, true, true); + await this.scheduleMedia([wasPlaying], null, chanDB, true, true, true); //Otherwise, if it has }else{ //Null out nowPlaying @@ -961,7 +966,7 @@ module.exports = class{ //Re-Schedule it in RAM - await this.scheduleMedia(mediaObj, null, chanDB, true, true, false); + await this.scheduleMedia([mediaObj], null, chanDB, true, true, false); }else{ //If the media should be playing now if(mediaObj.getEndTime() > now){ @@ -969,7 +974,7 @@ module.exports = class{ chanDB.media.nowPlaying = record; //Re-Schedule it in RAM - await this.scheduleMedia(mediaObj, null, chanDB, true, true, true); + await this.scheduleMedia([mediaObj], null, chanDB, true, true, true); //If it's been ended }else{ //Archive ended media diff --git a/src/app/channel/media/queuedMedia.js b/src/app/channel/media/queuedMedia.js index d0dbd67..0babd9e 100644 --- a/src/app/channel/media/queuedMedia.js +++ b/src/app/channel/media/queuedMedia.js @@ -54,6 +54,23 @@ module.exports = class extends media{ startTimeStamp); } + static fromMediaArray(mediaList, start){ + //Queued Media List + const queuedMediaList = []; + //Start Time Offset + let startOffset = 0; + + for(let media of mediaList){ + //Convert mediaObj to queuedMedia and push to the back of the list + queuedMediaList.push(this.fromMedia(media, start + startOffset, 0)); + + //Set start offset to end of the current item + startOffset += (media.duration * 1000) + 5; + } + + return queuedMediaList; + } + //methods genUUID(){ this.uuid = crypto.randomUUID(); diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 6d95023..c1f049b 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -605,13 +605,16 @@ channelSchema.methods.addToPlaylist = async function(name, media){ //If the playlist name matches if(playlist.name == name){ //Push the given media into the found playlist - //this.media.playlists[listIndex].push(media); //Make note of the found index foundIndex = listIndex } }); + //Set media status schema discriminator + media.status = 'saved'; + + //Add the media to the playlist this.media.playlists[foundIndex].media.push(media); //Save the changes made to the chan doc diff --git a/src/schemas/channel/media/mediaSchema.js b/src/schemas/channel/media/mediaSchema.js index c1d1f4d..a781e54 100644 --- a/src/schemas/channel/media/mediaSchema.js +++ b/src/schemas/channel/media/mediaSchema.js @@ -48,4 +48,5 @@ const mediaSchema = new mongoose.Schema({ } ); + module.exports = mediaSchema; \ No newline at end of file diff --git a/src/schemas/channel/media/playlistMediaSchema.js b/src/schemas/channel/media/playlistMediaSchema.js index 59c6c68..1cef135 100644 --- a/src/schemas/channel/media/playlistMediaSchema.js +++ b/src/schemas/channel/media/playlistMediaSchema.js @@ -25,7 +25,8 @@ const playlistMediaProperties = new mongoose.Schema({ uuid: { type: mongoose.SchemaTypes.UUID, required:true, - unique: true + unique: true, + default: crypto.randomUUID() } }, { diff --git a/src/schemas/channel/media/playlistSchema.js b/src/schemas/channel/media/playlistSchema.js index 5644c5b..e5c9750 100644 --- a/src/schemas/channel/media/playlistSchema.js +++ b/src/schemas/channel/media/playlistSchema.js @@ -20,7 +20,7 @@ const {mongoose} = require('mongoose'); //Local Imports const playlistMediaSchema = require('./playlistMediaSchema'); -module.exports = new mongoose.Schema({ +const playlistSchema = new mongoose.Schema({ name: { type: mongoose.SchemaTypes.String, required: true, @@ -31,4 +31,10 @@ module.exports = new mongoose.Schema({ required: true, default: [] }] -}); \ No newline at end of file +}); + +playlistSchema.methods.test = function(){ + console.log(this.name); +} + +module.exports = playlistSchema; \ No newline at end of file