浏览代码

Huge backend architecture change.

Removed the old "handler" stuff. Everything has been separated out into nice modular files. The front-end has been updated to reflect this. Implemented a temporary cache system similar to what we may do with Redis (which will get implemented soon).
Cameron Kline 8 年之前
父节点
当前提交
22c3b27c50

+ 0 - 73
backend/app.js

@@ -1,73 +0,0 @@
-'use strict';
-
-// nodejs modules
-const path = require('path');
-const fs   = require('fs');
-const os   = require('os');
-
-process.env.NODE_CONFIG_DIR = `${__dirname}/config`;
-
-// npm modules
-const express          = require('express'),
-      session          = require('express-session'),
-      mongoose         = require('mongoose'),
-      bodyParser       = require('body-parser'),
-      config           = require('config'),
-      cookieParser     = require('cookie-parser'),
-      cors             = require('cors'),
-      request          = require('request'),
-      bcrypt           = require('bcrypt'),
-      passportSocketIo = require("passport.socketio");
-
-// global module
-const globals = require('./logic/globals');
-
-// database
-globals.db.connection = mongoose.connect('mongodb://mongo:27017/musare').connection;
-
-globals.db.connection.on('error', err => console.log('Database error: ' + err.message));
-
-globals.db.connection.once('open', _ => {
-
-	console.log('Connected to database');
-
-	const app = express(globals.db.connection);
-	const server = app.listen(80);
-
-	globals.io = require('socket.io')(server);
-
-	// load all the schemas from the schemas folder into an object
-	globals.db.models =
-		// get an array of all the files in the schemas folder
-		fs.readdirSync(`${__dirname}/schemas`)
-		// remove the .js from the file names
-		.map(name => name.split('.').shift())
-		// create an object with
-		.reduce((db, name) => {
-			db[name] = mongoose.model(name, new mongoose.Schema(require(`${__dirname}/schemas/${name}`)));
-			return db;
-		}, {});
-
-	globals.db.store = new (require('connect-mongo')(session))({ 'mongooseConnection': globals.db.connection });
-
-	app.use(session({
-		secret: config.get('secret'),
-		key: 'connect.sid',
-		store: globals.db.store,
-		resave: true,
-		saveUninitialized: true,
-		cookie: { httpOnly: false }
-	}));
-
-	app.use(bodyParser.json());
-	app.use(bodyParser.urlencoded({ extended: true }));
-
-	let corsOptions = Object.assign({}, config.get('cors'));
-
-	app.use(cors(corsOptions));
-	app.options('*', cors(corsOptions));
-
-	const coreHandler = require('./logic/coreHandler');
-	require('./logic/socketHandler')(coreHandler, globals.io);
-	require('./logic/expressHandler')(coreHandler, app);
-});

+ 69 - 0
backend/index.js

@@ -0,0 +1,69 @@
+'use strict';
+
+process.env.NODE_CONFIG_DIR = `${__dirname}/config`;
+
+const async = require('async');
+
+const db = require('./logic/db');
+const app = require('./logic/app');
+const io = require('./logic/io');
+const cache = require('./logic/cache');
+const scheduler = require('./logic/scheduler');
+
+// setup our cache with the tables we need
+cache.addTable('sessions');
+cache.addTable('stations');
+
+async.waterfall([
+
+	// connect to our database
+	(next) => db.init('mongodb://mongo:27017/musare', next),
+
+	// load all the stations from the database into the cache (we won't actually do this in the future)
+	(next) => {
+
+		// this represents data directly from the DB (for now, lets just add some dummy stations)
+		let stations = [
+			{
+				id: '7dbf25fd-b10d-6863-2f48-637f6014b162',
+				name: 'edm',
+				genres: ['edm'],
+				displayName: 'EDM',
+				description: 'EDM Music',
+				playlist: [
+					'gCYcHz2k5x0'
+				]
+			},
+			{
+				id: '79cedff1-5341-7f0e-6542-50491c4797b4',
+				genres: ['chill'],
+				displayName: 'Chill',
+				description: 'Chill Music',
+				playlist: [
+					'gCYcHz2k5x0'
+				]
+			}
+		];
+
+		stations.forEach((station) => {
+			// add the station to the cache, adding the temporary data
+			cache.addRow('stations', Object.assign(station, {
+				skips: 0,
+				userCount: 0,
+				currentSongIndex: 0,
+				paused: false
+			}));
+		});
+
+		next();
+	},
+
+	// setup the express server (not used right now, but may be used for OAuth stuff later, or for an API)
+	(next) => app.init(next),
+
+	// setup the socket.io server (all client / server communication is done over this)
+	(next) => io.init(next)
+], (err) => {
+	if (err) return console.error(err);
+	console.log('Backend has been started');
+});

+ 28 - 0
backend/logic/actions/apis.js

@@ -0,0 +1,28 @@
+'use strict';
+
+const request = require('request');
+
+module.exports = {
+
+	searchYoutube: (session, query, cb) => {
+
+		const params = [
+			'part=snippet',
+			`q=${encodeURIComponent(query)}`,
+			`key=${config.get('apis.youtube.key')}`,
+			'type=video',
+			'maxResults=15'
+		].join('&');
+
+		request(`https://www.googleapis.com/youtube/v3/search?${params}`, (err, res, body) => {
+
+			if (err) {
+				console.error(err);
+				return cb({ status: 'error', message: 'Failed to search youtube with the requested query' });
+			}
+
+			cb({ status: 'success', data: JSON.parse(body) });
+		});
+	}
+
+};

+ 8 - 0
backend/logic/actions/index.js

@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = {
+	apis: require('./apis'),
+	songs: require('./songs'),
+	stations: require('./stations'),
+	users: require('./users')
+};

