# JSDoc: Source: app/channel/media/playlistHandler.js

# 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
     */
    module.exports = class{
        /**
         * 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){
            //Set server
            this.server = server
            //Set channel
            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);
            }
        }
    }