From cb3293359c599d827d0faee8819f2cbcf2f4c49c Mon Sep 17 00:00:00 2001 From: Paul Sori Date: Sun, 11 Dec 2016 04:04:14 -0500 Subject: [PATCH] Lots of new stuff --- README.md | 52 ++-- modules/auto-port-forwarding.js | 62 +++-- modules/configure-commander.js | 79 ++++-- modules/configure-json-file.js | 70 +++++- .../db-management/database-beets-manager.js | 71 ++++++ .../db-management/database-default-manager.js | 12 +- modules/db-management/database-master.js | 36 +++ modules/db-read/database-public-pouch.js | 0 modules/db-read/database-public-sqlite.js | 44 +--- modules/db-write/database-beets-mysql.js | 1 - modules/db-write/database-beets-sqlite.js | 1 - modules/db-write/database-default-pouch.js | 87 +++++++ modules/db-write/database-default-sqlite.js | 19 +- mstream.js | 225 +++++++----------- package.json | 4 +- public/js/mstream.js | 11 +- untitled folder/exampleNewJSON.json | 39 +-- untitled folder/skeleton.json | 4 + 18 files changed, 534 insertions(+), 283 deletions(-) create mode 100644 modules/db-management/database-beets-manager.js create mode 100644 modules/db-read/database-public-pouch.js delete mode 100644 modules/db-write/database-beets-mysql.js delete mode 100644 modules/db-write/database-beets-sqlite.js create mode 100644 modules/db-write/database-default-pouch.js create mode 100644 untitled folder/skeleton.json diff --git a/README.md b/README.md index 09fc2de..4250087 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/modules/auto-port-forwarding.js b/modules/auto-port-forwarding.js index 85dc394..1fa882b 100644 --- a/modules/auto-port-forwarding.js +++ b/modules/auto-port-forwarding.js @@ -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); +} diff --git a/modules/configure-commander.js b/modules/configure-commander.js index 5074c9d..8fe60cd 100644 --- a/modules/configure-commander.js +++ b/modules/configure-commander.js @@ -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 ', 'Select Port', /^\d+$/i, 3000) .option('-t, --tunnel', 'Use nat-pmp to configure port fowarding') - .option('-g, --gateip ', 'Manually set gateway IP for the tunnel option') + .option('-g, --gateway ', 'Manually set gateway IP for the tunnel option') + .option('-r, --refresh ', 'Refresh rate', /^\d+$/i) + .option('-o, --protocol ', 'Refresh rate', /^\d+$/i) .option('-u, --user ', 'Set Username') .option('-x, --password ', 'Set Password') - // .option('-e, --email ', 'Set User Email (optional)') + .option('-e, --email ', 'Set User Email (optional)') .option('-G, --guest ', 'Set Guest Username') .option('-X, --guestpassword ', 'Set Guest Password') - // .option('-k, --key ', 'Add SSL Key') - // .option('-c, --cert ', 'Add SSL Certificate') .option('-d, --database ', 'Specify Database Filepath', 'mstreamdb.lite') - // .option('-b, --beetspath ', '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 ', '', /^(default|beets)$/i, 'default') .option('-i, --userinterface ', 'Specify folder name that will be served as the UI', 'public') - .option('-f, --filepath ', 'Set the path of your music directory', process.cwd()) .option('-s, --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; } diff --git a/modules/configure-json-file.js b/modules/configure-json-file.js index e2544a4..e85603d 100644 --- a/modules/configure-json-file.js +++ b/modules/configure-json-file.js @@ -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 diff --git a/modules/db-management/database-beets-manager.js b/modules/db-management/database-beets-manager.js new file mode 100644 index 0000000..0f56db5 --- /dev/null +++ b/modules/db-management/database-beets-manager.js @@ -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: diff --git a/modules/db-management/database-default-manager.js b/modules/db-management/database-default-manager.js index af90e10..0c86361 100644 --- a/modules/db-management/database-default-manager.js +++ b/modules/db-management/database-default-manager.js @@ -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){ diff --git a/modules/db-management/database-master.js b/modules/db-management/database-master.js index 95ebfde..f320b5a 100644 --- a/modules/db-management/database-master.js +++ b/modules/db-management/database-master.js @@ -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 }); + }); + } diff --git a/modules/db-read/database-public-pouch.js b/modules/db-read/database-public-pouch.js new file mode 100644 index 0000000..e69de29 diff --git a/modules/db-read/database-public-sqlite.js b/modules/db-read/database-public-sqlite.js index 2ba09d4..c5d8fe2 100644 --- a/modules/db-read/database-public-sqlite.js +++ b/modules/db-read/database-public-sqlite.js @@ -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 }); - }); - - } diff --git a/modules/db-write/database-beets-mysql.js b/modules/db-write/database-beets-mysql.js deleted file mode 100644 index ce55153..0000000 --- a/modules/db-write/database-beets-mysql.js +++ /dev/null @@ -1 +0,0 @@ -// TODO: Function that copies a BeetsDB(private) into the MySQL master(public) DB diff --git a/modules/db-write/database-beets-sqlite.js b/modules/db-write/database-beets-sqlite.js deleted file mode 100644 index 92e8106..0000000 --- a/modules/db-write/database-beets-sqlite.js +++ /dev/null @@ -1 +0,0 @@ -// TODO: Function that copies a BeetsDB(private) into the SQLite master(public) DB diff --git a/modules/db-write/database-default-pouch.js b/modules/db-write/database-default-pouch.js new file mode 100644 index 0000000..cc4fccb --- /dev/null +++ b/modules/db-write/database-default-pouch.js @@ -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(); + }); +} diff --git a/modules/db-write/database-default-sqlite.js b/modules/db-write/database-default-sqlite.js index bf59c39..661b553 100644 --- a/modules/db-write/database-default-sqlite.js +++ b/modules/db-write/database-default-sqlite.js @@ -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(){ + +} diff --git a/mstream.js b/mstream.js index f71b65b..7b7a844 100755 --- a/mstream.js +++ b/mstream.js @@ -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 () {}); diff --git a/package.json b/package.json index e15fcf7..9675cd2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/js/mstream.js b/public/js/mstream.js index ecd72f4..308a828 100755 --- a/public/js/mstream.js +++ b/public/js/mstream.js @@ -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; } diff --git a/untitled folder/exampleNewJSON.json b/untitled folder/exampleNewJSON.json index a7a7537..f4da2c4 100644 --- a/untitled folder/exampleNewJSON.json +++ b/untitled folder/exampleNewJSON.json @@ -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" + } diff --git a/untitled folder/skeleton.json b/untitled folder/skeleton.json new file mode 100644 index 0000000..17b8c65 --- /dev/null +++ b/untitled folder/skeleton.json @@ -0,0 +1,4 @@ +{ + "port":5060, + "userinterface":"public", +}