mirror of
https://github.com/IrosTheBeggar/mStream.git
synced 2025-10-27 07:31:02 +00:00
Lots of new stuff
This commit is contained in:
parent
e952357eae
commit
cb3293359c
52
README.md
52
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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
71
modules/db-management/database-beets-manager.js
Normal file
71
modules/db-management/database-beets-manager.js
Normal 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:
|
||||
@ -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){
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
0
modules/db-read/database-public-pouch.js
Normal file
0
modules/db-read/database-public-pouch.js
Normal 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 });
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
// TODO: Function that copies a BeetsDB(private) into the MySQL master(public) DB
|
||||
@ -1 +0,0 @@
|
||||
// TODO: Function that copies a BeetsDB(private) into the SQLite master(public) DB
|
||||
87
modules/db-write/database-default-pouch.js
Normal file
87
modules/db-write/database-default-pouch.js
Normal 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();
|
||||
});
|
||||
}
|
||||
@ -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(){
|
||||
|
||||
}
|
||||
|
||||
225
mstream.js
225
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 () {});
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
}
|
||||
|
||||
4
untitled folder/skeleton.json
Normal file
4
untitled folder/skeleton.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"port":5060,
|
||||
"userinterface":"public",
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user