Finished up with Persistent queue storage in database.

This commit is contained in:
rainbow napkin 2025-02-15 11:02:58 -05:00
parent d60182ceae
commit 8a273d8055
3 changed files with 296 additions and 80 deletions

View file

@ -159,13 +159,13 @@ module.exports = class{
//If we don't have a valid UUID //If we don't have a valid UUID
if(!validator.isUUID(data.uuid)){ if(!validator.isUUID(data.uuid)){
//Bitch, moan, complain... //Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation"); loggerUtils.socketErrorHandler(socket, "Bad UUID!", "queue");
//and ignore it! //and ignore it!
return; return;
} }
//Remove media by UUID //Remove media by UUID
this.removeMedia(data.uuid, socket); await this.removeMedia(data.uuid, socket);
}catch(err){ }catch(err){
return loggerUtils.socketExceptionHandler(socket, err); return loggerUtils.socketExceptionHandler(socket, err);
} }
@ -181,7 +181,7 @@ module.exports = class{
//If we don't have a valid UUID //If we don't have a valid UUID
if(!validator.isUUID(data.uuid)){ if(!validator.isUUID(data.uuid)){
//Bitch, moan, complain... //Bitch, moan, complain...
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation"); loggerUtils.socketErrorHandler(socket, "Bad UUID!", "queue");
//and ignore it! //and ignore it!
return; return;
} }
@ -246,7 +246,7 @@ module.exports = class{
this.scheduleMedia(mediaObj, socket); this.scheduleMedia(mediaObj, socket);
} }
refreshNextTimer(){ refreshNextTimer(volatile = false){
//Grab the next item //Grab the next item
const nextItem = this.getNextItem(); const nextItem = this.getNextItem();
@ -258,7 +258,7 @@ module.exports = class{
//If we have a current item and it isn't currently playing //If we have a current item and it isn't currently playing
if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){ if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){
//Start the found item at w/ a pre-calculated time stamp to reflect the given start time //Start the found item at w/ a pre-calculated time stamp to reflect the given start time
this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000) + currentItem.startTimeStamp); this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000) + currentItem.startTimeStamp, volatile);
} }
//otherwise if we have an item //otherwise if we have an item
}else{ }else{
@ -268,22 +268,44 @@ module.exports = class{
//Clear out any item that might be up next //Clear out any item that might be up next
clearTimeout(this.nextTimer); clearTimeout(this.nextTimer);
//Set the next timer //Set the next timer
this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp)}, startsIn); this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp, volatile)}, startsIn);
} }
} }
removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket){ async removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket){
//Find items within given range //Find items within given range
const foundItems = this.getItemsBetweenEpochs(start, end); const foundItems = this.getItemsBetweenEpochs(start, end);
//For each item try{
for(let item of foundItems){ //DO everything ourselves since we don't have a fance end() function to do it
//Remove media const chanDB = await channelModel.findOne({name:this.channel.name});
this.removeMedia(item.uuid, socket);
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
}
//For each item
for(let item of foundItems){
//Remove media, passing down chanDB so we're not looking again and again
await this.removeMedia(item.uuid, socket, chanDB);
}
}catch(err){
//If this was originated by someone
if(socket != null){
//Bitch at them
loggerUtils.socketExceptionHandler(socket, err);
//If not
}else{
//Bitch to the console
loggerUtils.localExceptionHandler(err);
}
} }
} }
rescheduleMedia(uuid, start = new Date().getTime() + 5, socket){ async rescheduleMedia(uuid, start = new Date().getTime() + 5, socket){
//Find our media, don't remove it yet since we want to do some more testing first //Find our media, don't remove it yet since we want to do some more testing first
const media = this.getItemByUUID(uuid); const media = this.getItemByUUID(uuid);
@ -320,7 +342,7 @@ module.exports = class{
} }
//Remove the media from the schedule //Remove the media from the schedule
this.removeMedia(uuid); await this.removeMedia(uuid);
//Grab the old start time for safe keeping //Grab the old start time for safe keeping
const oldStart = media.startTime; const oldStart = media.startTime;
@ -333,7 +355,7 @@ module.exports = class{
//Attempt to schedule media at given time //Attempt to schedule media at given time
//Otherwise, if it returns false for fuckup //Otherwise, if it returns false for fuckup
if(!this.scheduleMedia(media, socket)){ if(!(await this.scheduleMedia(media, socket))){
//Reset start time //Reset start time
media.startTime = oldStart; media.startTime = oldStart;
@ -341,39 +363,112 @@ module.exports = class{
media.startTimeStamp = 0; media.startTimeStamp = 0;
//Schedule in old slot //Schedule in old slot
this.scheduleMedia(media, socket, true); this.scheduleMedia(media, socket, null, true);
} }
} }
removeMedia(uuid, socket){ async removeMedia(uuid, socket, chanDB){
//Get requested media //Get requested media
const media = this.getItemByUUID(uuid); const media = this.getItemByUUID(uuid);
//If we got a bad request //If we got a bad request
if(media == null){ if(media == null){
//If an originating socket was provided for this request try{
if(socket != null){ //DO everything ourselves since we don't have a fance end() function to do it
//Yell at the user for being an asshole chanDB = await channelModel.findOne({name:this.channel.name});
loggerUtils.socketErrorHandler(socket, "Cannot delete non-existant item!", "queue");
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
}
//Keep a copy of the archive that hasn't been changed
const preArchive = chanDB.media.archived;
//Filter out the requested item from the archive
chanDB.media.archived = chanDB.media.archived.filter((record)=>{
return record.uuid.toString() != uuid;
})
//If nothing changed in the archive
if(preArchive.length == chanDB.media.archived.length){
//If an originating socket was provided for this request
if(socket != null){
//Yell at the user for being an asshole
loggerUtils.socketErrorHandler(socket, "Cannot delete non-existant item!", "queue");
}
//Otherwise
}else{
//Broadcast changes
this.broadcastQueue(chanDB);
//Save changes to the DB
await chanDB.save();
}
}catch(err){
//If this was originated by someone
if(socket != null){
//Bitch at them
loggerUtils.socketExceptionHandler(socket, err);
//If not
}else{
//Bitch to the console
loggerUtils.localExceptionHandler(err);
}
} }
//Ignore it //Ignore it
return false; return false;
} }
//If we're currently playing the requested item. //Take the item out of the schedule map
if(this.nowPlaying != null && this.nowPlaying.uuid == uuid){
//End playback
this.end();
}
//Take the item out of the schedule
this.schedule.delete(media.startTime); this.schedule.delete(media.startTime);
//Refresh next timer //Refresh next timer
this.refreshNextTimer(); this.refreshNextTimer();
//Broadcast the channel queue //If we're currently playing the requested item.
this.broadcastQueue(); if(this.nowPlaying != null && this.nowPlaying.uuid == uuid){
//End playback
this.end(false, true);
//otherwise
}else{
try{
//DO everything ourselves since we don't have a fance end() function to do it
chanDB = await channelModel.findOne({name:this.channel.name});
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
}
//Filter media out by UUID
chanDB.media.scheduled = chanDB.media.scheduled.filter((record) => {
return record.uuid != uuid;
});
await chanDB.save();
//Broadcast the channel
this.broadcastQueue(chanDB);
}catch(err){
//Broadcast the channel
this.broadcastQueue();
//If this was originated by someone
if(socket != null){
//Bitch at them
loggerUtils.socketExceptionHandler(socket, err);
//If not
}else{
//Bitch to the console
loggerUtils.localExceptionHandler(err);
}
}
}
//return found media in-case our calling function needs it :P //return found media in-case our calling function needs it :P
return media; return media;
@ -400,12 +495,9 @@ module.exports = class{
//End the media //End the media
this.end(); this.end();
//Broadcast the channel queue
this.broadcastQueue();
} }
scheduleMedia(mediaObj, socket, force = false){ async scheduleMedia(mediaObj, socket, chanDB, force = false, volatile = false, startVolatile = false){
/* This is a fun method and I think it deserves it's own little explination... /* 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 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. I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass.
@ -490,17 +582,54 @@ module.exports = class{
this.broadcastQueue(); this.broadcastQueue();
//Refresh the next timer to ensure whatever comes on next is right //Refresh the next timer to ensure whatever comes on next is right
this.refreshNextTimer(); this.refreshNextTimer(startVolatile);
//If media has more than a minute before starting and DB transactions are enabled
if(mediaObj.startTime - new Date().getTime() > 1000 && !volatile){
//fuck you yoda you fucking nerd
try{
//If we didn't get handed a freebie
if(chanDB == null){
//Go out and get it done ourselves
chanDB = await channelModel.findOne({name:this.channel.name});
}
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while saving item to queue!`);
}
//Add media to the persistant schedule
chanDB.media.scheduled.push(mediaObj);
//Save the database
chanDB.save();
//If something fucked up
}catch(err){
//If this was originated by someone
if(socket != null){
//Bitch at them
loggerUtils.socketExceptionHandler(socket, err);
//If not
}else{
//Bitch to the console
loggerUtils.localExceptionHandler(err);
}
}
}
//return media object for use //return media object for use
return mediaObj; return mediaObj;
} }
async start(mediaObj, timestamp = mediaObj.startTimeStamp){ async start(mediaObj, timestamp = mediaObj.startTimeStamp, volatile = false){
//If something is already playing //If something is already playing
if(this.nowPlaying != null){ if(this.nowPlaying != null){
//Silently end the media //Silently end the media in RAM so the database isn't stepping on itself up ahead
this.end(true); //Alternatively we could've used await, but then we'd be doubling up on DB transactions :P
this.end(true, true, true);
} }
//reset current timestamp //reset current timestamp
@ -509,17 +638,31 @@ module.exports = class{
//Set current playing media //Set current playing media
this.nowPlaying = mediaObj; this.nowPlaying = mediaObj;
try{ //if DB transactions are enabled
//Get our channel if(!volatile){
const chanDB = await channelModel.findOne({name: this.channel.name}); try{
//Get our channel
const chanDB = await channelModel.findOne({name: this.channel.name});
//Set the now playing queued media document //If nowPlaying isn't null
chanDB.media.nowPlaying = mediaObj; if(chanDB.media.nowPlaying != null){
//Archive whats already in there since we're about to clobber the fuck out of it
chanDB.media.archived.push(chanDB.media.nowPlaying);
}
//Save the channel //Set the now playing queued media document
await chanDB.save(); chanDB.media.nowPlaying = mediaObj;
}catch(err){
loggerUtils.localExceptionHandler(err); //Filter media out of schedule by UUID
chanDB.media.scheduled = chanDB.media.scheduled.filter((record) => {
return record.uuid != mediaObj.uuid;
});
//Save the channel
await chanDB.save();
}catch(err){
loggerUtils.localExceptionHandler(err);
}
} }
//Send play signal out to the channel //Send play signal out to the channel
@ -555,7 +698,7 @@ module.exports = class{
} }
} }
async end(quiet = false){ async end(quiet = false, noArchive = false, volatile = false, chanDB){
try{ try{
//Call off any existing sync timer //Call off any existing sync timer
clearTimeout(this.syncTimer); clearTimeout(this.syncTimer);
@ -579,30 +722,41 @@ module.exports = class{
this.server.io.in(this.channel.name).emit('end', {}); this.server.io.in(this.channel.name).emit('end', {});
} }
//Now that everything is clean, we can take our time with the DB :P if(!volatile){
const chanDB = await channelModel.findOne({name:this.channel.name}); //Now that everything is clean, we can take our time with the DB :P
chanDB = await channelModel.findOne({name:this.channel.name});
//If we couldn't find the channel //If we couldn't find the channel
if(chanDB == null){ if(chanDB == null){
//FUCK //FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while ending queue item!`); throw new Error(`Unable to find channel document ${this.channel.name} while ending queue item!`);
}
//If we haven't changed 'nowPlaying' in the play list
if(chanDB.media.nowPlaying.uuid == wasPlaying.uuid){
//Take it out
await chanDB.media.nowPlaying.deleteOne();
}
//Take it out of the active schedule
this.schedule.delete(wasPlaying.startTime);
if(!noArchive){
//Add the item to the channel archive
chanDB.media.archived.push(wasPlaying);
}
//broadcast queue using unsaved archive
this.broadcastQueue(chanDB);
//Save our changes to the DB
await chanDB.save();
}else{
//broadcast queue using unsaved archive
this.broadcastQueue(chanDB);
} }
//If we haven't changed 'nowPlaying' in the play list
if(chanDB.media.nowPlaying.uuid == wasPlaying.uuid){
//Take it out
await chanDB.media.nowPlaying.deleteOne();
}
//Take it out of the active schedule
this.schedule.delete(wasPlaying.startTime);
//Add the item to the channel archive
chanDB.media.archived.push(wasPlaying);
//Save our changes to the DB
await chanDB.save();
}catch(err){ }catch(err){
this.broadcastQueue();
loggerUtils.localExceptionHandler(err); loggerUtils.localExceptionHandler(err);
} }
} }
@ -704,7 +858,7 @@ module.exports = class{
} }
async broadcastQueue(chanDB){ async broadcastQueue(chanDB){
this.server.io.in(this.channel.name).emit('queue',{queue: await this.prepQueue()}); this.server.io.in(this.channel.name).emit('queue',{queue: await this.prepQueue(chanDB)});
} }
async prepQueue(chanDB){ async prepQueue(chanDB){
@ -727,11 +881,10 @@ module.exports = class{
//Get yestedays epoch //Get yestedays epoch
const yesterday = new Date().setDate(new Date().getDate() - 1); const yesterday = new Date().setDate(new Date().getDate() - 1);
//Iterate through the channel archive backwards to save time //Iterate through the channel archive backwards to save time
for(let mediaIndex = chanDB.media.archived.length - 1; mediaIndex >= 0; mediaIndex--){ for(let mediaIndex = chanDB.media.archived.length - 1; mediaIndex >= 0; mediaIndex--){
//Grab the current media record //Grab the current media record
const media = chanDB.media.archived[mediaIndex]; let media = chanDB.media.archived[mediaIndex].rehydrate();
//If the media started within the last 24 hours //If the media started within the last 24 hours
if(media.startTime > yesterday){ if(media.startTime > yesterday){
@ -770,14 +923,66 @@ module.exports = class{
throw new Error(`Unable to find channel document ${this.channel.name} while rehydrating queue!`); throw new Error(`Unable to find channel document ${this.channel.name} while rehydrating queue!`);
} }
const now = new Date().getTime();
//Next: Update this function to handle ended items
//If something was playing //If something was playing
if(chanDB.media.nowPlaying != null){ if(chanDB.media.nowPlaying != null){
//Rehydrate the currently playing item //Rehydrate the currently playing item int oa queued media object
const wasPlaying = chanDB.media.nowPlaying.rehydrate(); const wasPlaying = chanDB.media.nowPlaying.rehydrate();
//Schedule it //If the media hasn't ended yet
this.scheduleMedia(wasPlaying, null, true); if(wasPlaying.getEndTime() > now){
} //Re-Schedule it in RAM
await this.scheduleMedia(wasPlaying, null, chanDB, true, true);
//Otherwise, if it has
}else{
//Null out nowPlaying
chanDB.media.nowPlaying = null;
//Archive the bitch
chanDB.media.archived.push(wasPlaying);
}
}
//Create a new array to hold the new schedule so we only have to write to the DB once.
let newSched = [];
//For every saved scheduled item
for(let record of chanDB.media.scheduled){
//Rehydrate the current record into a queued media object
const mediaObj = record.rehydrate();
//If the item hasn't started
if(mediaObj.startTime > now){
//Add record to new schedule
newSched.push(record);
//Re-Schedule it in RAM
await this.scheduleMedia(mediaObj, null, chanDB, true, true, false);
}else{
//If the media should be playing now
if(mediaObj.getEndTime() > now){
//Save record to nowPlaying in the DB
chanDB.media.nowPlaying = record;
//Re-Schedule it in RAM
await this.scheduleMedia(mediaObj, null, chanDB, true, true, true);
//If it's been ended
}else{
//Archive ended media
chanDB.media.archived.push(record);
}
}
}
//Update schedule to only contain what hasn't been played yet
chanDB.media.scheduled = newSched;
//Save the DB
await chanDB.save();
//if something fucked up //if something fucked up
}catch(err){ }catch(err){

View file

@ -535,6 +535,10 @@ div.now-playing{
text-shadow: var(--focus-glow0); text-shadow: var(--focus-glow0);
} }
div.archived{
color: var(--bg2-alt1);
}
/* altcha theming*/ /* altcha theming*/
div.altcha{ div.altcha{
box-shadow: 4px 4px 1px var(--bg1-alt0) inset; box-shadow: 4px 4px 1px var(--bg1-alt0) inset;

View file

@ -566,7 +566,7 @@ class queuePanel extends panelObj{
`Source: ${entry[1].type}`, `Source: ${entry[1].type}`,
`Duration: ${entry[1].duration}`, `Duration: ${entry[1].duration}`,
`Start Time: ${new Date(entry[1].startTime).toLocaleString()}${entry[1].startTimeStamp == 0 ? '' : ' (Started Late)'}`, `Start Time: ${new Date(entry[1].startTime).toLocaleString()}${entry[1].startTimeStamp == 0 ? '' : ' (Started Late)'}`,
`End Time: ${new Date(this.getMediaEnd(entry[1])).toLocaleString()}` `End Time: ${new Date(this.getMediaEnd(entry[1])).toLocaleString()}${entry[1].earlyEnd == null ? '' : ' (Ended Early)'}`
]){ ]){
//Create a 'p' node //Create a 'p' node
const component = document.createElement('p'); const component = document.createElement('p');
@ -587,19 +587,23 @@ class queuePanel extends panelObj{
//Create context menu map //Create context menu map
const menuMap = new Map(); const menuMap = new Map();
const now = new Date();
//If the item hasn't started yet //If the item hasn't started yet
if(entry[1].startTime > new Date().getTime()){ if(entry[1].startTime > now.getTime()){
//Add 'Play' option to context menu //Add 'Play' option to context menu
menuMap.set("Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})}); menuMap.set("Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})});
//Add 'Move To...' option to context menu //Add 'Move To...' option to context menu
menuMap.set("Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1], null, this.ownerDoc)}); menuMap.set("Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1], null, this.ownerDoc)});
//Otherwise, if the item is currently playing //Otherwise, if the item is currently playing
}else if(this.getMediaEnd(entry[1]) > new Date().getTime()){ }else if(this.getMediaEnd(entry[1]) > now.getTime()){
//Add 'Stop' option to context menu //Add 'Stop' option to context menu
menuMap.set("Stop", ()=>{this.client.socket.emit('stop', {uuid: entry[1].uuid})}); menuMap.set("Stop", ()=>{this.client.socket.emit('stop', {uuid: entry[1].uuid})});
//Add the Now Playing glow, not the prettiest place to add this, but why let a good conditional go to waste? //Add the Now Playing glow, not the prettiest place to add this, but why let a good conditional go to waste?
entryDiv.classList.add('now-playing'); entryDiv.classList.add('now-playing');
//Otherwise, if the item has been archived
}else{
entryDiv.classList.add('archived');
} }
//Add 'Delete' option to context menu //Add 'Delete' option to context menu
@ -609,8 +613,11 @@ class queuePanel extends panelObj{
//Add 'Copy URL' option to context menu //Add 'Copy URL' option to context menu
menuMap.set("Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);}) menuMap.set("Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);})
//Setup drag n drop //If the item hasn't yet ended
entryDiv.addEventListener('mousedown', clickEntry.bind(this)); if(this.getMediaEnd(entry[1]) > now.getTime()){
//Setup drag n drop
entryDiv.addEventListener('mousedown', clickEntry.bind(this));
}
//Setup context menu //Setup context menu
entryDiv.addEventListener('contextmenu', (event)=>{ entryDiv.addEventListener('contextmenu', (event)=>{