Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/master'

luveti 8 gadi atpakaļ
vecāks
revīzija
be4afbc625

+ 1 - 1
backend/index.js

@@ -85,6 +85,6 @@ async.waterfall([
 		console.error('An error occurred while initializing the backend server');
 		console.error(err);
 	} else {
-		console.log('Backend server has been successfully started');
+		console.info('Backend server has been successfully started');
 	}
 });

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

@@ -5,9 +5,7 @@ const db = require('../db');
 module.exports = {
 
 	index: (session, cb) => {
-		console.log(1234);
 		db.models.news.find({}).sort({ released: 'desc' }).exec((err, news) => {
-			console.log(123456, err);
 			if (err) throw err;
 			else cb({ status: 'success', data: news });
 		});

+ 28 - 34
backend/logic/actions/queueSongs.js

@@ -3,21 +3,22 @@
 const db = require('../db');
 const utils = require('../utils');
 const notifications = require('../notifications');
+const cache = require('../cache');
 const async = require('async');
 const config = require('config');
 const request = require('request');
 
-notifications.subscribe("queue.newSong", function(songId) {
-	io.to('admin.queue').emit("event:song.new", { songId });
+notifications.subscribe('queue.newSong', songId => {
+	io.to('admin.queue').emit('event:song.new', { songId });
 });
 
-notifications.subscribe("queue.removedSong", function(songId) {
-	io.to('admin.queue').emit("event:song.removed", { songId });
+notifications.subscribe('queue.removedSong', songId => {
+	io.to('admin.queue').emit('event:song.removed', { songId });
 });
 
-notifications.subscribe("queue.updatedSong", function(songId) {
+notifications.subscribe('queue.updatedSong', songId => {
 	//TODO Retrieve new Song object
-	io.to('admin.queue').emit("event:song.updated", { songId });
+	io.to('admin.queue').emit('event:song.updated', { songId });
 });
 
 module.exports = {
@@ -35,30 +36,31 @@ module.exports = {
 		//TODO Check if id and updatedSong is valid
 		db.models.queueSong.findOne({ id }, function(err, queueSong) {
 			if (err) throw err;
-			//List of properties that are allowed to be changed
-			const updatableProperties = ["id", "title", "artists", "genres", "thumbnail", "explicit", "duration", "skipDuration"];
+			// List of properties that are allowed to be changed
+			const updatableProperties = ['id', 'title', 'artists', 'genres', 'thumbnail', 'explicit', 'duration', 'skipDuration'];
 			//TODO Check if new id, if any, is already in use in queue or on rotation
 			let updated = false;
 			for (let prop in queueSong) {
-				if (updatableProperties.indexOf(prop) !== -1 && updatedSong.hasOwnProperty("prop") && updatedSong[prop] !== queueSong[prop]) {
+				if (updatableProperties.indexOf(prop) !== -1 && updatedSong.hasOwnProperty('prop') && updatedSong[prop] !== queueSong[prop]) {
 					queueSong[prop] = updatedSong[prop];
 					updated = true;
 				}
 			}
-			if (!updated) return cb({ status: 'failure', message: 'No properties changed.' });
+			if (!updated) return cb({ status: 'failure', message: 'No properties changed' });
 
 			queueSong.save((err) => {
-				if (err) return cb({ status: 'failure', message: 'Couldn\'t save to Database.' });
+				if (err) return cb({ status: 'failure', message: 'Couldn\'t save to Database' });
 
-				return cb({ status: 'success', message: 'Successfully updated the queueSong object.' });
+				return cb({ status: 'success', message: 'Successfully updated the queueSong object' });
 			});
 
 		});
 	},
 
-	remove: (session, id, cb) => {
-		//TODO Require admin/login
-		db.models.queueSong.find({ id }).remove().exec();
+	remove: (session, _id, cb) => {
+		// TODO Require admin/login
+		db.models.queueSong.find({ _id }).remove().exec();
+		cb({ status: 'success', message: 'Song was removed successfully' });
 	},
 
 	add: (session, id, cb) => {
@@ -72,7 +74,6 @@ module.exports = {
 		async.waterfall([
 			// Get YouTube data from id
 			(next) => {
-				console.log(111, id);
 				const youtubeParams = [
 					'part=snippet,contentDetails,statistics,status',
 					`id=${encodeURIComponent(id)}`,
@@ -89,24 +90,23 @@ module.exports = {
 					body = JSON.parse(body);
 
 					//TODO Clean up duration converter
-					console.log(body);
 					let dur = body.items[0].contentDetails.duration;
-					dur = dur.replace("PT", "");
+					dur = dur.replace('PT', '');
 					let durInSec = 0;
 					dur = dur.replace(/([\d]*)H/, function(v, v2) {
 						v2 = Number(v2);
 						durInSec = (v2 * 60 * 60)
-						return "";
+						return '';
 					});
 					dur = dur.replace(/([\d]*)M/, function(v, v2) {
 						v2 = Number(v2);
 						durInSec = (v2 * 60)
-						return "";
+						return '';
 					});
 					dur = dur.replace(/([\d]*)S/, function(v, v2) {
 						v2 = Number(v2);
 						durInSec += v2;
-						return "";
+						return '';
 					});
 
 					let newSong = {
@@ -126,7 +126,6 @@ module.exports = {
 				});
 			},
 			(newSong, next) => {
-				console.log(222);
 				const spotifyParams = [
 					`q=${encodeURIComponent(newSong.title)}`,
 					`type=track`
@@ -145,6 +144,7 @@ module.exports = {
 					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++) {
@@ -155,7 +155,7 @@ module.exports = {
 							}
 							if (hasArtist && newSong.title.indexOf(item.name) !== -1) {
 								newSong.duration = item.duration_ms / 1000;
-								newSong.artists = item.map(artist => {
+								newSong.artists = item.artists.map(artist => {
 									return artist.name;
 								});
 								newSong.title = item.name;
@@ -163,6 +163,7 @@ module.exports = {
 								newSong.thumbnail = item.album.images[1].url;
 								break durationArtistLoop;
 							}
+
 						}
 					}
 
@@ -170,31 +171,24 @@ module.exports = {
 				});
 			},
 			(newSong, next) => {
-				console.log(333);
 				const song = new db.models.queueSong(newSong);
 
 				song.save(err => {
 
 					if (err) {
 						console.error(err);
-						return next('Failed to add song to database.');
+						return next('Failed to add song to database');
 					}
 
 					//stations.getStation(station).playlist.push(newSong);
-
 					next(null, newSong);
 				});
 			}
 		],
 		(err, newSong) => {
-			console.log(444, err);
-			if (err) {
-				return cb({ status: 'failure', message: err });
-			}
-
-			//TODO Emit to Redis
-			notifications.emit("queue.newSong", newSong._id);
-			return cb({ status: 'success', message: 'Successfully added that song to the queue.' });
+			if (err) return cb({ status: 'error', message: err });
+			cache.pub('queue.newSong', newSong._id);
+			return cb({ status: 'success', message: 'Successfully added that song to the queue' });
 		});
 	}
 

+ 18 - 0
backend/logic/actions/reports.js

@@ -0,0 +1,18 @@
+'use strict';
+
+const db = require('../db');
+
+module.exports = {
+
+	index: (session, cb) => {
+		db.models.reports.find({}).sort({ released: 'desc' }).exec((err, reports) => {
+			if (err) throw err;
+			else cb({ status: 'success', data: reports });
+		});
+	},
+
+	add: (session, report, cb) => {
+		console.log(report);
+	}
+
+};

+ 10 - 4
backend/logic/actions/songs.js

@@ -19,17 +19,23 @@ module.exports = {
 		});
 	},
 
-	remove: (session, id, cb) => {
+	remove: (session, _id, cb) => {
 		//TODO Require admin/login
-		db.models.song.find({ id }).remove().exec();
+		db.models.song.find({ _id }).remove().exec();
 	},
 
-	add: (session, id, cb) => {
+	add: (session, song, cb) => {
 		//TODO Require admin/login
+		console.log(session.logged_in);
 		// if (!session.logged_in) return cb({ status: 'failure', message: 'You must be logged in to add a song' });
+		const newSong = new db.models.song(song);
+		newSong.save(err => {
+			if (err) throw err;
+			else cb({ status: 'success', message: 'Song has been moved from Queue' })
+		});
 		//TODO Check if video already in songs list
 		//TODO Check if video is in queue
-		//TODO Move video over, if it has the proper properties, and add the song to the appropriate stations
+		//TODO Add the song to the appropriate stations
 	}
 
 };

+ 26 - 19
backend/logic/actions/stations.js

@@ -10,6 +10,7 @@ const cache = require('../cache');
 const notifications = require('../notifications');
 const utils = require('../utils');
 const stations = require('../stations');
+
 const defaultSong = {
 	_id: '60ItHLz5WEA',
 	title: 'Faded',
@@ -21,23 +22,23 @@ const defaultSong = {
 	thumbnail: 'https://i.scdn.co/image/2ddde58427f632037093857ebb71a67ddbdec34b'
 };
 
-cache.sub('station.locked', (stationName) => {
+cache.sub('station.locked', stationName => {
 	io.to(`station.${stationName}`).emit("event:station.locked");
 });
 
-cache.sub('station.unlocked', (stationName) => {
+cache.sub('station.unlocked', stationName => {
 	io.to(`station.${stationName}`).emit("event:station.unlocked");
 });
 
-cache.sub('station.pause', (stationName) => {
+cache.sub('station.pause', stationName => {
 	io.to(`station.${stationName}`).emit("event:station.pause");
 });
 
-cache.sub('station.resume', (stationName) => {
+cache.sub('station.resume', stationName => {
 	io.to(`station.${stationName}`).emit("event:station.resume");
 });
 
-cache.sub('station.create', (stationId) => {
+cache.sub('station.create', stationId => {
 	stations.initializeAndReturnStation(stationId, (err, station) => {
 		//TODO Emit to homepage and admin station page
 		if (!err) {
@@ -122,7 +123,7 @@ module.exports = {
 		if (!session) return cb({ status: 'failure', message: 'You must be logged in to skip a song!' });
 
 		stations.initializeAndReturnStation(stationId, (err, station) => {
-
+			
 			if (err && err !== true) {
 				return cb({ status: 'error', message: 'An error occurred while skipping the station' });
 			}
@@ -173,7 +174,7 @@ module.exports = {
 		});
 	},
 
-	lock: (session, stationId, cb) => {
+	lock: (sessionId, stationId, cb) => {
 		//TODO Require admin
 		stations.initializeAndReturnStation(stationId, (err, station) => {
 			if (err && err !== true) {
@@ -187,7 +188,7 @@ module.exports = {
 		});
 	},
 
-	unlock: (session, stationId, cb) => {
+	unlock: (sessionId, stationId, cb) => {
 		//TODO Require admin
 		stations.initializeAndReturnStation(stationId, (err, station) => {
 			if (err && err !== true) {
@@ -201,12 +202,19 @@ module.exports = {
 		});
 	},
 
+	remove: (sessionId, stationId, cb) => {
+		cache.hdel('stations', stationId, () => {
+			// TODO: Update Mongo
+			return cb({ status: 'success', message: 'Station successfully removed' });
+		});
+	},
+
 	create: (sessionId, data, cb) => {
 		//TODO Require admin
 		async.waterfall([
 
 			(next) => {
-				return (data) ? next() : cb({ 'status': 'failure', 'message': 'Invalid data.' });
+				return (data) ? next() : cb({ 'status': 'failure', 'message': 'Invalid data' });
 			},
 
 			// check the cache for the station
@@ -214,27 +222,26 @@ module.exports = {
 
 			// if the cached version exist
 			(station, next) => {
-				if (station) return next({ 'status': 'failure', 'message': 'A station with that name already exists.' });
+				if (station) return next({ 'status': 'failure', 'message': 'A station with that name already exists' });
 				db.models.station.findOne({ _id: data.name }, next);
 			},
 
 			(station, next) => {
-				if (station) return next({ 'status': 'failure', 'message': 'A station with that name already exists.' });
+				if (station) return next({ 'status': 'failure', 'message': 'A station with that name already exists' });
+				const { _id, displayName, description, genres, playlist } = data;
 				db.models.station.create({
-					_id: data.name,
-					displayName: data.displayName,
-					description: data.description,
+					_id,
+					displayName,
+					description,
 					type: "official",
-					playlist: [defaultSong._id],
-					genres: ["edm"],
-					locked: true,
+					playlist,
+					genres,
 					currentSong: defaultSong
 				}, next);
 			}
 
 		], (err, station) => {
-			console.log(err, 123986);
-			if (err) return cb(err);
+			if (err) throw err;
 			stations.calculateSongForStation(station, () => {
 				cache.pub('station.create', data.name);
 				return cb(null, { 'status': 'success', 'message': 'Successfully created station.' });

+ 2 - 5
backend/logic/actions/users.js

@@ -35,9 +35,7 @@ module.exports = {
 						let userSessionId = utils.guid();
 						cache.hset('userSessions', userSessionId, cache.schemas.userSession(user._id), (err) => {
 							if (!err) {
-								console.log(sessionId, 222);
 								cache.hget('sessions', sessionId, (err, session) => {
-									console.log(err, session, 333);
 									session.userSessionId = userSessionId;
 									cache.hset('sessions', sessionId, session, (err) => {
 										next(null, { status: 'success', message: 'Login successful', user, SID: userSessionId });
@@ -84,8 +82,7 @@ module.exports = {
 			// check if the response from Google recaptcha is successful
 			// if it is, we check if a user with the requested username already exists
 			(/*response, body, */next) => {
-				/*let json = JSON.parse(body);
-				console.log(json);*/
+				/*let json = JSON.parse(body);*/
 				//if (json.success !== true) return next('Response from recaptcha was not successful');
 				db.models.user.findOne({ username }, next);
 			},
@@ -176,7 +173,7 @@ module.exports = {
 					data: {
 						_id: account._id,
 						username: account.username,
-						admin: account.admin,
+						role: account.role,
 						email: account.email.address,
 						password: '',
 						createdAt: account.createdAt,

+ 5 - 3
backend/logic/db/index.js

@@ -12,7 +12,7 @@ let lib = {
 
 		lib.connection = mongoose.connect(url).connection;
 
-		lib.connection.on('error', err => console.log('Database error: ' + err.message));
+		lib.connection.on('error', err => console.error('Database error: ' + err.message));
 
 		lib.connection.once('open', _ => {
 
@@ -21,7 +21,8 @@ let lib = {
 				queueSong: new mongoose.Schema(require(`./schemas/queueSong`)),
 				station: new mongoose.Schema(require(`./schemas/station`)),
 				user: new mongoose.Schema(require(`./schemas/user`)),
-				news: new mongoose.Schema(require(`./schemas/news`))
+				news: new mongoose.Schema(require(`./schemas/news`)),
+				reports: new mongoose.Schema(require(`./schemas/reports`))
 			};
 
 			lib.models = {
@@ -29,7 +30,8 @@ let lib = {
 				queueSong: mongoose.model('queueSong', lib.schemas.queueSong),
 				station: mongoose.model('station', lib.schemas.station),
 				user: mongoose.model('user', lib.schemas.user),
-				news: mongoose.model('news', lib.schemas.news)
+				news: mongoose.model('news', lib.schemas.news),
+				reports: mongoose.model('reports', lib.schemas.reports)
 			};
 
 			cb();

+ 6 - 0
backend/logic/db/schemas/reports.js

@@ -0,0 +1,6 @@
+module.exports = {
+	title: { type: String, required: true },
+	description: { type: String, required: true },
+	createdBy: { type: String, required: true },
+	createdAt: { type: Date, default: Date.now(), required: true }
+};

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

@@ -18,6 +18,6 @@ module.exports = {
 	timePaused: { type: Number, default: 0, required: true },
 	playlist: { type: Array, required: true },
 	genres: [{ type: String }],
-	privacy: { type: String, enum: ["public", "unlisted", "private"], default: "private" },//Used for Community stations
-	locked: { type: Boolean, default: true }//Used for Official stations
+	privacy: { type: String, enum: ["public", "unlisted", "private"], default: "private" },
+	locked: { type: Boolean, default: false }
 };

+ 14 - 27
backend/logic/io.js

@@ -21,10 +21,7 @@ module.exports = {
 			let SID = utils.cookies.parseCookies(cookies).SID;
 
 			cache.hget('userSessions', SID, (err, userSession) => {
-				console.log(err, userSession);
-				if (err) {
-					SID = null;
-				}
+				if (err) SID = null;
 				let sessionId = utils.guid();
 				cache.hset('sessions', sessionId, cache.schemas.session(SID), (err) => {
 					socket.sessionId = sessionId;
@@ -35,7 +32,7 @@ module.exports = {
 
 		this.io.on('connection', socket => {
 			socket.join("SomeRoom");
-			console.log("io: User has connected");
+			console.info('User has connected');
 
 			// catch when the socket has been disconnected
 			socket.on('disconnect', () => {
@@ -47,11 +44,11 @@ module.exports = {
 					cache.hdel('sessions', socket.sessionId);
 				}
 
-				console.log('io: User has disconnected');
+				console.info('User has disconnected');
 			});
 
 			// catch errors on the socket (internal to socket.io)
-			socket.on('error', err => console.log(err));
+			socket.on('error', err => console.error(err));
 
 			// have the socket listen for each action
 			Object.keys(actions).forEach((namespace) => {
@@ -92,31 +89,21 @@ module.exports = {
 
 			//TODO check if session is valid before returning true/false
 			if (socket.sessionId !== undefined) cache.hget('sessions', socket.sessionId, (err, session) => {
-				if (err && err !== true) {
-					socket.emit('ready', false);
-				} else if (session) {
+				if (err && err !== true) socket.emit('ready', false);
+				else if (session) {
 					if (!!session.userSessionId) {
-						cache.hget('userSessions', session.userSessionId, (err2, userSession) => {
-							if (err2 && err2 !== true) {
-								socket.emit('ready', false);
-							} else if (userSession) {
-								db.models.user.findOne({_id: userSession.userId}, (err, user) => {
+						cache.hget('userSessions', session.userSessionId, (err, userSession) => {
+							if (err && err !== true) socket.emit('ready', false);
+							else if (userSession) {
+								db.models.user.findOne({ _id: userSession.userId }, (err, user) => {
 									let role = 'default';
-									if (user) {
-										role = user.role;
-									}
+									if (user) role = user.role;
 									socket.emit('ready', true, role);
 								});
-							} else {
-								socket.emit('ready', false);
-							}
+							} else socket.emit('ready', false);
 						});
-					} else {
-						socket.emit('ready', false);
-					}
-				} else {
-					socket.emit('ready', false);
-				}
+					} else socket.emit('ready', false);
+				} else socket.emit('ready', false);
 			});
 		});
 

+ 5 - 6
backend/logic/stations.js

@@ -14,11 +14,10 @@ module.exports = {
 
 	init: function(cb) {
 		let _this = this;
-		console.log("Init stations");
 		db.models.station.find({}, (err, stations) => {
 			if (!err) {
 				stations.forEach((station) => {
-					console.log("Initing " + station._id);
+					console.info("Initializing Station: " + station._id);
 					_this.initializeAndReturnStation(station._id, (err, station) => {
 						//TODO Emit to homepage and admin station list
 					});
@@ -94,7 +93,6 @@ module.exports = {
 			/*let notification = notifications.subscribe(`stations.nextSong?id=${station._id}`, () => {*/
 			function skipSongTemp() {
 				// get the station from the cache
-				console.log('NOTIFICATION');
 				//TODO Recalculate songs if the last song of the station playlist is getting played
 				cache.hget('stations', station._id, (err, station) => {
 					if (station) {
@@ -104,7 +102,7 @@ module.exports = {
 							(next) => {
 								if (station.currentSongIndex < station.playlist.length - 1) {
 									station.currentSongIndex++;
-									db.models.song.findOne({_id: station.playlist[station.currentSongIndex]}, (err, song) => {
+									db.models.song.findOne({ _id: station.playlist[station.currentSongIndex] }, (err, song) => {
 										if (!err) {
 											station.currentSong = {
 												_id: song._id,
@@ -123,9 +121,10 @@ module.exports = {
 								} else {
 									station.currentSongIndex = 0;
 									_this.calculateSongForStation(station, (err, newPlaylist) => {
+										console.log('New playlist: ', newPlaylist)
 										if (!err) {
-											db.models.song.findOne({_id: newPlaylist[0]}, (err, song) => {
-												if (!err) {
+											db.models.song.findOne({ _id: newPlaylist[0] }, (err, song) => {
+												if (song) {
 													station.currentSong = {
 														_id: song._id,
 														title: song.title,

+ 8 - 6
frontend/App.vue

@@ -58,22 +58,24 @@
 		events: {
 			'register': function () {
 				let { register: { email, username, password } } = this;
+				let _this = this;
 				this.socket.emit('users.register', username, email, password, /*grecaptcha.getResponse()*/null, result => {
 					Toast.methods.addToast(`User ${username} has been registered`, 2000);
-					setTimeout(location.reload(), 2500);
+					_this.$router.go('/');
+					location.reload();
 				});
 			},
 			'login': function () {
 				let { login: { email, password } } = this;
-
+				let _this = this;
 				this.socket.emit('users.login', email, password, result => {
-					console.log(result);
 					if (result.status === 'success') {
 						let date = new Date();
-						date.setTime(new Date().getTime() + (2*365*24*60*60*1000));
-						document.cookie = "SID=" + result.SID + "; expires="+ date.toGMTString() +"; path=/";
+						date.setTime(new Date().getTime() + (2 * 365 * 24 * 60 * 60 * 1000));
+						document.cookie = `SID=${result.SID}; expires=${date.toGMTString()}; path=/`;
 						Toast.methods.addToast(`You have been successfully logged in`, 2000);
-						setTimeout(location.reload(), 2500);
+						_this.$router.go('/');
+						location.reload();
 					} else {
 						Toast.methods.addToast(result.message, 2000);
 					}

+ 113 - 29
frontend/components/Admin/QueueSongs.vue

@@ -1,74 +1,158 @@
 <template>
-	<div class="columns is-mobile">
-		<div class="column is-8-desktop is-offset-2-desktop is-12-mobile">
-			<table class="table is-striped">
+	<div class='columns is-mobile'>
+		<div class='column is-8-desktop is-offset-2-desktop is-12-mobile'>
+			<table class='table is-striped'>
 				<thead>
 					<tr>
-						<td>YouTube ID</td>
 						<td>Title</td>
-						<td>Thumbnail</td>
+						<td>YouTube ID</td>
 						<td>Artists</td>
+						<td>Genres</td>
 						<td>Options</td>
 					</tr>
 				</thead>
 				<tbody>
-					<tr v-for="(index, song) in songs" track-by="$index">
+					<tr v-for='(index, song) in songs' track-by='$index'>
 						<td>
-							<p class="control">
-								<input class="input" type="text" :value="song._id" v-model="song._id">
+							<p class='control'>
+								<input class='input' type='text' v-model='song.title'>
 							</p>
 						</td>
 						<td>
-							<p class="control">
-								<input class="input" type="text" :value="song.title" v-model="song.title">
+							<p class='control'>
+								<input class='input' type='text' v-model='song._id'>
 							</p>
 						</td>
 						<td>
-							<p class="control">
-								<input class="input" type="text" :value="song.thumbnail" v-model="song.thumbnail">
-							</p>
+							<div class='control'>
+								<input v-for='artist in song.artists' track-by='$index' class='input' type='text' v-model='artist'>
+							</div>
 						</td>
 						<td>
-							<div class="control">
-								<input v-for="artist in song.artists" track-by="$index" class="input" type="text" :value="artist" v-model="artist">
+							<div class='control'>
+								<input v-for='genre in song.genres' track-by='$index' class='input' type='text' v-model='genre'>
 							</div>
 						</td>
 						<td>
-							<a class="button is-danger" @click="remove(song, index)">Remove</a>
-							<a class="button is-success" @click="update(song)">Save Changes</a>
+							<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>
 				</tbody>
 			</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'><strong>Thumbnail Preview</strong></h5>
+				<img class='thumbnail-preview' :src='beingEdited.song.thumbnail'>
+				<label class='label'>Thumbnail URL</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='beingEdited.song.thumbnail'>
+				</p>
+				<h5 class='has-text-centered'><strong>Edit Info</strong></h5>
+				<!--<label class='label'>Email</label>
+				<p class='control'>
+					<input class='input' type='text' placeholder='Email...' v-model='$parent.register.email'>
+				</p>-->
+				<label class='label'>Song ID</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='beingEdited.song._id'>
+				</p>
+				<label class='label'>Song Title</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='beingEdited.song.title'>
+				</p>
+				<label class='label'>Artists</label>
+				<div class='control'>
+					<input v-for='artist in beingEdited.song.artists' track-by='$index' class='input' type='text' v-model='artist'>
+				</div>
+				<label class='label'>Genres</label>
+				<div class='control'>
+					<input v-for='genre in beingEdited.song.genres' track-by='$index' class='input' type='text' v-model='genre'>
+				</div>
+				<label class='label'>Song Duration</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='beingEdited.song.duration'>
+				</p>
+				<label class='label'>Skip Duration</label>
+				<p class='control'>
+					<input class='input' type='text' v-model='beingEdited.song.skipDuration'>
+				</p>
+			</section>
+			<footer class='modal-card-foot'>
+				<button class='delete' @click='toggleModal()'></button>
+			</footer>
+		</div>
+	</div>
 </template>
 
 <script>
+	import { Toast } from 'vue-roaster';
+
 	export default {
 		data() {
 			return {
-				songs: []
+				songs: [],
+				isEditActive: false,
+				beingEdited: {
+					index: 0,
+					song: {}
+				}
 			}
 		},
 		methods: {
-			update (song) {
-				this.socket.emit('queueSongs.update', song);
+			toggleModal: function () {
+				this.isEditActive = !this.isEditActive;
 			},
-			remove (songId) {
-				this.socket.emit('queueSongs.remove', songId);
+			edit: function (song, index) {
+				this.beingEdited = { index, song };
+				this.isEditActive = true;
+			},
+			add: function (song) {
+				this.socket.emit('queueSongs.remove', song._id);
+				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('queueSongs.remove', id, res => {
+					if (res.status == 'success') Toast.methods.addToast(res.message, 2000);
+				});
 			}
 		},
-		ready: function() {
+		ready: function () {
 			let _this = this;
-			_this.socket = _this.$parent.$parent.socket;
-			_this.socket.emit('queueSongs.index', (data) => {
-				console.log(data);
-				_this.songs = data;
-			});
+			let socketInterval = setInterval(() => {
+				if (!!_this.$parent.$parent.socket) {
+					_this.socket = _this.$parent.$parent.socket;
+					_this.socket.emit('queueSongs.index', data => {
+						_this.songs = data;
+					});
+					clearInterval(socketInterval);
+				}
+			}, 100);
 		}
 	}
 </script>
 
-<style lang="scss" scoped>
+<style lang='scss' scoped>
+	.thumbnail-preview {
+		display: flex;
+		margin: 0 auto;
+		padding: 10px 0 20px 0;
+	}
+
+	.modal-card-body, .modal-card-foot {
+		border-top: 0;
+		background-color: rgb(66, 165, 245);
+	}
+
+	.label, strong {
+		color: #fff;
+	}
 </style>

+ 56 - 22
frontend/components/Admin/Stations.vue

@@ -15,7 +15,7 @@
 					<tr v-for="(index, station) in stations" track-by="$index">
 						<td>
 							<p class="control">
-								<input class="input" type="text" :value="station.id" v-model="station.id">
+								<input class="input" type="text" :value="station.id" v-model="station._id">
 							</p>
 						</td>
 						<td>
@@ -34,7 +34,7 @@
 							</p>
 						</td>
 						<td>
-							<a class="button is-danger" @click="stations.splice(index, 1)">Remove</a>
+							<a class="button is-danger" @click="removeStation(index)">Remove</a>
 						</td>
 					</tr>
 				</tbody>
@@ -50,14 +50,13 @@
 				</header>
 				<div class="card-content">
 					<div class="content">
-						<label class="label">Name</label>
 						<div class="control is-horizontal">
 							<div class="control is-grouped">
 								<p class="control is-expanded">
-									<input class="input" type="text" placeholder="Locale name" v-model="newStation.name">
+									<input class="input" type="text" placeholder="Unique Identifier" v-model="newStation._id">
 								</p>
 								<p class="control is-expanded">
-									<input class="input" type="text" placeholder="Display name" v-model="newStation.displayName">
+									<input class="input" type="text" placeholder="Display Name" v-model="newStation.displayName">
 								</p>
 							</div>
 						</div>
@@ -65,14 +64,16 @@
 						<p class="control is-expanded">
 							<input class="input" type="text" placeholder="Short description" v-model="newStation.description">
 						</p>
+						<label class="label">Default Song</label>
+						<p class="control is-expanded">
+							<input class="input" type="text" placeholder="YouTube ID" v-model="newStation.defaultSong">
+						</p>
 						<label class="label">Genres</label>
 						<p class="control has-addons">
-							<input class="input" type="text" placeholder="Genre" v-model="newStationGenre">
-							<a class="button is-info">Add genre</a>
+							<input class="input" id="new-genre" type="text" placeholder="Genre">
+							<a class="button is-info" @click="addGenre()">Add genre</a>
 						</p>
-						<span class="tag is-info">Bar<button class="delete is-info"></button></span>
-						<span class="tag is-info">Bar<button class="delete is-info"></button></span>
-						<span class="tag is-info">Bar<button class="delete is-info"></button></span>
+						<span class="tag is-info" v-for="genre in newStation.genres" track-by="$index">{{ genre }}<button class="delete is-info"></button></span>
 					</div>
 				</div>
 				<footer class="card-footer">
@@ -84,11 +85,15 @@
 </template>
 
 <script>
+	import { Toast } from 'vue-roaster';
+
 	export default {
 		data() {
 			return {
 				stations: [],
-				newStation: {}
+				newStation: {
+					genres: []
+				}
 			}
 		},
 		methods: {
@@ -113,22 +118,49 @@
 			// 		console.log(data);
 			// 	});
 			// },
-			createStation: function() {
+			createStation: function () {
 				let _this = this;
-				let { newStation: { name, displayName, description } } = this;
-				let data = { name, displayName, description, genres: ['edm'] };
-				_this.socket.emit('stations.create', data, result => {
+				let { newStation: { _id, displayName, description, genres } } = this;
+
+				let playlist = [];
+				playlist.push(this.newStation.defaultSong);
+
+				if (_id == undefined) return Toast.methods.addToast('Field (YouTube ID) cannot be empty', 2000);
+				if (displayName == undefined) return Toast.methods.addToast('Field (Display Name) cannot be empty', 2000);
+				if (description == undefined) return Toast.methods.addToast('Field (Description) cannot be empty', 2000);
+
+				_this.socket.emit('stations.create', {
+					_id,
+					type: "official",
+					displayName,
+					description,
+					playlist,
+					genres,
+				}, result => {
 					console.log(result);
 				});
+			},
+			removeStation: function (index) {
+				this.socket.emit('stations.remove', this.stations[index]._id, res => {
+					if (res.status == 'success') this.stations.splice(index, 1); Toast.methods.addToast(res.message, 2000);
+				});
+			},
+			addGenre: function () {
+				if ($("#new-genre").val() !== "") this.newStation.genres.push($("#new-genre").val());
+				else Toast.methods.addToast('Genre cannot be empty', 2000);
 			}
 		},
-		ready: function() {
-			let socket = this.socket = this.$parent.$parent.socket;
-			socket.emit("stations.index", (data) => {
-				console.log(data);
-				this.stations = data;
-			});
-			console.log('ready');
+		ready: function () {
+			let _this = this;
+			let socketInterval = setInterval(() => {
+				if (!!_this.$parent.$parent.socket) {
+					_this.socket = _this.$parent.$parent.socket;
+					_this.socket.emit("stations.index", data => {
+						_this.stations = data.stations;
+					});
+					clearInterval(socketInterval);
+				}
+			}, 100);
 		}
 	}
 </script>
@@ -137,4 +169,6 @@
 	.is-success {
 		width: 100%;
 	}
+
+	.tag:not(:last-child) { margin-right: 5px; }
 </style>

+ 0 - 11
frontend/components/MainHeader.vue

@@ -44,20 +44,9 @@
 
 <script>
 	export default {
-		data() {
-			return {
-
-			}
-		},
-		ready: function() {
-			let _this = this;
-		},
 		methods: {
 			toggleModal: function (type) {
 				this.$dispatch('toggleModal', type);
-			},
-			isAdmin: function() {
-				_this = this;
 			}
 		}
 	}

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

@@ -68,7 +68,7 @@
 							</td>
 							<td>{{result.title}}</td>
 							<td>
-								<a class="button is-success" @click="addSongToQueue(result)">
+								<a class="button is-success" @click="addSongToQueue(result.id)">
 									Add
 								</a>
 							</td>
@@ -81,6 +81,8 @@
 </template>
 
 <script>
+	import { Toast } from 'vue-roaster';
+
 	import StationHeader from './StationHeader.vue';
 
 	export default {
@@ -105,7 +107,6 @@
 			},
 			youtubeReady: function() {
 				let local = this;
-				console.log(123457)
 				local.player = new YT.Player("player", {
 					height: 270,
 					width: 480,
@@ -113,7 +114,6 @@
 					playerVars: {controls: 1, iv_load_policy: 3, rel: 0, showinfo: 0},
 					events: {
 						'onReady': function(event) {
-							console.log(4540590459);
 							local.playerReady = true;
 							let volume = parseInt(localStorage.getItem("volume"));
 							volume = (typeof volume === "number") ? volume : 20;
@@ -124,11 +124,9 @@
 							local.playVideo();
 						},
 						'onStateChange': function(event) {
-							console.log(event);
 							if (event.data === 1 && local.videoLoading === true) {
 								local.videoLoading = false;
 								local.player.seekTo(local.getTimeElapsed() / 1000, true);
-								console.log(local.paused);
 								if (local.paused) {
 									local.player.pauseVideo();
 								}
@@ -185,8 +183,8 @@
 				let duration = (Date.now() - local.startedAt - local.timePaused) / 1000;
 				let songDuration = local.currentSong.duration;
 				if (songDuration <= duration) {
-					console.log("PAUSE!");
-					console.log(songDuration, duration);
+					// console.log("PAUSE!");
+					// console.log(songDuration, duration);
 					local.player.pauseVideo();
 				}
 
@@ -221,12 +219,11 @@
 				}
 			},
 			addSongToQueue: function(songId) {
-				console.log('add', songId);
 				let local = this;
-				local.socket.emit('queueSongs.add', songId, function(data) {
-					if (data) console.log(data);
+				local.socket.emit('queueSongs.add', songId, res => {
+					if (res.status == 'success') Toast.methods.addToast(res.message, 2000);
 				});
-			}/*,
+			},
 			submitQuery: function() {
 				let local = this;
 				local.socket.emit('apis.searchYoutube', local.querySearch, function(results) {
@@ -241,7 +238,7 @@
 						});
 					}
 				});
-			}*/
+			}
 		},
 		ready: function() {
 			let _this = this;
@@ -252,7 +249,6 @@
 			_this.socket = _this.$parent.socket;
 
 			_this.socket.emit('stations.join', _this.stationId, data => {
-				console.log(data);
 				if (data.status === "success") {
 					_this.currentSong = data.currentSong;
 					_this.startedAt = data.startedAt;
@@ -266,7 +262,6 @@
 			});
 
 			_this.socket.on('event:songs.next', data => {
-				console.log("NEXT SONG");
 				_this.currentSong = data.currentSong;
 				_this.startedAt = data.startedAt;
 				_this.paused = data.paused;

+ 47 - 50
frontend/components/User/Settings.vue

@@ -1,53 +1,51 @@
 <template>
-	<div v-if="isLoggedIn">
-		<main-header></main-header>
-		<div class="container">
-			<!--Implement Validation-->
-			<label class="label">Username</label>
-			<div class="control is-grouped">
-				<p class="control is-expanded has-icon has-icon-right">
-					<input class="input is-success" type="text" placeholder="Change username" v-model="user.username">
-					<!--Remove validation if it's their own without changing-->
-					<i class="fa fa-check"></i>
-					<span class="help is-success">This username is available</span>
-				</p>
-				<p class="control">
-					<button class="button is-success" @click="changeUsername()">Save Changes</button>
-				</p>
-			</div>
-			<label class="label">Email</label>
-			<div class="control is-grouped">
-				<p class="control is-expanded has-icon has-icon-right">
-					<input class="input is-danger" type="text" placeholder="Change email address" v-model="user.email.address">
-					<!--Remove validation if it's their own without changing-->
-					<i class="fa fa-warning"></i>
-					<span class="help is-danger">This email is invalid</span>
-				</p>
-				<p class="control is-expanded">
-					<button class="button is-success" @click="changeEmail()">Save Changes</button>
-				</p>
-			</div>
-			<label class="label">Change Password</label>
-			<div class="control is-grouped">
-				<p class="control is-expanded has-icon has-icon-right">
-					<input class="input is-danger" type="text" placeholder="Enter current password" v-model="currentPassword">
-					<!-- Check if correct -->
-					<i class="fa fa-warning"></i>
-					<span class="help is-danger">This password is invalid</span>
-				</p>
-				<p class="control is-expanded has-icon has-icon-right">
-					<input class="input is-danger" type="text" placeholder="Enter new password" v-model="newPassword">
-					<!--Check if longer than x chars, has x, x and x. Kris likes x too ;)-->
-					<i class="fa fa-warning"></i>
-					<span class="help is-danger">This password is invalid</span>
-				</p>
-				<p class="control is-expanded">
-					<button class="button is-success" @click="changePassword()">Save Changes</button>
-				</p>
-			</div>
+	<main-header></main-header>
+	<div class="container">
+		<!--Implement Validation-->
+		<label class="label">Username</label>
+		<div class="control is-grouped">
+			<p class="control is-expanded has-icon has-icon-right">
+				<input class="input is-success" type="text" placeholder="Change username" v-model="user.username">
+				<!--Remove validation if it's their own without changing-->
+				<i class="fa fa-check"></i>
+				<span class="help is-success">This username is available</span>
+			</p>
+			<p class="control">
+				<button class="button is-success" @click="changeUsername()">Save Changes</button>
+			</p>
+		</div>
+		<label class="label">Email</label>
+		<div class="control is-grouped">
+			<p class="control is-expanded has-icon has-icon-right">
+				<input class="input is-danger" type="text" placeholder="Change email address" v-model="user.email.address">
+				<!--Remove validation if it's their own without changing-->
+				<i class="fa fa-warning"></i>
+				<span class="help is-danger">This email is invalid</span>
+			</p>
+			<p class="control is-expanded">
+				<button class="button is-success" @click="changeEmail()">Save Changes</button>
+			</p>
+		</div>
+		<label class="label">Change Password</label>
+		<div class="control is-grouped">
+			<p class="control is-expanded has-icon has-icon-right">
+				<input class="input is-danger" type="text" placeholder="Enter current password" v-model="currentPassword">
+				<!-- Check if correct -->
+				<i class="fa fa-warning"></i>
+				<span class="help is-danger">This password is invalid</span>
+			</p>
+			<p class="control is-expanded has-icon has-icon-right">
+				<input class="input is-danger" type="text" placeholder="Enter new password" v-model="newPassword">
+				<!--Check if longer than x chars, has x, x and x. Kris likes x too ;)-->
+				<i class="fa fa-warning"></i>
+				<span class="help is-danger">This password is invalid</span>
+			</p>
+			<p class="control is-expanded">
+				<button class="button is-success" @click="changePassword()">Save Changes</button>
+			</p>
 		</div>
-		<main-footer></main-footer>
 	</div>
+	<main-footer></main-footer>
 </template>
 
 <script>
@@ -63,8 +61,7 @@
 			return {
 				currentPassword: '',
 				newPassword: '',
-				user: {},
-				isLoggedIn: false,
+				user: {}
 			}
 		},
 		ready: function() {
@@ -73,7 +70,7 @@
 				if (!!_this.$parent.socket) {
 					_this.socket = _this.$parent.socket;
 					_this.socket.emit('users.findBySession', res => {
-						if (res.status == 'success') { _this.user = res.data; _this.isLoggedIn = true; } else {
+						if (res.status == 'success') { _this.user = res.data; } else {
 							_this.$parent.isLoginActive = true;
 							Toast.methods.addToast('Your are currently not signed in', 3000);
 						}

+ 4 - 4
frontend/components/User/Show.vue

@@ -5,8 +5,8 @@
 			<img class="avatar" src="https://avatars2.githubusercontent.com/u/11198912?v=3&s=460"/>
 			<h2 class="has-text-centered">@{{user.username}}</h2>
 			<div class="admin-functionality">
-				<a class="button is-small is-info is-outlined" @click="changeRank('admin')" v-if="!user.admin">Promote to Admin</a>
-				<a class="button is-small is-danger is-outlined" @click="changeRank('user')" v-else>Demote to User</a>
+				<a class="button is-small is-info is-outlined" @click="changeRank('admin')" v-if="user.role == 'default'">Promote to Admin</a>
+				<a class="button is-small is-danger is-outlined" @click="changeRank('default')" v-else>Demote to User</a>
 			</div>
 			<nav class="level">
 				<div class="level-item has-text-centered">
@@ -46,9 +46,9 @@
 		},
 		methods: {
 			changeRank(newRank) {
-				this.socket.emit('users.update', this.user._id, 'admin', ((newRank == 'admin') ? true : false), res => {
+				this.socket.emit('users.update', this.user._id, 'role', ((newRank == 'admin') ? 'admin' : 'default'), res => {
 					if (res.status == 'error') Toast.methods.addToast(res.message, 2000);
-					else Toast.methods.addToast(`User ${this.$route.params.username}'s rank has been changed to: ${newRank}`, 2000);
+					else this.user.role = newRank; Toast.methods.addToast(`User ${this.$route.params.username}'s rank has been changed to: ${newRank}`, 2000);
 				});
 			}
 		},

+ 3 - 4
frontend/components/pages/News.vue

@@ -50,7 +50,7 @@
 	export default {
 		components: { MainHeader, MainFooter },
 		methods: {
-			formatDate: (unix) => {
+			formatDate: unix => {
 				return moment(unix).format("DD-MM-YYYY");
 			},
 		},
@@ -62,9 +62,8 @@
 		ready: function () {
 			let _this = this;
 			let socket = this.socket = this.$parent.socket;
-			socket.emit("news.index", function(result) {
-				_this.news = result.data;
-				console.log(_this.news)
+			socket.emit("news.index", res => {
+				_this.news = res.data;
 			});
 		}
 	}

+ 9 - 10
frontend/main.js

@@ -10,6 +10,7 @@ import Admin from './components/pages/Admin.vue';
 import News from './components/pages/News.vue';
 import User from './components/User/Show.vue';
 import Settings from './components/User/Settings.vue';
+import Login from './components/Modals/Login.vue';
 
 Vue.use(VueRouter);
 
@@ -17,7 +18,6 @@ let router = new VueRouter({ history: true });
 let _this = this;
 
 lofig.folder = '../config/default.json';
-
 lofig.get('socket.url', function(res) {
 	let socket = window.socket = io(window.location.protocol + '//' + res);
 	socket.on("ready", (status, role) => {
@@ -25,19 +25,15 @@ lofig.get('socket.url', function(res) {
 	});
 });
 
-router.beforeEach((transition) => {
+router.beforeEach(transition => {
 	if (transition.to.loginRequired || transition.to.adminRequired) {
 		auth.getStatus((authenticated, role) => {
-			if (transition.to.loginRequired && !authenticated) {
-				transition.redirect('/login')
-			} else if (transition.to.adminRequired && role !== 'admin') {
-				transition.redirect('/adminRequired');
-			} else {
-				transition.next();
-			}
+			if (transition.to.loginRequired && !authenticated) transition.redirect('/login');
+			else if (transition.to.adminRequired && role !== 'admin') transition.redirect('/');
+			else transition.next();
 		});
 	} else {
-		transition.next()
+		transition.next();
 	}
 });
 
@@ -58,6 +54,9 @@ router.map({
 		component: Settings,
 		loginRequired: true
 	},
+	'/login': {
+		component: Login
+	},
 	'/admin': {
 		component: Admin,
 		adminRequired: true