+ 25 - 0
backend/logic/actions/songs.js

@@ -0,0 +1,25 @@
+'use strict';
+
+const db = require('../db');
+
+module.exports = {
+
+	index: (session, cb) => {
+		db.models.song.find({}, (err, songs) => {
+			if (err) throw err;
+			cb(songs);
+		});
+	},
+
+	update: (session, song, cb) => {
+		db.models.song.findOneAndUpdate({ id: song.id }, song, { upsert: true }, (err, updatedSong) => {
+			if (err) throw err;
+			cb(updatedSong);
+		});
+	},
+
+	remove: (session, song, cb) => {
+		db.models.song.find({ id: song.id }).remove().exec();
+	}
+
+};

+ 188 - 0
backend/logic/actions/stations.js

@@ -0,0 +1,188 @@
+'use strict';
+
+const async = require('async');
+const request = require('request');
+
+const db = require('../db');
+const cache = require('../cache');
+const utils = require('../utils');
+
+module.exports = {
+
+	index: (session, cb) => {
+		cb(cache.getAllRows('stations'));
+	},
+
+	join: (session, stationId, cb) => {
+
+		async.waterfall([
+
+			// first check the cache for the station
+			(next) => next(null, cache.findRow('stations', 'id', stationId)),
+
+			// if the cached version exist use it, otherwise check the DB for it
+			(station, next) => {
+				if (station) return next(true, station);
+				db.models.station.find({ id: stationId }, next);
+			},
+
+			// add the station from the DB to the cache, adding the temporary data
+			(station, next) => {
+				cache.addRow('stations', Object.assign(station, {
+					skips: 0,
+					userCount: 0,
+					currentSongIndex: 0,
+					paused: false
+				}));
+				next(null, station);
+			}
+
+		], (err, station) => {
+
+			if (err && err !== true) {
+				return cb({ status: 'error', message: 'An error occurred while joining the station' });
+			}
+
+			if (station) {
+				if (session) session.stationId = stationId;
+				station.userCount++;
+				cb({ status: 'success', userCount: station.userCount });
+			}
+			else {
+				cb({ status: 'failure', message: `That station doesn't exist` });
+			}
+		});
+	},
+
+	skip: (session, cb) => {
+
+		if (!session) return cb({ status: 'failure', message: 'You must be logged in to skip a song!' });
+
+		async.waterfall([
+
+			// first check the cache for the station
+			(next) => next(null, cache.findRow('stations', 'id', session.stationId)),
+
+			// if the cached version exist use it, otherwise check the DB for it
+			(station, next) => {
+				if (station) return next(true, station);
+				db.models.station.find({ id: session.stationId }, next);
+			},
+
+			// add the station from the DB to the cache, adding the temporary data
+			(station, next) => {
+				cache.addRow('stations', Object.assign(station, {
+					skips: 0,
+					userCount: 0,
+					currentSongIndex: 0,
+					paused: false
+				}));
+				next(null, station);
+			}
+
+		], (err, station) => {
+
+			if (err && err !== true) {
+				return cb({ status: 'error', message: 'An error occurred while skipping the station' });
+			}
+
+			if (station) {
+				station.skips++;
+				if (station.skips > Math.floor(station.userCount / 2)) {
+					// TODO: skip to the next song if the skips is higher then half the total users
+				}
+				cb({ status: 'success', skips: station.skips });
+			}
+			else {
+				cb({ status: 'failure', message: `That station doesn't exist` });
+			}
+		});
+	},
+
+	// leaves the users current station
+	// returns the count of users that are still in that station
+	leave: (session, cb) => {
+
+		// if they're not logged in, we don't need to do anything below
+		if (!session) return cb({ status: 'success' });
+
+		async.waterfall([
+
+			// first check the cache for the station
+			(next) => next(null, cache.findRow('stations', 'id', session.stationId)),
+
+			// if the cached version exist use it, otherwise check the DB for it
+			(station, next) => {
+				if (station) return next(true, station);
+				db.models.station.find({ id: session.stationId }, next);
+			},
+
+			// add the station from the DB to the cache, adding the temporary data
+			(station, next) => {
+				cache.addRow('stations', Object.assign(station, {
+					skips: 0,
+					userCount: 0,
+					currentSongIndex: 0,
+					paused: false
+				}));
+				next(null, station);
+			}
+
+		], (err, station) => {
+
+			if (err && err !== true) {
+				return cb({ status: 'error', message: 'An error occurred while leaving the station' });
+			}
+
+			session.stationId = null;
+
+			if (station) {
+				station.userCount--;
+				cb({ status: 'success', userCount: station.userCount });
+			}
+			else {
+				cb({ status: 'failure', message: `That station doesn't exist` });
+			}
+		});
+	},
+
+	addSong: (session, station, song, cb) => {
+
+		// if (!session.logged_in) return cb({ status: 'failure', message: 'You must be logged in to add a song' });
+
+		const params = [
+			'part=snippet,contentDetails,statistics,status',
+			`id=${encodeURIComponent(song.id)}`,
+			`key=${config.get('apis.youtube.key')}`
+		].join('&');
+
+		request(`https://www.googleapis.com/youtube/v3/videos?${params}`, (err, res, body) => {
+
+			if (err) {
+				console.error(err);
+				return cb({ status: 'error', message: 'Failed to find song from youtube' });
+			}
+
+			const newSong = new db.models.song({
+				id: json.items[0].id,
+				title: json.items[0].snippet.title,
+				duration: utils.convertTime(json.items[0].contentDetails.duration),
+				thumbnail: json.items[0].snippet.thumbnails.high.url
+			});
+
+			// save the song to the database
+			newSong.save(err => {
+
+				if (err) {
+					console.error(err);
+					return cb({ status: 'error', message: 'Failed to save song from youtube to the database' });
+				}
+
+				stations.getStation(station).playlist.push(newSong);
+
+				cb({ status: 'success', data: stations.getStation(station.playlist) });
+			});
+		});
+	}
+
+};

