Started work on playlist management UI

This commit is contained in:
rainbow napkin 2025-03-30 14:11:49 -04:00
parent 65b5ae9371
commit 1723e8ebd0
9 changed files with 586 additions and 104 deletions

View file

@ -32,106 +32,196 @@ module.exports = class{
}
defineListeners(socket){
//socket.on("queue", (data) => {this.queueURL(socket, data)});
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("renameChannelPlaylist", (data) => {this.renameChannelPlaylist(socket, data)});
}
//--- USER-FACING PLAYLIST FUNCTIONS ---
async getChannelPlaylists(socket, chanDB){
//if we wherent handed a channel document
if(chanDB == null){
//Pull it based on channel name
chanDB = await channelModel.findOne({name: this.channel.name});
}
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());
//Return playlists
socket.emit('chanPlaylists', chanDB.getPlaylists());
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err);
}
}
async createChannelPlaylist(socket, data, chanDB){
//if we wherent handed a channel document
if(chanDB == null){
//Pull it based on channel name
chanDB = await channelModel.findOne({name: this.channel.name});
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 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));
//Add playlist to the channel doc
chanDB.media.playlists.push({
name,
defaultTitles: data.defaultTitles
});
//Save the channel doc
await chanDB.save();
//Return playlists from channel doc
socket.emit('chanPlaylists', chanDB.getPlaylists());
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err);
}
//Add playlist to the channel doc
chanDB.media.playlists.push({
name: data.playlist,
defaultTitles: data.defaultTitles
});
//Save the channel doc
await chanDB.save();
//Return playlists from channel doc
socket.emit('chanPlaylists', chanDB.getPlaylists());
}
async deleteChannelPlaylist(socket, data, chanDB){
//if we wherent handed a channel document
if(chanDB == null){
//Pull it based on channel name
chanDB = await channelModel.findOne({name: this.channel.name});
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});
}
//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);
}
//Delete playlist name
await chanDB.deletePlaylistByName(data.playlist);
//Return playlists from channel doc
socket.emit('chanPlaylists', chanDB.getPlaylists());
}
async addToChannelPlaylist(socket, data, chanDB){
//if we wherent handed a channel document
if(chanDB == null){
//Pull it based on channel name
chanDB = await channelModel.findOne({name: this.channel.name});
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});
}
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);
}
//Pull media metadata
let mediaList = await yanker.yankMedia(data.url);
//Add media object to the given playlist
await chanDB.addToPlaylist(data.playlist, mediaList[0]);
//Return playlists from channel doc
socket.emit('chanPlaylists', chanDB.getPlaylists());
}
async queueChannelPlaylist(socket, data, chanDB){
//if we wherent handed a channel document
if(chanDB == null){
//Pull it based on channel name
chanDB = await channelModel.findOne({name: this.channel.name});
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});
}
//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);
}
}
//Pull a valid start time from input, or make one up if we can't
let start = this.channel.queue.getStart(data.start);
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});
}
//Grab playlist from the DB
let playlist = chanDB.getPlaylistByName(data.playlist);
//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;
}
//Create an empty array to hold our media list
const mediaList = [];
//Escape/trim the playlist name
const name = validator.escape(validator.trim(data.name));
//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();
//Find playlist
let playlist = chanDB.getPlaylistByName(data.playlist);
//Set media title from default titles
mediaObj.title = playlist.defaultTitles[Math.floor(Math.random() * playlist.defaultTitles.length)];
//Change playlist name
chanDB.media.playlists[playlist.listIndex].name = name;
//Push rehydrated item on to the mediaList
mediaList.push(mediaObj);
//Save channel document
await chanDB.save();
//Return playlists from channel doc
socket.emit('chanPlaylists', chanDB.getPlaylists());
}catch(err){
return loggerUtils.socketExceptionHandler(socket, err);
}
//Convert array of standard media objects to queued media objects, and push to schedule
this.channel.queue.scheduleMedia(queuedMedia.fromMediaArray(mediaList, start), socket, chanDB);
}
}

View file

