/*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 .*/ //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'); module.exports = class{ constructor(server, chanDB, channel){ //Set server this.server = server //Set channel this.channel = channel; } defineListeners(socket){ socket.on("getChannelPlaylists", () => {this.getChannelPlaylists(socket)}); socket.on("createChannelPlaylist", (data) => {this.createChannelPlaylist(socket, data)}); socket.on("deleteChannelPlaylist", (data) => {this.deleteChannelPlaylist(socket, data)}); socket.on("deleteChannelPlaylistMedia", (data) => {this.deleteChannelPlaylistMedia(socket, data)}); socket.on("addToChannelPlaylist", (data) => {this.addToChannelPlaylist(socket, data)}); socket.on("queueChannelPlaylist", (data) => {this.queueChannelPlaylist(socket, data)}); socket.on("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)}); socket.on("changeDefaultTitlesChannelPlaylist", (data) => {this.changeDefaultTitlesChannelPlaylist(socket, data)}); } //--- USER-FACING PLAYLIST FUNCTIONS --- 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); } } async createChannelPlaylist(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')){ //If the title is too long if(!validator.isLength(data.playlist, {max:30})){ //Bitch, moan, complain... loggerUtils.socketErrorHandler(socket, "Playlist name too long!", "validation"); //and ignore it! return; } //Escape/trim the playlist name const name = validator.escape(validator.trim(data.playlist)); //If the channel already exists if(chanDB.getPlaylistByName(name) != null){ //Bitch, moan, complain... loggerUtils.socketErrorHandler(socket, `Playlist named '${name}' already exists!`, "validation"); //and ignore it! return; } //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 if(validator.isLength(title, {min:1, max:30})){ //Add it to the safe title list safeTitles.push(validator.escape(validator.trim(title))) } } //Add playlist to the channel doc chanDB.media.playlists.push({ name, defaultTitles: safeTitles }); //Save the channel doc await chanDB.save(); //Return playlists from channel doc socket.emit('chanPlaylists', chanDB.getPlaylists()); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } 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(await chanDB.permCheck(socket.user, 'editChannelPlaylists')){ //Delete playlist name await chanDB.deletePlaylistByName(data.playlist); //Return playlists from channel doc socket.emit('chanPlaylists', chanDB.getPlaylists()); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } 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')){ let url = data.url //If we where given a bad URL if(!validator.isURL(url)){ //Attempt to fix the situation by encoding it url = encodeURI(url); //If it's still bad if(!validator.isURL(url)){ //Bitch, moan, complain... loggerUtils.socketErrorHandler(socket, "Bad URL!", "validation"); //and ignore it! return; } } //Pull media metadata let 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; } //Add media object to the given playlist await chanDB.addToPlaylist(data.playlist, mediaList[0]); //Return playlists from channel doc socket.emit('chanPlaylists', chanDB.getPlaylists()); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } 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); //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.defaultTitles[Math.floor(Math.random() * playlist.defaultTitles.length)]; //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); } } async renameChannelPlaylist(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')){ //If the title is too long if(!validator.isLength(data.name, {max:30})){ //Bitch, moan, complain... loggerUtils.socketErrorHandler(socket, "Playlist name too long!", "validation"); //and ignore it! return; } //Escape/trim the playlist name const name = validator.escape(validator.trim(data.name)); //If the channel 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); //Change playlist name chanDB.media.playlists[playlist.listIndex].name = name; //Save channel document await chanDB.save(); //Return playlists from channel doc socket.emit('chanPlaylists', chanDB.getPlaylists()); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } 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); //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(validator.isLength(title, {min: 1, max:30})){ //Add it to the safe title list safeTitles.push(validator.escape(validator.trim(title))) } } //Change playlist name chanDB.media.playlists[playlist.listIndex].defaultTitles = safeTitles; //Save channel document await chanDB.save(); //Return playlists from channel doc socket.emit('chanPlaylists', chanDB.getPlaylists()); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } async deleteChannelPlaylistMedia(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')){ //If we don't have a valid UUID if(!validator.isUUID(data.uuid)){ //Bitch, moan, complain... loggerUtils.socketErrorHandler(socket, `'${data.uuid}' is not a valid UUID!`, "validation"); //and ignore it! return; } //Delete media from channel playlist chanDB.deletePlaylistMediaByUUID(data.playlist, data.uuid); //Return playlists from channel doc socket.emit('chanPlaylists', chanDB.getPlaylists()); } }catch(err){ return loggerUtils.socketExceptionHandler(socket, err); } } }