+ 159 - 0
backend/logic/actions/users.js

@@ -0,0 +1,159 @@
+'use strict';
+
+const async = require('async');
+const config = require('config');
+const request = require('request');
+const bcrypt = require('bcrypt');
+
+const db = require('../db');
+const cache = require('../cache');
+const utils = require('../utils');
+
+module.exports = {
+
+	login: (session, identifier, password, cb) => {
+
+		async.waterfall([
+
+			// check if a user with the requested identifier exists
+			(next) => db.models.user.findOne({
+				$or: [{ 'username': identifier }, { 'email.address': identifier }]
+			}, next),
+
+			// if the user doesn't exist, respond with a failure
+			// otherwise compare the requested password and the actual users password
+			(user, next) => {
+				if (!user) return next(true, { status: 'failure', message: 'User not found' });
+				bcrypt.compare(password, user.services.password.password, (err, match) => {
+
+					if (err) return next(err);
+
+					// if the passwords match
+					if (match) {
+
+						// store the session in the cache
+						cache.addRow('sessions', Object.assign(user, { sessionId: utils.guid() }));
+
+						next(null, { status: 'failure', message: 'Login successful', user });
+					}
+					else {
+						next(null, { status: 'failure', message: 'User not found' });
+					}
+				});
+			}
+
+		], (err, payload) => {
+
+			// log this error somewhere
+			if (err && err !== true) {
+				console.error(err);
+				return cb({ status: 'error', message: 'An error occurred while logging in' });
+			}
+
+			cb(payload);
+		});
+
+	},
+
+	register: (session, username, email, password, recaptcha, cb) => {
+
+		async.waterfall([
+
+			// verify the request with google recaptcha
+			(next) => {
+				request({
+					url: 'https://www.google.com/recaptcha/api/siteverify',
+					method: 'POST',
+					form: {
+						'secret': config.get("apis.recaptcha.secret"),
+						'response': recaptcha
+					}
+				}, next);
+			},
+
+			// 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);
+				if (json.success !== true) return next('Response from recaptcha was not successful');
+				db.models.user.findOne({ 'username': username }, next);
+			},
+
+			// if the user already exists, respond with that
+			// otherwise check if a user with the requested email already exists
+			(user, next) => {
+				if (user) return next(true, { status: 'failure', message: 'A user with that username already exists' });
+				db.models.user.findOne({ 'email.address': email }, next);
+			},
+
+			// if the user already exists, respond with that
+			// otherwise, generate a salt to use with hashing the new users password
+			(user, next) => {
+				if (user) return next(true, { status: 'failure', message: 'A user with that email already exists' });
+				bcrypt.genSalt(10, next);
+			},
+
+			// hash the password
+			(salt, next) => {
+				bcrypt.hash(password, salt, next)
+			},
+
+			// save the new user to the database
+			(hash, next) => {
+				db.models.user.create({
+					username: username,
+					email: {
+						address: email,
+						verificationToken: utils.generateRandomString(64)
+					},
+					services: {
+						password: {
+							password: hash
+						}
+					}
+				}, next);
+			},
+
+			// respond with the new user
+			(newUser, next) => {
+				next(null, { status: 'success', user: newUser })
+			}
+
+		], (err, payload) => {
+			// log this error somewhere
+			if (err && err !== true) {
+				console.error(err);
+				return cb({ status: 'error', message: 'An error occurred while registering for an account' });
+			}
+			// respond with the payload that was passed to us earlier
+			cb(payload);
+		});
+
+	},
+
+	logout: (session, cb) => {
+
+		if (!session) return cb({ status: 'failure', message: `You're not currently logged in` });
+
+		session = null;
+
+		cb({ status: 'success', message: `You've been successfully logged out` });
+	},
+
+	findByUsername: (session, username, cb) => {
+		db.models.user.find({ username }, (err, account) => {
+			if (err) throw err;
+			account = account[0];
+			cb({
+				status: 'success',
+				data: {
+					username: account.username,
+					createdAt: account.createdAt,
+					statistics: account.statistics
+				}
+			});
+		});
+	}
+
+};

+ 34 - 0
backend/logic/app.js

@@ -0,0 +1,34 @@
+'use strict';
+
+// This file contains all the logic for Express
+
+const express = require('express');
+const bodyParser = require('body-parser');
+const cors = require('cors');
+
+const config = require('config');
+
+const lib = {
+
+	app: null,
+	server: null,
+
+	init: (cb) => {
+
+		let app = lib.app = express();
+
+		lib.server = app.listen(80);
+
+		app.use(bodyParser.json());
+		app.use(bodyParser.urlencoded({ extended: true }));
+
+		let corsOptions = Object.assign({}, config.get('cors'));
+
+		app.use(cors(corsOptions));
+		app.options('*', cors(corsOptions));
+
+		cb();
+	}
+};
+
+module.exports = lib;

+ 19 - 0
backend/logic/cache.js

@@ -0,0 +1,19 @@
+'use strict';
+
+// ! ! ! TEMPORARY ! ! !
+// this is a temporary placeholder for holding cached data until we get Redis implemented
+
+const tables = {};
+
+const lib = {
+	addTable: (tableName) => tables[tableName] = [],
+	addRow: (tableName, data) => tables[tableName].push(data),
+	delTable: (tableName) => delete tables[tableName],
+	delRow: (tableName, index) => tables[tableName].splice(index),
+	getRow: (tableName, index) => tables[tableName][index],
+	getAllRows: (tableName) => tables[tableName],
+	findRow: (tableName, key, value) => tables[tableName].find((row) => row[key] == value),
+	findRowIndex: (tableName, key, value) => tables[tableName].indexOf(lib.findRow(tableName, key, value))
+};
+
+module.exports = lib;

