Source: app/channel/media/playlistHandler.js

/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.*/

//NPM imports
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');
const { userModel } = require('../../../schemas/user/userSchema');

/**
 * Class containing playlist management logic for a single channel
 */
class playlistHandler{
    /**
     * Instantiates a new object to handle playlist management for a single channel
     * @param {channelManager} server - Parent server object
     * @param {activeChannel} channel - Parent Channel object for desired channel queue
     */
    constructor(server, channel){
        /**
         * Parent Server Object
         */
        this.server = server
        
        /**
         * Parent Channel Object for desired channel queue
         */
        this.channel = channel;
    }

    /**
     * Defines server-side socket.io listeners for newly connected sockets
     * @param {Socket} socket - Newly connected socket to define listeners against
     */
    defineListeners(socket){
        //Channel Playlist Listeners
        socket.on("getChannelPlaylists", () => {this.getChannelPlaylists(socket)});
        socket.on("createChannelPlaylist", (data) => {this.createChannelPlaylist(socket, data)});
        socket.on("deleteChannelPlaylist", (data) => {this.deleteChannelPlaylist(socket, data)});
        socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)});
        socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)});
        socket.on("queueFromChannelPlaylist", (data) => {this.queueFromChannelPlaylist(socket, data)});
        socket.on("queueRandomFromChannelPlaylist", (data) => {this.queueRandomFromChannelPlaylist(socket, data)});
        socket.on("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)});
        socket.on("changeDefaultTitlesChannelPlaylist", (data) => {this.changeDefaultTitlesChannelPlaylist(socket, data)});
        socket.on("deleteChannelPlaylistMedia", (data) => {this.deleteChannelPlaylistMedia(socket, data)});

        //User Playlist Listeners
        socket.on("getUserPlaylists", () => {this.getUserPlaylists(socket)});
        socket.on("createUserPlaylist", (data) => {this.createUserPlaylist(socket, data)});
        socket.on("deleteUserPlaylist", (data) => {this.deleteUserPlaylist(socket, data)});
        socket.on("addToUserPlaylist", (data) => {this.addToUserPlaylist(socket, data)});
        socket.on("queueUserPlaylist", (data) => {this.queueUserPlaylist(socket, data)});
        socket.on("queueFromUserPlaylist", (data) => {this.queueFromUserPlaylist(socket, data)});
        socket.on("queueRandomFromUserPlaylist", (data) => {this.queueRandomFromUserPlaylist(socket, data)});
        socket.on("renameUserPlaylist", (data) => {this.renameUserPlaylist(socket, data)});
        socket.on("changeDefaultTitlesUserPlaylist", (data) => {this.changeDefaultTitlesUserPlaylist(socket, data)});
        socket.on("deleteUserPlaylistMedia", (data) => {this.deleteUserPlaylistMedia(socket, data)});
    }

    //Validation/Sanatization functions
    /**
     * Validates client requests to create a playlist
     * @param {Socket} socket - Newly connected socket to define listeners against
     * @param {Object} data - Data handed over from the client
     * @returns {Object} returns validated titles
     */
    createPlaylistValidator(socket, data){
        //Create empty array to hold titles
        const safeTitles = [];       

        //If the title is too long
        if(typeof data.playlist != 'string' || !validator.isLength(data.playlist, {min: 1, max:30})){
            //Bitch, moan, complain...
            loggerUtils.socketErrorHandler(socket, "Invalid Playlist Name!", "validation");
            //and ignore it!
            return;
        }

        if(data.defaultTitles != null){
            //For each default title passed by the data
            for(let title of data.defaultTitles){
                //If the title isn't too long
                if(typeof title != 'string' || validator.isLength(title, {min:1, max:30})){
                    //Add it to the safe title list
                    safeTitles.push(validator.escape(validator.trim(title)))
                }
            }
        }

        //Escape/trim the playlist name
        return {
            playlist: validator.escape(validator.trim(data.playlist)),
            defaultTitles: safeTitles
        }
    }

    /**
     * Validates client requests to add media to a playlist
     * @param {Socket} socket - Newly connected socket to define listeners against
     * @param {String} URL - URL String handed over from the client
     * @returns {Array} List of media objects which where added
     */
    async addToPlaylistValidator(socket, url){
        //If we where given a bad URL
        if(typeof url != 'string' || !validator.isURL(url)){
            //Attempt to fix the situation by encoding it
            url = encodeURI(url);

            //If it's still bad
            if(typeof url != 'string' || !validator.isURL(url)){
                //Bitch, moan, complain...
                loggerUtils.socketErrorHandler(socket, "Bad URL!", "validation");
                //and ignore it!
                return;
            }
        }

        //Pull media metadata
        const mediaList = await yanker.yankMedia(url);

        //If we didn't get any media
        if(mediaList.length == 0 || mediaList == null){
            //Bitch, moan, complain...
            loggerUtils.socketErrorHandler(socket, "No media found!", "queue");
            //and ignore it!
            return;               
        }

        return mediaList;
    }

    /**
     * Validates client requests to queue media from a playlist
     * @param {Socket} socket - Newly connected socket to define listeners against
     * @param {Object} data - Data handed over from the client
     * @returns {Number} returns validated start time on success
     */
    queueFromChannelPlaylistValidator(socket, data){
        //Validate UUID
        if(typeof data.uuid != 'string' || !validator.isUUID(data.uuid)){
            //Bitch, moan, complain...
            loggerUtils.socketErrorHandler(socket, `'${data.uuid}' is not a valid UUID!`, "validation");
            //and ignore it!
            return;
        }

        //The UUID is only validated, not processed so we just return the new time :P
        return this.channel.queue.getStart(data.start)
    }

    /**
     * Validates client requests to rename the playlist validator
     * @param {Socket} socket - Newly connected socket to define listeners against
     * @param {Object} data - Data handed over from the client
     * @returns {String} returns escaped/trimmed name upon success
     */
    renameChannelPlaylistValidator(socket, data){
        //If the title is too long
        if(typeof data.name != 'string' || !validator.isLength(data.name, {min: 1, max:30})){
            //Bitch, moan, complain...
            loggerUtils.socketErrorHandler(socket, "Invalid playlist name!", "validation");
            //and ignore it!
            return;
        }

        //Escape/trim the playlist name
        return validator.escape(validator.trim(data.name));
    }

    /**
     * Validates client requests to change default titles for a given playlist
     * @param {Object} data - Data handed over from the client
     * @returns {Array} Array of strings containing valid titles from the output
     */
    changeDefaultTitlesValidator(data){
        //Create empty array to hold titles
        const safeTitles = [];

        //For each default title passed by the data
        for(let title of data.defaultTitles){
            //If the title isn't too long or too short
            if(typeof title != 'string' || validator.isLength(title, {min: 1, max:30})){
                //Add it to the safe title list
                safeTitles.push(validator.escape(validator.trim(title)))
            }
        }

        //return safe titles
        return safeTitles;
    }

    /**
     * Validates client requests to rename the playlist validator
     * @param {Socket} socket - Newly connected socket to define listeners against
     * @param {Object} data - Data handed over from the client
     */
    deletePlaylistMediaValidator(socket, data){
        //If we don't have a valid UUID
        if(typeof data.uuid != 'string' || !validator.isUUID(data.uuid)){
            //Bitch, moan, complain...
            loggerUtils.socketErrorHandler(socket, `'${data.uuid}' is not a valid UUID!`, "validation");
            //and ignore it!
            return;
        }

        return data.uuid;
    }

    //Get playlist functions
    /**
     * Sends channel playlist data to a requesting socket
     * @param {Socket} socket - Newly connected socket to define listeners against
     * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access
     */
    async getChannelPlaylists(socket, 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});
            }

            //Return playlists
            socket.emit('chanPlaylists', chanDB.getPlaylists());
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Sends user playlist data to a requesting socket
     * @param {Socket} socket - Newly connected socket to define listeners against
     * @param {Mongoose.Document} userDB - Channnel Document Passthrough to save on DB Access
     */
    async getUserPlaylists(socket, userDB){
        try{
            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //Return playlists
            socket.emit('userPlaylists', userDB.getPlaylists());
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    //Create playlist functions
    /**
     * Creates a new channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access
     */
    async createChannelPlaylist(socket, data, chanDB){
        try{
            //Validate Data
            const validData = this.createPlaylistValidator(socket, data);

            //If we got bad data
            if(validData == null){
                //Do nothing
                return;
            }

            //if we wherent handed a channel document
            if(chanDB == null){
                //Pull it based on channel name
                chanDB = await channelModel.findOne({name: this.channel.name});
            }

            if(await chanDB.permCheck(socket.user, 'editChannelPlaylists')){
                //If the channel already exists
                if(chanDB.getPlaylistByName(validData.playlist) != null){
                    //Bitch, moan, complain...
                    loggerUtils.socketErrorHandler(socket, `Playlist named '${validData.playlist}' already exists!`, "validation");
                    //and ignore it!
                    return;
                } 

                //Add playlist to the channel doc
                chanDB.media.playlists.push({
                    name: validData.playlist,
                    defaultTitles: validData.defaultTitles
                });

                //Save the channel doc
                await chanDB.save();

                //Return playlists from channel doc
                this.getChannelPlaylists(socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Creates a new user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     */
    async createUserPlaylist(socket, data, userDB){
        try{
            //Validate Data
            const validData = this.createPlaylistValidator(socket, data);

            //If we got bad data
            if(validData == null){
                //Do nothing
                return;
            }

            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //If the channel already exists
            if(userDB.getPlaylistByName(validData.playlist) != null){
                //Bitch, moan, complain...
                loggerUtils.socketErrorHandler(socket, `Playlist named '${validData.playlist}' already exists!`, "validation");
                //and ignore it!
                return;
            } 

            //Add playlist to the channel doc
            userDB.playlists.push({
                name: validData.playlist,
                defaultTitles: validData.defaultTitles
            });

            //Save the channel doc
            await userDB.save();

            //Return playlists from channel doc
            this.getUserPlaylists(socket, userDB);
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    //Delete playlist functions
    /**
     * Deletes a user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     */
    async deleteChannelPlaylist(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});
            }

            //If the channel doesn't exist
            if(chanDB.getPlaylistByName(data.playlist) == null){
                //Bitch, moan, complain...
                loggerUtils.socketErrorHandler(socket, `Playlist named '${data.playlist}' doesn't exist!`, "validation");
                //and ignore it!
                return;
            } 

            if(await chanDB.permCheck(socket.user, 'editChannelPlaylists')){
                //Delete playlist name
                await chanDB.deletePlaylistByName(data.playlist);

                //Return playlists from channel doc
                this.getChannelPlaylists(socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Deletes a Channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access
     */
    async deleteUserPlaylist(socket, data, userDB){
        try{
            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //If the channel doesn't exist
            if(userDB.getPlaylistByName(data.playlist) == null){
                //Bitch, moan, complain...
                loggerUtils.socketErrorHandler(socket, `Playlist named '${data.playlist}' doesn't exist!`, "validation");
                //and ignore it!
                return;
            } 

            //Delete playlist name
            await userDB.deletePlaylistByName(data.playlist);

            //Return playlists from channel doc
            this.getUserPlaylists(socket, userDB);
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    //Add Media Functions
    /**
     * Adds media to channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channnel Document Passthrough to save on DB Access
     */
    async addToChannelPlaylist(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});
            }

            if(await chanDB.permCheck(socket.user, 'editChannelPlaylists')){
                //Normally I put validators before the DB call
                //But this reqs a server-side fetch, so I'll do the perm check first :P
                //Validate URL and pull media
                const mediaList = await this.addToPlaylistValidator(socket, data.url);

                //If we encountered an error during validation
                if(mediaList == null){
                    //Fuck off and die
                    return;
                }

                //Find the 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;
                }

                //delete media from playlist
                chanDB.media.playlists[playlist.listIndex].addMedia(mediaList);

                //save the channel document
                await chanDB.save();

                //Return playlists from channel doc
                this.getChannelPlaylists(socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Adds media to user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     */
    async addToUserPlaylist(socket, data, userDB){
        try{
            //Validate URL and pull media
            const mediaList = await this.addToPlaylistValidator(socket, data.url);

            //If we encountered an error during validation
            if(mediaList == null){
                //Fuck off and die
                return;
            }

            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //Find the playlist
            const playlist = userDB.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
            userDB.playlists[playlist.listIndex].addMedia(mediaList);

            //save the channel document
            await userDB.save();

            //Return playlists from channel doc
            this.getUserPlaylists(socket, userDB);
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    //Queuing Functions
    /**
     * Queues an entire channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async queueChannelPlaylist(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 to make sure the user can fuck w/ the queue
            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 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 = [];

                //Iterate through playlist media
                for(let item of playlist.media){
                    //Rehydrate a full phat media object from the flat DB entry
                    let mediaObj = item.rehydrate();

                    //Set media title from default titles
                    mediaObj.title = playlist.pickDefaultTitle(mediaObj.title);

                    //Push rehydrated item on to the mediaList
                    mediaList.push(mediaObj);
                }

                //Convert array of standard media objects to queued media objects, and push to schedule
                this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray(mediaList, start), socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Queues an entire user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async queueUserPlaylist(socket, data, userDB, chanDB){
        try{
            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //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 to make sure the user can fuck w/ the queue
            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 from the DB
                let playlist = userDB.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 = [];

                //Iterate through playlist media
                for(let item of playlist.media){
                    //Rehydrate a full phat media object from the flat DB entry
                    let mediaObj = item.rehydrate();

                    //Set media title from default titles
                    mediaObj.title = playlist.pickDefaultTitle(mediaObj.title);

                    //Push rehydrated item on to the mediaList
                    mediaList.push(mediaObj);
                }

                //Convert array of standard media objects to queued media objects, and push to schedule
                this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray(mediaList, start), socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Queues media from a given channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async queueFromChannelPlaylist(socket, data, chanDB){
        try{
            //Validate data
            const start = this.queueFromChannelPlaylistValidator(socket, data);

            //If we had a validation issue
            if(start == null){
                //Fuck off and die
                return;
            }

            //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')){       
                //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;
                } 

                //Pull and rehydrate media from playlist
                const media = playlist.findMediaByUUID(data.uuid).rehydrate();

                //Set title from default titles
                media.title = playlist.pickDefaultTitle(media.title);

                //Queue found media
                this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray([media], start), socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Queues media from a given user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async queueFromUserPlaylist(socket, data, userDB, chanDB){
        try{
            //Validate data
            const start = this.queueFromChannelPlaylistValidator(socket, data);

            //If we had a validation issue
            if(start == null){
                //Fuck off and die
                return;
            }

            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //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')){       
                //Grab playlist
                const playlist = userDB.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;
                } 

                //Pull and rehydrate media from playlist
                const media = playlist.findMediaByUUID(data.uuid).rehydrate();

                //Set title from default titles
                media.title = playlist.pickDefaultTitle(media.title);

                //Queue found media
                this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray([media], start), socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Queues random media from a given channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    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(foundMedia.title);

                //Queue found media
                this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray([foundMedia], start), socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Queues random media from a given user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async queueRandomFromUserPlaylist(socket, data, userDB, 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});
            }

            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //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 = userDB.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(foundMedia.title);

                //Queue found media
                this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray([foundMedia], start), socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    //Rename playlist functions
    /**
     * Renames a channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async renameChannelPlaylist(socket, data, chanDB){
        try{
            //Validate and Sanatize name
            const name = this.renameChannelPlaylistValidator(socket, data);

            //If validation fucked up
            if(name == null){
                //STOP
                return;
            }           

            //if we wherent handed a channel document
            if(chanDB == null){
                //Pull it based on channel name
                chanDB = await channelModel.findOne({name: this.channel.name});
            }

            if(await chanDB.permCheck(socket.user, 'editChannelPlaylists')){
                //If the new name already exists
                if(chanDB.getPlaylistByName(name) != null){
                    //Bitch, moan, complain...
                    loggerUtils.socketErrorHandler(socket, `Playlist named '${name}' already exists!`, "validation");
                    //and ignore it!
                    return;
                }

                //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;

                //Save channel document
                await chanDB.save();

                //Return playlists from channel doc
                this.getChannelPlaylists(socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Renames a user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     */
    async renameUserPlaylist(socket, data, userDB){
        try{
            //Validate and Sanatize name
            const name = this.renameChannelPlaylistValidator(socket, data);

            //If validation fucked up
            if(name == null){
                //STOP
                return;
            }           

            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //If the new name already exists
            if(userDB.getPlaylistByName(name) != null){
                //Bitch, moan, complain...
                loggerUtils.socketErrorHandler(socket, `Playlist named '${name}' already exists!`, "validation");
                //and ignore it!
                return;
            }

            //Find playlist
            let playlist = userDB.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
            userDB.playlists[playlist.listIndex].name = name;

            //Save channel document
            await userDB.save();

            //Return playlists from channel doc
            this.getUserPlaylists(socket, userDB);
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    //Change default title list functions
    /**
     * Changes default titles for a given channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async changeDefaultTitlesChannelPlaylist(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});
            }

            if(await chanDB.permCheck(socket.user, 'editChannelPlaylists')){
                //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;
                }

                //Keep valid default titles
                chanDB.media.playlists[playlist.listIndex].defaultTitles = this.changeDefaultTitlesValidator(data);

                //Save channel document
                await chanDB.save();

                //Return playlists from channel doc
                this.getChannelPlaylists(socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Changes default titles for a given user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     */
    async changeDefaultTitlesUserPlaylist(socket, data, userDB){
        try{
            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //Find playlist
            let playlist = userDB.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;
            }

            
            //Keep valid de.mediafault titles
            userDB.playlists[playlist.listIndex].defaultTitles = this.changeDefaultTitlesValidator(data);

            //Save user document
            await userDB.save();

            //Return playlists from user doc
            this.getUserPlaylists(socket, userDB);
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    //Delete playlist media functions
    /**
     * Deletes media from a given channel playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} chanDB - Channel Document Passthrough to save on DB Access
     */
    async deleteChannelPlaylistMedia(socket, data, chanDB){
        try{
            //Validate UUID
            const uuid = this.deletePlaylistMediaValidator(socket, data);

            //If we failed validation
            if(uuid == null){
                //fuck off
                return;
            }

            //if we wherent handed a channel document
            if(chanDB == null){
                //Pull it based on channel name
                chanDB = await channelModel.findOne({name: this.channel.name});
            }

            if(await chanDB.permCheck(socket.user, 'editChannelPlaylists')){
                //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
                this.getChannelPlaylists(socket, chanDB);
            }
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }

    /**
     * Deletes media from a given user playlist
     * @param {Socket} socket - Requesting socket
     * @param {Object} data - Data handed over from the client
     * @param {Mongoose.Document} userDB - User Document Passthrough to save on DB Access
     */
    async deleteUserPlaylistMedia(socket, data, userDB){
        try{
            //Validate UUID
            const uuid = this.deletePlaylistMediaValidator(socket, data);

            //If we failed validation
            if(uuid == null){
                //fuck off
                return;
            }

            //if we wherent handed a user document
            if(userDB == null){
                //Find the user in the Database
                userDB = await userModel.findOne({user: socket.request.session.user.user});
            }

            //Find the playlist
            let playlist = userDB.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
            userDB.playlists[playlist.listIndex].deleteMedia(data.uuid);

            //save the user document
            await userDB.save();

            //Return playlists from user doc
            this.getUserPlaylists(socket, userDB);
        }catch(err){
            return loggerUtils.socketExceptionHandler(socket, err);
        }
    }
}

module.exports = playlistHandler;