Lots of new stuff

This commit is contained in:
Paul Sori 2016-12-11 04:04:14 -05:00
parent e952357eae
commit cb3293359c
18 changed files with 534 additions and 283 deletions

View File

@ -1,13 +1,13 @@
## mStream
mStream is an music streaming server written in NodeJS. It's focus is on ease of installation and FLAC streaming. mStream will work right out of the box without any configuration.
### Live Demo
### Demo
Check it out: http://darncoyotes.mstream.io/
### Main Features
* Supports FLAC streaming
* DB Plugin System. Use SQLite, MySQL, LokiJS or roll your own custom DB system
* DB Plugin System. Choose the DB that best fits your needs
* Works on Mac, Linux and Windows
* [Integrates easily with Beets DB](https://github.com/beetbox/beets)
* Allows multiple users
@ -55,6 +55,7 @@ sudo npm install -g node-gyp
### Using Docker
##### NOTE: This instructions are outdated and need to be updated
Download the Dockerfile, or clone the repository, then run the following
commands:
@ -72,23 +73,14 @@ default installation.
docker run --rm -v /path/to/my/music:/music local/mstream -l -u username -x password
```
## Options
## Usage
mStream can be configured by loading a JSON config file
```shell
-p, --port -> set port number
-l, --login -> enable user login
-u, --user -> add user
-x, --password -> set Password
-G, --guest -> set guest username
-X, --guestpassword -> set guest password
-d, --database -> set the database file
-t, --tunnel -> tunnel
-g, --gateway -> set gateway for tunnelling
-i, --userinterface -> use an alternative UI. Currently only the value 'jplayer' works
mstream server.json
```
## User System
The current user system is a simple as it comes. There are two users you can have, main and guest. Guest users do not have any access to API functions that write to the file system. Currently guest users cannot access the save-playlist, recursive-scan, or delete-playlist functions
@ -101,26 +93,37 @@ mstream -l -u [username] -x [password]
The user system is simple for a few reasons. First, I wanted to have a user system that doesn't need a database to work. Secondly, mStream is a personal server and most users don't need anything more complex than this.
## Database
## Database Options
mStream currently uses a SQLite database for a music library. You have the option of using a beets DB or having a mStream create it's own DB.
mSTream's datbase will work right out of the box without
#### Beets DB
### Database Plugin System
mStream 2.0 is written so that you can choose what DB system you use. Currently only sqlite is supported, but in the future their will be more options:
- SQLite
- MySQL
- PouchDB: A NoSQL alternative
### Import DB
http://beets.io/
mStream can use your beets database without any configuration.
```shell
mstream -d path/to/beets.db
```
Currently using beets is the recommended way to create a music database.
User's can choose how their files are managed. By default mstream will manage the user's DB. User's also have the option to import their DB from somewhere else
#### beets DB
#### use mStream to build your DB
Use the /db/recursive-scan API call to kickoff a full scan of your library. Currently this is the only way to add files to the library. Version 2 of mStream will include new functions to update the library more efficiently
## Automatically setup port forwarding
#### Please note that this feature is still experimental
@ -150,3 +153,6 @@ Please note that not all routers will allow this. Some routers may close this p
- Ability to store hashed passwords
- Scripts that help construct configs
- MySQL DB plugin
- LokiJS or PuchDB plugin
- Move to LokiJS/PouchDB as default DB
- SSL Support

View File

@ -2,10 +2,15 @@ const natupnp = require('nat-upnp');
const getIP = require('external-ip')();
const natpmp = require('nat-pmp');
var gateway;
exports.tunnel_uPNP = function(port){
function set_gateway (gateIP){
gateway = gateIP;
}
function tunnel_uPNP (port){
try{
console.log('Preparing to tunnel via upnp protocol');
var client = natupnp.createClient();
@ -29,22 +34,14 @@ exports.tunnel_uPNP = function(port){
}
}
exports.tunnel_NAT_PMP = function tunnel_NAT_PMP(port){
function tunnel_NAT_PMP(port){
try{
console.log('Preparing to tunnel via nat-pmp protocol');
// Use the user supplied Gateway IP or try to find it manually
if(program.gateway){
var gateway = program.gateway;
}else{
var netroute = require('netroute');
var gateway = netroute.getGateway();
if(!gateway){
gateway = require('netroute').getGateway();
}
console.log('Attempting to tunnel via gateway: ' + gateway);
var client = new natpmp.Client(gateway);
client.portMapping({ public: port, private: port }, function (err, info) {
if (err) {
@ -52,9 +49,6 @@ exports.tunnel_NAT_PMP = function tunnel_NAT_PMP(port){
}
client.close();
});
}
catch (e) {
console.log('WARNING: mStream nat-pmp tunnel functionality has failed. Your network may not allow functionality');
@ -64,7 +58,7 @@ exports.tunnel_NAT_PMP = function tunnel_NAT_PMP(port){
exports.logUrl = function(port){
function logUrl (port){
getIP(function (err, ip) {
if (err) {
// every service in the list has failed
@ -73,3 +67,37 @@ exports.logUrl = function(port){
console.log('Access mStream on the internet: http://' + ip + ':' + port);
});
}
exports.setup = function(args, port){
if(args.gateway){
tunnel.set_gateway(args.gateway);
}
console.log('Preparing to tunnel via nat-pmp protocol');
// TODO: Clean this up, this it so lazy...
if(args.protocol && args.protocol === 'upnp'){
// Run it on an interval ?
if(args.refreshInterval){
setInterval( function() {
tunnel_uPNP(port);
}, args.refreshInterval);
}else{
tunnel_uPNP(port);
}
}else{
// Run it on an interval ?
if(args.refreshInterval){
setInterval( function() {
tunnel_NAT_PMP(port);
}, argsrefreshInterval);
}else{
tunnel_NAT_PMP(port);
}
}
logUrl(port);
}

View File

@ -1,29 +1,78 @@
exports.setup = function(args){
// TODO: This exists to have a simple setup from the command line
// Removed beets support from cli launch
// Setup Command Line Interface
var program = require('commander');
const program = require('commander');
program
.version('1.30.0')
.version('1.21.0')
.option('-p, --port <port>', 'Select Port', /^\d+$/i, 3000)
.option('-t, --tunnel', 'Use nat-pmp to configure port fowarding')
.option('-g, --gateip <gateip>', 'Manually set gateway IP for the tunnel option')
.option('-g, --gateway <gateway>', 'Manually set gateway IP for the tunnel option')
.option('-r, --refresh <refresh>', 'Refresh rate', /^\d+$/i)
.option('-o, --protocol <refresh>', 'Refresh rate', /^\d+$/i)
.option('-u, --user <user>', 'Set Username')
.option('-x, --password <password>', 'Set Password')
// .option('-e, --email <email>', 'Set User Email (optional)')
.option('-e, --email <email>', 'Set User Email (optional)')
.option('-G, --guest <guestname>', 'Set Guest Username')
.option('-X, --guestpassword <guestpassword>', 'Set Guest Password')
// .option('-k, --key <key>', 'Add SSL Key')
// .option('-c, --cert <cert>', 'Add SSL Certificate')
.option('-d, --database <path>', 'Specify Database Filepath', 'mstreamdb.lite')
// .option('-b, --beetspath <folder>', 'Specify Folder where Beets DB should import music from. This also overides the normal DB functions with functions that integrate with beets DB')
// .option('-b, --databaseplugin <folder>', '', /^(default|beets)$/i, 'default')
.option('-i, --userinterface <folder>', 'Specify folder name that will be served as the UI', 'public')
.option('-f, --filepath <folder>', 'Set the path of your music directory', process.cwd())
.option('-s, --secret <secret>', 'Set the login secret key')
.parse(args);
return program;
let program3 = {
port:program.port,
userinterface:program.userinterface,
}
if(program.secret){
program3.secret = program.sectet;
}
if(program.salt){
program3.salt = program.salt;
}
// User account
if(program.user && program.password){
program3.users = {};
program3.users[program.user] = {
password:program.password,
musicDir:process.cwd()
};
if(program.email){
program3.users[program.user].email = program.email;
}
// Guest account
if(program.guestname && program.guestpassword){
program3.users[program.guestname] = {
password:program.guestpassword,
guestTo:program.user
};
}
}
// db plugins
program3.database_plugin = {
type:"sqlite",
dbPath:program.database
};
// TODO: Add support for other DBs when ready
// port forwarding
if(program.tunnel){
program3.tunnel = {};
if(program.refresh){
program3.tunnel.refreshInterval = program.refresh;
}
if(program.gateway){
program3.tunnel.gateway = program.gateway;
}
if(program.protocol){
program3.tunnel.protocol = program.protocol;
}
}
return program3;
}

View File

@ -1,27 +1,73 @@
const fs = require('graceful-fs'); // File System
const fs = require('fs'); // File System
const fe = require('path');
exports.setup = function(args){
// Open File
exports.setup = function(args, rootDir){
let loadJson;
try{
var loadJson = JSON.parse(fs.readFileSync(args[args.length-1], 'utf8'));
}catch(err){
console.log('Failed to parse JSON file');
return false;
if(fe.extname(args[args.length-1]) === '.json' && fs.statSync(args[args.length-1]).isFile()){
loadJson = JSON.parse(fs.readFileSync(args[args.length-1], 'utf8'));
}else{
return require('./configure-commander.js').setup(args);
}
}catch(error){
console.log(error);
return {error:"Failed to parse JSON file"};
}
if(!loadJson.port){
loadJson.port = 5050;
}
if(!isInt(loadJson.port) || loadJson.port < 0 || loadJson.port > 65535){
return {error:"BAD PORT, WILL ABORT"};
}
// TODO: Add comprehensive DB checks
if(!loadJson.database_plugin){
console.log('Please Configure DB');
return false;
return {error:"Please Configure DB"};
}
if(!loadJson.userinterface){
loadJson.userinterface = "public";
if(loadJson.userinterface){
if(!fs.statSync( fe.join(rootDir, loadJson.userinterface) ).isDirectory()){
return {error:"Could not find userinterface"};
}
}
// TODO; Preform a full range of checks
// Normalize for all OS
// Make sure it's a directory
// Loop through and makeure all user Dirs are real
if(loadJson.users){
for (let username in loadJson.users) {
// TODO: Check usernames for forbidden chars
// TODO: Make sure all music directories are unique
// TODO: No subsets/super-sets/duplicates
if(!loadJson.users[username].guestTo && !fs.statSync( loadJson.users[username].musicDir ).isDirectory()){
return {error:loadJson.users[username].username + " music directory could not be found"};
}
}
}
// TODO: Preform a full range of checks
if(!isInt(loadJson.tunnel.refreshInterval)){
return {error:"Refresh interval must be an integer"};
}
// Export JSON
return loadJson;
}
function isInt(value) {
if (isNaN(value)) {
return false;
}
var x = parseFloat(value);
return (x | 0) === x;
}
// TODO: This should sum up all errors before returing to user

View File

@ -0,0 +1,71 @@
// TODO: Function that copies a BeetsDB(private) into the SQLite master(public) DB
const sqlite3 = require('sqlite3').verbose();
// This is designed to run as it's own process
// It takes in a json array
// {
// "username":"lol",
// "privateDBOptions":{
// "privateDB":"BEETS",
// "importDB":"path/to/sqlite3.db",
// "beetspath":"/path/to/beets/music/dir",
// "quickSync": true
// },
// "userDir":"/Users/psori/Desktop/Blockhead",
// "dbSettings":{
// "type":"sqlite",
// "dbPath":"/Users/psori/Desktop/LATESTGREATEST.DB"
// }
// }
try{
var loadJson = JSON.parse(process.argv[process.argv.length-1], 'utf8');
}catch(error){
console.log('Cannot parse JSON input');
process.exit();
}
const dbPublic = require('../db-write/database-default-'+loadJson.dbSettings.type+'.js');
const beetsDB = new sqlite3.Database(dbPath);
if(loadJson.dbSettings.type == 'sqlite'){
dbPublic.setup(loadJson.dbSettings.dbPath); // TODO: Pass this in
}
run();
// TODO: It might be worth combing this into the default-sqlite files since they share some functions
// SELECT * FROM items LEFT JOIN item_attributes ON
// item_attributes.entity_id = items.id
// AND item_attributes.key = 'checksum'
// GROUP BY items.id, item_attributes.key;
function run(){
let sql = "SELECT * FROM items LEFT JOIN item_attributes ON item_attributes.entity_id = items.id AND item_attributes.key = 'checksum' GROUP BY items.id, item_attributes.key;";
beetsDB.all(sql, function(err, files){
files = dbPublic.reformatData(files);
dbPublic.purgeDB(username);
dbPublic.addToDB(files);
if(loadJson.privateDBOptions.quickSync === false){
smokeThatHash();
}
});
}
// TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:
function smokeThatHash( blazeItEveryDay = false){
// Pull all files from DB
// Get hash
// Update DB
// if hash is available or blazeItEveryDay=true then hash file
// TODO: Shoudl we add the hash to the beets DB?
}
// TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:TODO:

View File

@ -23,7 +23,7 @@ try{
var loadJson = JSON.parse(process.argv[process.argv.length-1], 'utf8');
}catch(error){
console.log('JSON file does not appear to exist');
console.log('Cannot parse JSON input');
process.exit();
}
@ -44,8 +44,9 @@ var arrayOfSongs = []; // Holds songs for DB to process // TODO: Move out of glo
var arrayOfScannedFiles = []; // Holds files for from recursive scan
//TODO: Pull in correct module
// Pull in correct module
console.log(loadJson.dbSettings.type);
// TODO: Rename this var
const dbRead = require('../db-write/database-default-'+loadJson.dbSettings.type+'.js');
if(loadJson.dbSettings.type == 'sqlite'){
dbRead.setup(loadJson.dbSettings.dbPath); // TODO: Pass this in
@ -55,13 +56,6 @@ if(loadJson.dbSettings.type == 'sqlite'){
// New way to start it
const parseFilesGenerator = rescanAllDirectories(loadJson.userDir);
parseFilesGenerator.next();
// Old way to start it
// rescanAllDirectoriesWrapper(loadJson.userDir);
// function rescanAllDirectoriesWrapper(dir){
// parseFilesGenerator = rescanAllDirectories(dir);
// parseFilesGenerator.next();
// }
function *rescanAllDirectories(directoryToScan){

View File

@ -104,4 +104,40 @@ exports.setup = function(mstream, program){
res.send('Coming Soon!');
});
mstream.get('/db/download-db', function(req, res){
// Check user for beets db
if(!req.user.privateDB || req.user.privateDB != 'BEETS'){
res.status(500).json({ error: 'DB Error' });
return;
}
// Download File
res.download(req.user.privateDBOptions.importDB);
});
// Get hash of database
// TODO: Change the name of this endpoint
mstream.get( '/db/hash', function(req, res){
// Check if user is using beets
if(!req.user.privateDB || req.user.privateDB != 'BEETS'){
res.status(500).json({ error: 'DB Error' });
return;
}
var hash = crypto.createHash('sha256');
hash.setEncoding('hex');
var fileStream = fs.createReadStream(req.user.privateDBOptions.importDB);
fileStream.on('end', function () {
hash.end();
res.json( {hash:String(hash.read())} );
});
fileStream.pipe(hash, { end: false });
});
}

View File

View File

@ -57,8 +57,11 @@ exports.setup = function(mstream, dbSettings){
sql2 += "(?, ?, ?), ";
sqlParser.push(title);
sqlParser.push( fe.join(req.user.musicDir, song) ); // TODO: User music dir
sqlParser.push( req.user.username ); // TODO: User music dir
// TODO: We need to strip out the vPath
// We need to allow pre-set vPaths in the config file
// Then strip out the vPath here
sqlParser.push( fe.join(req.user.musicDir, song) );
sqlParser.push( req.user.username );
}
@ -103,7 +106,7 @@ exports.setup = function(mstream, dbSettings){
var tempName = fe.basename(rows[i].filepath);
var extension = getFileType(rows[i].filepath);
var filepath = slash(fe.relative(req.user.musicDir, rows[i].filepath)); // TODO
console.log(filepath);
returnThis.push({name: tempName, file: filepath, filetype: extension });
}
@ -254,7 +257,6 @@ exports.setup = function(mstream, dbSettings){
}
// Format data for API
// rows = setLocalFileLocation(rows);
for(var i in rows ){
var path = String(rows[i]['cast(path as TEXT)']);
@ -267,38 +269,4 @@ exports.setup = function(mstream, dbSettings){
});
});
mstream.get('/db/download-db', function(req, res){
// Check user for beets db
if(!req.user.privateDB || req.user.privateDB != 'BEETS'){
res.status(500).json({ error: 'DB Error' });
return;
}
// Download File
res.download(req.user.privateDBOptions.importDB);
});
// Get hash of database
// TODO: Change the name of this endpoint
mstream.get( '/db/hash', function(req, res){
// Check if user is using beets
if(!req.user.privateDB || req.user.privateDB != 'BEETS'){
res.status(500).json({ error: 'DB Error' });
return;
}
var hash = crypto.createHash('sha256');
hash.setEncoding('hex');
var fileStream = fs.createReadStream(req.user.privateDBOptions.importDB);
fileStream.on('end', function () {
hash.end();
res.json( {hash:String(hash.read())} );
});
fileStream.pipe(hash, { end: false });
});
}

View File

@ -1 +0,0 @@
// TODO: Function that copies a BeetsDB(private) into the MySQL master(public) DB

View File

@ -1 +0,0 @@
// TODO: Function that copies a BeetsDB(private) into the SQLite master(public) DB

View File

@ -0,0 +1,87 @@
const PouchDB = require('pouchdb');
exports.setup = function(dbName){
db = new PouchDB(dbName);
}
exports.getUserFiles = function(thisUser, callback){
db.all("SELECT path, file_modified_date FROM items WHERE user = ?;", thisUser.username, function(err, rows){
// Format results
var returnThis = rows;
// callback function
callback(returnThis);
});
}
exports.insertEntries = function(arrayOfSongs, username, callback){
let bulkDocs = [];
while(arrayOfSongs.length > 0) {
var song = arrayOfSongs.pop();
var songTitle = null;
var songYear = null;
var songAlbum = null;
var artistString = null;
if(song.artist && song.artist.length > 0){
artistString = '';
for (var i = 0; i < song.artist.length; i++) {
artistString += song.artist[i] + ', ';
}
artistString = artistString.slice(0, -2);
}
if(song.title && song.title.length > 0){
songTitle = song.title;
}
if(song.year && song.year.length > 0){
songYear = song.year;
}
if(song.album && song.album.length > 0){
songAlbum = song.album;
}
// TODO: Update SQL
// sqlParser.push(songTitle);
// sqlParser.push(artistString);
// sqlParser.push(songYear);
// sqlParser.push(songAlbum);
// sqlParser.push(song.filePath);
// sqlParser.push(song.format);
// sqlParser.push(song.track.no);
// sqlParser.push(song.disk.no);
// sqlParser.push(username);
// sqlParser.push(song.filesize);
// sqlParser.push(song.modified);
// sqlParser.push(song.created);
"insert into items (title,artist,year,album,path,format, track, disk, user, filesize, file_modified_date, file_created_date) values ";
bulkDocs.push( {
_id: song.filePath,
title: songTitle,
artist: artistString,
year: songYear,
album: songAlbum,
path: song.filePath, // FIXME: Redundant data
format:song.format,
track:song.track.no,
disk: song.disk.no,
user:username,
filesize:song.filesize,
file_modified_date:song.modified,
file_created_date:song.created,
});
}
db.bulkDocs(bulkDocs, function() {
console.log('ITS DONE');
callback();
});
}

View File

@ -8,11 +8,9 @@ exports.setup = function(dbPath){
}
exports.getUserFiles = function(thisUser, callback){
console.log(thisUser.username);
db.all("SELECT path, file_modified_date FROM items WHERE user = ?;", thisUser.username, function(err, rows){
// Format results
var returnThis = rows;
console.log(rows);
// callback function
callback(returnThis);
@ -50,7 +48,6 @@ exports.insertEntries = function(arrayOfSongs, username, callback){
songAlbum = song.album;
}
// TODO: Update SQL
sql2 += "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), ";
sqlParser.push(songTitle);
sqlParser.push(artistString);
@ -60,7 +57,7 @@ exports.insertEntries = function(arrayOfSongs, username, callback){
sqlParser.push(song.format);
sqlParser.push(song.track.no);
sqlParser.push(song.disk.no);
sqlParser.push(username); // TODO: User
sqlParser.push(username);
sqlParser.push(song.filesize);
sqlParser.push(song.modified);
sqlParser.push(song.created);
@ -76,3 +73,17 @@ exports.insertEntries = function(arrayOfSongs, username, callback){
callback();
});
}
// Function that reformats data from beets
exports.reformatData = function(username){
let sql = "DELETE FROM items WHERE user = ?";
db.all(sql, username, function(err, rows){
});
}
// Function that removes all files from the given DB
exports.purgeDB = function(){
}

View File

@ -10,54 +10,29 @@ const archiver = require('archiver'); // Zip Compression
const os = require('os');
const crypto = require('crypto');
const slash = require('slash');
const uuidV4 = require('uuid/v4');
// If the user gives a json file then try pulling the config from that
try{
var startup = 'configure-commander';
if(fe.extname(process.argv[process.argv.length-1]) == '.json' && fs.statSync(process.argv[process.argv.length-1]).isFile()){
startup = 'configure-json-file';
}
}catch(error){
console.log('JSON file does not appear to exist');
// Get the server config
const program = require('./modules/configure-json-file.js').setup(process.argv, __dirname);
if(program.error){
console.log(program.error);
process.exit();
}
const program = require('./modules/' + startup + '.js').setup(process.argv);
if(program == false){
process.exit();
}
// Normalize for all OS
// Make sure it's a directory
// Loop through and makeure all user Dirs are real
// TODO: Move all checks to the JSON module
if(program.users){
for (let i = 0; i < program.users.length; i++) {
//TODO: Check usernames for forbidden chars
// TODO: Assure all usernames are unique
// TODO: Or update JSON so usernames have to be unique
// TODO: Assure only one user per filepath
if(!fs.statSync( program.users[i].musicDir ).isDirectory()){
console.log(program.users[i].username + " music directory could not be found");
process.exit();
}
}
}
// Magic Middleware Things
mstream.use(bodyParser.json()); // support json encoded bodies
mstream.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies
// Setup WebApp
if(program.userinterface){
mstream.use( express.static(fe.join(__dirname, program.userinterface) ));
// Serve the webapp
mstream.get('/', function (req, res) {
res.sendFile( fe.join(program.userinterface, 'mstream.html'), { root: __dirname });
});
}
// Print the local network IP
console.log('Access mStream locally: http://localhost:' + program.port);
@ -65,30 +40,12 @@ console.log('Access mStream on your local network: http://' + require('my-local-
// Handle Port Forwarding
// TODO: Portforwarding could use a feature that re-opens it on a timed interval
// TODO: Switch between uPNP and nat-pmp
if(program.tunnel){
const tunnel = require('./modules/auto-port-forwarding.js');
tunnel.tunnel_uPNP(program.port);
tunnel.logUrl(program.port);
const tunnel = require('./modules/auto-port-forwarding.js').setup(program.tunnel, program.port);
}
// Check that this is a real dir
if(!fs.statSync( fe.join(__dirname, program.userinterface) ).isDirectory()){
console.log('The userinterface was not found. Closing...');
process.exit();
}
mstream.use( express.static(fe.join(__dirname, program.userinterface) ));
// Serve the webapp
mstream.get('/', function (req, res) {
res.sendFile( fe.join(program.userinterface, 'mstream.html'), { root: __dirname });
});
// Login functionality
if(program.users){
// Use bcrypt for password storage
@ -126,53 +83,52 @@ if(program.users){
// TODO: password change function
if(false == true){
mstream.post('/change-password-request', function (req, res) {
// Get email address from request
// validate email against user array
// Generate change password token
// Invalidate all other change password tokens
// Email the user the token
mstream.post('/change-password-request', function (req, res) {
// Get email address from request
// validate email against user array
// Generate change password token
// Invalidate all other change password tokens
// Email the user the token
res.sendFile( 'COMING SOON!' );
});
res.sendFile( 'COMING SOON!' );
});
mstream.post('/change-password', function (req, res){
// Check token
// Get new password
// Hash password and update user array
mstream.post('/change-password', function (req, res){
// Check token
// Get new password
// Hash password and update user array
res.sendFile( 'COMING SOON!' );
});
}
res.sendFile( 'COMING SOON!' );
});
// Create the user array
var Users = {};
// Construct user array
for (let i = 0; i < program.users.length; i++) {
Users[program.users[i].username] = {
musicDir:program.users[i].musicDir,
// var Users = {};
var Users = program.users;
for (let username in Users) {
let permissionsMap = {};
generateSaltedPassword(username, Users[username]["password"]);
if(Users[username].guestTo){
// DO NOTHING!
}else if ( !(Users[username].musicDir in permissionsMap) ){
// Generate unique vPath if necessary
// Th best way is to store the vPath in the JSON file
if(!Users[username].vPath){
Users[username].vPath = uuidV4();
}
// Add to permissionsMap
permissionsMap[Users[username].musicDir] = Users[username].vPath;
// Add dir to express
mstream.use( '/' + Users[username].vPath + '/' , express.static( Users[username].musicDir ));
}else{
Users[username].vPath = permissionsMap[Users[username].musicDir];
}
if(program.users[i].email){
Users[program.users[i].username].email = program.users[i].email;
}
if(program.users[i].privateDB){
Users[program.users[i].username].email = program.users[i].privateDB;
}
if(program.users[i].privateDBOptions){
Users[program.users[i].username].email = program.users[i].privateDBOptions;
}
generateSaltedPassword(program.users[i].username, program.users[i].password);
////////////////////////////////
// TODO: Handle Guest Options //
////////////////////////////////
// TODO: We could use a better way of mapping users to vPaths
mstream.use( '/' + program.users[i].username + '/' , express.static( program.users[i].musicDir ));
}
@ -226,7 +182,7 @@ if(program.users){
var sendThis = {
success: true,
message: 'Welcome To mStream',
vPath: user.username,
vPath: user.vPath,
token: jwt.sign(user, secret) // Make the token
};
@ -254,25 +210,34 @@ if(program.users){
}
// Deny guest access
if(decoded.guest === true && forbiddenFunctions.indexOf(req.path) != -1){
// TODO: Modify this based on parameters set in json file
if(decoded.guestTo && forbiddenFunctions.indexOf(req.path) != -1){
return res.redirect('/guest-access-denied');
}
// Set user request data
req.user = decoded;
//
if(decoded.guestTo){
req.user.username = req.user.guestTo;
// TODO: We should probably set the vPath elsewhere
req.user.vPath = Users[req.user.guestTo].vPath;
req.user.musicDir = Users[req.user.guestTo].musicDir;
}
next();
});
});
// TODO: Middleware that prevents users from accessing another users files
// TODO: Strip all password info out
}else{
// Dummy data
mstream.use(function(req, res, next) {
req.user = {
username:"",
musicDir:program.filepath
musicDir:process.cwd()
};
next();
});
@ -283,18 +248,14 @@ if(program.users){
// Test function
// Used to determine the user has a working login token
mstream.get('/ping', function(req, res){
// TODO: Guest status
var returnObject = {
vPath: req.user.username,
res.json({
vPath: req.user.vPath,
guest: false
};
res.send(JSON.stringify(returnObject));
});
});
@ -304,20 +265,18 @@ mstream.post('/dirparser', function (req, res) {
var directories = [];
var filesArray = [];
// Make sure directory exits
// TODO Get music dir from request
// TODO: Make sure path is a sub-path of the user's music dir
var path = fe.join(req.user.musicDir, req.body.dir);
// if(path == ""){
// path = rootDir;
// }else{
// path = fe.join(rootDir, path);
// }
// Make sure it's a directory
if(!fs.statSync( path).isDirectory()){
res.status(500).json({ error: 'Not a directory' });
return;
}
// Will only show these files. Prevents people from snooping around
// TODO: Move to global vairable
var masterFileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a"];
const masterFileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a"];
var fileTypesArray;
if(req.body.filetypes){
fileTypesArray = JSON.parse(req.body.filetypes);
}else{
@ -325,19 +284,11 @@ mstream.post('/dirparser', function (req, res) {
}
// Make sure it's a directory
if(!fs.statSync( path).isDirectory()){
// TODO: Write an error output
// 500 Output?
res.send("");
return;
}
// get directory contents
var files = fs.readdirSync( path);
// loop through files
for (var i=0; i < files.length; i++) {
for (let i=0; i < files.length; i++) {
try{
var stat = fs.statSync(fe.join(path, files[i]));
@ -365,15 +316,14 @@ mstream.post('/dirparser', function (req, res) {
}
var returnPath = slash( fe.relative(req.user.musicDir, path) );
if(returnPath.slice(-1) !== '/'){
returnPath += '/';
}
// Combine list of directories and mp3s
var finalArray = { path:returnPath, contents:filesArray.concat(directories)};
// Send back some JSON
res.send(JSON.stringify(finalArray));
// Send back combined list of directories and mp3s
res.send(
JSON.stringify({ path:returnPath, contents:filesArray.concat(directories)})
);
});
@ -421,24 +371,16 @@ mstream.post('/download', function (req, res){
// ============================================================================
//
// // Old way
// //const mstreamDB = require('./modules/database-'+program.databaseplugin+'.js');
// // mstreamDB.setup(mstream, program.users, db); // TODO: ROOTDIR
//
// // New Way
// // TODO: We need to pull this from the program var
var publicDBType = 'sqlite3'; // Can be sqlite3/mysql/LokiJS
var dbSettings = program.database_plugin;
const mstreamDB = require('./modules/db-management/database-master.js');
// mstreamDB.setup(mstream, program.users, publicDBType, dbSettings);
mstreamDB.setup(mstream, program);
// ============================================================================
// TODO: Add individual song
mstream.get('/db/add-songs', function(req, res){
res.send('Coming Soon!');
@ -457,4 +399,5 @@ mstream.post( '/get-album-art', function(req, res){
// Start the server!
// TODO: Check if port is in use befoe firing up server
const server = mstream.listen(program.port, function () {});

View File

@ -24,8 +24,10 @@
"jsonwebtoken": "^7.1.9",
"musicmetadata": "^2.0.3",
"my-local-ip": "^1.0.0",
"pouchdb": "^6.0.7",
"slash": "^1.0.0",
"sqlite3": "^3.1.4"
"sqlite3": "^3.1.4",
"uuid": "^3.0.1"
},
"optionalDependencies": {
"nat-upnp": "^1.0.2",

View File

@ -83,10 +83,9 @@ $(document).ready(function(){
request.done(function( msg ) {
// Remove login screen
// TODO: set virtualDirectory
var decoded = $.parseJSON(msg);
// set virtualDirectory
var decoded = msg;
virtualDirectory = decoded.vPath;
});
request.fail(function( jqXHR, textStatus ) {
@ -196,6 +195,9 @@ $(document).ready(function(){
function jPlayerSetMedia(fileLocation, filetype){
if(virtualDirectory){
fileLocation = virtualDirectory + '/' + fileLocation;
}
document.getElementById("mplayer").setAttribute("src", fileLocation);
document.getElementById("mplayer").setAttribute("title", fileLocation.split('/').pop());
}
@ -206,9 +208,6 @@ $(document).ready(function(){
function addFile2(that){
var filename = $(that).attr("id");
var file_location = $(that).data("file_location");
if(virtualDirectory){
file_location = virtualDirectory + '/' + file_location;
}
if(accessKey){
file_location += '?token=' + accessKey;
}

View File

@ -1,29 +1,29 @@
{
"port":3031,
"users":[
{
"username":"paul",
"users":{
"paul":{
"password":"glassjar98",
"email":"paul@wall.com",
"musicDir":"/path/to/music",
"guest":true,
"guestUsername":"guest name",
"guestPassword":"abcd",
"privateDB":"BEETS",
"privateDBOptions":{
"privateDB":"BEETS",
"importDB":"path/to/sqlite3.db",
"beetspath":"/path/to/beets/music/dir"
"beetCommand":"Run This Command Update Call",
"quickSync": true,
},
"bannedGuestFunctions":"todo. make this optional"
"bannedGuestFunctions":"todo. make this optional",
"vPath":"UUID-GOES-HERE"
},
{
"username":"paul2",
"paul2":{
"password":"glassjar98",
"email":"paul@wall2.com",
"musicDir":"/path/to/music2",
"guest":"false",
},
"paul2-guest":{
"password":"glassjar98",
"guestTo":"paul2",
}
],
},
"userinterface":"public",
"database_plugin":{
"type":"sqlite",
@ -38,6 +38,15 @@
"database_plugin-LOKI":{
"comming":"soon",
},
"secret":"SECRET GOES HERE. OR A PATH TO THE SECRET",
"tunnel":true,
"secret":"Secret for JSON web token",
"tunnel":{
"gateway":"1.1.1.1",
"refreshInterval":10000,
"protocol":"upnp"
},
"tunnelOptions":{},
"salt":"Salt for passwords. optional. Necessarry forthe noHash function which allows hashed passwords to be stored in users array",
"noHash":true,
"ssl":"SSL OPTIONS"
}

View File

@ -0,0 +1,4 @@
{
"port":5060,
"userinterface":"public",
}