+ 0 - 311
backend/logic/coreHandler.js

@@ -1,311 +0,0 @@
-'use strict';
-
-// nodejs modules
-const path   = require('path'),
-      fs     = require('fs'),
-      os     = require('os'),
-      events = require('events');
-
-// npm modules
-const config    = require('config'),
-      request   = require('request'),
-      waterfall = require('async/waterfall'),
-      bcrypt    = require('bcrypt'),
-      passport  = require('passport');
-
-// custom modules
-const globals   = require('./globals'),
-      stations = require('./stations');
-
-var eventEmitter = new events.EventEmitter();
-
-const edmStation = new stations.Station("edm", {
-	"genres": ["edm"],
-	playlist: [
-		'gCYcHz2k5x0'
-	],
-	currentSongIndex: 0,
-	paused: false,
-	displayName: "EDM",
-	description: "EDM Music"
-});
-
-const chillStation = new stations.Station("chill", {
-	"genres": ["chill"],
-	playlist: [
-		'gCYcHz2k5x0'
-	],
-	currentSongIndex: 0,
-	paused: false,
-	displayName: "Chill",
-	description: "Chill Music"
-});
-
-stations.addStation(edmStation);
-stations.addStation(chillStation);
-
-module.exports = {
-
-	// module functions
-
-	on: (name, cb) => eventEmitter.on(name, cb),
-
-	emit: (name, data) => eventEmitter.emit(name, data),
-
-	// core route handlers
-
-	'/users/register': (user, username, email, password, recaptcha, cb) => {
-
-		waterfall([
-
-			// verify the request with google recaptcha
-			(next) => {
-				request({
-					url: 'https://www.google.com/recaptcha/api/siteverify',
-					method: 'POST',
-					form: {
-						'secret': config.get("apis.recaptcha.secret"),
-						'response': recaptcha
-					}
-				}, next);
-			},
-
-			// 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);
-				if (json.success !== true) return next('Response from recaptcha was not successful');
-				globals.db.models.user.findOne({ 'username': username }, next);
-			},
-
-			// if the user already exists, respond with that
-			// otherwise check if a user with the requested email already exists
-			(user, next) => {
-				if (user) return next(true, { status: 'failure', message: 'A user with that username already exists' });
-				globals.db.models.user.findOne({ 'email.address': email }, next);
-			},
-
-			// if the user already exists, respond with that
-			// otherwise, generate a salt to use with hashing the new users password
-			(user, next) => {
-				if (user) return next(true, { status: 'failure', message: 'A user with that email already exists' });
-				bcrypt.genSalt(10, next);
-			},
-
-			// hash the password
-			(salt, next) => {
-				bcrypt.hash(password, salt, next)
-			},
-
-			// save the new user to the database
-			(hash, next) => {
-				globals.db.models.user.create({
-					username: username,
-					email: {
-						address: email,
-						verificationToken: globals.utils.generateRandomString(64)
-					},
-					services: {
-						password: {
-							password: hash
-						}
-					}
-				}, next);
-			},
-
-			// respond with the new user
-			(newUser, next) => {
-				next(null, { status: 'success', user: newUser })
-			}
-
-		], (err, payload) => {
-			// log this error somewhere
-			if (err && err !== true) {
-				console.error(err);
-				return cb({ status: 'error', message: 'An error occurred while registering for an account' });
-			}
-			// respond with the payload that was passed to us earlier
-			cb(payload);
-		});
-	},
-
-	'/users/login': (user, identifier, password, cb) => {
-
-		waterfall([
-
-			// check if a user with the requested identifier exists
-			(next) => globals.db.models.user.findOne({
-				$or: [{ 'username': identifier }, { 'email.address': identifier }]
-			}, next),
-
-			// if the user doesn't exist, respond with a failure
-			// otherwise compare the requested password and the actual users password
-			(user, next) => {
-				if (!user) return next(true, { status: 'failure', message: 'User not found' });
-				bcrypt.compare(password, user.services.password.password, next);
-			},
-
-			// if the user exists, and the passwords match, respond with a success
-			(result, next) => {
-
-				// TODO: Authenticate the user with Passport here I think?
-				// TODO: We need to figure out how other login methods will work
-
-				next(null, {
-					status: result ? 'success': 'failure',
-					message: result ? 'Logged in' : 'User not found'
-				});
-			}
-
-		], (err, payload) => {
-			// log this error somewhere
-			if (err && err !== true) {
-				console.error(err);
-				return cb({ status: 'error', message: 'An error occurred while logging in' });
-			}
-			// respond with the payload that was passed to us earlier
-			cb(payload);
-		});
-
-	},
-
-	'/u/:username': (username, cb) => {
-		globals.db.models.user.find({ username }, (err, account) => {
-			if (err) throw err;
-			account = account[0];
-			cb({status: 'success', data: {
-				username: account.username,
-				createdAt: account.createdAt,
-				statistics: account.statistics
-			}});
-		});
-	},
-
-	'/users/logout': (req, cb) => {
-		if (!req.user || !req.user.logged_in) return cb({ status: 'failure', message: `You're not currently logged in` });
-
-		req.logout();
-
-		cb({ status: 'success', message: `You've been successfully logged out` });
-	},
-
-	'/stations': cb => {
-		cb(stations.getStations().map(station => {
-			return {
-				id: station.id,
-				playlist: station.playlist,
-				displayName: station.displayName,
-				description: station.description,
-				currentSongIndex: station.currentSongIndex,
-				users: station.users
-			}
-		}));
-	},
-
-	'/stations/join/:id': (id, cb) => {
-
-		let station = stations.getStation(id);
-
-		if (!station) return cb({ status: 'error', message: `Station with id '${id}' does not exist` });
-
-		// session.station_id = id;
-		station.users++;
-
-		cb({ status: 'success', users: station.users });
-	},
-
-	// leaves the users current station
-	// returns the count of users that are still in that station
-	'/stations/leave': cb => {
-
-		// let station = stations.getStation(session.station_id);
-		let station = stations.getStation("edm"); // will be removed
-		if (!station) return cb({ status: 'failure', message: `Not currently in a station, or station doesn't exist` });
-
-		// session.station_id = "";
-		// station.users--;
-
-		cb({ status: 'success', users: station.users });
-	},
-
-	'/youtube/getVideo/:query': (query, cb) => {
-
-		const params = [
-			'part=snippet',
-			`q=${encodeURIComponent(query)}`,
-			`key=${config.get('apis.youtube.key')}`,
-			'type=video',
-			'maxResults=15'
-		].join('&');
-
-		request(`https://www.googleapis.com/youtube/v3/search?${params}`, (err, res, body) => {
-
-			if (err) {
-				console.error(err);
-				return cb({ status: 'error', message: 'Failed to search youtube with the requested query' });
-			}
-
-			cb({ status: 'success', data: JSON.parse(body) });
-		});
-	},
-
-	'/stations/add/:song': (station, song, cb) => {
-
-		// if (!session.logged_in) return cb({ status: 'failure', message: 'You must be logged in to add a song' });
-
-		const params = [
-			'part=snippet,contentDetails,statistics,status',
-			`id=${encodeURIComponent(song.id)}`,
-			`key=${config.get('apis.youtube.key')}`
-		].join('&');
-
-		request(`https://www.googleapis.com/youtube/v3/videos?${params}`, (err, res, body) => {
-
-			// TODO: Get data from Wikipedia and Spotify
-
-			if (err) {
-				console.error(err);
-				return cb({ status: 'error', message: 'Failed to find song from youtube' });
-			}
-
-			const newSong = new globals.db.models.song({
-				id: json.items[0].id,
-				title: json.items[0].snippet.title,
-				duration: globals.utils.convertTime(json.items[0].contentDetails.duration),
-				thumbnail: json.items[0].snippet.thumbnails.high.url
-			});
-
-			// save the song to the database
-			newSong.save(err => {
-
-				if (err) {
-					console.error(err);
-					return cb({ status: 'error', message: 'Failed to save song from youtube to the database' });
-				}
-
-				stations.getStation(station).playlist.push(newSong);
-
-				cb({ status: 'success', data: stations.getStation(station.playlist) });
-			});
-		});
-	},
-
-	'/songs': cb => {
-		globals.db.models.song.find({}, (err, songs) => {
-			if (err) throw err;
-			cb(songs);
-		});
-	},
-
-	'/songs/:song/update': (song, cb) => {
-		globals.db.models.song.findOneAndUpdate({ id: song.id }, song, { upsert: true }, (err, updatedSong) => {
-			if (err) throw err;
-			cb(updatedSong);
-		});
-	},
-
-	'/songs/:song/remove': (song, cb) => {
-		globals.db.models.song.find({ id: song.id }).remove().exec();
-	}
-};

