Bladeren bron

Merge pull request #29 from Musare/staging

Updating Backup of staging
Jonathan 8 jaren geleden
bovenliggende
commit
6326df91e1

+ 4 - 0
backend/index.js

@@ -16,6 +16,7 @@ const playlists = require('./logic/playlists');
 const cache = require('./logic/cache');
 const notifications = require('./logic/notifications');
 const logger = require('./logic/logger');
+const tasks = require('./logic/tasks');
 const config = require('config');
 
 process.on('uncaughtException', err => {
@@ -62,6 +63,9 @@ async.waterfall([
 	// setup the logger
 	(next) => logger.init(next),
 
+	// setup the tasks system
+	(next) => tasks.init(next),
+
 	// setup the frontend for local setups
 	(next) => {
 		if (!config.get("isDocker")) {

+ 1 - 1
backend/logic/actions/playlists.js

@@ -270,7 +270,7 @@ let lib = {
 				});
 			},
 			(newSong, next) => {
-				db.models.playlist.update({ _id: playlistId }, { $push: { songs: newSong } }, (err) => {
+				db.models.playlist.update({_id: playlistId}, {$push: {songs: newSong}}, {runValidators: true}, (err) => {
 					if (err) return next(err);
 					playlists.updatePlaylist(playlistId, (err, playlist) => {
 						next(err, playlist, newSong);

+ 1 - 0
backend/logic/actions/queueSongs.js

@@ -176,6 +176,7 @@ module.exports = {
 			(newSong, next) => {
 				const song = new db.models.queueSong(newSong);
 				song.save((err, song) => {
+					console.log(err);
 					if (err) return next(err);
 					next(null, song);
 				});

+ 7 - 7
backend/logic/actions/songs.js

@@ -176,12 +176,6 @@ module.exports = {
 	 */
 	add: hooks.adminRequired((session, song, cb, userId) => {
 		async.waterfall([
-			(next) => {
-				queueSongs.remove(session, song._id, () => {
-					next();
-				});
-			},
-
 			(next) => {
 				db.models.song.findOne({songId: song.songId}, next);
 			},
@@ -196,7 +190,13 @@ module.exports = {
 				newSong.acceptedBy = userId;
 				newSong.acceptedAt = Date.now();
 				newSong.save(next);
-			}
+			},
+
+			(next) => {
+				queueSongs.remove(session, song._id, () => {
+					next();
+				});
+			},
 		], (err) => {
 			if (err) {
 				err = utils.getError(err);

+ 3 - 6
backend/logic/actions/stations.js

@@ -236,7 +236,6 @@ module.exports = {
 				});
 			}
 		], (err, stations) => {
-			console.log(err, stations);
 			if (err) {
 				err = utils.getError(err);
 				logger.error("STATIONS_INDEX", `Indexing stations failed. "${err}"`);
@@ -379,7 +378,7 @@ module.exports = {
 				if (!data.currentSong || !data.currentSong.title) return next(null, data);
 				utils.socketJoinSongRoom(session.socketId, `song.${data.currentSong.songId}`);
 				data.currentSong.skipVotes = data.currentSong.skipVotes.length;
-				songs.getSong(data.currentSong.songId, (err, song) => {
+				songs.getSongFromId(data.currentSong.songId, (err, song) => {
 					if (!err && song) {
 						data.currentSong.likes = song.likes;
 						data.currentSong.dislikes = song.dislikes;
@@ -767,7 +766,6 @@ module.exports = {
 	 * @param userId
 	 */
 	create: hooks.loginRequired((session, data, cb, userId) => {
-		console.log(data);
 		data.name = data.name.toLowerCase();
 		let blacklist = ["country", "edm", "musare", "hip-hop", "rap", "top-hits", "todays-hits", "old-school", "christmas", "about", "support", "staff", "help", "news", "terms", "privacy", "profile", "c", "community", "tos", "login", "register", "p", "official", "o", "trap", "faq", "team", "donate", "buy", "shop", "forums", "explore", "settings", "admin", "auth", "reset_password"];
 		async.waterfall([
@@ -816,7 +814,6 @@ module.exports = {
 			}
 		], (err, station) => {
 			if (err) {
-				console.log(err);
 				err = utils.getError(err);
 				logger.error("STATIONS_CREATE", `Creating station failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
@@ -857,7 +854,6 @@ module.exports = {
 			(station, next) => {
 				songs.getSong(songId, (err, song) => {
 					if (!err && song) return next(null, song);
-					console.log(53, songId);
 					utils.getSongFromYouTube(songId, (song) => {
 						song.artists = [];
 						song.skipDuration = 0;
@@ -872,7 +868,7 @@ module.exports = {
 
 			(song, next) => {
 				song.requestedBy = userId;
-				db.models.station.update({_id: stationId}, {$push: {queue: song}}, next);
+				db.models.station.update({_id: stationId}, {$push: {queue: song}}, {runValidators: true}, next);
 			},
 
 			(res, next) => {
@@ -1004,6 +1000,7 @@ module.exports = {
 				return cb({'status': 'failure', 'message': err});
 			}
 			logger.success("STATIONS_SELECT_PRIVATE_PLAYLIST", `Selected private playlist "${playlistId}" for station "${stationId}" successfully.`);
+			notifications.unschedule(`stations.nextSong?id${stationId}`);
 			if (!station.partyMode) stations.skipStation(stationId)();
 			cache.pub('privatePlaylist.selected', {playlistId, stationId});
 			return cb({'status': 'success', 'message': 'Successfully selected playlist.'});

+ 1 - 0
backend/logic/cache/schemas/session.js

@@ -4,6 +4,7 @@ module.exports = (sessionId, userId) => {
 	return {
 		sessionId: sessionId,
 		userId: userId,
+		refreshDate: Date.now(),
 		created: Date.now()
 	};
 };

+ 81 - 18
backend/logic/db/index.js

@@ -8,7 +8,7 @@ const regex = {
 	azAZ09_: /^[A-Za-z0-9_]+$/,
 	az09_: /^[a-z0-9_]+$/,
 	emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
-	password: /[a-z]+[A-Z]+[0-9]+[^a-zA-Z0-9]+/,
+	password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]/,
 	ascii: /^[\x00-\x7F]+$/
 };
 
@@ -45,6 +45,16 @@ let lib = {
 				report: new mongoose.Schema(require(`./schemas/report`))
 			};
 
+			lib.models = {
+				song: mongoose.model('song', lib.schemas.song),
+				queueSong: mongoose.model('queueSong', lib.schemas.queueSong),
+				station: mongoose.model('station', lib.schemas.station),
+				user: mongoose.model('user', lib.schemas.user),
+				playlist: mongoose.model('playlist', lib.schemas.playlist),
+				news: mongoose.model('news', lib.schemas.news),
+				report: mongoose.model('report', lib.schemas.report)
+			};
+
 			lib.schemas.user.path('username').validate((username) => {
 				return (isLength(username, 2, 32) && regex.azAZ09_.test(username));
 			}, 'Invalid username.');
@@ -67,19 +77,66 @@ let lib = {
 				if (!isLength(description, 2, 200)) return false;
 				let characters = description.split("");
 				return characters.filter((character) => {
-					console.log(character.charCodeAt(0), character.charCodeAt(0) === 21328);
 					return character.charCodeAt(0) === 21328;
 				}).length === 0;
 			}, 'Invalid display name.');
 
+
+			lib.schemas.station.path('owner').validate((owner, callback) => {
+				lib.models.station.count({owner: owner}, (err, c) => {
+					callback(!(err || c >= 3));
+				});
+			}, 'User already has 3 stations.');
+
+			lib.schemas.station.path('queue').validate((queue, callback) => {
+				let totalDuration = 0;
+				queue.forEach((song) => {
+					totalDuration += song.duration;
+				});
+				return callback(totalDuration <= 3600);
+			}, 'The max length of the queue is 3 hours.');
+
+			lib.schemas.station.path('queue').validate((queue, callback) => {
+				if (queue.length === 0) return callback(true);
+				let totalDuration = 0;
+				const userId = queue[queue.length - 1].requestedBy;
+				queue.forEach((song) => {
+					if (userId === song.requestedBy) {
+						totalDuration += song.duration;
+					}
+				});
+				return callback(totalDuration <= 900);
+			}, 'The max length of songs per user is 15 minutes.');
+
+			lib.schemas.station.path('queue').validate((queue, callback) => {
+				if (queue.length === 0) return callback(true);
+				let totalSongs = 0;
+				const userId = queue[queue.length - 1].requestedBy;
+				queue.forEach((song) => {
+					if (userId === song.requestedBy) {
+						totalSongs++;
+					}
+				});
+				if (totalSongs <= 2) return callback(true);
+				if (totalSongs > 3) return callback(false);
+				if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId) return callback(true);
+				return callback(false);
+			}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
+
 			let songTitle = (title) => {
 				return (isLength(title, 1, 64) && regex.ascii.test(title));
 			};
 			lib.schemas.song.path('title').validate(songTitle, 'Invalid title.');
 			lib.schemas.queueSong.path('title').validate(songTitle, 'Invalid title.');
 
+			lib.schemas.song.path('artists').validate((artists) => {
+				return !(artists.length < 1 || artists.length > 10);
+			}, 'Invalid artists.');
+			lib.schemas.queueSong.path('artists').validate((artists) => {
+				return !(artists.length < 0 || artists.length > 10);
+			}, 'Invalid artists.');
+
 			let songArtists = (artists) => {
-				if (artists.length < 1 || artists.length > 10) return false;
 				return artists.filter((artist) => {
 						return (isLength(artist, 1, 32) && regex.ascii.test(artist) && artist !== "NONE");
 					}).length === artists.length;
@@ -95,30 +152,36 @@ let lib = {
 			lib.schemas.song.path('genres').validate(songGenres, 'Invalid genres.');
 			lib.schemas.queueSong.path('genres').validate(songGenres, 'Invalid genres.');
 
-			let songThumbnail = (thumbnail) => {
-				return isLength(thumbnail, 8, 256);
-			};
-			lib.schemas.song.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
-			lib.schemas.queueSong.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
+			lib.schemas.song.path('thumbnail').validate((thumbnail) => {
+				return isLength(thumbnail, 0, 256);
+			}, 'Invalid thumbnail.');
+			lib.schemas.queueSong.path('thumbnail').validate((thumbnail) => {
+				return isLength(thumbnail, 0, 256);
+			}, 'Invalid thumbnail.');
 
 			lib.schemas.playlist.path('displayName').validate((displayName) => {
 				return (isLength(displayName, 1, 16) && regex.ascii.test(displayName));
 			}, 'Invalid display name.');
 
+			lib.schemas.playlist.path('createdBy').validate((createdBy, callback) => {
+				lib.models.playlist.count({createdBy: createdBy}, (err, c) => {
+					callback(!(err || c >= 10));
+				});
+			}, 'Max 10 playlists per user.');
+
+			lib.schemas.playlist.path('songs').validate((songs) => {
+				return songs.length <= 2000;
+			}, 'Max 2000 songs per playlist.');
+
+			lib.schemas.playlist.path('songs').validate((songs) => {
+				if (songs.length === 0) return true;
+				return songs[0].duration <= 10800;
+			}, 'Max 3 hours per song.');
+
 			lib.schemas.report.path('description').validate((description) => {
 				return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
 			}, 'Invalid description.');
 
-			lib.models = {
-				song: mongoose.model('song', lib.schemas.song),
-				queueSong: mongoose.model('queueSong', lib.schemas.queueSong),
-				station: mongoose.model('station', lib.schemas.station),
-				user: mongoose.model('user', lib.schemas.user),
-				playlist: mongoose.model('playlist', lib.schemas.playlist),
-				news: mongoose.model('news', lib.schemas.news),
-				report: mongoose.model('report', lib.schemas.report)
-			};
-
 			cb();
 		});
 	},

+ 20 - 6
backend/logic/io.js

@@ -4,6 +4,7 @@
 
 const app = require('./app');
 const actions = require('./actions');
+const async = require('async');
 const cache = require('./cache');
 const utils = require('./utils');
 const db = require('./db');
@@ -20,12 +21,25 @@ module.exports = {
 			let cookies = socket.request.headers.cookie;
 			let SID = utils.cookies.parseCookies(cookies).SID;
 
-			if (!SID) SID = "NONE";
-			cache.hget('sessions', SID, (err, session) => {
-				if (err) SID = null;
-				socket.session = (session) ? session : {};
-				socket.session.socketId = socket.id;
-				return next();
+			async.waterfall([
+				(next) => {
+					if (!SID) return next('No SID.');
+					next();
+				},
+				(next) => {
+					cache.hget('sessions', SID, next);
+				},
+				(session, next) => {
+					if (!session) return next('No session found.');
+					session.refreshDate = Date.now();
+					socket.session = session;
+					cache.hset('sessions', SID, session, next);
+				}
+			], () => {
+				if (!socket.session) {
+					socket.session = {socketId: socket.id};
+				} else socket.session.socketId = socket.id;
+				next();
 			});
 		});
 

+ 6 - 6
backend/logic/logger.js

@@ -97,8 +97,8 @@ module.exports = {
 		successThisHour++;
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
-			fs.appendFile(__dirname + '/../../all.log', `${timeString} SUCCESS - ${type} - ${message}\n`);
-			fs.appendFile(__dirname + '/../../success.log', `${timeString} SUCCESS - ${type} - ${message}\n`);
+			fs.appendFile(__dirname + '/../../all.log', `${timeString} SUCCESS - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(__dirname + '/../../success.log', `${timeString} SUCCESS - ${type} - ${message}\n`, ()=>{});
 			console.info('\x1b[32m', timeString, 'SUCCESS', '-', type, '-', message, '\x1b[0m');
 		});
 	},
@@ -108,8 +108,8 @@ module.exports = {
 		errorThisHour++;
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
-			fs.appendFile(__dirname + '/../../all.log', `${timeString} ERROR - ${type} - ${message}\n`);
-			fs.appendFile(__dirname + '/../../error.log', `${timeString} ERROR - ${type} - ${message}\n`);
+			fs.appendFile(__dirname + '/../../all.log', `${timeString} ERROR - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(__dirname + '/../../error.log', `${timeString} ERROR - ${type} - ${message}\n`, ()=>{});
 			console.warn('\x1b[31m', timeString, 'ERROR', '-', type, '-', message, '\x1b[0m');
 		});
 	},
@@ -119,8 +119,8 @@ module.exports = {
 		infoThisHour++;
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
-			fs.appendFile(__dirname + '/../../all.log', `${timeString} INFO - ${type} - ${message}\n`);
-			fs.appendFile(__dirname + '/../../info.log', `${timeString} INFO - ${type} - ${message}\n`);
+			fs.appendFile(__dirname + '/../../all.log', `${timeString} INFO - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(__dirname + '/../../info.log', `${timeString} INFO - ${type} - ${message}\n`, ()=>{});
 
 			console.info('\x1b[36m', timeString, 'INFO', '-', type, '-', message, '\x1b[0m');
 		});

+ 2 - 2
backend/logic/playlists.js

@@ -119,7 +119,7 @@ module.exports = {
 			}
 
 		], (err, playlist) => {
-			if (err && err !== true) cb(err);
+			if (err && err !== true) return cb(err);
 			cb(null, playlist);
 		});
 	},
@@ -142,7 +142,7 @@ module.exports = {
 			}
 
 		], (err) => {
-			if (err && err !== true) cb(err);
+			if (err && err !== true) return cb(err);
 
 			cb(null);
 		});

+ 1 - 1
backend/logic/songs.js

@@ -124,7 +124,7 @@ module.exports = {
 			}
 
 		], (err, song) => {
-			if (err && err !== true) cb(err);
+			if (err && err !== true) return cb(err);
 
 			cb(null, song);
 		});

+ 4 - 4
backend/logic/stations.js

@@ -133,13 +133,13 @@ module.exports = {
 					db.models.song.find({genres: genre}, (err, songs) => {
 						if (!err) {
 							songs.forEach((song) => {
-								if (songList.indexOf(song.songId) === -1) {
+								if (songList.indexOf(song._id) === -1) {
 									let found = false;
 									song.genres.forEach((songGenre) => {
 										if (station.blacklistedGenres.indexOf(songGenre) !== -1) found = true;
 									});
 									if (!found) {
-										songList.push(song.songId);
+										songList.push(song._id);
 									}
 								}
 							});
@@ -205,7 +205,7 @@ module.exports = {
 			},
 
 		], (err, station) => {
-			if (err && err !== true) cb(err);
+			if (err && err !== true) return cb(err);
 			cb(null, station);
 		});
 	},
@@ -355,7 +355,7 @@ module.exports = {
 							} else {
 								_this.calculateSongForStation(station, (err, newPlaylist) => {
 									if (err) return next(null, _this.defaultSong, 0);
-									songs.getSongFromId(newPlaylist[0], (err, song) => {
+									songs.getSong(newPlaylist[0], (err, song) => {
 										if (err || !song) return next(null, _this.defaultSong, 0);
 										station.playlist = newPlaylist;
 										next(null, song, 0);

+ 128 - 0
backend/logic/tasks.js

@@ -0,0 +1,128 @@
+'use strict';
+
+const cache = require("./cache");
+const logger = require("./logger");
+const Stations = require("./stations");
+const async = require("async");
+let utils;
+let tasks = {};
+
+let testTask = (callback) => {
+	//Stuff
+	console.log("Starting task");
+	setTimeout(() => {
+		console.log("Callback");
+		callback();
+	}, 10000);
+};
+
+let checkStationSkipTask = (callback) => {
+	logger.info("TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`);
+	async.waterfall([
+		(next) => {
+			cache.hgetall('stations', next);
+		},
+		(stations, next) => {
+			async.each(stations, (station, next2) => {
+				if (station.paused || !station.currentSong || !station.currentSong.title) return next2();
+				const timeElapsed = Date.now() - station.startedAt - station.timePaused;
+				if (timeElapsed <= station.currentSong.duration) return next2();
+				else {
+					logger.error("TASK_STATIONS_SKIP_CHECK", `Skipping ${station._id} as it should have skipped already.`);
+					stations.skipStation(station._id);
+					next2();
+				}
+			}, () => {
+				next();
+			});
+		}
+	], () => {
+		callback();
+	});
+};
+
+let sessionClearingTask = (callback) => {
+	logger.info("TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`);
+	async.waterfall([
+		(next) => {
+			cache.hgetall('sessions', next);
+		},
+		(sessions, next) => {
+			if (!sessions) return next();
+			let keys = Object.keys(sessions);
+			async.each(keys, (sessionId, next2) => {
+				let session = sessions[sessionId];
+				if (session && session.refreshDate && (Date.now() - session.refreshDate) < (60 * 60 * 24 * 30 * 1000)) return next2();
+				if (!session) {
+					logger.info("TASK_SESSION_CLEAR", 'Removing an empty session.');
+					cache.hdel('sessions', sessionId, () => {
+						next2();
+					});
+				} else if (!session.refreshDate) {
+					session.refreshDate = Date.now();
+					cache.hset('sessions', sessionId, session, () => {
+						next2();
+					});
+				} else if ((Date.now() - session.refreshDate) > (60 * 60 * 24 * 30 * 1000)) {
+					utils.socketsFromSessionId(session.sessionId, (sockets) => {
+						if (sockets.length > 0) {
+							session.refreshDate = Date.now();
+							cache.hset('sessions', sessionId, session, () => {
+								next2()
+							});
+						} else {
+							logger.info("TASK_SESSION_CLEAR", `Removing session ${sessionId} for user ${session.userId} since inactive for 30 days and not currently in use.`);
+							cache.hdel('sessions', session.sessionId, () => {
+								next2();
+							});
+						}
+					});
+				} else {
+					logger.error("TASK_SESSION_CLEAR", "This should never log.");
+					next2();
+				}
+			}, () => {
+				next();
+			});
+		}
+	], () => {
+		callback();
+	});
+};
+
+module.exports = {
+	init: function(cb) {
+		utils = require('./utils');
+		this.createTask("testTask", testTask, 5000, true);
+		this.createTask("stationSkipTask", checkStationSkipTask, 1000 * 60 * 30);
+		this.createTask("sessionClearTask", sessionClearingTask, 1000 * 60 * 60 * 6);
+
+		cb();
+	},
+	createTask: function(name, fn, timeout, paused = false) {
+		tasks[name] = {
+			name,
+			fn,
+			timeout,
+			lastRan: 0,
+			timer: null
+		};
+		if (!paused) this.handleTask(tasks[name]);
+	},
+	pauseTask: (name) => {
+		tasks[name].timer.pause();
+	},
+	resumeTask: (name) => {
+		tasks[name].timer.resume();
+	},
+	handleTask: function(task) {
+		if (task.timer) task.timer.pause();
+
+		task.fn(() => {
+			task.lastRan = Date.now();
+			task.timer = new utils.Timer(() => {
+				this.handleTask(task);
+			}, task.timeout, false);
+		});
+	}
+};

+ 14 - 1
backend/logic/utils.js

@@ -13,7 +13,7 @@ class Timer {
 		this.timerId = undefined;
 		this.start = undefined;
 		this.paused = paused;
-		this.remaining = moment.duration(delay, "hh:mm:ss").asSeconds() * 1000;
+		this.remaining = delay;
 		this.timeWhenPaused = 0;
 		this.timePaused = Date.now();
 
@@ -142,6 +142,19 @@ module.exports = {
 			return ns.connected[socketId];
 		}
 	},
+	socketsFromSessionId: function(sessionId, cb) {
+		let ns = io.io.of("/");
+		let sockets = [];
+		if (ns) {
+			async.each(Object.keys(ns.connected), (id, next) => {
+				let session = ns.connected[id].session;
+				if (session.sessionId === sessionId) sockets.push(session.sessionId);
+				next();
+			}, () => {
+				cb(sockets);
+			});
+		}
+	},
 	socketsFromUser: function(userId, cb) {
 		let ns = io.io.of("/");
 		let sockets = [];

+ 0 - 0
.babelrc → frontend/.babelrc


+ 1 - 1
frontend/Dockerfile

@@ -3,7 +3,7 @@ FROM node
 RUN apt-get update
 RUN apt-get install nginx -y
 
-RUN npm install -g webpack
+RUN npm install -g webpack@1.14.0
 
 RUN mkdir -p /opt
 WORKDIR /opt

+ 2 - 0
frontend/components/Admin/QueueSongs.vue

@@ -86,12 +86,14 @@
 			add: function (song) {
 				this.socket.emit('songs.add', song, res => {
 					if (res.status == 'success') Toast.methods.addToast(res.message, 2000);
+					else Toast.methods.addToast(res.message, 4000);
 				});
 			},
 			remove: function (id, index) {
 				console.log("Removing ", id);
 				this.socket.emit('queueSongs.remove', id, res => {
 					if (res.status == 'success') Toast.methods.addToast(res.message, 2000);
+				else Toast.methods.addToast(res.message, 4000);
 				});
 			},
 			init: function() {

+ 92 - 0
frontend/components/Modals/AddSongToPlaylist.vue

@@ -0,0 +1,92 @@
+<template>
+	<modal title='Add Song To Playlist'>
+		<div slot='body'>
+			<aside class="menu">
+				<p class="menu-label">
+					Playlists
+				</p>
+				<ul class="menu-list">
+					<li v-for='playlist in playlists'>
+						<div class='playlist'>
+							<span class='icon is-small' @click='removeSongFromPlaylist(playlist._id)' v-if='playlistContains(playlist._id)'>
+								<i class="material-icons">playlist_add_check</i>
+							</span>
+							<span class='icon is-small' @click='addSongToPlaylist(playlist._id)' v-else>
+								<i class="material-icons">playlist_add</i>
+							</span>
+							{{ playlist.displayName }}
+						</div>
+					</li>
+				</ul>
+				</aside>
+		</div>
+	</modal>
+</template>
+
+<script>
+	import { Toast } from 'vue-roaster';
+	import Modal from './Modal.vue';
+	import io from '../../io';
+	import auth from '../../auth';
+
+	export default {
+		data() {
+			return {
+				playlists: {}
+			}
+		},
+		methods: {
+			playlistContains: function (playlistId) {
+				let _this = this;
+				let toReturn = false;
+
+				let playlist = this.playlists.filter(playlist => {
+				    return playlist._id === playlistId;
+				})[0];
+
+				for (let i = 0; i < playlist.songs.length; i++) {
+					if (playlist.songs[i].songId === _this.$parent.currentSong.songId) {
+						toReturn = true;
+					}
+				}
+
+				return toReturn;
+			},
+			addSongToPlaylist: function (playlistId) {
+				let _this = this;
+				this.socket.emit('playlists.addSongToPlaylist', this.$parent.currentSong.songId, playlistId, res => {
+					Toast.methods.addToast(res.message, 4000);
+					this.$parent.modals.addSongToPlaylist = false;
+				});
+			},
+			removeSongFromPlaylist: function (playlistId) {
+				let _this = this;
+				this.socket.emit('playlists.removeSongFromPlaylist', this.$parent.currentSong.songId, playlistId, res => {
+					Toast.methods.addToast(res.message, 4000);
+					this.$parent.modals.addSongToPlaylist = false;
+				});
+			}
+		},
+		ready: function () {
+			let _this = this;
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('playlists.indexForUser', res => {
+					if (res.status === 'success') _this.playlists = res.data;
+				});
+			});
+		},
+		events: {
+			closeModal: function () {
+				this.$parent.modals.addSongToPlaylist = !this.$parent.modals.addSongToPlaylist;
+			}
+		},
+		components: { Modal }
+	}
+</script>
+
+<style type='scss' scoped>
+	.icon.is-small {
+		margin-right: 10px !important;
+	}
+</style>

+ 1 - 1
frontend/components/Modals/AddSongToQueue.vue

@@ -79,7 +79,7 @@
 			addSongToQueue: function (songId) {
 				let _this = this;
 				if (_this.$parent.type === 'community') {
-					_this.socket.emit('stations.addToQueue', _this.$parent.stationId, songId, data => {
+					_this.socket.emit('stations.addToQueue', _this.$parent.station._id, songId, data => {
 						if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
 						else Toast.methods.addToast(`${data.message}`, 4000);
 					});

+ 2 - 1
frontend/components/Modals/EditSong.vue

@@ -248,8 +248,9 @@
 			getSpotifySongs: function() {
 				this.socket.emit('apis.getSpotifySongs', this.spotify.title, this.spotify.artist, (res) => {
 					if (res.status === 'success') {
+						Toast.methods.addToast(`Succesfully got ${res.songs.length} song${(res.songs.length !== 1) ? 's' : ''}.`, 3000);
 						this.spotify.songs = res.songs;
-					}
+					} else Toast.methods.addToast(`Failed to get songs. ${res.message}`, 3000);
 				});
 			}
 		},

+ 1 - 1
frontend/components/Sidebars/Playlist.vue

@@ -9,7 +9,7 @@
 						<span>{{ playlist.displayName }}</span>
 						<!--Will play playlist in community station Kris-->
 						<div class='icons-group'>
-							<a href='#' @click='selectPlaylist(playlist._id)' v-if="isNotSelected(playlist._id)">
+							<a href='#' @click='selectPlaylist(playlist._id)' v-if="isNotSelected(playlist._id) && !this.$parent.$parent.station.partyMode">
 								<i class='material-icons'>play_arrow</i>
 							</a>
 							<a href='#' @click='editPlaylist(playlist._id)'>

+ 125 - 39
frontend/components/Station/CommunityHeader.vue

@@ -4,37 +4,8 @@
 			<a class='nav-item logo' href='#' v-link='{ path: "/" }' @click='this.$dispatch("leaveStation", title)'>
 				Musare
 			</a>
-			<a class='nav-item' href='#' v-if='isOwner()' @click='$parent.editStation()'>
-				<span class='icon'>
-					<i class='material-icons'>settings</i>
-				</span>
-			</a>
-			<a v-if='isOwner()' class='nav-item' href='#' @click='$parent.skipStation()'>
-				<span class='icon'>
-					<i class='material-icons'>skip_next</i>
-				</span>
-			</a>
-			<a v-if='!isOwner() && $parent.$parent.loggedIn && !$parent.noSong' class='nav-item' href='#' @click='$parent.voteSkipStation()'>
-				<span class='icon'>
-					<i class='material-icons'>skip_next</i>
-				</span>
-				<span class="skip-votes">{{ $parent.currentSong.skipVotes }}</span>
-			</a>
-			<a v-if='$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
-				<span class='icon'>
-					<i class='material-icons'>report</i>
-				</span>
-			</a>
-			<a class='nav-item' href='#' v-if='isOwner() && $parent.paused' @click='$parent.resumeStation()'>
-				<span class='icon'>
-					<i class='material-icons'>play_arrow</i>
-				</span>
-			</a>
-			<a class='nav-item' href='#' v-if='isOwner() && !$parent.paused' @click='$parent.pauseStation()'>
-				<span class='icon'>
-					<i class='material-icons'>pause</i>
-				</span>
-			</a>
+
+
 		</div>
 
 		<div class='nav-center stationDisplayName'>
@@ -48,28 +19,80 @@
 		</span>
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
-			<a class='nav-item' href='#' @click='$parent.toggleSidebar("songslist")' v-if='$parent.station.partyMode === true'>
+			<!-- DUPLICATE BUTTON TO HOLD FORMATTING -->
+			<a class='nav-item' href='#' @click='$parent.toggleSidebar("users")'>
 				<span class='icon'>
-					<i class='material-icons'>queue_music</i>
+					<i class='material-icons'>people</i>
 				</span>
 			</a>
-			<!--<a class='nav-item' href='#'>
+		</div>
+	</nav>
+	<div class="admin-sidebar">
+		<div class='inner-wrapper'>
+			<hr class="sidebar-top-hr">
+			<div v-if='isOwner()'>
+				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
+					<span class='icon'>
+						<i class='material-icons'>settings</i>
+					</span>
+					<span class="icon-purpose">Station settings</span>
+				</a>
+				<a v-if='isOwner()' class="sidebar-item" href='#' @click='$parent.skipStation()'>
+					<span class='icon'>
+						<i class='material-icons'>skip_next</i>
+					</span>
+					<span class="icon-purpose">Skip current song</span>
+				</a>
+				<a class="sidebar-item" href='#' v-if='isOwner() && $parent.paused' @click='$parent.resumeStation()'>
+					<span class='icon'>
+						<i class='material-icons'>play_arrow</i>
+					</span>
+					<span class="icon-purpose">Resume station</span>
+				</a>
+				<a class="sidebar-item" href='#' v-if='isOwner() && !$parent.paused' @click='$parent.pauseStation()'>
+					<span class='icon'>
+						<i class='material-icons'>pause</i>
+					</span>
+					<span class="icon-purpose">Pause station</span>
+				</a>
+				<hr>
+			</div>
+			<div v-if="$parent.$parent.loggedIn && !$parent.noSong">
+				<a v-if='!isOwner() && $parent.$parent.loggedIn && !$parent.noSong' class="sidebar-item" href='#' @click='$parent.voteSkipStation()'>
+					<span class='icon'>
+						<i class='material-icons'>skip_next</i>
+					</span>
+					<span class="skip-votes">{{ $parent.currentSong.skipVotes }}</span>
+					<span class="icon-purpose">Skip current song</span>
+				</a>
+				<a v-if='$parent.$parent.loggedIn && !$parent.noSong' class="sidebar-item" href='#' @click='$parent.modals.addSongToPlaylist = true'>
+					<span class='icon'>
+						<i class='material-icons'>playlist_add</i>
+					</span>
+					<span class="icon-purpose">Add current song to playlist</span>
+				</a>
+				<hr>
+			</div>
+			<a class="sidebar-item" href='#' @click='$parent.toggleSidebar("songslist")' v-if='$parent.station.partyMode === true'>
 				<span class='icon'>
-					<i class='material-icons'>chat</i>
+					<i class='material-icons'>queue_music</i>
 				</span>
-			</a>-->
-			<a class='nav-item' href='#' @click='$parent.toggleSidebar("users")'>
+				<span class="icon-purpose">Show the station queue</span>
+			</a>
+			<a class="sidebar-item" href='#' @click='$parent.toggleSidebar("users")'>
 				<span class='icon'>
 					<i class='material-icons'>people</i>
 				</span>
+				<span class="icon-purpose">Display users in the station</span>
 			</a>
-			<a class='nav-item' href='#' @click='$parent.toggleSidebar("playlist")' v-if='$parent.$parent.loggedIn'>
+			<a class="sidebar-item" href='#' @click='$parent.toggleSidebar("playlist")' v-if='$parent.$parent.loggedIn'>
 				<span class='icon'>
 					<i class='material-icons'>library_music</i>
 				</span>
+				<span class="icon-purpose">Show your playlists</span>
 			</a>
 		</div>
-	</nav>
+	</div>
 </template>
 
 <script>
@@ -140,4 +163,67 @@
 		background: #03a9f4;
     	border: 0;
 	}
+
+	.admin-sidebar {
+		position: fixed;
+		z-index: 1;
+		top: 0;
+		left: 0;
+		width: 64px;
+		height: 100vh;
+		background-color: #03a9f4;
+		box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+	}
+
+	.inner-wrapper {
+		top: 64px;
+		position: relative;
+	}
+
+	.admin-sidebar .material-icons {
+		width: 100%;
+		font-size: 2rem;
+	}
+	.admin-sidebar .sidebar-item {
+		font-size: 2rem;
+		height: 50px;
+		color: white;
+		-webkit-box-align: center;
+		-ms-flex-align: center;
+		align-items: center;
+		display: -webkit-box;
+		display: -ms-flexbox;
+		display: flex;
+		-webkit-box-flex: 0;
+		-ms-flex-positive: 0;
+		flex-grow: 0;
+		-ms-flex-negative: 0;
+		flex-shrink: 0;
+		-webkit-box-pack: center;
+		-ms-flex-pack: center;
+		justify-content: center;
+		width: 100%;
+		position: relative;
+	}
+	.admin-sidebar .sidebar-top-hr {
+		margin: 0 0 20px 0;
+	}
+
+	.sidebar-item .icon-purpose {
+    visibility: hidden;
+    width: 150px;
+		font-size: 12px;
+    background-color: rgba(3, 169, 244,0.8);
+    color: #fff;
+    text-align: center;
+    border-radius: 6px;
+    padding: 5px 0;
+    position: absolute;
+    z-index: 1;
+    left: 105%;
+	}
+
+	.sidebar-item:hover .icon-purpose {
+    visibility: visible;
+	}
 </style>

+ 137 - 41
frontend/components/Station/OfficialHeader.vue

@@ -4,42 +4,6 @@
 			<a class='nav-item logo' href='#' v-link='{ path: "/" }' @click='this.$dispatch("leaveStation", title)'>
 				Musare
 			</a>
-			<a class='nav-item' href='#' v-if='isOwner()' @click='$parent.editStation()'>
-				<span class='icon'>
-					<i class='material-icons'>settings</i>
-				</span>
-			</a>
-			<a class='nav-item' href='#' @click='$parent.modals.addSongToQueue = !$parent.modals.addSongToQueue' v-if='$parent.type === "official" && $parent.$parent.loggedIn'>
-				<span class='icon'>
-					<i class='material-icons'>queue_music</i>
-				</span>
-			</a>
-			<a v-if='isOwner()' class='nav-item' href='#' @click='$parent.skipStation()'>
-				<span class='icon'>
-					<i class='material-icons'>skip_next</i>
-				</span>
-			</a>
-			<a v-if='!isOwner() && $parent.$parent.loggedIn && !$parent.noSong' class='nav-item' href='#' @click='$parent.voteSkipStation()'>
-				<span class='icon'>
-					<i class='material-icons'>skip_next</i>
-				</span>
-				<span class="skip-votes">{{$parent.currentSong.skipVotes}}</span>
-			</a>
-			<a v-if='$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
-				<span class='icon'>
-					<i class='material-icons'>report</i>
-				</span>
-			</a>
-			<a class='nav-item' href='#' v-if='isOwner() && $parent.paused' @click='$parent.resumeStation()'>
-				<span class='icon'>
-					<i class='material-icons'>play_arrow</i>
-				</span>
-			</a>
-			<a class='nav-item' href='#' v-if='isOwner() && !$parent.paused' @click='$parent.pauseStation()'>
-				<span class='icon'>
-					<i class='material-icons'>pause</i>
-				</span>
-			</a>
 		</div>
 
 		<div class='nav-center stationDisplayName'>
@@ -53,23 +17,86 @@
 		</span>
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
+			<!-- DUPLICATE BUTTON TO HOLD FORMATTING -->
 			<a class='nav-item' href='#' @click='$parent.toggleSidebar("songslist")'>
 				<span class='icon'>
 					<i class='material-icons'>queue_music</i>
 				</span>
 			</a>
-			<!--<a class='nav-item' href='#'>
+		</div>
+	</nav>
+	<div class="admin-sidebar">
+		<div class='inner-wrapper'>
+			<hr class="sidebar-top-hr">
+			<div v-if='isOwner()'>
+				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
+					<span class='icon'>
+						<i class='material-icons'>settings</i>
+					</span>
+					<span class="icon-purpose">Station settings</span>
+				</a>
+				<a v-if='isOwner()' class="sidebar-item" href='#' @click='$parent.skipStation()'>
+					<span class='icon'>
+						<i class='material-icons'>skip_next</i>
+					</span>
+					<span class="icon-purpose">Skip current song</span>
+				</a>
+				<a class="sidebar-item" href='#' v-if='isOwner() && !$parent.paused' @click='$parent.pauseStation()'>
+					<span class='icon'>
+						<i class='material-icons'>pause</i>
+					</span>
+					<span class="icon-purpose">Pause station</span>
+				</a>
+				<a class="sidebar-item" href='#' v-if='isOwner() && $parent.paused' @click='$parent.resumeStation()'>
+					<span class='icon'>
+						<i class='material-icons'>play_arrow</i>
+					</span>
+					<span class="icon-purpose">Resume station</span>
+				</a>
+				<hr>
+			</div>
+			<div v-if="$parent.$parent.loggedIn">
+				<a class="sidebar-item" href='#' @click='$parent.modals.addSongToQueue = !$parent.modals.addSongToQueue' v-if='$parent.type === "official" && $parent.$parent.loggedIn'>
+					<span class='icon'>
+						<i class='material-icons'>queue</i>
+					</span>
+					<span class="icon-purpose">Add song to queue</span>
+				</a>
+				<a v-if='!isOwner() && $parent.$parent.loggedIn && !$parent.noSong' class="sidebar-item" href='#' @click='$parent.voteSkipStation()'>
+					<span class='icon'>
+						<i class='material-icons'>skip_next</i>
+					</span>
+					<span class="skip-votes">{{$parent.currentSong.skipVotes}}</span>
+					<span class="icon-purpose">Skip current song</span>
+				</a>
+				<a v-if='$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong' class="sidebar-item" href='#' @click='$parent.modals.report = !$parent.modals.report'>
+					<span class='icon'>
+						<i class='material-icons'>report</i>
+					</span>
+					<span class="icon-purpose">Report a song</span>
+				</a>
+				<a v-if='$parent.$parent.loggedIn && !$parent.noSong' class="sidebar-item" href='#' @click='$parent.modals.addSongToPlaylist = true'>
+					<span class='icon'>
+						<i class='material-icons'>playlist_add</i>
+					</span>
+					<span class="icon-purpose">Add current song to playlist</span>
+				</a>
+				<hr>
+			</div>
+			<a class="sidebar-item" href='#' @click='$parent.toggleSidebar("songslist")'>
 				<span class='icon'>
-					<i class='material-icons'>chat</i>
+					<i class='material-icons'>queue_music</i>
 				</span>
-			</a>-->
-			<a class='nav-item' href='#' @click='$parent.toggleSidebar("users")'>
+				<span class="icon-purpose">Show the station queue</span>
+			</a>
+			<a class="sidebar-item" href='#' @click='$parent.toggleSidebar("users")'>
 				<span class='icon'>
 					<i class='material-icons'>people</i>
 				</span>
+				<span class="icon-purpose">Display users in the station</span>
 			</a>
 		</div>
-	</nav>
+	</div>
 </template>
 
 <script>
@@ -140,4 +167,73 @@
 		background: #03a9f4;
     	border: 0;
 	}
+
+	.hidden {
+		display: none;
+	}
+
+	.admin-sidebar {
+		position: fixed;
+		z-index: 1;
+		top: 0;
+		left: 0;
+		width: 64px;
+		height: 100vh;
+		background-color: #03a9f4;
+		box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+		overflow-y: auto;
+		overflow-x: hidden;
+	}
+
+	.inner-wrapper {
+		top: 64px;
+		position: relative;
+	}
+
+	.admin-sidebar .material-icons {
+		width: 100%;
+		font-size: 2rem;
+	}
+	.admin-sidebar .sidebar-item {
+		font-size: 2rem;
+		height: 50px;
+		color: white;
+		-webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-flex: 0;
+    -ms-flex-positive: 0;
+    flex-grow: 0;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    -webkit-box-pack: center;
+    -ms-flex-pack: center;
+    justify-content: center;
+		width: 100%;
+		position: relative;
+	}
+	.admin-sidebar .sidebar-top-hr {
+		margin: 0 0 20px 0;
+	}
+
+	.sidebar-item .icon-purpose {
+		visibility: hidden;
+		width: 150px;
+		font-size: 12px;
+		background-color: rgba(3, 169, 244,0.8);
+		color: #fff;
+		text-align: center;
+		border-radius: 6px;
+		padding: 5px 0;
+		position: absolute;
+		z-index: 1;
+		left: 105%;
+	}
+
+	.sidebar-item:hover .icon-purpose {
+		visibility: visible;
+	}
 </style>

+ 77 - 7
frontend/components/Station/Station.vue

@@ -3,6 +3,7 @@
 	<community-header v-if='type == "community"'></community-header>
 
 	<song-queue v-if='modals.addSongToQueue'></song-queue>
+	<add-to-playlist v-if='modals.addSongToPlaylist'></add-to-playlist>
 	<edit-playlist v-if='modals.editPlaylist'></edit-playlist>
 	<create-playlist v-if='modals.createPlaylist'></create-playlist>
 	<edit-station v-show='modals.editStation'></edit-station>
@@ -23,8 +24,8 @@
 			</h4>
 			<h1 v-if='type === "community" && !station.partyMode && $parent.userId === station.owner && station.privatePlaylist'>Maybe you can add some songs to your selected private playlist and then press the skip button</h1>
 		</div>
-		<div class="columns is-mobile" v-show="!noSong">
-			<div class="column is-8-desktop is-offset-2-desktop is-12-mobile">
+		<div class="columns" v-show="!noSong">
+			<div class="column is-8-desktop is-offset-2-desktop is-11-mobile">
 				<div class="video-container">
 					<div id="player"></div>
 					<div class="seeker-bar-container white" id="preview-progress">
@@ -33,10 +34,10 @@
 				</div>
 			</div>
 		</div>
-		<div class="columns is-mobile" v-show="!noSong">
+		<div class="desktop-only columns is-mobile" v-show="!noSong">
 			<div class="column is-8-desktop is-offset-2-desktop is-12-mobile">
 				<div class="columns is-mobile">
-					<div class="column is-12-mobile" v-bind:class="{'is-8-desktop': !simpleSong}">
+					<div class="column is-11-desktop" v-bind:class="{'is-7-desktop': !simpleSong}">
 						<h4 id="time-display">{{timeElapsed}} / {{formatTime(currentSong.duration)}}</h4>
 						<h3>{{currentSong.title}}</h3>
 						<h4 class="thin" style="margin-left: 0">{{currentSong.artists}}</h4>
@@ -71,6 +72,41 @@
 				</div>
 			</div>
 		</div>
+		<div class="mobile-only" v-show="!noSong">
+			<div>
+				<div>
+					<div>
+						<h3>{{currentSong.title}}</h3>
+						<h4 class="thin">{{currentSong.artists}}</h4>
+						<h5>{{timeElapsed}} / {{formatTime(currentSong.duration)}}</h5>
+						<div>
+							<form class="columns" action="#">
+								<p class='column is-11-mobile volume-slider-wrapper'>
+									<i class="material-icons" @click='toggleMute()' v-if='muted'>volume_mute</i>
+									<i class="material-icons" @click='toggleMute()' v-else>volume_down</i>
+									<input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
+									<i class="material-icons" @click='increaseVolume()'>volume_up</i>
+								</p>
+							</form>
+							<div>
+								<ul id="ratings" style="display: inline-block;" v-if="currentSong.likes !== -1 && currentSong.dislikes !== -1">
+									<li id="dislike" style="display: inline-block;margin-right: 10px;" @click="toggleDislike()">
+										<span class="flow-text">{{currentSong.dislikes}} </span>
+										<i id="thumbs_down" class="material-icons grey-text" v-bind:class="{ disliked: disliked }">thumb_down</i>
+										<a class='absolute-a behind' @click="toggleDislike()" href='#'></a>
+									</li>
+									<li id="like" style="display: inline-block;" @click="toggleLike()">
+										<span class="flow-text">{{currentSong.likes}} </span>
+										<i id="thumbs_up" class="material-icons grey-text" v-bind:class="{ liked: liked }">thumb_up</i>
+										<a class='absolute-a behind' @click="toggleLike()" href='#'></a>
+									</li>
+								</ul>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
 	</div>
 </template>
 
@@ -78,6 +114,7 @@
 	import { Toast } from 'vue-roaster';
 
 	import SongQueue from '../Modals/AddSongToQueue.vue';
+	import AddToPlaylist from '../Modals/AddSongToPlaylist.vue';
 	import EditPlaylist from '../Modals/Playlists/Edit.vue';
 	import CreatePlaylist from '../Modals/Playlists/Create.vue';
 	import EditStation from '../Modals/EditStation.vue';
@@ -109,6 +146,7 @@
 				disliked: false,
 				modals: {
 					addSongToQueue: false,
+					addSongToPlaylist: false,
 					editPlaylist: false,
 					createPlaylist: false,
 					editStation: false,
@@ -376,7 +414,6 @@
 							owner: res.data.owner,
 							privatePlaylist: res.data.privatePlaylist
 						};
-						console.log(res.data.currentSong);
 						_this.currentSong = (res.data.currentSong) ? res.data.currentSong : {};
 						_this.type = res.data.type;
 						_this.startedAt = res.data.startedAt;
@@ -412,6 +449,9 @@
 								if (res.status == 'success') _this.songsList = res.data;
 							});
 						}
+					} else {
+						_this.$router.go('/404');
+						Toast.methods.addToast(res.message, 3000);
 					}
 					// UNIX client time before ping
 					let beforePing = Date.now();
@@ -471,7 +511,7 @@
 						if (_this.simpleSong) _this.currentSong.skipDuration = 0;
 						if (!_this.playerReady) _this.youtubeReady();
 						else _this.playVideo();
-						_this.socket.emit('songs.getOwnSongRatings', data.currentSong._id, (data) => {
+						_this.socket.emit('songs.getOwnSongRatings', data.currentSong.songId, (data) => {
 							if (_this.currentSong.songId === data.songId) {
 								_this.liked = data.liked;
 								_this.disliked = data.disliked;
@@ -596,6 +636,7 @@
 			OfficialHeader,
 			CommunityHeader,
 			SongQueue,
+			AddToPlaylist,
 			EditPlaylist,
 			CreatePlaylist,
 			EditStation,
@@ -632,6 +673,12 @@
 		color: white !important;
 	}
 
+	.add-to-playlist {
+		display: flex;
+	    align-items: center;
+	    justify-content: center;
+	}
+
 	.slideout {
 		top: 50px;
 		height: 100%;
@@ -663,7 +710,7 @@
 		padding-top: 0.5vw;
 		transition: all 0.1s;
 		margin: 0 auto;
-		max-width: 1280px;
+		max-width: 100%;
 		width: 90%;
 
 		@media only screen and (min-width: 993px) {
@@ -674,6 +721,29 @@
 			width: 85%;
 		}
 
+		@media (min-width: 881px) {
+			.mobile-only {
+				display: none;
+			}
+			.desktop-only {
+				display: block;
+			}
+		}
+		@media (max-width: 880px) {
+			margin-left: 64px;
+			.mobile-only {
+				display: block;
+			}
+			.desktop-only {
+				display: none;
+				visibility: hidden;
+			}
+		}
+
+		.mobile-only {
+			text-align: center;
+		}
+
 		input[type=range] {
 			-webkit-appearance: none;
 			width: 100%;

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

@@ -233,7 +233,7 @@
 		cursor: pointer;
 		transition: .25s ease color;
 		font-size: 30px;
-		color: black;
+		color: #4a4a4a;
 	}
 
 	.community-button:hover { color: #03a9f4; }

+ 33 - 9
frontend/main.js

@@ -22,7 +22,10 @@ import Register from './components/Modals/Register.vue';
 
 Vue.use(VueRouter);
 
-let router = new VueRouter({ history: true });
+let router = new VueRouter({
+	history: true,
+	suppressTransitionError: true
+});
 let _this = this;
 
 lofig.folder = '../config/default.json';
@@ -46,9 +49,7 @@ router.beforeEach(transition => {
 		clearInterval(window.stationInterval);
 		window.stationInterval = 0;
 	}
-	if (window.socket) {
-		io.removeAllListeners();
-	}
+	if (window.socket) io.removeAllListeners();
 	io.clear();
 	if (transition.to.loginRequired || transition.to.adminRequired) {
 		auth.getStatus((authenticated, role) => {
@@ -56,8 +57,28 @@ router.beforeEach(transition => {
 			else if (transition.to.adminRequired && role !== 'admin') transition.redirect('/');
 			else transition.next();
 		});
-	} else {
-		transition.next();
+	} else transition.next();
+
+	if (transition.to.officialRequired) {
+		io.getSocket(socket => {
+			socket.emit('stations.findByName', transition.to.params.id, res => {
+				if (res.status === 'success') {
+					if (res.data.type === 'community') transition.redirect(`/community/${transition.to.params.id}`);
+					else transition.next();
+				}
+			});
+		});
+	}
+
+	if (transition.to.communityRequired) {
+		io.getSocket(socket => {
+			socket.emit('stations.findByName', transition.to.params.id, res => {
+				if (res.status === 'success') {
+					if (res.data.type === 'official') transition.redirect(`/official/${transition.to.params.id}`);
+					else transition.next();
+				}
+			});
+		});
 	}
 });
 
@@ -111,13 +132,16 @@ router.map({
 		adminRequired: true
 	},
 	'/official/:id': {
-		component: Station
+		component: Station,
+		officialRequired: true
 	},
 	'/:id': {
-		component: Station
+		component: Station,
+		officialRequired: true
 	},
 	'/community/:id': {
-		component: Station
+		component: Station,
+		communityRequired: true
 	}
 });
 

+ 7 - 7
frontend/webpack.config.js

@@ -13,13 +13,7 @@ module.exports = {
 				enforce: 'pre',
 				test: /\.vue$/,
 				loader: 'vue-loader',
-				exclude: /node_modules/,
-				options: {
-					loaders: {
-						sass: 'style-loader!css-loader!sass-loader?indentedSyntax',
-						scss: 'style-loader!css-loader!sass-loader'
-					}
-				}
+				exclude: /node_modules/
 			},
 			{
 				test: /\.js$/,
@@ -31,5 +25,11 @@ module.exports = {
 				loader: 'css-loader!sass-loader'
 			}
 		]
+	},
+	vue: {
+		loaders: {
+			sass: 'style-loader!css-loader!sass-loader?indentedSyntax',
+			scss: 'style-loader!css-loader!sass-loader'
+		}
 	}
 };