Added proper handling of items that begin late and end early.

This commit is contained in:
rainbow napkin 2025-02-09 17:29:07 -05:00
parent 330c4c275b
commit 179a10fb72
5 changed files with 286 additions and 69 deletions

View file

@ -51,6 +51,7 @@ module.exports = class{
defineListeners(socket){ defineListeners(socket){
socket.on("queue", (data) => {this.queueURL(socket, data)}); 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("delete", (data) => {this.deleteMedia(socket, data)});
socket.on("move", (data) => {this.moveMedia(socket, data)}); socket.on("move", (data) => {this.moveMedia(socket, data)});
socket.on("clear", (data) => {this.deleteRange(socket, data)}); socket.on("clear", (data) => {this.deleteRange(socket, data)});
@ -110,18 +111,6 @@ module.exports = class{
return; return;
} }
//If we have an invalid time
if(start == null || start < (new Date).getTime()){
//Get last item from schedule
const lastItem = (Array.from(this.schedule)[this.schedule.size - 1]);
//if we have a last item
if(lastItem != null){
//Throw it on five ms after the last item
start = lastItem[1].startTime + (lastItem[1].duration * 1000) + 5;
}
}
//Queue the first media object given //Queue the first media object given
this.queueMedia(mediaList[0], start, socket); this.queueMedia(mediaList[0], start, socket);
}catch(err){ }catch(err){
@ -143,7 +132,6 @@ module.exports = class{
//and ignore it! //and ignore it!
return; return;
} }
//If end time isn't an integer //If end time isn't an integer
if(data.end != null && !validator.isInt(String(data.end))){ if(data.end != null && !validator.isInt(String(data.end))){
//Bitch, moan, complain... //Bitch, moan, complain...
@ -195,7 +183,7 @@ module.exports = class{
return; return;
} }
//If start time isn't an integer after the current epoch //If start time isn't an integer
if(data.start != null && !validator.isInt(String(data.start))){ if(data.start != null && !validator.isInt(String(data.start))){
//Null out time to tell the later parts of the function to start it now //Null out time to tell the later parts of the function to start it now
data.start = undefined; data.start = undefined;
@ -224,9 +212,32 @@ module.exports = class{
} }
//Default start time to now + half a second to give everyone time to process shit //Default start time to now + half a second to give everyone time to process shit
queueMedia(inputMedia, start = new Date().getTime() + 50, socket){ queueMedia(inputMedia, start, socket){
//If we have an invalid time
if(start == null || start < (new Date).getTime()){
//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
}else{
//Throw it on five ms after the last item
start = lastItem[1].getEndTime() + 5;
}
}else{
//Throw it on five ms after the last item
start = now + 5;
}
}
//Create a new media queued object, set start time to now //Create a new media queued object, set start time to now
const mediaObj = queuedMedia.fromMedia(inputMedia, start); const mediaObj = queuedMedia.fromMedia(inputMedia, start, 0);
//schedule the media //schedule the media
this.scheduleMedia(mediaObj, socket); this.scheduleMedia(mediaObj, socket);
@ -244,7 +255,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)); this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000) + currentItem.startTimeStamp);
} }
//otherwise if we have an item //otherwise if we have an item
}else{ }else{
@ -254,7 +265,7 @@ 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)}, startsIn); this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp)}, startsIn);
} }
} }
@ -269,9 +280,9 @@ module.exports = class{
} }
} }
rescheduleMedia(uuid, start = new Date().getTime() + 50, socket){ rescheduleMedia(uuid, start = new Date().getTime() + 5, socket){
//Find and remove media from the schedule by UUID //Find our media, don't remove it yet since we want to do some more testing first
const media = this.removeMedia(uuid); const media = this.getItemByUUID(uuid);
//If we got a bad request //If we got a bad request
if(media == null){ if(media == null){
@ -284,20 +295,50 @@ module.exports = class{
return; return;
} }
//If someone is trying to re-schedule something that starts in the past
if(media.startTime < new Date().getTime()){
//If an originating socket was provided for this request
if(socket != null){
//If the item is currently playing
if(media.getEndTime() > new Date().getTime()){
//Yell at the user for being an asshole
loggerUtils.socketErrorHandler(socket, "You cannot move an actively playing video!", "queue");
//Otherwise, if it's already ended
}else{
//Yell at the user for being an asshole
loggerUtils.socketErrorHandler(socket, "You cannot alter the past!", "queue");
}
}
//Ignore it
return;
}
//Remove the media from the schedule
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;
//Set media time //Set media time
media.startTime = start; media.startTime = start;
//Reset the start time stamp for re-calculation
media.startTimeStamp = 0;
//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(!this.scheduleMedia(media, socket)){
//Reset start time //Reset start time
media.startTime = oldStart; media.startTime = oldStart;
//Reset the start time stamp for re-calculation
media.startTimeStamp = 0;
//Schedule in old slot //Schedule in old slot
this.scheduleMedia(media, socket); this.scheduleMedia(media, socket, true);
} }
} }
@ -335,7 +376,33 @@ module.exports = class{
return media; return media;
} }
scheduleMedia(mediaObj, socket){ 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;
//End the media
this.end();
//Get difference between current time and start time and set as early end
stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000;
//Broadcast the channel queue
this.broadcastQueue();
}
scheduleMedia(mediaObj, socket, force = 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.
@ -364,8 +431,26 @@ module.exports = class{
https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it
*/ */
//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
if(socket != null){
//Yell at the user for being an asshole
loggerUtils.socketErrorHandler(socket, "You cannot alter the past!", "queue");
}
return false;
}
//If the item has already started and it's not being forced
if((mediaObj.startTime < new Date().getTime())){
//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;
}
//If there's already something queued right now //If there's already something queued right now
if(this.getItemAtEpoch(mediaObj.startTime) != null || this.getItemAtEpoch(mediaObj.startTime + (mediaObj.duration * 1000))){ if(this.getItemAtEpoch(mediaObj.startTime) != null || this.getItemAtEpoch(mediaObj.getEndTime())){
//If an originating socket was provided for this request //If an originating socket was provided for this request
if(socket != null){ if(socket != null){
//Yell at the user for being an asshole //Yell at the user for being an asshole
@ -375,7 +460,6 @@ module.exports = class{
return false; return false;
} }
//Create an empty temp array to sparsley populate with our schedule //Create an empty temp array to sparsley populate with our schedule
const tempSchedule = []; const tempSchedule = [];
//Create new map to replace our current schedule map //Create new map to replace our current schedule map
@ -409,7 +493,7 @@ module.exports = class{
return mediaObj; return mediaObj;
} }
start(mediaObj, timestamp = 0){ start(mediaObj, timestamp = mediaObj.startTimeStamp){
//Silently end the media //Silently end the media
this.end(true); this.end(true);

View file

@ -18,11 +18,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const media = require('./media'); const media = require('./media');
module.exports = class extends media{ module.exports = class extends media{
constructor(title, fileName, url, id, type, duration, startTime){ constructor(title, fileName, url, id, type, duration, startTime, startTimeStamp){
//Call derived constructor //Call derived constructor
super(title, fileName, url, id, type, duration); super(title, fileName, url, id, type, duration);
//Set media start time //Set media start time
this.startTime = startTime; this.startTime = startTime;
//Set the media start time stamp
this.startTimeStamp = startTimeStamp;
//Create empty variable to hold early end if media is stopped early
this.earlyEnd = null;
//Generate id unique to this specific entry of this specific file within this specific channel's queue //Generate id unique to this specific entry of this specific file within this specific channel's queue
//That way even if we have six copies of the same video queued, we can still uniquely idenitify each instance //That way even if we have six copies of the same video queued, we can still uniquely idenitify each instance
@ -30,9 +34,17 @@ module.exports = class extends media{
} }
//statics //statics
static fromMedia(media, startTime){ static fromMedia(media, startTime, startTimeStamp){
//Create and return queuedMedia object from given media object and arguments //Create and return queuedMedia object from given media object and arguments
return new this(media.title, media.fileName, media.url, media.id, media.type, media.duration, startTime); return new this(
media.title,
media.fileName,
media.url,
media.id,
media.type,
media.duration,
startTime,
startTimeStamp);
} }
//methods //methods
@ -41,6 +53,13 @@ module.exports = class extends media{
} }
getEndTime(){ getEndTime(){
return this.startTime + (this.duration * 1000); //If we have an early ending
if(this.earlyEnd == null){
//Calculate our ending
return this.startTime + ((this.duration - this.startTimeStamp) * 1000);
}else{
//Return our early end
return this.startTime + (this.earlyEnd * 1000);
}
} }
} }

View file

@ -505,18 +505,36 @@ div.queue-entry{
background-color: var(--bg2-alt1); background-color: var(--bg2-alt1);
} }
div.started-before-today{ div.started-yesterday{
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
border-top: 1px dashed var(--accent1); border-top: 1px dashed var(--accent1);
} }
div.ends-after-today{ div.ends-tomorrow{
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-bottom: 1px dashed var(--accent1); border-bottom: 1px dashed var(--accent1);
} }
div.started-late{
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 1px dashed var(--danger0-alt1);
}
div.ended-early{
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom: 1px dashed var(--danger0-alt1);
}
div.now-playing{
color: var(--focus0);
box-shadow: var(--focus-glow0);
text-shadow: var(--focus-glow0);
}
/* 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

@ -83,6 +83,7 @@ class queuePanel extends panelObj{
this.client.socket.on("clientMetadata", (data) => {this.renderQueue();}); this.client.socket.on("clientMetadata", (data) => {this.renderQueue();});
this.client.socket.on("queue", (data) => {this.renderQueue();}); this.client.socket.on("queue", (data) => {this.renderQueue();});
this.client.socket.on("lock", this.handleScheduleLock.bind(this)); this.client.socket.on("lock", this.handleScheduleLock.bind(this));
this.client.socket.on("error", this.handleQueueError.bind(this));
} }
setupInput(){ setupInput(){
@ -123,6 +124,26 @@ class queuePanel extends panelObj{
} }
} }
handleQueueError(data){
//Create a flag to reload the queue
let reloadQueue = false;
//Check errors
for(let error of data.errors){
//If we have a queue error
if(error.type == "queue"){
//Throw the reload flag
reloadQueue = true;
}
}
//If we need to reload the queue
if(reloadQueue){
//Do so
this.renderQueue();
}
}
/* queue control button functions */ /* queue control button functions */
toggleAddMedia(event){ toggleAddMedia(event){
//If the div is hidden //If the div is hidden
@ -491,13 +512,13 @@ class queuePanel extends panelObj{
entryDiv.style.top = `${this.offsetByDate(dawn)}px`; entryDiv.style.top = `${this.offsetByDate(dawn)}px`;
//Run apply the rest of the styling rules //Run apply the rest of the styling rules
entryDiv.classList.add('started-before-today'); entryDiv.classList.add('started-yesterday');
} }
//If the item ends today //If the item ends today
if(endsToday){ if(endsToday){
//Place the bottom of the entry div based on time //Place the bottom of the entry div based on time
entryDiv.style.bottom = `${this.offsetByDate(new Date(entry[1].startTime + (entry[1].duration * 1000)), true)}px`; entryDiv.style.bottom = `${this.offsetByDate(new Date(this.getMediaEnd(entry[1])), true)}px`;
}else{ }else{
//Get midnight //Get midnight
const dusk = new Date(); const dusk = new Date();
@ -507,7 +528,17 @@ class queuePanel extends panelObj{
entryDiv.style.bottom = `${this.offsetByDate(dusk, true)}px`; entryDiv.style.bottom = `${this.offsetByDate(dusk, true)}px`;
//Run apply the rest of the styling rules //Run apply the rest of the styling rules
entryDiv.classList.add('ends-after-today'); entryDiv.classList.add('ends-tomorrow');
}
//If we started in the middle of the video
if(entry[1].startTimeStamp > 0){
entryDiv.classList.add('started-late');
}
//If we ended early
if(entry[1].earlyEnd != null){
entryDiv.classList.add('ended-early');
} }
//Create entry title //Create entry title
@ -526,8 +557,8 @@ class queuePanel extends panelObj{
`File Name: ${entry[1].fileName}`, `File Name: ${entry[1].fileName}`,
`Source: ${entry[1].type}`, `Source: ${entry[1].type}`,
`Duration: ${entry[1].duration}`, `Duration: ${entry[1].duration}`,
`Start Time: ${new Date(entry[1].startTime).toLocaleString()}`, `Start Time: ${new Date(entry[1].startTime).toLocaleString()}${entry[1].startTimeStamp == 0 ? '' : ' (Started Late)'}`,
`End Time: ${new Date(entry[1].startTime + (entry[1].duration * 1000)).toLocaleString()}` `End Time: ${new Date(this.getMediaEnd(entry[1])).toLocaleString()}`
]){ ]){
//Create a 'p' node //Create a 'p' node
const component = document.createElement('p'); const component = document.createElement('p');
@ -546,14 +577,29 @@ class queuePanel extends panelObj{
} }
}); });
//context menu //Create context menu map
const menuMap = new Map([ const menuMap = new Map();
["Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})}],
["Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1], null, this.ownerDoc)}], //If the item hasn't started yet
["Delete", ()=>{this.client.socket.emit('delete', {uuid: entry[1].uuid})}], if(entry[1].startTime > new Date().getTime()){
["Open in New Tab", ()=>{window.open(entry[1].url, '_blank').focus();}], //Add 'Play' option to context menu
["Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);}], menuMap.set("Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})});
]); //Add 'Move To...' option to context menu
menuMap.set("Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1], null, this.ownerDoc)});
//Otherwise, if the item is currently playing
}else if(this.getMediaEnd(entry[1]) > new Date().getTime()){
//Add 'Stop' option to context menu
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?
entryDiv.classList.add('now-playing');
}
//Add 'Delete' option to context menu
menuMap.set("Delete", ()=>{this.client.socket.emit('delete', {uuid: entry[1].uuid})})
//Add 'New Tab' option to context menu
menuMap.set("Open in New Tab", ()=>{window.open(entry[1].url, '_blank').focus();})
//Add 'Copy URL' option to context menu
menuMap.set("Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);})
//Setup drag n drop //Setup drag n drop
entryDiv.addEventListener('mousedown', clickEntry.bind(this)); entryDiv.addEventListener('mousedown', clickEntry.bind(this));
@ -582,8 +628,29 @@ class queuePanel extends panelObj{
return; return;
} }
//Create variables to hold width and height //Grab existing height
const height = event.target.offsetHeight; let height = event.target.offsetHeight;
let cutoffOffset = 0;
//If the item got cut-off at the bottom
if(event.target.classList.contains("ends-tomorrow") || event.target.classList.contains("ended-early")){
//Calculate height from duration
height = this.offsetByMilliseconds(Number(event.target.dataset['duration']) * 1000);
//If the item got cut-off at the top
}else if(event.target.classList.contains('started-yesterday') || event.target.classList.contains("started-late")){
//Keep old height for now
const oldHeight = height;
//Calculate height from duration
height = this.offsetByMilliseconds(Number(event.target.dataset['duration']) * 1000);
//Calculate the mouse offset needed to keep it properly placed relative to the original click point
cutoffOffset = height - oldHeight;
}
//Remove any cut-off borders
event.target.classList.remove('ends-tomorrow', 'started-yesterday', 'ended-early', 'started-late');
//If we havent set height or width //If we havent set height or width
if(event.target.style.height == ""){ if(event.target.style.height == ""){
@ -611,7 +678,7 @@ class queuePanel extends panelObj{
this.ownerDoc.body.style.userSelect = 'none'; this.ownerDoc.body.style.userSelect = 'none';
//Save top of target relative to window minus the mouse position as our drag offset //Save top of target relative to window minus the mouse position as our drag offset
event.target.dataset['dragoffset'] = (event.target.offsetTop + this.ownerDoc.defaultView.scrollY) - event.clientY; event.target.dataset['dragoffset'] = (event.target.offsetTop + this.ownerDoc.defaultView.scrollY) - event.clientY - cutoffOffset;
//Call the drag entry function to move the entry on click without re-writing the wheel //Call the drag entry function to move the entry on click without re-writing the wheel
(dragEntry.bind(this))(event, event.target, timetip); (dragEntry.bind(this))(event, event.target, timetip);
@ -711,7 +778,7 @@ class queuePanel extends panelObj{
function dropEntry(event, target, timetip){ function dropEntry(event, target, timetip){
//Gross but works :P //Gross but works :P
if(!target.isConnected){ if(!target.isConnected || target.dataset['drag'] != "true"){
return; return;
} }
@ -861,24 +928,9 @@ class queuePanel extends panelObj{
const dayEpoch = new Date(date).setHours(0,0,0,0); const dayEpoch = new Date(date).setHours(0,0,0,0);
//Get difference between now and day epoch to get time since the start of the current day in milliseconds //Get difference between now and day epoch to get time since the start of the current day in milliseconds
const curTime = date.getTime() - dayEpoch; const curTime = date.getTime() - dayEpoch;
//Devide by amount of milliseconds in a day to convert time over to a floating point number between 0 and 1
//Make sure to reverse it if bottomOffset is set to true
const timeFloat = bottomOffset ? 1 - (curTime / 86400000) : curTime / 86400000;
//Get queue markers
const markers = this.panelDocument.querySelectorAll('span.queue-marker');
//If the marker is null for some reason //Calculate the offset from todays milliseconds
if(markers[0] == null){ return this.offsetByMilliseconds(curTime, bottomOffset);
return null;
}
//Get marker position range
const range = [markers[0].offsetTop, markers[markers.length - 1].offsetTop]
//Get maximum position relative to the range itself
const offsetMax = range[1] - range[0];
//return position relative to parent
return (offsetMax * timeFloat) + range[0];
} }
dateByOffset(input = 0){ dateByOffset(input = 0){
@ -902,6 +954,38 @@ class queuePanel extends panelObj{
//return our date //return our date
return date; return date;
} }
offsetByMilliseconds(input = 0, bottomOffset = false){
//Convert amount of milliseconds to a float, 0 representing the start of the day and 1 representing the end.
//Make sure to reverse it if bottomOffset is set to true
const timeFloat = bottomOffset ? 1 - (input / 86400000) : input / 86400000;
//Get queue markers
const markers = this.panelDocument.querySelectorAll('span.queue-marker');
//If the marker is null for some reason
if(markers[0] == null){
return null;
}
//Get marker position range
const range = [markers[0].offsetTop, markers[markers.length - 1].offsetTop]
//Get maximum position relative to the range itself
const offsetMax = range[1] - range[0];
//return position relative to parent
return (offsetMax * timeFloat) + range[0];
}
getMediaEnd(media){
//If we have an early end
if(media.earlyEnd != null){
return media.startTime + (media.earlyEnd * 1000);
//Otherwise
}else{
return media.startTime + ((media.duration - media.startTimeStamp) * 1000);
}
}
} }
class schedulePopup{ class schedulePopup{

View file

@ -553,7 +553,19 @@ class canopyUXUtils{
} }
fixCutoff(standalone = true, pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width){ fixCutoff(standalone = true, pageBreak){
//If we have no pagebreak
if(pageBreak == null){
//If we have a document body
if(document.body != null){
pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width
//Otherwise
}else{
//Pretend nothing happened because we probably have bigger issues then a fucked up click-dragger cutoff
return;
}
}
//Fix the page width //Fix the page width
if(this.flex){ if(this.flex){
this.element.style.flexBasis = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak)}vw`; this.element.style.flexBasis = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak)}vw`;