+ 36 - 0
backend/logic/db/index.js

@@ -0,0 +1,36 @@
+'use strict';
+
+const mongoose = require('mongoose');
+
+let lib = {
+
+	connection: null,
+	schemas: {},
+	models: {},
+
+	init: (url, cb) => {
+
+		lib.connection = mongoose.connect(url).connection;
+
+		lib.connection.on('error', err => console.log('Database error: ' + err.message));
+
+		lib.connection.once('open', _ => {
+
+			lib.schemas = {
+				song: new mongoose.Schema(require(`./schemas/song`)),
+				station: new mongoose.Schema(require(`./schemas/station`)),
+				user: new mongoose.Schema(require(`./schemas/user`))
+			};
+
+			lib.models = {
+				song: mongoose.model('song', lib.schemas.song),
+				station: mongoose.model('station', lib.schemas.station),
+				user: mongoose.model('user', lib.schemas.user)
+			};
+
+			cb();
+		});
+	}
+};
+
+module.exports = lib;

+ 0 - 0
backend/schemas/song.js → backend/logic/db/schemas/song.js


+ 0 - 0
backend/schemas/station.js → backend/logic/db/schemas/station.js


+ 0 - 0
backend/schemas/user.js → backend/logic/db/schemas/user.js


+ 0 - 25
backend/logic/expressHandler.js

@@ -1,25 +0,0 @@
-'use strict';
-
-const passport = require('passport');
-
-module.exports = (core, app) => {
-
-	app.post('/users/login', (req, res) => {
-		console.log('posted', req.user);
-		core['/users/login'](req.user, req.body.identifier, req.body.password, result => {
- 			res.end(JSON.stringify(result));
- 		});
-	});
-
-	app.post('/users/register', (req, res) => {
-		core['/users/register'](req.user, req.body.username, req.body.email, req.body.password, req.body.recaptcha, result => {
-			res.end(JSON.stringify(result));
-		});
-	});
-
-	app.get('/users/logout', (req, res) => {
-		core['/users/logout'](req, result => {
-			res.end(JSON.stringify(result));
-		})
-	})
-};

+ 72 - 0
backend/logic/io.js