@ -626,7 +626,7 @@ module.exports = class{
}
//If we fucked with the DB during the main loop
if(chanDB == null && !volatile){
if(chanDB != null && !volatile){
try{
//Save the database
chanDB.save();

View file

@ -559,10 +559,7 @@ channelSchema.methods.getPlaylists = function(){
//this.media.playlists.forEach((playlist) => {
for(let playlist of this.media.playlists){
//Push an object with select information from the emote to the emote list
playlists.push({
name: playlist.name,
count: playlist.media.length
});
playlists.push(playlist.dehydrate());
}
//return the emote list
@ -584,11 +581,13 @@ channelSchema.methods.getPlaylistByName = function(name){
let foundPlaylist = null;
//Crawl through active playlists
this.playlistCrawl((playlist) => {
this.playlistCrawl((playlist, listIndex) => {
//If we found a match based on name
if(playlist.name == name){
//Keep it
foundPlaylist = playlist;
//Pass down the list index
foundPlaylist.listIndex = listIndex;
}
});
@ -597,45 +596,25 @@ channelSchema.methods.getPlaylistByName = function(name){
}
channelSchema.methods.deletePlaylistByName = async function(name){
//Create null value to hold our found playlist
let foundIndex = null;
//Crawl through active playlists
this.playlistCrawl((playlist, listIndex) => {
//If we found a match based on name
if(playlist.name == name){
//Yoink it's index
foundIndex = listIndex;
}
});
//Find the playlist
let playlist = this.getPlaylistByName(name);
//splice out the given playlist
this.media.playlists.splice(foundIndex, 1);
this.media.playlists.splice(playlist.listIndex, 1);
//save the channel document
await this.save();
}
channelSchema.methods.addToPlaylist = async function(name, media){
//Create variable to hold found index
let foundIndex = null
//Crawl through active playlists
this.playlistCrawl((playlist, listIndex) => {
//If the playlist name matches
if(playlist.name == name){
//Push the given media into the found playlist
//Make note of the found index
foundIndex = listIndex
}
});
//Find the playlist
let playlist = this.getPlaylistByName(name);
//Set media status schema discriminator
media.status = 'saved';
//Add the media to the playlist
this.media.playlists[foundIndex].media.push(media);
this.media.playlists[playlist.listIndex].media.push(media);
//Save the changes made to the chan doc
await this.save();

View file

@ -24,6 +24,7 @@ const playlistSchema = new mongoose.Schema({
name: {
type: mongoose.SchemaTypes.String,
required: true,
unique: true
},
media: [playlistMediaSchema],
defaultTitles:[{
@ -33,8 +34,26 @@ const playlistSchema = new mongoose.Schema({
}]
});
playlistSchema.methods.test = function(){
console.log(this.name);
//methods
playlistSchema.methods.dehydrate = function(){
//Create empty array to hold media
const mediaArray = [];
//Fill media array
for(let media of this.media){
mediaArray.push({
title: media.title,
url: media.url,
duration: media.duration
});
}
//return dehydrated playlist
return {
name: this.name,
media: mediaArray,
defaultTitles: this.defaultTitles
}
}
module.exports = playlistSchema;

View file

@ -41,6 +41,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<button id="queue-at-button" class="positive-button">Queue At...</button>
</div>
</div>
<div id="queue-playlist-prompts" style="display: none;">
<span class="queue-playlist-label-span interactive" id="queue-add-playlist-span">
<i class="bi-plus" id="queue-add-playlist-icon"></i>
<p id="queue-add-playlist-label">Create Playlist</p>
</span>
<span class="queue-playlist-label-span interactive" id="queue-channel-playlist-span">
<i class="bi-caret-right-fill" id="queue-channel-playlist-toggle"></i>
<p id="queue-channel-playlist-label">Channel Playlists</p>
</span>
<div style="display: none;" id="queue-channel-playlist-div" class="queue-playlists-div"></div>
<span class="queue-playlist-label-span interactive" id="queue-user-playlist-span">
<i class="bi-caret-right-fill" id="queue-user-playlist-toggle"></i>
<p id="queue-user-playlist-label">User Playlists</p>
</span>
<div style="display: none;" id="queue-user-playlist-div" class="queue-playlists-div"></div>
</div>
</div>
<div id="queue-control-offset"></div>
<div id="queue">

View file

@ -0,0 +1,28 @@
<%# 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/>. %>
<h3 class="popup-title">Create Playlist</h3>
<div id="queue-create-playlist-popup-div">
<input id="queue-create-playlist-popup-name" placeholder="Name">
<textarea id="queue-create-playlist-popup-default-titles" placeholder="Default Titles"></textarea>
<span>
<label for="location">Save To:</label>
<select name="location" id="queue-create-playlist-popup-location">
<option value="channel">Channel</option>
<option value="user">Account</option>
</select>
</span>
<button id="queue-create-playlist-popup-save" class="positive-button">Create Playlist</button>
</div>