瀏覽代碼

Merge pull request #6 from Musare/staging

Community Stations and Playlists Implemented, Beta Release
Jonathan 8 年之前
父節點
當前提交
5d89861d68

+ 2 - 2
backend/logic/actions/news.js

@@ -5,14 +5,14 @@ const db = require('../db');
 module.exports = {
 
 	index: (session, cb) => {
-		db.models.news.find({}).sort({ released: 'desc' }).exec((err, news) => {
+		db.models.news.find({}).sort({ createdAt: 'desc' }).exec((err, news) => {
 			if (err) throw err;
 			else cb({ status: 'success', data: news });
 		});
 	},
 
 	newest: (session, cb) => {
-		db.models.news.findOne({}).sort({ released: 'asc' }).exec((err, news) => {
+		db.models.news.findOne({}).sort({ createdAt: 'desc' }).exec((err, news) => {
 			if (err) throw err;
 			else cb({ status: 'success', data: news });
 		});

+ 162 - 7
backend/logic/actions/playlists.js

@@ -8,7 +8,7 @@ const hooks = require('./hooks');
 const async = require('async');
 const playlists = require('../playlists');
 
-module.exports = {
+let lib = {
 
 	indexForUser: (session, createdBy, cb) => {
 		db.models.playlist.find({ createdBy }, (err, playlists) => {
@@ -49,9 +49,9 @@ module.exports = {
 			}
 
 		], (err, playlist) => {
-			if (err) {console.log(err); return cb({ 'status': 'failure', 'message': 'Something went wrong'});}
+			if (err) return cb({ 'status': 'failure', 'message': 'Something went wrong'});
 			cache.pub('playlist.create', data._id);
-			return cb(null, { 'status': 'success', 'message': 'Successfully created playlist' });
+			return cb({ 'status': 'success', 'message': 'Successfully created playlist' });
 		});
 	},
 
@@ -71,15 +71,170 @@ module.exports = {
 		});
 	},
 
+	addSongToPlaylist: (session, songId, playlistId, cb) => {
+		async.waterfall([
+			(next) => {
+				utils.getSongFromYouTube(songId, (song) => {
+					song.artists = [];
+					song.genres = [];
+					song.skipDuration = 0;
+					song.thumbnail = 'empty';
+					song.explicit = false;
+					song.requestedBy = 'temp';
+					song.requestedAt = Date.now();
+					next(null, song);
+				});
+			},
+			(newSong, next) => {
+				utils.getSongFromSpotify(newSong, (song) => {
+					next(null, song);
+				});
+			},
+			(newSong, next) => {
+				db.models.playlist.findOne({ _id: playlistId }, (err, playlist) => {
+					if (err) throw err;
+
+					playlist.songs.push(newSong);
+					playlist.save(err => {
+						if (err) {
+							console.error(err);
+							return next('Failed to add song to playlist');
+						}
+
+						cache.hset('playlists', playlistId, playlist);
+						next(null, playlist);
+					});
+				});
+			}
+		],
+		(err, playlist) => {
+			if (err) return cb({ status: 'error', message: err });
+			else return cb({ status: 'success', message: 'Song has been successfully added to the playlist', data: playlist.songs });
+		});
+	},
+	
+	addSetToPlaylist: (session, url, playlistId, cb) => {
+		async.waterfall([
+			(next) => {
+				utils.getPlaylistFromYouTube(url, songs => {
+					next(null, songs);
+				});
+			},
+			(songs, next) => {
+				for (let s = 0; s < songs.length; s++) {
+					lib.addSongToPlaylist(session, songs[s].contentDetails.videoId, playlistId, (res) => {});
+				}
+				next(null);
+			},
+			(next) => {
+				db.models.playlist.findOne({ _id: playlistId }, (err, playlist) => {
+					if (err) throw err;
+
+					next(null, playlist);
+				});
+			}
+		],
+		(err, playlist) => {
+			if (err) return cb({ status: 'error', message: err });
+			else return cb({ status: 'success', message: 'Playlist has been successfully added', data: playlist.songs });
+		});
+	},
+
+
+	removeSongFromPlaylist: (session, songId, playlistId, cb) => {
+		db.models.playlist.findOne({ _id: playlistId }, (err, playlist) => {
+			if (err) throw err;
+
+			for (let z = 0; z < playlist.songs.length; z++) {
+				if (playlist.songs[z]._id == songId) playlist.songs.shift(playlist.songs[z]);
+			}
+
+			playlist.save(err => {
+				if (err) {
+					console.error(err);
+					return next('Failed to remove song to playlist');
+				}
+
+				cache.hset('playlists', playlistId, playlist);
+				return cb({ status: 'success', message: 'Song has been successfully removed from playlist', data: playlist.songs });
+			});
+		});
+	},
+
 	updateDisplayName: (session, _id, displayName, cb) => {
-		db.models.playlist.findOneAndUpdate({ _id }, { displayName }, { upsert: true }, (err, data) => {
+		db.models.playlist.findOneAndUpdate({ _id }, { displayName }, { upsert: true }, (err, res) => {
 			if (err) throw err;
-			return cb({ status: 'success', message: 'Playlist has been successfully updated', data });
+			cache.hset('playlists', _id, res);
+			return cb({ status: 'success', message: 'Playlist has been successfully updated' });
+		});
+	},
+
+	updatePlaylistId: (session, oldId, newId, cb) => {
+		db.models.playlist.findOne({ _id: oldId }).exec((err, doc) => {
+			if (err) throw err;
+			doc._id = newId;
+			let newPlaylist = new db.models.playlist(doc);
+			newPlaylist.isNew = true;
+			newPlaylist.save(err => {
+				if (err) console.error(err);
+			});
+			db.models.playlist.remove({ _id: oldId });
+			cache.hdel('playlists', oldId, () => {
+				cache.hset('playlists', newId, doc);
+				return cb({ status: 'success', data: doc });
+			});
+		});
+	},
+
+	promoteSong: (session, playlistId, fromIndex, cb) => {
+		db.models.playlist.findOne({ _id: playlistId }, (err, playlist) => {
+			if (err) throw err;
+
+			let song = playlist.songs[fromIndex];
+			playlist.songs.splice(fromIndex, 1);
+			playlist.songs.splice((fromIndex + 1), 0, song);
+
+			playlist.save(err => {
+				if (err) {
+					console.error(err);
+					return next('Failed to promote song');
+				}
+
+				cache.hset('playlists', playlistId, playlist);
+				return cb({ status: 'success', data: playlist.songs });
+			});
+		});
+	},
+
+	demoteSong: (session, playlistId, fromIndex, cb) => {
+		db.models.playlist.findOne({ _id: playlistId }, (err, playlist) => {
+			if (err) throw err;
+
+			let song = playlist.songs[fromIndex];
+			playlist.songs.splice(fromIndex, 1);
+			playlist.songs.splice((fromIndex - 1), 0, song);
+
+			playlist.save(err => {
+				if (err) {
+					console.error(err);
+					return next('Failed to demote song');
+				}
+
+				cache.hset('playlists', playlistId, playlist);
+				return cb({ status: 'success', data: playlist.songs });
+			});
 		});
 	},
 
 	remove: (session, _id, cb) => {
-		db.models.playlist.remove({ _id });
+		db.models.playlist.remove({ _id }).exec(err => {
+			if (err) throw err;
+			cache.hdel('playlists', _id, () => {
+				return cb({ status: 'success', message: 'Playlist successfully removed' });
+			});
+		});
 	}
 
-};
+};
+
+module.exports = lib;

+ 8 - 47
backend/logic/actions/queueSongs.js

@@ -48,15 +48,16 @@ module.exports = {
 		});
 	}),
 
-	remove: hooks.adminRequired((session, _id, cb) => {
-		// TODO Require admin/login
-		db.models.queueSong.remove({ _id });
-		return cb({ status: 'success', message: 'Song was removed successfully' });
+	remove: hooks.adminRequired((session, songId, cb) => {
+		db.models.queueSong.remove({ _id: songId }, (err, res) => {
+			if (err) return cb({ status: 'failure', message: err.message });
+			//TODO Pub/sub for (queue)songs on admin pages.
+			cb({ status: 'success', message: 'Song was removed successfully' });
+		});
 	}),
 
 	add: hooks.loginRequired((session, songId, cb, userId) => {
 		//TODO Check if id is valid
-		//TODO Check if id is already in queue/rotation
 
 		let requestedAt = Date.now();
 
@@ -91,48 +92,8 @@ module.exports = {
 				});
 			},
 			(newSong, next) => {
-				const spotifyParams = [
-					`q=${encodeURIComponent(newSong.title)}`,
-					`type=track`
-				].join('&');
-
-				request(`https://api.spotify.com/v1/search?${spotifyParams}`, (err, res, body) => {
-
-					if (err) {
-						console.error(err);
-						return next('Failed to find song from Spotify');
-					}
-
-					body = JSON.parse(body);
-
-					durationArtistLoop:
-					for (let i in body) {
-						let items = body[i].items;
-						for (let j in items) {
-
-							let item = items[j];
-							let hasArtist = false;
-							for (let k = 0; k < item.artists.length; k++) {
-								let artist = item.artists[k];
-								if (newSong.title.indexOf(artist.name) !== -1) {
-									hasArtist = true;
-								}
-							}
-							if (hasArtist && newSong.title.indexOf(item.name) !== -1) {
-								newSong.duration = item.duration_ms / 1000;
-								newSong.artists = item.artists.map(artist => {
-									return artist.name;
-								});
-								newSong.title = item.name;
-								newSong.explicit = item.explicit;
-								newSong.thumbnail = item.album.images[1].url;
-								break durationArtistLoop;
-							}
-
-						}
-					}
-
-					next(null, newSong);
+				utils.getSongFromSpotify(newSong, (song) => {
+					next(null, song);
 				});
 			},
 			(newSong, next) => {

+ 18 - 9
backend/logic/actions/songs.js

@@ -6,6 +6,7 @@ const songs = require('../songs');
 const cache = require('../cache');
 const utils = require('../utils');
 const hooks = require('./hooks');
+const queueSongs = require('./queueSongs');
 
 cache.sub('song.like', (data) => {
 	io.io.to(`song.${data.songId}`).emit('event:song.like', {songId: data.songId, undisliked: data.undisliked});
@@ -53,9 +54,12 @@ module.exports = {
 	},
 
 	update: hooks.adminRequired((session, songId, song, cb) => {
-		db.models.song.findOneAndUpdate({ _id: songId }, song, { upsert: true }, (err, updatedSong) => {
+		db.models.song.update({ _id: songId }, song, { upsert: true }, (err, updatedSong) => {
 			if (err) throw err;
-			return cb({ status: 'success', message: 'Song has been successfully updated', data: updatedSong });
+			songs.updateSong(songId, (err, song) => {
+				if (err) throw err;
+				cb({ status: 'success', message: 'Song has been successfully updated', data: song });
+			});
 		});
 	}),
 
@@ -63,16 +67,21 @@ module.exports = {
 		db.models.song.remove({ _id: songId });
 	}),
 
-	add: hooks.adminRequired((session, song, cb) => {
-		const newSong = new db.models.song(song);
-		db.models.song.findOne({ _id: song._id }, (err, existingSong) => {
-			if (err) throw err;
-			if (!existingSong) newSong.save(err => {
+	add: hooks.adminRequired((session, song, cb, userId) => {
+		queueSongs.remove(session, song._id, () => {
+			const newSong = new db.models.song(song);
+			db.models.song.findOne({ _id: song._id }, (err, existingSong) => {
 				if (err) throw err;
-				else cb({ status: 'success', message: 'Song has been moved from Queue' })
+				newSong.acceptedBy = userId;
+				newSong.acceptedAt = Date.now();
+				if (!existingSong) newSong.save(err => {
+					console.log(err, 1);
+					if (err) throw err;
+					else cb({ status: 'success', message: 'Song has been moved from Queue' })
+				});
 			});
+			//TODO Check if video is in queue and Add the song to the appropriate stations
 		});
-		//TODO Check if video is in queue and Add the song to the appropriate stations
 	}),
 
 	like: hooks.loginRequired((session, songId, cb, userId) => {

+ 102 - 55
backend/logic/actions/stations.js

@@ -68,11 +68,38 @@ module.exports = {
 			}
 
 			let arr = [];
+			let done = 0;
 			for (let prop in stations) {
-				arr.push(stations[prop]);
+				// TODO If community, check if on whitelist
+				let station = stations[prop];
+				if (station.privacy === 'public') add(true, station);
+				else if (!session.sessionId) add(false);
+				else {
+					cache.hget('sessions', session.sessionId, (err, session) => {
+						if (err || !session) {
+							add(false);
+						} else {
+							db.models.user.findOne({_id: session.userId}, (err, user) => {
+								if (err || !user) add(false);
+								else if (user.role === 'admin') add(true, station);
+								else if (station.type === 'official') add(false);
+								else if (station.owner === session.userId) add(true, station);
+								else add(false);
+							});
+						}
+					});
+				}
 			}
 
-			cb({ status: 'success', stations: arr });
+			function add(add, station) {
+				console.log("ADD!", add, station);
+				if (add) arr.push(station);
+				done++;
+				if (done === Object.keys(stations).length) {
+					console.log("DONE!", done);
+					cb({ status: 'success', stations: arr });
+				}
+			}
 		});
 	},
 
@@ -108,48 +135,64 @@ module.exports = {
 
 			if (station) {
 
-				//TODO Loop through all sockets, see if socket with same session exists, and if so leave all other station rooms and join this stationRoom
-
-				/*cache.client.hincrby('station.userCounts', stationId, 1, (err, userCount) => {
-					if (err) return cb({ status: 'error', message: 'An error occurred while joining the station' });*/
-				utils.socketJoinRoom(session.socketId, `station.${stationId}`);
-				if (station.currentSong) {
-					utils.socketJoinSongRoom(session.socketId, `song.${station.currentSong._id}`);
-					//TODO Emit to cache, listen on cache
-					songs.getSong(station.currentSong._id, (err, song) => {
-						if (!err && song) {
-							station.currentSong.likes = song.likes;
-							station.currentSong.dislikes = song.dislikes;
-						} else {
-							station.currentSong.likes = -1;
-							station.currentSong.dislikes = -1;
-						}
+				if (station.privacy !== 'private') {
+					func();
+				} else {
+					// TODO If community, check if on whitelist
+					if (!session.userId) return cb({ status: 'error', message: 'An error occurred while joining the station1' });
+					db.models.user.findOne({_id: session.userId}, (err, user) => {
+						if (err || !user) return cb({ status: 'error', message: 'An error occurred while joining the station2' });
+						if (user.role === 'admin') return func();
+						if (station.type === 'official') return cb({ status: 'error', message: 'An error occurred while joining the station3' });
+						if (station.owner === session.userId) return func();
+						return cb({ status: 'error', message: 'An error occurred while joining the station4' });
+					});
+				}
+
+				function func() {
+					utils.socketJoinRoom(session.socketId, `station.${stationId}`);
+					if (station.currentSong) {
+						utils.socketJoinSongRoom(session.socketId, `song.${station.currentSong._id}`);
+						//TODO Emit to cache, listen on cache
+						songs.getSong(station.currentSong._id, (err, song) => {
+							if (!err && song) {
+								station.currentSong.likes = song.likes;
+								station.currentSong.dislikes = song.dislikes;
+							} else {
+								station.currentSong.likes = -1;
+								station.currentSong.dislikes = -1;
+							}
+							cb({
+								status: 'success',
+								data: {
+									type: station.type,
+									currentSong: station.currentSong,
+									startedAt: station.startedAt,
+									paused: station.paused,
+									timePaused: station.timePaused,
+									description: station.description,
+									displayName: station.displayName,
+									privacy: station.privacy
+								}
+							});
+						});
+					} else {
 						cb({
 							status: 'success',
 							data: {
 								type: station.type,
-								currentSong: station.currentSong,
+								currentSong: null,
 								startedAt: station.startedAt,
 								paused: station.paused,
-								timePaused: station.timePaused
+								timePaused: station.timePaused,
+								description: station.description,
+								displayName: station.displayName,
+								privacy: station.privacy
 							}
 						});
-					});
-				} else {
-					cb({
-						status: 'success',
-						data: {
-							type: station.type,
-							currentSong: null,
-							startedAt: station.startedAt,
-							paused: station.paused,
-							timePaused: station.timePaused
-						}
-					});
+					}
 				}
-				//});
-			}
-			else {
+			} else {
 				cb({ status: 'failure', message: `That station doesn't exist` });
 			}
 		});
@@ -237,29 +280,33 @@ module.exports = {
 		});
 	},
 
-	lock: hooks.adminRequired((session, stationId, cb) => {
-		stations.getStation(stationId, (err, station) => {
-			if (err && err !== true) {
-				return cb({ status: 'error', message: 'An error occurred while locking the station' });
-			} else if (station) {
-				// Add code to update Mongo and Redis
-				cb({ status: 'success' });
-			} else {
-				cb({ status: 'failure', message: `That station doesn't exist, it may have been deleted` });
-			}
+	updateDisplayName: hooks.adminRequired((session, stationId, newDisplayName, cb) => {
+		db.models.station.update({_id: stationId}, {$set: {displayName: newDisplayName}}, (err) => {
+			if (err) return cb({ status: 'failure', message: 'Something went wrong when saving the station.' });
+			stations.updateStation(stationId, () => {
+				//TODO Pub/sub for displayName change
+				cb({ status: 'success', message: 'Successfully updated the display name.' });
+			})
 		});
 	}),
 
-	unlock: hooks.adminRequired((session, stationId, cb) => {
-		stations.getStation(stationId, (err, station) => {
-			if (err && err !== true) {
-				return cb({ status: 'error', message: 'An error occurred while unlocking the station' });
-			} else if (station) {
-				// Add code to update Mongo and Redis
-				cb({ status: 'success' });
-			} else {
-				cb({ status: 'failure', message: `That station doesn't exist, it may have been deleted` });
-			}
+	updateDescription: hooks.adminRequired((session, stationId, newDescription, cb) => {
+		db.models.station.update({_id: stationId}, {$set: {description: newDescription}}, (err) => {
+			if (err) return cb({ status: 'failure', message: 'Something went wrong when saving the station.' });
+			stations.updateStation(stationId, () => {
+				//TODO Pub/sub for description change
+				cb({ status: 'success', message: 'Successfully updated the description.' });
+			})
+		});
+	}),
+
+	updatePrivacy: hooks.adminRequired((session, stationId, newPrivacy, cb) => {
+		db.models.station.update({_id: stationId}, {$set: {privacy: newPrivacy}}, (err) => {
+			if (err) return cb({ status: 'failure', message: 'Something went wrong when saving the station.' });
+			stations.updateStation(stationId, () => {
+				//TODO Pub/sub for privacy change
+				cb({ status: 'success', message: 'Successfully updated the privacy.' });
+			})
 		});
 	}),
 

+ 3 - 3
backend/logic/db/schemas/news.js

@@ -1,10 +1,10 @@
 module.exports = {
 	title: { type: String, required: true },
 	description: { type: String, required: true },
-	fixes: [{ type: String }],
+	bugs: [{ type: String }],
 	features: [{ type: String }],
-	changes: [{ type: String }],
+	improvements: [{ type: String }],
 	upcoming: [{ type: String }],
 	createdBy: { type: String, required: true },
-	createdAt: { type: Date, default: Date.now(), required: true }
+	createdAt: { type: Number, default: Date.now(), required: true }
 };

+ 2 - 1
backend/logic/db/schemas/station.js

@@ -32,5 +32,6 @@ module.exports = {
 		likes: { type: Number, default: -1 },
 		dislikes: { type: Number, default: -1 },
 		requestedBy: { type: String, required: true }
-	}]
+	}],
+	owner: { type: String }
 };

+ 2 - 2
backend/logic/stations.js

@@ -191,8 +191,7 @@ module.exports = {
 								if (station.playlist.length > 0) {
 									function func() {
 										if (station.currentSongIndex < station.playlist.length - 1) {
-											station.currentSongIndex++;
-											songs.getSong(station.playlist[station.currentSongIndex], (err, song) => {
+											songs.getSong(station.playlist[station.currentSongIndex + 1], (err, song) => {
 												if (!err) {
 													let $set = {};
 
@@ -208,6 +207,7 @@ module.exports = {
 													};
 													$set.startedAt = Date.now();
 													$set.timePaused = 0;
+													$set.currentSongIndex = station.currentSongIndex + 1;
 													next(null, $set);
 												} else {
 													db.models.station.update({_id: station._id}, {$inc: {currentSongIndex: 1}}, (err) => {

+ 66 - 5
backend/logic/utils.js

@@ -1,10 +1,10 @@
 'use strict';
 
-const 	moment = require('moment'),
-		io = require('./io'),
-		config = require('config'),
-		request = require('request'),
-		cache = require('./cache');
+const moment  = require('moment'),
+	  io      = require('./io'),
+	  config  = require('config'),
+	  request = require('request'),
+	  cache   = require('./cache');
 
 class Timer {
 	constructor(callback, delay, paused) {
@@ -249,6 +249,67 @@ module.exports = {
 				title: body.items[0].snippet.title,
 				duration
 			};
+			cb(song);
+		});
+	},
+	getPlaylistFromYouTube: (url, cb) => {
+		
+		let name = 'list'.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
+		var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
+		let playlistId = regex.exec(url)[1];
+
+		const youtubeParams = [
+			'part=contentDetails',
+			`playlistId=${encodeURIComponent(playlistId)}`,
+			`maxResults=50`,
+			`key=${config.get('apis.youtube.key')}`
+		].join('&');
+
+		request(`https://www.googleapis.com/youtube/v3/playlistItems?${youtubeParams}`, (err, res, body) => {
+			if (err) {
+				console.error(err);
+				return next('Failed to find playlist from YouTube');
+			}
+
+			body = JSON.parse(body);
+			cb(body.items);
+		});
+	},
+	getSongFromSpotify: (song, cb) => {
+		const spotifyParams = [
+			`q=${encodeURIComponent(song.title)}`,
+			`type=track`
+		].join('&');
+
+		request(`https://api.spotify.com/v1/search?${spotifyParams}`, (err, res, body) => {
+
+			if (err) console.error(err);
+
+			body = JSON.parse(body);
+
+			durationArtistLoop:
+			for (let i in body) {
+				let items = body[i].items;
+				for (let j in items) {
+					let item = items[j];
+					let hasArtist = false;
+					for (let k = 0; k < item.artists.length; k++) {
+						let artist = item.artists[k];
+						if (song.title.indexOf(artist.name) !== -1) hasArtist = true;
+					}
+					if (hasArtist && song.title.indexOf(item.name) !== -1) {
+						song.duration = item.duration_ms / 1000;
+						song.artists = item.artists.map(artist => {
+							return artist.name;
+						});
+						song.title = item.name;
+						song.explicit = item.explicit;
+						song.thumbnail = item.album.images[1].url;
+						break durationArtistLoop;
+					}
+				}
+			}
+
 			cb(song);
 		});
 	}

+ 6 - 137
frontend/components/Admin/QueueSongs.vue

@@ -35,106 +35,16 @@
 			</table>
 		</div>
 	</div>
-	<div class='modal' :class="{ 'is-active': isEditActive }">
-		<div class='modal-background'></div>
-		<div class='modal-card'>
-			<section class='modal-card-body'>
-
-				<h5 class='has-text-centered'>Video Preview</h5>
-				<div class='video-container'>
-					<div id='player'></div>
-					<p class='control has-addons'>
-						<a class='button'>
-							<i class='material-icons' @click='video.settings("pause")' v-if='!video.paused'>pause</i>
-							<i class='material-icons' @click='video.settings("play")' v-else>play_arrow</i>
-						</a>
-						<a class='button' @click='video.settings("stop")'>
-							<i class='material-icons'>stop</i>
-						</a>
-						<a class='button' @click='video.settings("skipToLast10Secs")'>
-							<i class='material-icons'>fast_forward</i>
-						</a>
-					</p>
-				</div>
-				<p style="margin-top: 0; position: relative;">
-					<input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
-				</p>
-
-				<h5 class='has-text-centered'>Thumbnail Preview</h5>
-				<img class='thumbnail-preview' :src='editing.song.thumbnail'>
-
-				<label class='label'>Thumbnail URL</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.thumbnail'>
-				</p>
-
-				<h5 class='has-text-centered'>Edit Info</h5>
-
-				<p class='control'>
-					<label class='checkbox'>
-						<input type='checkbox' v-model='editing.song.explicit'>
-						Explicit
-					</label>
-				</p>
-				<label class='label'>Song ID</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song._id'>
-				</p>
-				<label class='label'>Song Title</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.title'>
-				</p>
-				<div class='control is-horizontal'>
-					<div class='control is-grouped'>
-						<div>
-							<p class='control has-addons'>
-								<input class='input' id='new-artist' type='text' placeholder='Artist'>
-								<a class='button is-info' @click='addTag("artists")'>Add Artist</a>
-							</p>
-							<span class='tag is-info' v-for='(index, artist) in editing.song.artists' track-by='$index'>
-								{{ artist }}
-								<button class='delete is-info' @click='removeTag("artists", index)'></button>
-							</span>
-						</div>
-						<div>
-							<p class='control has-addons'>
-								<input class='input' id='new-genre' type='text' placeholder='Genre'>
-								<a class='button is-info' @click='addTag("genres")'>Add Genre</a>
-							</p>
-							<span class='tag is-info' v-for='(index, genre) in editing.song.genres' track-by='$index'>
-								{{ genre }}
-								<button class='delete is-info' @click='removeTag("genres", index)'></button>
-							</span>
-						</div>
-					</div>
-				</div>
-				<label class='label'>Song Duration</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.duration'>
-				</p>
-				<label class='label'>Skip Duration</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.skipDuration'>
-				</p>
-
-			</section>
-			<footer class='modal-card-foot'>
-				<a class='button is-success' @click='save(editing.song)'>
-					<i class='material-icons save-changes'>done</i>
-					<span>&nbsp;Save</span>
-				</a>
-				<a class='button is-danger' @click='cancel()'>
-					<span>&nbspCancel</span>
-				</a>
-			</footer>
-		</div>
-	</div>
+	<edit-song v-show='isEditActive'></edit-song>
 </template>
 
 <script>
 	import { Toast } from 'vue-roaster';
 
+	import EditSong from '../Modals/EditSong.vue';
+
 	export default {
+		components: { EditSong },
 		data() {
 			return {
 				songs: [],
@@ -174,7 +84,7 @@
 				let volume = $("#volumeSlider").val();
 				localStorage.setItem("volume", volume);
 				local.video.player.setVolume(volume);
-				if (volume > 0) local.player.unMute();
+				if (volume > 0) local.video.player.unMute();
 			},
 			toggleModal: function () {
 				this.isEditActive = !this.isEditActive;
@@ -211,22 +121,10 @@
 					_this.toggleModal();
 				});
 			},
-			cancel: function () {
-				let _this = this;
-				_this.toggleModal();
-			},
 			add: function (song) {
-				this.socket.emit('queueSongs.remove', song._id, res => {
+				this.socket.emit('songs.add', song, res => {
 					if (res.status == 'success') Toast.methods.addToast(res.message, 2000);
 				});
-				this.socket.emit('users.findBySession', res => {
-					if (res.status == 'success') {
-						song.acceptedBy = res.data.username;
-						this.socket.emit('songs.add', song, res => {
-							if (res.status == 'success') Toast.methods.addToast(res.message, 2000);
-						});
-					}
-				});
 			},
 			remove: function (id, index) {
 				this.songs.splice(index, 1);
@@ -276,33 +174,6 @@
 </script>
 
 <style lang='scss' scoped>
-	body { font-family: 'Roboto', sans-serif; }
-
-	.thumbnail-preview {
-		display: flex;
-		margin: 0 auto;
-		padding: 10px 0 20px 0;
-	}
-
-	.modal-card-body, .modal-card-foot { border-top: 0; }
-
-	.label, .checkbox, h5 {
-		font-weight: normal;
-	}
-
-	.video-container {
-		display: flex;
-		flex-direction: column;
-		align-items: center;
-		padding: 10px;
-
-		iframe {
-			pointer-events: none;
-		}
-	}
-
-	.save-changes { color: #fff; }
-
 	.song-thumbnail {
 		display: block;
 		max-width: 50px;
@@ -310,6 +181,4 @@
 	}
 
 	td { vertical-align: middle; }
-
-	.tag:not(:last-child) { margin-right: 5px; }	
 </style>

+ 5 - 132
frontend/components/Admin/Songs.vue

@@ -27,7 +27,6 @@
 						<td>{{ song.requestedBy }}</td>
 						<td>
 							<a class='button is-primary' @click='edit(song, index)'>Edit</a>
-							<a class='button is-success' @click='add(song)'>Add</a>
 							<a class='button is-danger' @click='remove(song._id, index)'>Remove</a>
 						</td>
 					</tr>
@@ -35,106 +34,16 @@
 			</table>
 		</div>
 	</div>
-	<div class='modal' :class="{ 'is-active': isEditActive }">
-		<div class='modal-background'></div>
-		<div class='modal-card'>
-			<section class='modal-card-body'>
-
-				<h5 class='has-text-centered'>Video Preview</h5>
-				<div class='video-container'>
-					<div id='player'></div>
-					<p class='control has-addons'>
-						<a class='button'>
-							<i class='material-icons' @click='video.settings("pause")' v-if='!video.paused'>pause</i>
-							<i class='material-icons' @click='video.settings("play")' v-else>play_arrow</i>
-						</a>
-						<a class='button' @click='video.settings("stop")'>
-							<i class='material-icons'>stop</i>
-						</a>
-						<a class='button' @click='video.settings("skipToLast10Secs")'>
-							<i class='material-icons'>fast_forward</i>
-						</a>
-					</p>
-				</div>
-				<p style="margin-top: 0; position: relative;">
-					<input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
-				</p>
-
-				<h5 class='has-text-centered'>Thumbnail Preview</h5>
-				<img class='thumbnail-preview' :src='editing.song.thumbnail' onerror="this.src='/assets/notes.png'">
-
-				<label class='label'>Thumbnail URL</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.thumbnail'>
-				</p>
-
-				<h5 class='has-text-centered'>Edit Info</h5>
-
-				<p class='control'>
-					<label class='checkbox'>
-						<input type='checkbox' v-model='editing.song.explicit'>
-						Explicit
-					</label>
-				</p>
-				<label class='label'>Song ID</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song._id'>
-				</p>
-				<label class='label'>Song Title</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.title'>
-				</p>
-				<div class='control is-horizontal'>
-					<div class='control is-grouped'>
-						<div>
-							<p class='control has-addons'>
-								<input class='input' id='new-artist' type='text' placeholder='Artist'>
-								<a class='button is-info' @click='addTag("artists")'>Add Artist</a>
-							</p>
-							<span class='tag is-info' v-for='(index, artist) in editing.song.artists' track-by='$index'>
-								{{ artist }}
-								<button class='delete is-info' @click='removeTag("artists", index)'></button>
-							</span>
-						</div>
-						<div>
-							<p class='control has-addons'>
-								<input class='input' id='new-genre' type='text' placeholder='Genre'>
-								<a class='button is-info' @click='addTag("genres")'>Add Genre</a>
-							</p>
-							<span class='tag is-info' v-for='(index, genre) in editing.song.genres' track-by='$index'>
-								{{ genre }}
-								<button class='delete is-info' @click='removeTag("genres", index)'></button>
-							</span>
-						</div>
-					</div>
-				</div>
-				<label class='label'>Song Duration</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.duration'>
-				</p>
-				<label class='label'>Skip Duration</label>
-				<p class='control'>
-					<input class='input' type='text' v-model='editing.song.skipDuration'>
-				</p>
-
-			</section>
-			<footer class='modal-card-foot'>
-				<a class='button is-success' @click='save(editing.song)'>
-					<i class='material-icons save-changes'>done</i>
-					<span>&nbsp;Save</span>
-				</a>
-				<a class='button is-danger' @click='cancel()'>
-					<span>&nbspCancel</span>
-				</a>
-			</footer>
-		</div>
-	</div>
+	<edit-song v-show='isEditActive'></edit-song>
 </template>
 
 <script>
 	import { Toast } from 'vue-roaster';
 
+	import EditSong from '../Modals/EditSong.vue';
+
 	export default {
+		components: { EditSong },
 		data() {
 			return {
 				songs: [],
@@ -174,7 +83,7 @@
 				let volume = $("#volumeSlider").val();
 				localStorage.setItem("volume", volume);
 				local.video.player.setVolume(volume);
-				if (volume > 0) local.player.unMute();
+				if (volume > 0) local.video.player.unMute();
 			},
 			toggleModal: function () {
 				this.isEditActive = !this.isEditActive;
@@ -211,15 +120,6 @@
 					_this.toggleModal();
 				});
 			},
-			cancel: function () {
-				let _this = this;
-				_this.toggleModal();
-			},
-			add: function (song) {
-				this.socket.emit('songs.add', song, res => {
-					if (res.status == 'success') Toast.methods.addToast(res.message, 2000);
-				});
-			},
 			remove: function (id, index) {
 				this.songs.splice(index, 1);
 				this.socket.emit('songs.remove', id, res => {
@@ -270,31 +170,6 @@
 <style lang='scss' scoped>
 	body { font-family: 'Roboto', sans-serif; }
 
-	.thumbnail-preview {
-		display: flex;
-		margin: 0 auto;
-		padding: 10px 0 20px 0;
-	}
-
-	.modal-card-body, .modal-card-foot { border-top: 0; }
-
-	.label, .checkbox, h5 {
-		font-weight: normal;
-	}
-
-	.video-container {
-		display: flex;
-		flex-direction: column;
-		align-items: center;
-		padding: 10px;
-
-		iframe {
-			pointer-events: none;
-		}
-	}
-
-	.save-changes { color: #fff; }
-
 	.song-thumbnail {
 		display: block;
 		max-width: 50px;
@@ -302,6 +177,4 @@
 	}
 
 	td { vertical-align: middle; }
-
-	.tag:not(:last-child) { margin-right: 5px; }	
 </style>

+ 226 - 0
frontend/components/Modals/EditSong.vue

@@ -0,0 +1,226 @@
+<template>
+	<div class='modal is-active'>
+		<div class='modal-background'></div>
+		<div class='modal-card'>
+			<section class='modal-card-body'>
+
+				<h5 class='has-text-centered'>Video Preview</h5>
+				<div class='video-container'>
+					<div id='player'></div>
+					<div class="controls">
+						<form action="#" class="column is-7-desktop is-4-mobile">
+							<p style="margin-top: 0; position: relative;">
+								<input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="$parent.changeVolume()" v-on:input="$parent.changeVolume()">
+							</p>
+						</form>
+						<p class='control has-addons'>
+							<a class='button'>
+								<i class='material-icons' @click='$parent.video.settings("pause")' v-if='!$parent.video.paused'>pause</i>
+								<i class='material-icons' @click='$parent.video.settings("play")' v-else>play_arrow</i>
+							</a>
+							<a class='button' @click='$parent.video.settings("stop")'>
+								<i class='material-icons'>stop</i>
+							</a>
+							<a class='button' @click='$parent.video.settings("skipToLast10Secs")'>
+								<i class='material-icons'>fast_forward</i>
+							</a>
+						</p>
+					</div>
+				</div>
+				<h5 class='has-text-centered'>Thumbnail Preview</h5>
+				<img class='thumbnail-preview' :src='$parent.editing.song.thumbnail' onerror="this.src='/assets/notes.png'">
+
+				<label class='label'>Thumbnail URL</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='$parent.editing.song.thumbnail'>
+				</p>
+
+				<h5 class='has-text-centered'>Edit Info</h5>
+
+				<p class='control'>
+					<label class='checkbox'>
+						<input type='checkbox' v-model='$parent.editing.song.explicit'>
+						Explicit
+					</label>
+				</p>
+				<label class='label'>Song ID</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='$parent.editing.song._id'>
+				</p>
+				<label class='label'>Song Title</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='$parent.editing.song.title'>
+				</p>
+				<div class='control is-horizontal'>
+					<div class='control is-grouped'>
+						<div>
+							<p class='control has-addons'>
+								<input class='input' id='new-artist' type='text' placeholder='Artist'>
+								<a class='button is-info' @click='$parent.addTag("artists")'>Add Artist</a>
+							</p>
+							<span class='tag is-info' v-for='(index, artist) in $parent.editing.song.artists' track-by='$index'>
+								{{ artist }}
+								<button class='delete is-info' @click='$parent.removeTag("artists", index)'></button>
+							</span>
+						</div>
+						<div>
+							<p class='control has-addons'>
+								<input class='input' id='new-genre' type='text' placeholder='Genre'>
+								<a class='button is-info' @click='$parent.addTag("genres")'>Add Genre</a>
+							</p>
+							<span class='tag is-info' v-for='(index, genre) in $parent.editing.song.genres' track-by='$index'>
+								{{ genre }}
+								<button class='delete is-info' @click='$parent.removeTag("genres", index)'></button>
+							</span>
+						</div>
+					</div>
+				</div>
+				<label class='label'>Song Duration</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='$parent.editing.song.duration'>
+				</p>
+				<label class='label'>Skip Duration</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='$parent.editing.song.skipDuration'>
+				</p>
+
+			</section>
+			<footer class='modal-card-foot'>
+				<a class='button is-success' @click='$parent.save(editing.song)'>
+					<i class='material-icons save-changes'>done</i>
+					<span>&nbsp;Save</span>
+				</a>
+				<a class='button is-danger' @click='$parent.toggleModal()'>
+					<span>&nbspCancel</span>
+				</a>
+			</footer>
+		</div>
+	</div>
+</template>
+
+<style type='scss' scoped>
+	input[type=range] {
+		-webkit-appearance: none;
+		width: 100%;
+		margin: 7.3px 0;
+	}
+
+	input[type=range]:focus {
+		outline: none;
+	}
+
+	input[type=range]::-webkit-slider-runnable-track {
+		width: 100%;
+		height: 5.2px;
+		cursor: pointer;
+		box-shadow: 0;
+		background: #c2c0c2;
+		border-radius: 0;
+		border: 0;
+	}
+
+	input[type=range]::-webkit-slider-thumb {
+		box-shadow: 0;
+		border: 0;
+		height: 19px;
+		width: 19px;
+		border-radius: 15px;
+		background: #03a9f4;
+		cursor: pointer;
+		-webkit-appearance: none;
+		margin-top: -6.5px;
+	}
+
+	input[type=range]::-moz-range-track {
+		width: 100%;
+		height: 5.2px;
+		cursor: pointer;
+		box-shadow: 0;
+		background: #c2c0c2;
+		border-radius: 0;
+		border: 0;
+	}
+
+	input[type=range]::-moz-range-thumb {
+		box-shadow: 0;
+		border: 0;
+		height: 19px;
+		width: 19px;
+		border-radius: 15px;
+		background: #03a9f4;
+		cursor: pointer;
+		-webkit-appearance: none;
+		margin-top: -6.5px;
+	}
+
+	input[type=range]::-ms-track {
+		width: 100%;
+		height: 5.2px;
+		cursor: pointer;
+		box-shadow: 0;
+		background: #c2c0c2;
+		border-radius: 1.3px;
+	}
+
+	input[type=range]::-ms-fill-lower {
+		background: #c2c0c2;
+		border: 0;
+		border-radius: 0;
+		box-shadow: 0;
+	}
+
+	input[type=range]::-ms-fill-upper {
+		background: #c2c0c2;
+		border: 0;
+		border-radius: 0;
+		box-shadow: 0;
+	}
+
+	input[type=range]::-ms-thumb {
+		box-shadow: 0;
+		border: 0;
+		height: 15px;
+		width: 15px;
+		border-radius: 15px;
+		background: #03a9f4;
+		cursor: pointer;
+		-webkit-appearance: none;
+		margin-top: 1.5px;
+	}
+
+	.controls {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+	}
+
+	#volumeSlider { margin-bottom: 15px; }
+
+	.has-text-centered { padding: 10px; }
+
+	.thumbnail-preview {
+		display: flex;
+		margin: 0 auto;
+		max-width: 200px;
+		width: 100%;
+	}
+
+	.modal-card-body, .modal-card-foot { border-top: 0; }
+
+	.label, .checkbox, h5 {
+		font-weight: normal;
+	}
+
+	.video-container {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		padding: 10px;
+
+		iframe { pointer-events: none; }
+	}
+
+	.save-changes { color: #fff; }
+
+	.tag:not(:last-child) { margin-right: 5px; }
+</style>

+ 97 - 0
frontend/components/Modals/EditStation.vue

@@ -0,0 +1,97 @@
+<template>
+	<div class='modal is-active'>
+		<div class='modal-background'></div>
+		<div class='modal-card'>
+			<header class='modal-card-head'>
+				<p class='modal-card-title'>Edit station</p>
+				<button class='delete' @click='$parent.toggleModal("editStation")'></button>
+			</header>
+			<section class='modal-card-body'>
+				<label class='label'>Display name</label>
+				<div class='control is-grouped'>
+					<p class='control is-expanded'>
+						<input class='input' type='text' placeholder='Station Display Name' v-model='$parent.station.displayName'>
+					</p>
+					<p class='control'>
+						<a class='button is-info' @click='updateDisplayName()'>Update</a>
+					</p>
+				</div>
+				<label class='label'>Description</label>
+				<div class='control is-grouped'>
+					<p class='control is-expanded'>
+						<input class='input' type='text' placeholder='Station Display Name' v-model='$parent.station.description'>
+					</p>
+					<p class='control'>
+						<a class='button is-info' @click='updateDescription()'>Update</a>
+					</p>
+				</div>
+				<label class='label'>Privacy</label>
+				<div class='control is-grouped'>
+					<p class='control is-expanded'>
+						<span class='select'>
+							<select v-model='$parent.station.privacy'>
+								<option v-bind:value=''public''>Public</option>
+								<option v-bind:value=''unlisted''>Unlisted</option>
+								<option v-bind:value=''private''>Private</option>
+							</select>
+						</span>
+					</p>
+					<p class='control'>
+						<a class='button is-info' @click='updatePrivacy()'>Update</a>
+					</p>
+				</div>
+			</section>
+		</div>
+	</div>
+</template>
+
+<script>
+	import { Toast } from 'vue-roaster';
+
+	export default {
+		methods: {
+			updateDisplayName: function () {
+				this.socket.emit('stations.updateDisplayName', this.$parent.stationId, this.$parent.station.displayName, res => {
+					if (res.status == 'success') return Toast.methods.addToast(res.message, 4000);
+					Toast.methods.addToast(res.message, 8000);
+				});
+			},
+			updateDescription: function () {
+				this.socket.emit('stations.updateDescription', this.$parent.stationId, this.$parent.station.description, res => {
+					if (res.status == 'success') return Toast.methods.addToast(res.message, 4000);
+					Toast.methods.addToast(res.message, 8000);
+				});
+			},
+			updatePrivacy: function () {
+				this.socket.emit('stations.updatePrivacy', this.$parent.stationId, this.$parent.station.privacy, res => {
+					if (res.status == 'success') return Toast.methods.addToast(res.message, 4000);
+					Toast.methods.addToast(res.message, 8000);
+				});
+			}
+		},
+		ready: function () {
+			let _this = this;
+			let socketInterval = setInterval(() => {
+				if (!!_this.$parent.$parent.socket) {
+					_this.socket = _this.$parent.$parent.socket;
+					clearInterval(socketInterval);
+				}
+			}, 100);
+		}
+	}
+</script>
+
+<style type='scss' scoped>
+	.controls {
+		display: flex;
+
+		a {
+			display: flex;
+    		align-items: center;
+		}
+	}
+
+	.table { margin-bottom: 0; }
+
+	h5 { padding: 20px 0; }
+</style>

+ 77 - 15
frontend/components/Modals/Playlists/Edit.vue

@@ -7,19 +7,25 @@
 				<button class='delete' @click='$parent.toggleModal("editPlaylist")'></button>
 			</header>
 			<section class='modal-card-body'>
-				<aside class='menu'>
+				<aside class='menu' v-if='playlist.songs.length > 0'>
 					<ul class='menu-list'>
-						<li v-for='song in playlist.songs'>
-							<a :href='' target='_blank'>{{ song.title }}</a>
+						<li v-for='song in playlist.songs' track-by='$index'>
+							<a :href='' target='_blank'>{{ song.title }} - {{ song.artists.join(', ') }}</a>
 							<div class='controls'>
-								<a href='#' @click=''><i class='material-icons'>keyboard_arrow_down</i></a>
-								<a href='#' @click=''><i class='material-icons'>keyboard_arrow_up</i></a>
+								<a href='#'>
+									<i class='material-icons' v-if='playlist.songs[0] !== song' @click='promoteSong($index)'>keyboard_arrow_up</i>
+									<i class='material-icons' v-else>error</i>
+								</a>
+								<a href='#' @click=''>
+									<i class='material-icons' v-if='playlist.songs.length - 1 !== $index' @click='demoteSong($index)'>keyboard_arrow_down</i>
+									<i class='material-icons' v-else>error</i>
+								</a>
 								<a href='#' @click='removeSongFromPlaylist(song._id)'><i class='material-icons'>delete</i></a>
 							</div>
 						</li>
 					</ul>
+					<br />
 				</aside>
-				<br />
 				<div class='control is-grouped'>
 					<p class='control is-expanded'>
 						<input class='input' type='text' placeholder='Search for Song to add' v-model='songQuery'>
@@ -45,10 +51,10 @@
 				</table>
 				<div class='control is-grouped'>
 					<p class='control is-expanded'>
-						<input class='input' type='text' placeholder='YouTube Playlist URL'>
+						<input class='input' type='text' placeholder='YouTube Playlist URL' v-model='importQuery'>
 					</p>
 					<p class='control'>
-						<a class='button is-info' @click='submitQuery()'>Import</a>
+						<a class='button is-info' @click='importPlaylist()'>Import</a>
 					</p>
 				</div>
 				<h5>Edit playlist details:</h5>
@@ -60,9 +66,17 @@
 						<a class='button is-info' @click='renamePlaylist()'>Rename</a>
 					</p>
 				</div>
+				<div class='control is-grouped'>
+					<p class='control is-expanded'>
+						<input class='input' type='text' placeholder='Playlist ID' v-model='playlist._id'>
+					</p>
+					<p class='control'>
+						<a class='button is-info' @click='renamePlaylistId()'>Rename</a>
+					</p>
+				</div>
 			</section>
 			<footer class='modal-card-foot'>
-				<a class='button is-danger' @click=''>Delete Playlist</a>
+				<a class='button is-danger' @click='removePlaylist()'>Remove Playlist</a>
 			</footer>
 		</div>
 	</div>
@@ -75,14 +89,13 @@
 		data() {
 			return {
 				playlist: {},
-				songQuery: '',
 				songQueryResults: []
 			}
 		},
 		methods: {
 			searchForSongs: function () {
 				let _this = this;
-				_this.socket.emit('apis.searchYoutube', _this.querySearch, res => {
+				_this.socket.emit('apis.searchYoutube', _this.songQuery, res => {
 					if (res.status == 'success') {
 						_this.songQueryResults = [];
 						for (let i = 0; i < res.data.items.length; i++) {
@@ -96,12 +109,61 @@
 					} else if (res.status == 'error') Toast.methods.addToast(res.message, 3000);
 				});
 			},
-			addSongToPlaylist: function (id) {},
-			removeSongFromPlaylist: function (id) {},
+			addSongToPlaylist: function (id) {
+				let _this = this;
+				_this.socket.emit('playlists.addSongToPlaylist', id, _this.playlist._id, res => {
+					if (res.status == 'success') {
+						Toast.methods.addToast(res.message, 3000);
+						_this.playlist.songs = res.data;
+					}
+				});
+			},
+			importPlaylist: function () {
+				let _this = this;
+				this.socket.emit('playlists.addSetToPlaylist', _this.importQuery, _this.playlist._id, res => {
+					if (res.status == 'success') _this.playlist.songs = res.data;
+				});
+			},
+			removeSongFromPlaylist: function (id) {
+				let _this = this;
+				this.socket.emit('playlists.removeSongFromPlaylist', id, _this.playlist._id, res => {
+					if (res.status == 'success') {
+						Toast.methods.addToast(res.message, 3000);
+						_this.playlist.songs = res.data;
+					}
+				});
+			},
 			renamePlaylist: function () {
-				_this.socket.emit('playlists.updateDisplayName', _this.playlist._id, _this.playlist.displayName, res => {
+				this.socket.emit('playlists.updateDisplayName', this.playlist._id, this.playlist.displayName, res => {
 					if (res.status == 'success') Toast.methods.addToast(res.message, 3000);
 				});
+			},
+			renamePlaylistId: function () {
+				let _this = this;
+				_this.socket.emit('playlists.updatePlaylistId', _this.playlist.oldId, _this.playlist._id, res => {
+					if (res.status == 'success') _this.playlist = res.data;
+				});
+			},
+			removePlaylist: function () {
+				let _this = this;
+				_this.socket.emit('playlists.remove', _this.playlist._id, res => {
+					if (res.status == 'success') {
+						Toast.methods.addToast(res.message, 3000);
+						_this.$parent.toggleModal('editPlaylist');
+					}
+				});
+			},
+			promoteSong: function (fromIndex) {
+				let _this = this;
+				_this.socket.emit('playlists.promoteSong', _this.playlist._id, fromIndex, res => {
+					if (res.status == 'success') _this.$set('playlist.songs', res.data); // bug: v-for is not updating
+				});
+			},
+			demoteSong: function (fromIndex) {
+				let _this = this;
+				_this.socket.emit('playlists.demoteSong', _this.playlist._id, fromIndex, res => {
+					if (res.status == 'success') _this.$set('playlist.songs', res.data); // bug: v-for is not updating
+				});
 			}
 		},
 		ready: function () {
@@ -110,7 +172,7 @@
 				if (!!_this.$parent.$parent.socket) {
 					_this.socket = _this.$parent.$parent.socket;
 					_this.socket.emit('playlists.getPlaylist', _this.$parent.playlistBeingEdited, res => {
-						if (res.status == 'success') _this.playlist = res.data;
+						if (res.status == 'success') _this.playlist = res.data; _this.playlist.oldId = res.data._id;
 					});
 					clearInterval(socketInterval);
 				}

+ 63 - 45
frontend/components/Modals/WhatIsNew.vue

@@ -1,26 +1,37 @@
 <template>
-	<div class="modal" :class="{ 'is-active': isModalActive }">
-		<div class="modal-background"></div>
-		<div class="modal-card">
-			<header class="modal-card-head">
-				<p class="modal-card-title"><strong>What's new</strong> (Nov 23, 2016)</p>
-				<button class="delete" @click="toggleModal()"></button>
+	<div class='modal' :class='{ "is-active": isModalActive }'>
+		<div class='modal-background'></div>
+		<div class='modal-card'>
+			<header class='modal-card-head'>
+				<p class='modal-card-title'><strong>{{ news.title }}</strong> ({{ formatDate(news.createdAt) }})</p>
+				<button class='delete' @click='toggleModal()'></button>
 			</header>
-			<section class="modal-card-body">
-				<div class="sect">
-					<div class="sect-head-success">Improvements</div>
-					<ul class="sect-body">
-						<li>Lorem ipsum dolor sit amet, consectetur adipisicing.</li>
-						<li>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium cum molestias minima saepe, iure aperiam quo necessitatibus quod?</li>
-						<li>Lorem ipsum dolor sit amet, consectetur.</li>
+			<section class='modal-card-body'>
+				<div class='content'>
+					<p>{{ news.description }}</p>
+				</div>
+				<div class='sect' v-show='news.features.length > 0'>
+					<div class='sect-head-features'>The features are so great</div>
+					<ul class='sect-body'>
+						<li v-for='li in news.features'>{{ li }}</li>
+					</ul>
+				</div>
+				<div class='sect' v-show='news.improvements.length > 0'>
+					<div class='sect-head-improvements'>Improvements</div>
+					<ul class='sect-body'>
+						<li v-for='li in news.improvements'>{{ li }}</li>
+					</ul>
+				</div>
+				<div class='sect' v-show='news.bugs.length > 0'>
+					<div class='sect-head-bugs'>Bugs Smashed</div>
+					<ul class='sect-body'>
+						<li v-for='li in news.bugs'>{{ li }}</li>
 					</ul>
 				</div>
-				<div class="sect">
-					<div class="sect-head-bugs">Bugs Smashed</div>
-					<ul class="sect-body">
-						<li>Lorem ipsum dolor sit amet, consectetur adipisicing.</li>
-						<li>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium cum molestias minima saepe, iure aperiam quo necessitatibus quod?</li>
-						<li>Lorem ipsum dolor sit amet, consectetur.</li>
+				<div class='sect' v-show='news.upcoming.length > 0'>
+					<div class='sect-head-upcoming'>Coming Soon to a Musare near you</div>
+					<ul class='sect-body'>
+						<li v-for='li in news.upcoming'>{{ li }}</li>
 					</ul>
 				</div>
 			</section>
@@ -32,68 +43,75 @@
 	export default {
 		data() {
 			return {
-				isModalActive: false
+				isModalActive: false,
+				news: {}
 			}
 		},
 		ready: function () {
-			// TODO: Setup so we can call this modal from anywhere and we can specify the values of everything when calling it. This should also get the improvements, bugs, date etc. to include in the modal.
-			// In future we will receive a date, if that date is newer than the one stored in localStorage, we will show modal, and then save that date to localStorage (how we keep track of which modal has been showed)
-			const data = {
-				date: 1479935887670
-			};
-
-			if (localStorage.getItem("whatIsNew")) {
-				if (localStorage.getItem("whatIsNew") < data.date) this.isModalActive = true;
-			} else {
-				localStorage.setItem("whatIsNew", data.date);
-				this.isModalActive = true;
-			}
+			let _this = this;
+			let socketInterval = setInterval(() => {
+				if (!!_this.$parent.socket) {
+					_this.socket = _this.$parent.socket;
+					_this.socket.emit('news.newest', res => {
+						_this.news = res.data;
+						if (localStorage.getItem('whatIsNew')) {
+							if (parseInt(localStorage.getItem('whatIsNew')) < res.data.createdAt) {
+								this.toggleModal();
+								localStorage.setItem('whatIsNew', res.data.createdAt);
+							}
+						} else {
+							this.toggleModal();
+							localStorage.setItem('whatIsNew', res.data.createdAt);
+						}
+					});
+					clearInterval(socketInterval);
+				}
+			}, 100);
 		},
 		methods: {
 			toggleModal: function () {
 				this.isModalActive = !this.isModalActive;
+			},
+			formatDate: unix => {
+				return moment(unix).format('DD-MM-YYYY');
 			}
 		}
 	}
 </script>
 
-<style lang="scss" scoped>
+<style lang='scss' scoped>
 	.modal-card-head {
 		border-bottom: none;
 		background-color: ghostwhite;
 		padding: 15px;
 	}
 
-	.modal-card-title {
-		font-size: 14px;
-	}
+	.modal-card-title { font-size: 14px; }
 
 	.delete {
 		background: transparent;
 		&:hover { background: transparent; }
 
-		&:before, &:after {
-			background-color: #bbb;
-		}
+		&:before, &:after { background-color: #bbb; }
 	}
 
 	.sect {
 		div[class^='sect-head'], div[class*=' sect-head']{
-			padding: 15px;
+			padding: 12px;
 			text-transform: uppercase;
 			font-weight: bold;
 			color: #fff;
 		}
 
-		.sect-head-success { background-color: seagreen; }
+		.sect-head-features { background-color: dodgerblue; }
+		.sect-head-improvements { background-color: seagreen; }
 		.sect-head-bugs { background-color: brown; }
+		.sect-head-upcoming { background-color: mediumpurple; }
 
 		.sect-body {
-			padding: 25px;
+			padding: 15px 25px;
 
-			li {
-				list-style-type: disc;
-			}
+			li { list-style-type: disc; }
 		}
 	}
 </style>

+ 11 - 7
frontend/components/Sidebars/Playlist.vue

@@ -7,9 +7,15 @@
 				<ul class='menu-list'>
 					<li v-for='playlist in playlists'>
 						<a href='#'>{{ playlist.displayName }}</a>
-						<a href='#' @click='editPlaylist(playlist._id)'>
-							<i class='material-icons'>edit</i>
-						</a>
+						<!--Will play playlist in community station Kris-->
+						<div class='icons-group'>
+							<a href='#' @click=''>
+								<i class='material-icons'>play_arrow</i>
+							</a>
+							<a href='#' @click='editPlaylist(playlist._id)'>
+								<i class='material-icons'>edit</i>
+							</a>
+						</div>
 					</li>
 				</ul>
 			</aside>
@@ -34,6 +40,7 @@
 			}
 		},
 		ready: function () {
+			// TODO: Update when playlist is removed/created
 			let _this = this;
 			let socketInterval = setInterval(() => {
 				if (!!_this.$parent.$parent.socket) {
@@ -100,10 +107,7 @@
 		justify-content: space-between;
 	}
 
-	li a {
-		display: flex;
-    	align-items: center;
-	}
+	.icons-group { display: flex; }
 
 	.none-found { text-align: center; }
 </style>

+ 7 - 2
frontend/components/Station/CommunityHeader.vue

@@ -4,6 +4,11 @@
 			<a class="nav-item logo" href="#" v-link="{ path: '/' }" @click="this.$dispatch('leaveStation', title)">
 				Musare
 			</a>
+			<a class="nav-item" href="#" v-if="$parent.$parent.role === 'admin'" @click="$parent.toggleModal('editStation')">
+				<span class="icon">
+					<i class="material-icons">settings</i>
+				</span>
+			</a>
 			<a v-if="$parent.$parent.role === 'admin'" class="nav-item" href="#" @click="$parent.skipStation()">
 				<span class="icon">
 					<i class="material-icons">skip_next</i>
@@ -57,11 +62,11 @@
 					<i class="material-icons">chat</i>
 				</span>
 			</a>-->
-			<a class="nav-item" href="#" @click='$parent.sidebars.users = !$parent.sidebars.users'>
+			<!--<a class="nav-item" href="#" @click='$parent.sidebars.users = !$parent.sidebars.users'>
 				<span class="icon">
 					<i class="material-icons">people</i>
 				</span>
-			</a>
+			</a>-->
 			<a class="nav-item" href="#" @click='$parent.sidebars.playlist = !$parent.sidebars.playlist'>
 				<span class="icon">
 					<i class="material-icons">library_music</i>

+ 4 - 4
frontend/components/Station/OfficialHeader.vue

@@ -9,9 +9,9 @@
 					<i class="material-icons">playlist_add</i>
 				</span>
 			</a>
-			<a class="nav-item" href="#">
+			<a class="nav-item" href="#" v-if="$parent.$parent.role === 'admin'" @click="$parent.toggleModal('editStation')">
 				<span class="icon">
-					<i class="material-icons">flag</i>
+					<i class="material-icons">settings</i>
 				</span>
 			</a>
 			<a v-if="$parent.$parent.role === 'admin'" class="nav-item" href="#" @click="$parent.skipStation()">
@@ -67,11 +67,11 @@
 					<i class="material-icons">chat</i>
 				</span>
 			</a>-->
-			<a class="nav-item" href="#" @click='$parent.sidebars.users = !$parent.sidebars.users'>
+			<!--<a class="nav-item" href="#" @click='$parent.sidebars.users = !$parent.sidebars.users'>
 				<span class="icon">
 					<i class="material-icons">people</i>
 				</span>
-			</a>
+			</a>-->
 		</div>
 	</nav>
 </template>

+ 20 - 9
frontend/components/Station/Station.vue

@@ -5,6 +5,7 @@
 	<song-queue v-if='modals.addSongToQueue'></song-queue>
 	<edit-playlist v-if='modals.editPlaylist'></edit-playlist>
 	<create-playlist v-if='modals.createPlaylist'></create-playlist>
+	<edit-station v-if='modals.editStation'></edit-station>
 
 	<queue-sidebar v-if='sidebars.queue'></queue-sidebar>
 	<playlist-sidebar v-if='sidebars.playlist'></playlist-sidebar>
@@ -64,6 +65,7 @@
 	import SongQueue from '../Modals/AddSongToQueue.vue';
 	import EditPlaylist from '../Modals/Playlists/Edit.vue';
 	import CreatePlaylist from '../Modals/Playlists/Create.vue';
+	import EditStation from '../Modals/EditStation.vue';
 
 	import QueueSidebar from '../Sidebars/Queue.vue';
 	import PlaylistSidebar from '../Sidebars/Playlist.vue';
@@ -87,7 +89,8 @@
 				modals: {
 					addSongToQueue: false,
 					editPlaylist: false,
-					createPlaylist: false
+					createPlaylist: false,
+					editStation: false
 				},
 				sidebars: {
 					queue: false,
@@ -97,7 +100,8 @@
 				noSong: false,
 				simpleSong: false,
 				queue: [],
-				timeBeforePause: 0
+				timeBeforePause: 0,
+				station: {}
 			}
 		},
 		methods: {
@@ -109,6 +113,7 @@
 				if (type == 'addSongToQueue') this.modals.addSongToQueue = !this.modals.addSongToQueue;
 				else if (type == 'editPlaylist') this.modals.editPlaylist = !this.modals.editPlaylist;
 				else if (type == 'createPlaylist') this.modals.createPlaylist = !this.modals.createPlaylist;
+				else if (type == 'editStation') this.modals.editStation = !this.modals.editStation;
 			},
 			youtubeReady: function() {
 				let local = this;
@@ -118,6 +123,7 @@
 						height: 270,
 						width: 480,
 						videoId: local.currentSong._id,
+						startSeconds: local.getTimeElapsed() / 1000 + local.currentSong.skipDuration,
 						playerVars: {controls: 0, iv_load_policy: 3, rel: 0, showinfo: 0},
 						events: {
 							'onReady': function (event) {
@@ -133,14 +139,14 @@
 							'onStateChange': function (event) {
 								if (event.data === 1 && local.videoLoading === true) {
 									local.videoLoading = false;
-									local.player.seekTo(local.getTimeElapsed() / 1000, true);
+									local.player.seekTo(local.getTimeElapsed() / 1000 + local.currentSong.skipDuration, true);
 									if (local.paused) local.player.pauseVideo();
 								} else if (event.data === 1 && local.paused) {
 									local.player.seekTo(local.timeBeforePause / 1000, true);
 									local.player.pauseVideo();
 								}
 								if (event.data === 2 && !local.paused && !local.noSong) {
-									local.player.seekTo(local.getTimeElapsed() / 1000, true);
+									local.player.seekTo(local.getTimeElapsed() / 1000 + local.currentSong.skipDuration, true);
 									local.player.playVideo();
 								}
 							}
@@ -162,7 +168,7 @@
 				if (local.playerReady) {
 					console.log("@@@1");
 					local.videoLoading = true;
-					local.player.loadVideoById(local.currentSong._id);
+					local.player.loadVideoById(local.currentSong._id, local.getTimeElapsed() / 1000 + local.currentSong.skipDuration);
 
 					if (local.currentSong.artists) local.currentSong.artists = local.currentSong.artists.join(", ");
 					if (window.stationInterval !== 0) clearInterval(window.stationInterval);
@@ -211,7 +217,7 @@
 				this.paused = false;
 				if (!this.noSong) {
 					if (this.playerReady) {
-						this.player.seekTo(this.getTimeElapsed() / 1000);
+						this.player.seekTo(this.getTimeElapsed() / 1000 + this.currentSong.skipDuration);
 						this.player.playVideo();
 					}
 				}
@@ -291,7 +297,12 @@
 					_this.socket = _this.$parent.socket;
 					_this.socket.removeAllListeners();
 					_this.socket.emit('stations.join', _this.stationId, res => {
-						if (res.status === "success") {
+						if (res.status === 'success') {
+							_this.station = {
+								displayName: res.data.displayName,
+								description: res.data.description,
+								privacy: res.data.privacy
+							};
 							_this.currentSong = (res.data.currentSong) ? res.data.currentSong : {};
 							_this.type = res.data.type;
 							_this.startedAt = res.data.startedAt;
@@ -311,7 +322,7 @@
 								});
 							} else {
 								if (_this.playerReady) _this.player.pauseVideo();
-								console.log("NO SONG TRUE1", res);
+								console.log("NO SONG TRUE1", res.data);
 								_this.noSong = true;
 							}
 							if (_this.type === 'community') {
@@ -412,7 +423,7 @@
 			volume = (typeof volume === "number") ? volume : 20;
 			$("#volumeSlider").val(volume);
 		},
-		components: { OfficialHeader, CommunityHeader, SongQueue, EditPlaylist, CreatePlaylist, QueueSidebar, PlaylistSidebar, UsersSidebar }
+		components: { OfficialHeader, CommunityHeader, SongQueue, EditPlaylist, CreatePlaylist, EditStation, QueueSidebar, PlaylistSidebar, UsersSidebar }
 	}
 </script>
 

+ 0 - 1
frontend/components/pages/Home.vue

@@ -60,7 +60,6 @@
 		},
 		ready() {
 			let _this = this;
-
 			let socketInterval = setInterval(() => {
 				if (!!_this.$parent.socket) {
 					_this.socket = _this.$parent.socket;

+ 55 - 36
frontend/components/pages/News.vue

@@ -1,39 +1,39 @@
 <template>
-	<div class="app">
+	<div class='app'>
 		<main-header></main-header>
-		<div class="container">
-			<div class="card is-fullwidth" v-for="item in news">
-				<header class="card-header">
-					<p class="card-header-title">
-						{{item.title}} - {{formatDate(item.createdAt)}}
+		<div class='container'>
+			<div class='card is-fullwidth' v-for='item in news'>
+				<header class='card-header'>
+					<p class='card-header-title'>
+						{{ item.title }} - {{ formatDate(item.createdAt) }}
 					</p>
 				</header>
-				<div class="card-content">
-					<div class="content">
-						<p>{{item.description}}</p>
+				<div class='card-content'>
+					<div class='content'>
+						<p>{{ item.description }}</p>
 					</div>
-					<div class="content" v-show="item.features.length > 0">
-						<div class="tile notification is-success">Features</div>
-						<ul>
-							<li v-for="li in item.features">{{li}}</li>
+					<div class='sect' v-show='item.features.length > 0'>
+						<div class='sect-head-features'>The features are so great</div>
+						<ul class='sect-body'>
+							<li v-for='li in item.features'>{{ li }}</li>
 						</ul>
 					</div>
-					<div class="content" v-show="item.changes.length > 0">
-						<div class="tile notification is-info">Changes</div>
-						<ul>
-							<li v-for="li in item.changes">{{li}}</li>
+					<div class='sect' v-show='item.improvements.length > 0'>
+						<div class='sect-head-improvements'>Improvements</div>
+						<ul class='sect-body'>
+							<li v-for='li in item.improvements'>{{ li }}</li>
 						</ul>
 					</div>
-					<div class="content" v-show="item.fixes.length > 0">
-						<div class="tile notification is-danger">Bug fixes</div>
-						<ul>
-							<li v-for="li in item.fixes">{{li}}</li>
+					<div class='sect' v-show='item.bugs.length > 0'>
+						<div class='sect-head-bugs'>Bugs Smashed</div>
+						<ul class='sect-body'>
+							<li v-for='li in item.bugs'>{{ li }}</li>
 						</ul>
 					</div>
-					<div class="content" v-show="item.upcoming.length > 0">
-						<div class="tile notification is-primary">Upcoming</div>
-						<ul>
-							<li v-for="li in item.upcoming">{{li}}</li>
+					<div class='sect' v-show='item.upcoming.length > 0'>
+						<div class='sect-head-upcoming'>Coming Soon to a Musare near you</div>
+						<ul class='sect-body'>
+							<li v-for='li in item.upcoming'>{{ li }}</li>
 						</ul>
 					</div>
 				</div>
@@ -51,7 +51,7 @@
 		components: { MainHeader, MainFooter },
 		methods: {
 			formatDate: unix => {
-				return moment(unix).format("DD-MM-YYYY");
+				return moment(unix).format('DD-MM-YYYY');
 			},
 		},
 		data() {
@@ -61,20 +61,39 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socket = this.socket = this.$parent.socket;
-			socket.emit("news.index", res => {
-				_this.news = res.data;
-			});
+			let socketInterval = setInterval(() => {
+				if (!!_this.$parent.socket) {
+					_this.socket = _this.$parent.socket;
+					_this.socket.emit('news.index', res => {
+						_this.news = res.data;
+					});
+					clearInterval(socketInterval);
+				}
+			}, 100);
 		}
 	}
 </script>
 
-<style lang="scss" scoped>
-	.card {
-		margin-top: 50px;
-	}
+<style lang='scss' scoped>
+	.card { margin-top: 50px; }
+
+	.sect {
+		div[class^='sect-head'], div[class*=' sect-head']{
+			padding: 12px;
+			text-transform: uppercase;
+			font-weight: bold;
+			color: #fff;
+		}
 
-	.notification {
-		padding: 10px !important;
+		.sect-head-features { background-color: dodgerblue; }
+		.sect-head-improvements { background-color: seagreen; }
+		.sect-head-bugs { background-color: brown; }
+		.sect-head-upcoming { background-color: mediumpurple; }
+
+		.sect-body {
+			padding: 15px 25px;
+
+			li { list-style-type: disc; }
+		}
 	}
 </style>