@@ -0,0 +1,72 @@
+'use strict';
+
+// This file contains all the logic for Socket.IO
+
+const app = require('./app');
+const actions = require('./actions');
+const cache = require('./cache');
+
+module.exports = {
+
+	io: null,
+
+	init: (cb) => {
+
+		this.io = require('socket.io')(app.server);
+
+		this.io.on('connection', socket => {
+
+			console.log("io: User has connected");
+
+			// catch when the socket has been disconnected
+			socket.on('disconnect', () => {
+
+				// remove the user from their current station
+				if (socket.sessionId) {
+					actions.stations.leave(socket.sessionId, result => {});
+					delete socket.sessionId;
+				}
+
+				console.log('io: User has disconnected');
+			});
+
+			// catch errors on the socket (internal to socket.io)
+			socket.on('error', err => console.log(err));
+
+			// have the socket listen for each action
+			Object.keys(actions).forEach((namespace) => {
+				Object.keys(actions[namespace]).forEach((action) => {
+
+					// the full name of the action
+					let name = `${namespace}.${action}`;
+
+					// listen for this action to be called
+					socket.on(name, function () {
+
+						let args = Array.prototype.slice.call(arguments, 0, -1);
+						let cb = arguments[arguments.length - 1];
+						let session = cache.findRow('sessions', 'id', socket.sessionId);
+
+						// if the action set 'session' to null, that means they want to delete it
+						if (session === null) delete socket.sessionId;
+
+						// call the action, passing it the session, and the arguments socket.io passed us
+						actions[namespace][action].apply(null, [session].concat(args).concat([
+							(result) => {
+								// store the session id, which we use later when the user disconnects
+								if (name == 'users.login' && result.user) socket.sessionId = result.user.sessionId;
+								// respond to the socket with our message
+								cb(result);
+							}
+						]));
+					})
+				})
+			});
+
+			socket.emit('ready');
+		});
+
+		cb();
+	}
+
+};

+ 33 - 0
backend/logic/scheduler.js

@@ -0,0 +1,33 @@
+'use strict';
+
+// Central place to register / store timeouts
+
+const timeouts = {}, intervals = {};
+
+module.exports = {
+
+	once: (name, time, cb) => {
+		timeouts[name] = setTimeout(() => {
+			delete timeouts[name];
+			cb();
+		}, time);
+	},
+
+	repeat: (name, time, cb) => {
+		intervals[name] = setInterval(() => {
+			delete intervals[name];
+			cb();
+		}, time);
+	},
+
+	cancel: (name) => {
+		if (timeouts[name]) {
+			clearTimeout(timeouts[name]);
+			delete timeouts[name];
+		}
+		if (!intervals[name]) {
+			clearInterval(intervals[name]);
+			delete intervals[name];
+		}
+	}
+};

+ 0 - 36
backend/logic/socketHandler.js

@@ -1,36 +0,0 @@
-'use strict';
-
-const passport = require('passport');
-
-module.exports = (core, io) => {
-
-	io.on('connection', socket => {
-
-		console.log("socketHandler: User has connected");
-
-		// let session = socket.request.user;
-
-		socket.on('disconnect', _ => {
-			core['/stations/leave'](result => {});
-			console.log('socketHandler: User has disconnected');
-		});
-
-		socket.on('error', err => console.log(err));
-
-		socket.on('/u/:username', (username, cb) => core['/u/:username'](username, result => cb(result)));
-
-		socket.on('/stations', (cb) => core['/stations'](result => cb(result)));
-		socket.on('/stations/join/:id', (id, cb) => core['/stations/join/:id'](id, result => cb(result)));
-		socket.on('/stations/leave', cb => core['/stations/leave'](result => cb(result)));
-		socket.on('/stations/add/:song', (station, song, cb) => core['/stations/add/:song'](station, song, result => cb(result)));
-
-		socket.on('/songs', (cb) => core['/songs'](result => cb(result)));
-		socket.on('/songs/:song/update', (song, cb) => core['/songs/:song/update'](song, result => cb(result)));
-		socket.on('/songs/:song/remove', (song, cb) => core['/songs/:song/remove'](song, result => cb(result)));
-
-		socket.on('/youtube/getVideo/:query', (query, cb) => core['/youtube/getVideo/:query'](query, result => cb(result)));
-
-		// this lets the client socket know that they can start making request
-		socket.emit('ready', false); // socket.request.user.logged_in
-	});
-};

+ 16 - 31
backend/logic/globals.js → backend/logic/utils.js

@@ -90,36 +90,21 @@ function convertTime (duration) {
 	return (hours < 10 ? ("0" + hours + ":") : (hours + ":")) + (minutes < 10 ? ("0" + minutes + ":") : (minutes + ":")) + (seconds < 10 ? ("0" + seconds) : seconds);
 }
 
