Continued work on channel-wide playlists.

This commit is contained in:
rainbow napkin 2025-03-25 08:23:58 -04:00
parent 72a89ae5ff
commit 70a68d9336
7 changed files with 151 additions and 89 deletions

View file

@ -18,6 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
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);
}
}

View file

@ -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)
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() + 5;
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

View file

@ -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();

View file

@ -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

View file

@ -48,4 +48,5 @@ const mediaSchema = new mongoose.Schema({
}
);
module.exports = mediaSchema;

View file

@ -25,7 +25,8 @@ const playlistMediaProperties = new mongoose.Schema({
uuid: {
type: mongoose.SchemaTypes.UUID,
required:true,
unique: true
unique: true,
default: crypto.randomUUID()
}
},
{

View file

@ -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,
@ -32,3 +32,9 @@ module.exports = new mongoose.Schema({
default: []
}]
});
playlistSchema.methods.test = function(){
console.log(this.name);
}
module.exports = playlistSchema;