-const globals  = {
-	// stores the socket.io server
-	io: null,
-	// stores stuff related to the Mongo Database
-	db: {
-		// our connection to the database
-		connection: null,
-		// the models we've defined in the database
-		models: null,
-		// the store express / socket.io uses to store sessions in the database
-		store: null
+module.exports = {
+	htmlEntities: str => String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
+	generateRandomString: len => {
+		let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
+		let result = [];
+		for (let i = 0; i < len; i++) {
+			result.push(chars[globals.utils.getRandomNumber(0, chars.length - 1)]);
+		}
+		return result.join("");
 	},
-	// various utilities used through out the code base
-	utils: {
-		htmlEntities: str => String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
-		generateRandomString: len => {
-			let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
-			let result = [];
-			for (let i = 0; i < len; i++) {
-				result.push(chars[globals.utils.getRandomNumber(0, chars.length - 1)]);
-			}
-			return result.join("");
-		},
-		getSocketFromId: function(socketId) {
-			return globals.io.sockets.sockets[socketId];
-		},
-		getRandomNumber: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
-		convertTime,
-		Timer
-	}
+	getSocketFromId: function(socketId) {
+		return globals.io.sockets.sockets[socketId];
+	},
+	getRandomNumber: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
+	convertTime,
+	Timer,
+	guid: () => [1,1,0,1,0,1,0,1,0,1,1,1].map(b => b ? Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) : '-').join('')
 };
-
-module.exports = globals;

+ 8 - 1
backend/logic/stations.js → backend/old/stations.js

@@ -1,6 +1,13 @@
 'use strict';
 
-const globals = require('./globals');
+////////////////////////////////////////////////////////////////////
+// This way of storing the state of a station is very OOP based   //
+// and is likely to not scale very efficiently. A more functional //
+// approach is needed. As in, the 'state' of a station should be  //
+// purely data based. Actions on Stations should operate directly //
+// on stored data, not on in-memory representations of them       //
+////////////////////////////////////////////////////////////////////
+
 const Promise = require('bluebird');
 const io = globals.io;
 let stations = [];

+ 1 - 1
backend/package.json

@@ -7,7 +7,7 @@
   "repository": "https://github.com/Musare/MusareNode",
   "scripts": {
     "development": "nodemon -L /opt/app",
-    "production": "node /opt/app/app.js"
+    "production": "node /opt/app"
   },
   "dependencies": {
     "async": "2.0.1",

+ 25 - 57
frontend/App.vue

@@ -26,74 +26,42 @@
 		},
 		methods: {
 			logout() {
-				this.socket.emit('/users/logout');
+				this.socket.emit('users.logout');
 				location.reload();
 			}
 		},
-		ready: function() {
-			let local = this;
-			local.socket = io(window.location.protocol + '//' + window.location.hostname + ':8081');
-
-			local.socket.on("ready", status => {
-				local.loggedIn = status;
-			});
-
-			local.socket.emit("/stations", function(data) {
-				local.stations = data;
-			});
+		ready: function () {
+			let socket = this.socket = io(window.location.protocol + '//' + window.location.hostname + ':8081');
+			socket.on("ready", status => this.loggedIn = status);
+			socket.emit("stations.index", data => this.stations = data);
 		},
 		events: {
-			'register': function() {
-				fetch(`${window.location.protocol + '//' + window.location.hostname + ':8081'}/users/register`, {
-					method: 'POST',
-					headers: {
-						'Accept': 'application/json',
-						'Content-Type': 'application/json; charset=utf-8'
-					},
-					body: JSON.stringify({
-						email: this.register.email,
-						username: this.register.username,
-						password: this.register.password,
-						recaptcha: grecaptcha.getResponse()
-					})
-				}).then(response => {
-					alert('Now sign in!');
-				})
+			'register': function () {
+
+				var { register: { email, username, password } } = this;
+
+				this.socket.emit('users.login', email, username, password, grecaptcha.getResponse(), (result) => {
+					console.log(result);
+					location.reload();
+				});
 			},
-			'login': function() {
-				fetch(`${window.location.protocol + '//' + window.location.hostname + ':8081'}/users/login`, {
-					method: 'POST',
-					headers: {
-						'Accept': 'application/json',
-						'Content-Type': 'application/json'
-					},
-					body: JSON.stringify({
-						email: this.login.email,
-						password: this.login.password
-					})
-				}).then(response => {
-					console.log(response);
+			'login': function () {
+
+				var { login: { email, password } } = this;
+
+				this.socket.emit('users.login', email, password, (result) => {
+					console.log(result);
 					location.reload();
 				});
 			},
-			'joinStation': function(id) {
-				let local = this;
-				local.socket.emit('/stations/join/:id', id, (result) => {
-					local.stations.forEach(function(station) {
-						if (station.id === id) {
-							station.users = result;
-						}
-					});
+			'joinStation': function (id) {
+				this.socket.emit('stations.join', id, (result) => {
+					this.stations.find(station => station.id === id).users = result.userCount;
 				});
 			},
-			'leaveStation': function(id) {
-				let local = this;
-				local.socket.emit('/stations/leave/:id', id, (result) => {
-					local.stations.forEach(function(station) {
-						if (station.id === id) {
-							station.users = result;
-						}
-					});
+			'leaveStation': function () {
+				this.socket.emit('stations.leave', (result) => {
+					//this.stations.find(station => station.id === id).users = result.userCount;
 				});
 			}
 		}

+ 7 - 10
frontend/components/Admin/Songs.vue

@@ -31,7 +31,7 @@
 						<td>
 							<div class="control">
 								<input v-for="artist in song.artists" track-by="$index" class="input" type="text" :value="artist" v-model="artist">
-							</p>
+							</div>
 						</td>
 						<td>
 							<a class="button is-danger" @click="remove(song, index)">Remove</a>
@@ -52,20 +52,17 @@
 			}
 		},
 		methods: {
-			update(song) {
-				this.socket.emit('/songs/:song/update', song);
+			update (song) {
+				this.socket.emit('songs.update', song);
 			},
-			remove(song, index) {
+			remove (song, index) {
 				this.songs.splice(index, 1);
-				this.socket.emit('/songs/:song/remove', song);
+				this.socket.emit('songs.remove', song);
 			}
 		},
 		ready: function() {
-			let local = this;
-			local.socket = local.$parent.$parent.socket;
-			local.socket.emit("/songs", function(data) {
-				local.songs = data;
-			});
+			let socket = this.socket = this.$parent.$parent.socket;
+			socket.emit('songs.index', (data) => this.songs = data);
 		}
 	}
 </script>

+ 5 - 4
frontend/components/Admin/Stations.vue

@@ -75,11 +75,12 @@
 			// }
 		},
 		ready: function() {
-			let local = this;
-			local.socket = local.$parent.$parent.socket;
-			local.socket.emit("/stations", function(data) {
-				local.stations = data;
+			let socket = this.socket = this.$parent.$parent.socket;
+			socket.emit("stations.index", (data) => {
+				console.log(data);
+				this.stations = data;
 			});
+			console.log('ready');
 		}
 	}
 </script>

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

@@ -29,7 +29,7 @@
 </template>
 
 <script>
-	import { Toast } from 'vue-roaster';
+	//import { Toast } from 'vue-roaster';
 
 	export default {
 		data() {
@@ -43,13 +43,13 @@
 		methods: {
 			changeRank(newRank) {
 				console.log(rank);
-				Toast.methods.addToast(`User ${this.$route.params.username} has been promoted to the rank of ${rank}`, 200000);
+				//Toast.methods.addToast(`User ${this.$route.params.username} has been promoted to the rank of ${rank}`, 200000);
 			}
 		},
 		ready: function() {
 			let local = this;
 			local.socket = local.$parent.socket;
-			local.socket.emit("/u/:username", local.$route.params.username, results => {
+			local.socket.emit('users.findByUsername', local.$route.params.username, results => {
 				local.user = results.data;
 				console.log(local.user)
 				local.liked = results.data.statistics.songsLiked.length;
@@ -57,7 +57,7 @@
 				local.requested = local.user.statistics.songsRequested;
 			});
 		},
-		components: { Toast }
+		//components: { Toast }
 	}
 </script>
 

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

@@ -55,7 +55,7 @@
 		<div class="group">
 			<!--<div class="group-title">{{group.name}}</div>-->
 			<div class="group-stations">
-				<div class="stations-station" v-for="station in $parent.stations" v-link="{ path: '/station/' + station.id }" @click="this.$dispatch('joinStation', station.id)">
+				<div class="stations-station" v-for="station in $parent.stations" v-link="{ path: '/station/' + station.name }" @click="this.$dispatch('joinStation', station.id)">
 					<img class="station-image" :src="station.playlist[station.currentSongIndex].thumbnail" />
 					<div class="station-info">
 						<div class="station-grid-left">
@@ -63,7 +63,7 @@
 							<p>{{ station.description }}</p>
 						</div>
 						<div class="station-grid-right">
-							<div>{{ station.users }}&nbsp;&nbsp;<i class="fa fa-user" aria-hidden="true"></i></div>
+							<div>{{ station.userCount }}&nbsp;&nbsp;<i class="fa fa-user" aria-hidden="true"></i></div>
 						</div>
 					</div>
 				</div>

+ 24 - 14
frontend/components/pages/Station.vue

@@ -212,13 +212,13 @@
 			},
 			addSongToQueue: function(song) {
 				let local = this;
-				local.socket.emit("/stations/add/:song", local.$route.params.id, song, function(data) {
+				local.socket.emit('stations.addSong', local.$route.params.id, song, function(data) {
 					if (data) console.log(data);
 				});
 			},
 			submitQuery: function() {
 				let local = this;
-				local.socket.emit("/youtube/getVideo/:query", local.querySearch, function(results) {
+				local.socket.emit('apis.searchYoutube', local.querySearch, function(results) {
 					results = results.data;
 					local.queryResults = [];
 					for (let i = 0; i < results.items.length; i++) {
@@ -233,13 +233,23 @@
 			}
 		},
 		ready: function() {
-			let local = this;
 
-			local.interval = 0;
+			this.interval = 0;
 
-			local.socket = this.$parent.socket;
-			local.stationSocket = io.connect(`${window.location.protocol + '//' + window.location.hostname + ':8081'}/${local.$route.params.id}`);
-			local.stationSocket.on("connected", function(data) {
+			this.socket = this.$parent.socket;
+
+			this.socket.on('event:songs.next', (data) => {
+				var {currentSong, startedAt} = data;
+				this.currentSong = currentSong;
+				this.startedAt = startedAt;
+				this.timePaused = 0;
+				this.playVideo();
+			});
+
+			/*this.stationSocket = io.connect(`${window.location.protocol + '//' + window.location.hostname + ':8081'}/${local.$route.params.id}`);
+
+			this.stationSocket.on("connected", (data) => {
+				console.log(data);
 				local.currentSong = data.currentSong;
 				local.startedAt = data.startedAt;
 				local.paused = data.paused;
@@ -247,14 +257,14 @@
 				local.currentTime  = data.currentTime;
 			});
 
-			local.youtubeReady();
+			this.youtubeReady();
 
-			local.stationSocket.on("nextSong", function(currentSong, startedAt) {
-				local.currentSong = currentSong;
-				local.startedAt = startedAt;
-				local.timePaused = 0;
-				local.playVideo();
-			});
+			this.stationSocket.on("nextSong", (currentSong, startedAt) => {
+				this.currentSong = currentSong;
+				this.startedAt = startedAt;
+				this.timePaused = 0;
+				this.playVideo();
+			});*/
 
 			let volume = parseInt(localStorage.getItem("volume"));
 			volume = (typeof volume === "number") ? volume : 20;

+ 3 - 0
frontend/js/utils.js

@@ -0,0 +1,3 @@
+export default {
+	guid: () => [1,1,0,1,0,1,0,1,0,1,1,1].map(b => b ? Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) : '-').join('')
+};

+ 2 - 1
frontend/main.js

@@ -10,6 +10,7 @@ import User from './components/User/Show.vue';
 import Settings from './components/User/Settings.vue';
 
 Vue.use(VueRouter);
+
 let router = new VueRouter({ history: true });
 
 router.map({
@@ -22,7 +23,7 @@ router.map({
 	'/u/:username/settings': {
 		component: Settings
 	},
-	'/station/:id': {
+	'/station/:name': {
 		component: Station
 	},
 	'/admin': {