Merge pull request #7 from IrosTheBeggar/multi-user-alpha

mStream - now with 100% more multi user functionality
This commit is contained in:
Paul 2016-12-14 23:50:55 -05:00 committed by GitHub
commit e754f122db
20 changed files with 1776 additions and 987 deletions

123
README.md
View File

@ -1,60 +1,51 @@
## 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
#### Main Features
* Supports FLAC streaming
* Built in SQLite DB. No need to setup MySQL
* Built in DB using SQLite. No need to run a separate DB
* Works on Mac, Linux and Windows
* [Integrates easily with Beets DB](https://github.com/beetbox/beets)
* User system with one admin and one guest account
* Allows multiple users
## Installation
### Windows Executable
There is work being done to port mStream to a Windows Executable. Check out the prototype here:
https://drive.google.com/file/d/0B1oiqEsIbjFidk8tVjR0TmZIb0k/view?usp=sharing
### Default
#### Dependencies
mStream has the following dependencies:
* NodeJS and NPM
* Python 2
* GCC and G++
* node-gyp
Once have all the dependencies you can install and setup mStream by doing the following
#### Install on Ubuntu
Install NodeJS
```shell
npm install -g node-gyp
npm install -g mstream
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo-apt-get update
sudo apt-get install -y nodejs
```
Install GCC and node-gyp
```shell
sudo apt-get install -y build-essential
sudo npm install -g node-gyp
```
Install mStream
```shell
sudo npm install -g mstream
cd /path/to/your/music
mstream
```
Make sure it's working by checking out http://localhost:3000/
### Install on Ubuntu
Copy and paste the following commands:
```shell
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo apt-get install -y build-essential
sudo npm install -g node-gyp
```
### Using Docker
#### Using Docker
Download the Dockerfile, or clone the repository, then run the following
commands:
@ -72,57 +63,52 @@ 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 using a JSON config file or by using flags in the command line. JSON config files are more flexible but more difficult to use.
This readme will not cover JSON config usage. See the examples folder to learn more.
#### Set Port
```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 -p 5050
```
## 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
mStream can have a single user and guest when being setup using the command line.
```shell
mstream -l -u [username] -x [password]
# Set User
mstream -u [username] -x [password]
# Set user and guest
mstream -u [username] -x [password] -G [guest name] -X [guest 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.
Multiple users can be set using JSON config files
## Database
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.
## Database Options
mStream uses sqlite by default. You can either use mStream's default database [or tap into BeetsDB](https://github.com/beetbox/beets)
#### Beets DB
http://beets.io/
mStream can use your beets database without any configuration.
```shell
mstream -d path/to/beets.db
mstream -D beets -d /path/to/beets.db
```
Currently using beets is the recommended way to create a music database.
When using Beets, mStream is put into a read only mode. mStream will not be able to write to any tables that are managed by Beets. Playlist functionality is not affected by this since playlists are stored in a separate table.
#### use mStream to build your DB
#### Built In 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
mStream can read metadata and write it's own database. By default mstream will create a database in the folder it's launched in called 'mstreamdb.lite'. You can manually set the databse file with:
```shell
mstream -d /path/to/mstream.db
```
## Automatically setup port forwarding
#### Please note that this feature is still experimental
mStream can try to automatically open a port to the internet. Use the '-t' command to try to setup port forwarding. Additionally you can use the '-g' command to set the gateway IP manually. If you don't include '-g', the program will use an extension to try to figure it out
@ -137,6 +123,12 @@ mstream musicDirectory/ -t -g 192.168.1.1
Please note that not all routers will allow this. Some routers may close this port after a period of time.
You can get around this by having mStream retry this on a regular interval
```
mstream -t -r [time in milliseconds]
mstream -t -r 10000
```
## Known Issues
@ -145,6 +137,11 @@ Please note that not all routers will allow this. Some routers may close this p
## TODO
- GET request to jump to playlist or directory
- Look into taglib for id3 info
- SSL support
- Album Art
- Reset Password Functions
- 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

View File

@ -0,0 +1,28 @@
{
"port":3031,
"users":{
"paul":{
"password":"glassjar98",
"email":"paul@wall.com",
"musicDir":"/path/to/music",
},
"paul2":{
"password":"glassjar98",
"email":"paul@wall2.com",
"musicDir":"/path/to/music2",
},
"paul2-guest":{
"password":"glassjar98",
"guestTo":"paul2",
}
},
"userinterface":"public",
"database_plugin":{
"type":"sqlite",
"dbPath":"/path/to/db",
},
"tunnel":{
"refreshInterval":10000,
"protocol":"upnp"
}
}

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,27 +1,78 @@
exports.setup = function(args){
// Setup Command Line Interface
var program = require('commander');
const program = require('commander');
program
.version('1.21.0')
.version('2.0.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('-l, --login', 'Require users to login')
.option('-g, --gateway <gateway>', 'Manually set gateway IP for the tunnel option')
.option('-r, --refresh <refresh>', 'Refresh rate', /^\d+$/i)
.option('-o, --protocol <protocol>', 'Protocol for tunneling', /^(upnp|natpnp)$/i, 'natpnp')
.option('-u, --user <user>', 'Set Username')
.option('-x, --password <password>', 'Set Password')
.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')
.option('-D, --databaseplugin <databaseplugin>', '', /^(sqlite|beets)$/i, 'sqlite') // TODO: Add support for other DBs when ready
.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:program.databaseplugin,
dbPath:program.database
};
// 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,30 +1,74 @@
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"};
}
// Check for validity
if(!loadJson.filepath){
loadJson.filepath = process.cwd();
if(!loadJson.port){
loadJson.port = 5050;
}
if(!isInt(loadJson.port) || loadJson.port < 0 || loadJson.port > 65535){
return {error:"BAD PORT, WILL ABORT"};
}
if(!loadJson.databaseplugin){
loadJson.databaseplugin = 'default';
}else{
var re = new RegExp("^(default|beets)$");
if(!re.test(loadJson.databaseplugin)){
console.log('Incorrect database plugin. Please update and try again');
return false;
// TODO: Add comprehensive DB checks
if(!loadJson.database_plugin){
return {error:"Please Configure DB"};
}
if(loadJson.userinterface){
if(!fs.statSync( fe.join(rootDir, loadJson.userinterface) ).isDirectory()){
return {error:"Could not find userinterface"};
}
}
// 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(loadJson.tunnel){
if(loadJson.tunnel.refreshInterval && !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

@ -1,111 +0,0 @@
// TODO: This thing has to be tested
const spawn = require('child_process').spawn;
var scanLock = false;
var yetAnotherArrayOfSongs = [];
var totalFileCount = 0;
exports.setup = function(mstream, program, rootDir, db){
const scanThisDir = program.beetspath; // TODO: Check that this is a real directory
mstream.get('/db/recursive-scan', function(req,res){
if(scanLock === true){
// Return error
res.status(401).send('{"error":"Scan in progress"}');
return;
}
scanLock = true;
var cmd = spawn('beet', [ 'import', '-A', '--group-albums' , scanThisDir]);
cmd.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
cmd.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
scanLock = false;
});
cmd.on('close', (code) => {
console.log(`child process exited with code ${code}`);
hashFileBeets();
// TODO: Remove all empty dirs
});
});
function hashFileBeets(){
// var hashCmd = spawn('beet check -a');
var hashCmd = spawn('beet', [ 'check', '-a']);
hashCmd.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
hashCmd.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
scanLock = false;
});
hashCmd.on('close', (code) => {
console.log(`child process exited with code ${code}`);
scanLock = false;
});
}
// TODO: Function that will remove all empty folders
function removeEmptyFolders(){
var hashCmd = spawn('beet', [ 'check', '-a']);
// 'find ~ -type d -empty -delete'
}
mstream.get('/db/status', function(req, res){
var returnObject = {};
returnObject.locked = scanLock;
if(scanLock){
// Currently we don't support filecount stats when using beets DB
// Dummy data
returnObject.totalFileCount = 0;
returnObject.filesLeft = 0;
res.json(returnObject);
}else{
var sql = 'SELECT Count(*) FROM items';
db.get(sql, function(err, row){
if(err){
console.log(err.message);
res.status(500).json({ error: err.message });
return;
}
var fileCountDB = row['Count(*)']; // TODO: Is this correct???
returnObject.totalFileCount = fileCountDB;
res.json(returnObject);
});
}
});
}

View File

@ -1,255 +0,0 @@
const metadata = require('musicmetadata'); // TODO: Look into replacing with taglib
const fs = require('graceful-fs'); // File System
const fe = require('path');
var dbCopy;
var arrayOfSongs = [];
var scanLock = false;
var yetAnotherArrayOfSongs = [];
var totalFileCount = 0;
function getFileType(filename){
return filename.split(".").pop();
}
function parseFile(thisSong){
var readableStream = fs.createReadStream(thisSong);
var parser = metadata(readableStream, function (err, songInfo) {
if(err){
// TODO: Do something
}
// TODO: Hash the file here and add the hash to the DB
console.log(songInfo);
// Close the stream
readableStream.close();
songInfo.filePath = thisSong;
songInfo.format = getFileType(thisSong);
arrayOfSongs.push(songInfo);
// if there are more than 100 entries, or if it's the last song
if(arrayOfSongs.length > 99){
insertEntries();
}
// For the generator
parse.next();
});
}
function *parseAllFiles(){
// Loop through local items
while(yetAnotherArrayOfSongs.length > 0) {
var file = yetAnotherArrayOfSongs.pop();
var resultX = yield parseFile(file);
}
insertEntries();
scanLock = false;
}
var parse;
// Insert
function insertEntries(){
var sql2 = "insert into items (title,artist,year,album,path,format, track, disk) values ";
var sqlParser = [];
while(arrayOfSongs.length > 0) {
var song = arrayOfSongs.pop();
// console.log(song);
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;
}
sql2 += "(?, ?, ?, ?, ?, ?, ?, ?), ";
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);
}
sql2 = sql2.slice(0, -2);
sql2 += ";";
console.log(sql2);
dbCopy.run(sql2, sqlParser);
}
// Count all files
function countFiles (dir, fileTypesArray) {
console.log('efwefwf');
var files = fs.readdirSync( dir );
console.log(files);
for (var i=0; i < files.length; i++) {
var filePath = fe.join(dir, files[i]);
var stat = fs.statSync(filePath);
console.log(filePath);
if(stat.isDirectory()){
console.log('qqq');
countFiles(filePath , fileTypesArray);
}else{
console.log('www');
var extension = getFileType(files[i]);
if (fileTypesArray.indexOf(extension) > -1 ) {
yetAnotherArrayOfSongs.push(filePath);
}
}
}
}
exports.setup = function(mstream, program, rootDir, db){
const rootDirCopy = rootDir;
dbCopy = db;
// scan and screate database
mstream.get('/db/recursive-scan', function(req,res){
console.log('xxx');
// Check if this is already running
if(scanLock === true){
// Return error
res.status(401).send('{"error":"Scan in progress"}');
return;
}
try{
// turn on scan lock
scanLock = true;
// Make sure directory exits
var fileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a"];
console.log(rootDirCopy);
countFiles(rootDirCopy, fileTypesArray);
totalFileCount = yetAnotherArrayOfSongs.length;
console.log(totalFileCount);
dbCopy.serialize(function() {
// These two queries will run sequentially.
dbCopy.run("drop table if exists items;");
dbCopy.run("CREATE TABLE items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title varchar DEFAULT NULL, artist varchar DEFAULT NULL, year int DEFAULT NULL, album varchar DEFAULT NULL, path text, format varchar, track INTEGER, disk INTEGER);", function() {
// These queries will run in parallel and the second query will probably
// fail because the table might not exist yet.
console.log('TABLES CREATED');
parse = parseAllFiles();
parse.next();
});
});
}catch(err){
// Remove lock
scanLock = false;
// // Log error
// res.status(500).send('{"error":"'+err+'"}');
// console.log(err);
return;
}
res.send("YA DID IT");
});
mstream.get('/db/status', function(req, res){
var returnObject = {};
returnObject.locked = scanLock;
if(scanLock){
returnObject.totalFileCount = totalFileCount;
returnObject.filesLeft = yetAnotherArrayOfSongs.length;
res.json(returnObject);
}else{
var sql = 'SELECT Count(*) FROM items';
dbCopy.get(sql, function(err, row){
if(err){
console.log(err.message);
res.status(500).json({ error: err.message });
return;
}
var fileCountDB = row['Count(*)']; // TODO: Is this correct???
returnObject.totalFileCount = fileCountDB;
res.json(returnObject);
});
}
});
}

View File

@ -0,0 +1,96 @@
// 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();
// 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);
// TODO: We can make this more efficient by comparing the differences and just adding/deleting the changes
dbPublic.purgeDB(username);
dbPublic.addToDB(files);
if(loadJson.privateDBOptions.quickSync === false){
smokeThatHash();
}
});
}
function insertEntries(numberToInsert = 99, loopToEnd = false){
var insertThese = [];
while(insertThese.length != numberToInsert ){
if(arrayOfSongs.length == 0){
break;
}
insertThese.push(arrayOfSongs.pop());
}
dbRead.insertEntries(insertThese, loadJson.username, function(){
// Recursivly run this function until all songs have been added
if(loopToEnd && arrayOfSongs.length != 0){
insertEntries(numberToInsert, true);
}else{
// For the generator
parseFilesGenerator.next();
}
});
}
// 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

@ -0,0 +1,357 @@
#!/usr/bin/env node
"use strict";
// This is designed to run as it's own process
// It takes in a json array
// {
// "username":"lol",
// "userDir":"/Users/psori/Desktop/Blockhead",
// "dbSettings":{
// "type":"sqlite",
// "dbPath":"/Users/psori/Desktop/LATESTGREATEST.DB"
// }
// }
const metadata = require('musicmetadata');
const fs = require('fs');
const fe = require('path');
const crypto = require('crypto');
try{
var loadJson = JSON.parse(process.argv[process.argv.length-1], 'utf8');
}catch(error){
console.log('Cannot parse JSON input');
process.exit();
}
// TODO: Check JSON for nencessary info
// 2.0
// Get all files from DB
// Hash file. If file and hash are found in array, then skip
// Seperate into new files and files that need to be updated
// Send these arrays to functions in database-default-X.js
var arrayOfSongs = []; // Holds songs for DB to process // TODO: Move out of global scope
var arrayOfScannedFiles = []; // Holds files for from recursive scan
// 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
}
// New way to start it
const parseFilesGenerator = rescanAllDirectories(loadJson.userDir);
parseFilesGenerator.next();
function *rescanAllDirectories(directoryToScan){
// Scan the directory for new, modified, and deleted files
var filesToProcess = yield rescanDirectory(directoryToScan);
// Process all new files
if(filesToProcess.newFiles.length != 0){
while(filesToProcess.newFiles.length > 0) {
yield parseFile(filesToProcess.newFiles.pop());
}
// Finish inserting all new entries
yield insertEntries(50, true);
}
// process all updated files
while(filesToProcess.updatedFiles.length > 0) {
// Handle Editted songs
yield parseUpdatedSong(filesToProcess.updatedFiles.pop());
yield insertEntries(50, true);
}
// TODO: Process deleted files
while(filesToProcess.deletedFiles.length > 0) {
// Handle Editted songs
yield deleteFile(filesToProcess.deletedFiles.pop());
}
// Exit
process.exit(0);
}
function rescanDirectory(dir){
// Get all files from DB
// TODO: Move This
dbRead.getUserFiles(loadJson, function(rows){
// Scan through files
var fileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a"];
recursiveScan(dir, fileTypesArray);
var latestFileList = arrayOfScannedFiles;
arrayOfScannedFiles = [];
var dbFileList = [];
var dbFileListTimestamp = [];
for(var s of rows){
console.log(s);
dbFileList.push(s.path);
dbFileListTimestamp.push(s.path + '::' + s.file_modified_date);
}
console.log(dbFileList);
console.log(dbFileListTimestamp);
var arrayOfUpdatedSongsToProcess = [];
var arrayOfSongsToProcess = [];
var deletedFiles = []; // TODO: Global variable ???
var checkForModifications = [];
var latestFileListSet = new Set(latestFileList);
var dbFileListSet = new Set(dbFileList);
var dbFileListTimestampSet = new Set(dbFileListTimestamp);
// Get deleted files
dbFileList.filter(function(x) {
if( latestFileListSet.has(x) ){
}else{
// It's deleted ( i think)
deletedFiles.push(x);
}
});
console.log('DELETED FILES');
console.log(deletedFiles);
// Get new files
latestFileList.filter(function(x) {
console.log(x);
if(dbFileListSet.has(x)){
checkForModifications.push(x + "::" + fs.statSync(x).mtime.getTime() );
console.log('yes');
}else{
// New files
arrayOfSongsToProcess.push(x);
console.log('no');
}
});
console.log('NEW FILES');
console.log(arrayOfSongsToProcess);
// loop through checkForModifications
// Append timestamp to all path strings
// Compare dbFileList clone with path strings appended
checkForModifications.filter(function(x) {
if(dbFileListTimestampSet.has(x)){
}else{
// File x has been updated
var filePath = x.split("::")[0];
arrayOfUpdatedSongsToProcess.push(filePath);
}
});
console.log('POTENTIALLY UPDATED SONGS');
console.log(arrayOfUpdatedSongsToProcess);
// TODO: Handle deleted files
// We need to prompt users to see if they want to delete files on the server side
// We can store a default behaviour
var returnArray = {
"newFiles":arrayOfSongsToProcess,
"updatedFiles":arrayOfUpdatedSongsToProcess,
"deletedFiles":deletedFiles
};
parseFilesGenerator.next(returnArray);
});
}
// TODO: Fix this
function parseUpdatedSong(filePath){
// Check sha256 hash and confirm it has changed
// Update file status in DB accordingly
var fileStream = fs.createReadStream(filePath);
var hash = crypto.createHash('sha256');
hash.setEncoding('hex');
fileStream.on('end', function () {
hash.end();
var hashIt = String(hash.read());
// compare hashes
//db.all("SELECT * FROM files WHERE path=? AND hash=?", [filePath, hashIt], function(err, rows){
dbRead.getHashedEntry(hashIt, filePath, loadJson.username, function(rows){
console.log(rows);
// No match found, file needs to be updated
if( !rows || rows.length === 0 ){
// TODO: delete entry
dbRead.deleteFile(filePath, loadJson.username, function(){
// Re-add entry
parseFile(filePath);
});
}else{
parseFilesGenerator.next();
}
});
});
fileStream.pipe(hash);
}
function deleteFile(filepath){
dbRead.deleteFile(filepath, loadJson.username, function(){
// Re-add entry
parseFilesGenerator.next();
});
}
function parseFile(thisSong){
var filestat = fs.statSync(thisSong);
if(!filestat.isFile()){
// TODO: Something is fucky, log it
parseFilesGenerator.next();
return;
}
// Stores all data that needs to be added to DB
var songInfo;
// TODO: Hash the file here and add the hash to the DB
var hash = crypto.createHash('sha256');
hash.setEncoding('hex');
var readableStream = fs.createReadStream(thisSong);
var parser = metadata(readableStream, function (err, thisMetadata) {
if(err){
// TODO: Do something
}
console.log(songInfo);
console.log(filestat);
songInfo = thisMetadata;
songInfo.filesize = filestat.size;
songInfo.created = filestat.birthtime.getTime();
songInfo.modified = filestat.mtime.getTime();
songInfo.filePath = thisSong;
songInfo.format = getFileType(thisSong);
readableStream.on('end', function () {
hash.end();
readableStream.close();
songInfo.hash = String(hash.read());
console.log('XXXXXXXXXXXXXXXxx');
console.log(songInfo);
arrayOfSongs.push(songInfo);
// if there are more than 100 entries, or if it's the last song
if(arrayOfSongs.length > 99){
// Insert entries into DB
insertEntries(99, false);
}else{
// For the generator
parseFilesGenerator.next();
}
});
});
readableStream.pipe(hash);
}
function getFileType(filename){
return filename.split(".").pop();
}
function recursiveScan(dir, fileTypesArray){
var files = fs.readdirSync( dir );
// loop through files
for (var i=0; i < files.length; i++) {
// var filePath = dir + files[i];
var filePath = fe.join(dir, files[i]);
// console.log(filePath);
var stat = fs.statSync(filePath);
if(stat.isDirectory()){
recursiveScan(filePath, fileTypesArray);
}else{
var extension = getFileType(files[i]);
// Make sure this is in our list of allowed files
if (fileTypesArray.indexOf(extension) > -1 ) {
arrayOfScannedFiles.push(filePath);
}
}
}
}
function insertEntries(numberToInsert = 99, loopToEnd = false){
var insertThese = [];
while(insertThese.length != numberToInsert ){
if(arrayOfSongs.length == 0){
break;
}
insertThese.push(arrayOfSongs.pop());
}
dbRead.insertEntries(insertThese, loadJson.username, function(){
// Recursivly run this function until all songs have been added
if(loopToEnd && arrayOfSongs.length != 0){
insertEntries(numberToInsert, true);
}else{
// For the generator
parseFilesGenerator.next();
}
});
}

View File

@ -0,0 +1,158 @@
//exports.setup = function(mstream, users, publicDBType, dbSettings){
exports.setup = function(mstream, program){
const child = require('child_process');
// Load the public DB plugin
// sqlite3 and mysql to start, add lokiJS support latet (and maybe posgres later), maybe a NO DB option even later
// The following api calls are all handled on a public level
// They can be moved into a public plugin
// pull plugin from masterDBType
const mstreamReadPublicDB = require('../db-read/database-public-'+program.database_plugin.type+'.js');
mstreamReadPublicDB.setup(mstream, program.database_plugin);
var userDBStatus = {};
// Certain setups are readonly
if(program.database_plugin.type != 'beets'){
mstream.get('/db/recursive-scan', function(req,res){
// Check if user is already being scanned
if(userDBStatus[req.user.username] == true){
res.send('In Process. Please check status.');
return;
}
//
userDBStatus[req.user.username] = true;
// Get user's db setup
if(!req.user.privateDB || req.user.privateDB == 'DEFAULT'){
forkDefault(req.user, program.database_plugin);
res.send('IT\'S HAPPENING!');
return;
}
if(req.user.privateDB == 'BEETS'){
forkBeets(req.user);
res.send('IT\'S HAPPENING! \n NOW WITH 60% MORE BEETS!');
return;
}
// YOUR CONFIG IS BAD AND YOU SHOULD FEEL BAD
userDBStatus[req.user.username] = false;
res.send('YOUR CONFIG IS BAD AND YOU SHOULD FEEL BAD. ABORTING!');
});
///////////////////////////
// TODO: Should we have a API call that can kill any process associated with a user and reset their scan value to false?
///////////////////////////
///////////////////////////
// TODO: We could use some kind of manager to make sure we don't spawn to many child processes
// For now we spawn indiscriminately and let the CPU sort it out
///////////////////////////
// TODO: Fill this out
function forkBeets(user, publicDBType, dbSettings){
// Pull beets commands from config
// Run commands
// beet import -A --group-albums /path/to/music
// beet check -a
// find ~ -type d -empty -delete
}
function forkDefault(user, dbSettings){
// TODO: IMPLEMENT FORK PROPERLY
// SEND JSON DATA TO WORKER PROCESS
var jsonLoad = {
username:user.username,
userDir:user.musicDir,
dbSettings:dbSettings
}
const forkedScan = child.fork(__dirname + '/database-default-manager.js', [JSON.stringify(jsonLoad)]);
// forkedScan.stdout.on('data', (data) => {
// console.log(`stdout: ${data}`);
// });
//
// forkedScan.stderr.on('data', (data) => {
// console.log(`stderr: ${data}`);
// });
forkedScan.on('close', (code) => {
userDBStatus[user.username] = false;
console.log(`child process exited with code ${code}`);
});
}
}
// TODO: Special function that just transfers fiels from users private DB to public DB
mstream.get('/db/import-DB', function(req,res){
// Get user info
// Pull user's private DB config
// Return if user is not using private DB
// Delete users files
// Pull all files from DB and add to publicDB
if(program.database_plugin.beets_command){
// TODO: Run Command
}else if(req.user.privateDBOptions.beets_command){
// TODO: Run Command
}else{
// res.send('NO GO');
}
res.send('Coming Soon!');
});
// TODO: Handle user status
mstream.get('/db/status-mstream', function(req, res){
res.send('Coming Soon!');
});
// TODO: Modify this to use the public DB
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

@ -0,0 +1,264 @@
const sqlite3 = require('sqlite3').verbose();
const slash = require('slash');
const fe = require('path');
const crypto = require('crypto');
// function that takes in a json array of songs and saves them to the sqlite db
// must contain the username and filepath for each song
// function that gets artist info and returns json array of albums
// function that searches db and returns json array of albums and artists
// function that takes ina playlist name and searchs db for that playlist and returns a json array of songs for that playlist
// BASICALLY, all the functions we have no but de-couple them from the Express API calls
// TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO:
// TODO: Pass in user music dir and compare it to the path using LIKE
// This way If the user is set to use a sub folder of the DB folder, we can ignore all folders above what the user has permission for
function getFileType(filename){
return filename.split(".").pop();
}
exports.setup = function(mstream, dbSettings){
const db = new sqlite3.Database(dbSettings.dbPath);
// Create a playlist table
db.run("CREATE TABLE IF NOT EXISTS mstream_playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, playlist_name varchar, filepath varchar, hide int DEFAULT 0, user VARCHAR, created datetime default current_timestamp);", function() {
});
// TODO: This needs to be tested to see if it works on extra large playlists (think thousands of entries)
// TODO: Ban saving playlists that are > 10,000 items long
mstream.post('/saveplaylist', function (req, res){
var title = req.body.title;
var songs = req.body.stuff;
// Check if this playlist already exists
db.all("SELECT id FROM mstream_playlists WHERE playlist_name = ?", [title], function(err, rows) {
db.serialize(function() {
// We need to delete anys existing entries
if(rows && rows.length > 0){
db.run("DELETE FROM mstream_playlists WHERE playlist_name = ?;", [title]);
}
// Now we add the new entries
var sql2 = "insert into mstream_playlists (playlist_name, filepath) values ";
var sqlParser = [];
while(songs.length > 0) {
var song = songs.shift();
sql2 += "(?, ?), ";
sqlParser.push(title);
// 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) );
}
sql2 = sql2.slice(0, -2);
sql2 += ";";
db.run(sql2, sqlParser, function(){
res.send('DONE');
});
});
});
});
// Attach API calls to functions
mstream.get('/getallplaylists', function (req, res){
// TODO: In V2 we need to change this to ignore hidden playlists
// TODO: db.all("SELECT DISTINCT playlist_name FROM mstream_playlists WHERE hide=0;", function(err, rows){
db.all("SELECT DISTINCT playlist_name FROM mstream_playlists", function(err, rows){
var playlists = [];
// loop through files
for (var i = 0; i < rows.length; i++) {
if(rows[i].playlist_name){
playlists.push({name: rows[i].playlist_name});
}
}
res.send(JSON.stringify(playlists));
});
});
mstream.get('/loadplaylist', function (req, res){
var playlist = req.query.playlistname;
db.all("SELECT * FROM mstream_playlists WHERE playlist_name = ? ORDER BY id COLLATE NOCASE ASC", [playlist], function(err, rows){
var returnThis = [];
for (var i = 0; i < rows.length; i++) {
// var tempName = rows[i].filepath.split('/').slice(-1)[0];
var tempName = fe.basename(rows[i].filepath);
var extension = getFileType(rows[i].filepath);
var filepath = slash(fe.relative(req.user.musicDir, rows[i].filepath));
console.log(filepath);
returnThis.push({name: tempName, file: filepath, filetype: extension });
}
res.send(JSON.stringify(returnThis));
});
});
mstream.get('/deleteplaylist', function(req, res){
var playlistname = req.query.playlistname;
// Handle a soft delete
if(req.query.hide && parseInt(req.query.hide) === 1 ){
db.run("UPDATE mstream_playlists SET hide = 1 WHERE playlist_name = ?;", [playlistname], function(){
res.send('DONE');
});
}else{ // Permentaly delete
// Delete playlist from DB
db.run("DELETE FROM mstream_playlists WHERE playlist_name = ?;", [playlistname], function(){
res.send('DONE');
});
}
});
mstream.post('/db/search', function(req, res){
var searchTerm = "%" + req.body.search + "%" ;
var returnThis = {"albums":[], "artists":[]};
// TODO: Combine SQL calls into one
db.serialize(function() {
var sqlAlbum = "SELECT DISTINCT album FROM items WHERE items.album LIKE ? ORDER BY album COLLATE NOCASE ASC;";
db.all(sqlAlbum, [searchTerm], function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
returnThis.albums.push(rows[i].album);
}
}
});
var sqlArtist = "SELECT DISTINCT artist FROM items WHERE items.artist LIKE ? ORDER BY artist COLLATE NOCASE ASC;";
db.all(sqlArtist, [searchTerm], function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
for (var i = 0; i < rows.length; i++) {
if(rows[i].artist){
returnThis.artists.push(rows[i].artist);
}
}
res.send(JSON.stringify(returnThis));
});
});
});
mstream.get('/db/artists', function (req, res) {
var artists = {"artists":[]};
var sql = "SELECT DISTINCT artist FROM items ORDER BY artist COLLATE NOCASE ASC;";
db.all(sql, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].artist){
// rows.splice(i, 1);
artists.artists.push(rows[i].artist);
}
}
res.send(JSON.stringify(artists));
});
});
mstream.post('/db/artists-albums', function (req, res) {
var albums = {"albums":[]};
// TODO: Make a list of all songs without null albums and add them to the response
var sql = "SELECT DISTINCT album FROM items WHERE artist = ? ORDER BY album COLLATE NOCASE ASC;";
db.all(sql, [req.body.artist], function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
albums.albums.push(rows[i].album);
}
}
res.send(JSON.stringify(albums));
});
});
mstream.get('/db/albums', function (req, res) {
var albums = {"albums":[]};
var sql = "SELECT DISTINCT album FROM items ORDER BY album COLLATE NOCASE ASC;";
db.all(sql, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
albums.albums.push(rows[i].album);
}
}
res.send(JSON.stringify(albums));
});
});
mstream.post('/db/album-songs', function (req, res) {
var sql = "SELECT title, artist, album, format, year, cast(path as TEXT), track FROM items WHERE album = ? ORDER BY track ASC;";
db.all(sql, [req.body.album], function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
// Format data for API
for(var i in rows ){
var path = String(rows[i]['cast(path as TEXT)']);
rows[i].format = rows[i].format.toLowerCase(); // make sure the format is lowecase
rows[i].file_location = slash(fe.relative(req.user.musicDir, path)); // Get the local file location
rows[i].filename = fe.basename( path ); // Ge the filname
}
res.send(JSON.stringify(rows));
});
});
}

View File

View File

@ -0,0 +1,272 @@
const sqlite3 = require('sqlite3').verbose();
const slash = require('slash');
const fe = require('path');
const crypto = require('crypto');
// function that takes in a json array of songs and saves them to the sqlite db
// must contain the username and filepath for each song
// function that gets artist info and returns json array of albums
// function that searches db and returns json array of albums and artists
// function that takes ina playlsit name and searchs db for that playlist and returns a json array of songs for that playlist
// BASICALLY, all the functions we have no but de-couple them from the Express API calls
function getFileType(filename){
return filename.split(".").pop();
}
exports.setup = function(mstream, dbSettings){
const db = new sqlite3.Database(dbSettings.dbPath);
// Setup DB
db.run("CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title varchar DEFAULT NULL, artist varchar DEFAULT NULL, year int DEFAULT NULL, album varchar DEFAULT NULL, path TEXT NOT NULL UNIQUE, format VARCHAR, track INTEGER, disk INTEGER, user VARCHAR, filesize INTEGER, file_created_date INTEGER, file_modified_date INTEGER);", function() {
});
// Create a playlist table
db.run("CREATE TABLE IF NOT EXISTS mstream_playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, playlist_name varchar, filepath varchar, hide int DEFAULT 0, user VARCHAR, created datetime default current_timestamp);", function() {
});
// TODO: This needs to be tested to see if it works on extra large playlists (think thousands of entries)
// TODO: Ban saving playlists that are > 10,000 items long
mstream.post('/saveplaylist', function (req, res){
var title = req.body.title;
var songs = req.body.stuff;
// Check if this playlist already exists
db.all("SELECT id FROM mstream_playlists WHERE playlist_name = ? AND user = ?;", [title, req.user.username], function(err, rows) {
db.serialize(function() {
// We need to delete anys existing entries
if(rows && rows.length > 0){
db.run("DELETE FROM mstream_playlists WHERE playlist_name = ? AND user = ?;", [title, req.user.username]);
}
// Now we add the new entries
var sql2 = "insert into mstream_playlists (playlist_name, filepath, user) values ";
var sqlParser = [];
while(songs.length > 0) {
var song = songs.shift();
sql2 += "(?, ?, ?), ";
sqlParser.push(title);
// 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 );
}
sql2 = sql2.slice(0, -2);
sql2 += ";";
db.run(sql2, sqlParser, function(){
res.send('DONE');
});
});
});
});
// Attach API calls to functions
mstream.get('/getallplaylists', function (req, res){
// TODO: In V2 we need to change this to ignore hidden playlists
// TODO: db.all("SELECT DISTINCT playlist_name FROM mstream_playlists WHERE hide=0;", function(err, rows){
db.all("SELECT DISTINCT playlist_name FROM mstream_playlists WHERE user = ?", [req.user.username], function(err, rows){
var playlists = [];
// loop through files
for (var i = 0; i < rows.length; i++) {
if(rows[i].playlist_name){
playlists.push({name: rows[i].playlist_name});
}
}
res.send(JSON.stringify(playlists));
});
});
mstream.get('/loadplaylist', function (req, res){
var playlist = req.query.playlistname;
db.all("SELECT * FROM mstream_playlists WHERE playlist_name = ? AND user = ? ORDER BY id COLLATE NOCASE ASC", [playlist, req.user.username], function(err, rows){
var returnThis = [];
for (var i = 0; i < rows.length; i++) {
// var tempName = rows[i].filepath.split('/').slice(-1)[0];
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 });
}
res.send(JSON.stringify(returnThis));
});
});
mstream.get('/deleteplaylist', function(req, res){
var playlistname = req.query.playlistname;
// Handle a soft delete
if(req.query.hide && parseInt(req.query.hide) === 1 ){
db.run("UPDATE mstream_playlists SET hide = 1 WHERE playlist_name = ? AND user = ?;", [playlistname, req.user.username], function(){
res.send('DONE');
});
}else{ // Permentaly delete
// Delete playlist from DB
db.run("DELETE FROM mstream_playlists WHERE playlist_name = ? AND user = ?;", [playlistname, req.user.username], function(){
res.send('DONE');
});
}
});
mstream.post('/db/search', function(req, res){
var searchTerm = "%" + req.body.search + "%" ;
var returnThis = {"albums":[], "artists":[]};
// TODO: Combine SQL calls into one
db.serialize(function() {
var sqlAlbum = "SELECT DISTINCT album FROM items WHERE items.album LIKE ? AND user = ? ORDER BY album COLLATE NOCASE ASC;";
db.all(sqlAlbum, [searchTerm, req.user.username], function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
returnThis.albums.push(rows[i].album);
}
}
});
var sqlArtist = "SELECT DISTINCT artist FROM items WHERE items.artist LIKE ? AND user = ? ORDER BY artist COLLATE NOCASE ASC;";
db.all(sqlArtist, [searchTerm, req.user.username], function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
for (var i = 0; i < rows.length; i++) {
if(rows[i].artist){
returnThis.artists.push(rows[i].artist);
}
}
res.send(JSON.stringify(returnThis));
});
});
});
mstream.get('/db/artists', function (req, res) {
var artists = {"artists":[]};
var sql = "SELECT DISTINCT artist FROM items WHERE user = ? ORDER BY artist COLLATE NOCASE ASC;";
db.all(sql, [req.user.username], function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].artist){
// rows.splice(i, 1);
artists.artists.push(rows[i].artist);
}
}
res.send(JSON.stringify(artists));
});
});
mstream.post('/db/artists-albums', function (req, res) {
var albums = {"albums":[]};
// TODO: Make a list of all songs without null albums and add them to the response
var sql = "SELECT DISTINCT album FROM items WHERE artist = ? AND user = ? ORDER BY album COLLATE NOCASE ASC;";
var searchTerms = [];
searchTerms.push(req.body.artist);
searchTerms.push(req.user.username);
db.all(sql, searchTerms, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
albums.albums.push(rows[i].album);
}
}
res.send(JSON.stringify(albums));
});
});
mstream.get('/db/albums', function (req, res) {
var albums = {"albums":[]};
var sql = "SELECT DISTINCT album FROM items WHERE user = ? ORDER BY album COLLATE NOCASE ASC;";
db.all(sql, req.user.username, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
albums.albums.push(rows[i].album);
}
}
res.send(JSON.stringify(albums));
});
});
mstream.post('/db/album-songs', function (req, res) {
var sql = "SELECT title, artist, album, format, year, cast(path as TEXT), track FROM items WHERE album = ? AND user = ? ORDER BY track ASC;";
var searchTerms = [];
searchTerms.push(req.body.album);
searchTerms.push(req.user.username);
db.all(sql, searchTerms, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
// Format data for API
for(var i in rows ){
var path = String(rows[i]['cast(path as TEXT)']);
rows[i].format = rows[i].format.toLowerCase(); // make sure the format is lowecase
rows[i].file_location = slash(fe.relative(req.user.musicDir, path)); // Get the local file location
rows[i].filename = fe.basename( path ); // Ge the filname
}
res.send(JSON.stringify(rows));
});
});
}

View File

@ -0,0 +1,2 @@
// functions that store data into the MySQL DB
// These functions will take in JSON arrays of song data and then save that dat to the 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

@ -0,0 +1,108 @@
// functions that store data into the SQLite DB
// These functions will take in JSON arrays of song data and then save that dat to the DB
const sqlite3 = require('sqlite3').verbose();
var db;
exports.setup = function(dbPath){
db = new sqlite3.Database(dbPath);
}
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){
var sql2 = "insert into items (title,artist,year,album,path,format, track, disk, user, filesize, file_modified_date, file_created_date) values ";
var sqlParser = [];
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;
}
sql2 += "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), ";
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);
}
sql2 = sql2.slice(0, -2);
sql2 += ";";
console.log(sql2);
db.run(sql2, sqlParser, function() {
console.log('ITS DONE');
callback();
});
}
// Function that reformats data from beets
// TODO: Fix this
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(){
}
exports.deleteFile = function(path, user, callback){
let sql = "DELETE FROM items WHERE path = ? AND user = ?;";
db.run(sql, [path, user], function() {
console.log('ITS DONE');
callback();
});
}
exports.getHashedEntry = function(hash, path, user, callback){
db.all("SELECT path, hash FROM items WHERE hash = ? AND path = ? AND user = ?;", [hash, path, user], function(err, rows){
console.log(rows);
// callback function
callback(rows);
});
}

View File

@ -3,141 +3,57 @@
const express = require('express');
const mstream = express();
const fs = require('graceful-fs'); // File System
const fs = require('fs'); // File System
const fe = require('path');
const bodyParser = require('body-parser');
const archiver = require('archiver'); // Zip Compression
const os = require('os');
const crypto = require('crypto');
const slash = require('slash');
const sqlite3 = require('sqlite3').verbose();
const uuidV4 = require('uuid/v4');
var startup = 'configure-commander';
// If the user gives a json file then try pulling the config from that
try{
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();
}
const db = new sqlite3.Database(program.database);
// If we are not using Beets DB, we need to prep the DB
if(program.databaseplugin === 'default'){
db.run("CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title varchar DEFAULT NULL, artist varchar DEFAULT NULL, year int DEFAULT NULL, album varchar DEFAULT NULL, path text, format varchar, track INTEGER, disk INTEGER);", function() {
// console.log('TABLES CREATED');
});
}
// Create a playlist table
db.run("CREATE TABLE IF NOT EXISTS mstream_playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, playlist_name varchar, filepath varchar, hide int DEFAULT 0, created datetime default current_timestamp);", function() {
// console.log('PLAYLIST TABLE CREATED');
});
// Normalize for all OS
// Make sure it's a directory
if(!fs.statSync( program.filepath ).isDirectory()){
console.log('GIVEN DIRECTORY DOES NOT APPEAR TO BE REAL');
process.exit();
}
const rootDir = fe.normalize(program.filepath);
// Normalize It
if(!fe.isAbsolute(program.filepath) ){
rootDir = fe.join(process.cwd, rootDir);
}
// 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();
}
// Static files
mstream.use( express.static(fe.join(__dirname, program.userinterface) ));
mstream.use( '/' , express.static( rootDir ));
// 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) ));
// Handle ports. Default is 3000
const port = program.port;
console.log('Access mStream locally: http://localhost:' + port);
// 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);
console.log('Access mStream on your local network: http://' + require('my-local-ip')() + ':' + program.port);
// 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(port);
const tunnel = require('./modules/auto-port-forwarding.js').setup(program.tunnel, program.port);
}
// Print the local network IP
console.log('Access mStream on your local network: http://' + require('my-local-ip')() + ':' + port);
// Serve the webapp
mstream.get('/', function (req, res) {
res.sendFile( fe.join(program.userinterface, 'mstream.html'), { root: __dirname });
});
// Login functionality
if(program.login){
if(!program.password || !program.user){
console.log('User credentials are missing. Please make sure to supply both a username and password via the -u and -p commands respectivly. Aborting');
process.exit(1);
}
// TODO: password change function
if(program.email){
mstream.get('/change-password-request', function (req, res) {
// Generate change password token
// Invalidate all other change password tokens
// Email the user the token
res.sendFile( 'COMING SOON!' );
});
mstream.post('/change-password', function (req, res){
// Check token
// Get new password
// Hash password and update user array
res.sendFile( 'COMING SOON!' );
});
}
if(program.users){
// Use bcrypt for password storage
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens
var secret;
var secretIsFile = false;
// Check for filepath
try{
if(fs.statSync(program.secret).isFile()){
@ -145,45 +61,82 @@ if(program.login){
}
}catch(error){}
if(secretIsFile === true){
// If the given secret is a filepath
secret = fs.readFileSync(program.secret, 'utf8');
}else if(program.secret){
// Otherwise just use secret as is
secret = String(program.secret);
}else{
// If no secret was given, generate one
require('crypto').randomBytes(48, function(err, buffer) {
secret = buffer.toString('hex');
});
}
// Create the user array
var Users = {};
Users[program.user] = {
'guest': false,
'password':'',
}
// TODO: Add New user functionality
// Check for root user and password
// Add credentials to user array
// Encrypt the password
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(program.password, salt, function(err, hash) {
// Store hash in your password DB.
Users[program.user]['password'] = hash;
});
// TODO: Need a way to store and use already hashed passwords
// TODO: password change function
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!' );
});
// Handle guest account
if(program.guest && program.guestpassword){
Users[program.guest] = {
'guest': true,
'password':'',
mstream.post('/change-password', function (req, res){
// Check token
// Get new password
// Hash password and update user array
res.sendFile( 'COMING SOON!' );
});
// Create the user array
// 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];
}
// Encrypt the password
}
function generateSaltedPassword(username, password){
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(program.guestpassword, salt, function(err, hash) {
bcrypt.hash(password, salt, function(err, hash) {
// Store hash in your password DB.
Users[program.guest]['password'] = hash;
Users[username]['password'] = hash;
});
});
}
@ -223,19 +176,17 @@ if(program.login){
}
var user = Users[username];
user['id'] = username;
// Make a token for the user
var token = jwt.sign(user, secret);
user['username'] = username;
// return the information including token as JSON
var sendThis = {
success: true,
message: 'Welcome To mStream',
token: token };
vPath: user.vPath,
token: jwt.sign(user, secret) // Make the token
};
res.send(JSON.stringify(sendThis));
});
});
@ -259,25 +210,52 @@ if(program.login){
}
// Deny guest access
req.decoded = decoded;
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{
// TODO: Authenticate all HTTP requests for music files (mp3 and other formats)
// Dummy data
mstream.use(function(req, res, next) {
req.user = {
username:"",
musicDir:process.cwd()
};
next();
});
mstream.use( '/' , express.static( process.cwd() ));
}
// Test function
// Used to determine the user has a working login token
mstream.get('/ping', function(req, res){
res.send('pong');
// TODO: Guest status
res.json({
vPath: req.user.vPath,
guest: false
});
});
@ -287,19 +265,18 @@ mstream.post('/dirparser', function (req, res) {
var directories = [];
var filesArray = [];
// Make sure directory exits
var path = req.body.dir;
if(path == ""){
path = rootDir;
}else{
path = fe.join(rootDir, path);
// TODO: Make sure path is a sub-path of the user's music dir
var path = fe.join(req.user.musicDir, req.body.dir);
// 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{
@ -307,65 +284,46 @@ 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++) {
var tempDirArray = {};
var tempFileArray = {};
for (let i=0; i < files.length; i++) {
var filePath = fe.join(path, files[i]);
try{
var stat = fs.statSync(filePath);
var stat = fs.statSync(fe.join(path, files[i]));
}catch(error){
// Bad file, ignore and continue
// TODO: Log This
continue;
}
// Handle Directories
if(stat.isDirectory()){
tempDirArray["type"] = 'directory';
tempDirArray["name"] = files[i];
directories.push(tempDirArray);
directories.push({
type:"directory",
name:files[i]
});
}else{ // Handle Files
// Get the file extension
var extension = getFileType(files[i]);
if (fileTypesArray.indexOf(extension) > -1 && masterFileTypesArray.indexOf(extension) > -1) {
tempFileArray["type"] = extension;
tempFileArray["name"] = files[i];
filesArray.push(tempFileArray);
filesArray.push({
type:extension,
name:files[i]
});
}
}
}
var returnPath = slash( fe.relative(rootDir, path) );
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)})
);
});
@ -375,121 +333,17 @@ function getFileType(filename){
}
mstream.post('/saveplaylist', function (req, res){
var title = req.body.title;
var songs = req.body.stuff;
// Check if this playlist already exists
db.all("SELECT id FROM mstream_playlists WHERE playlist_name = ?;", title, function(err, rows) {
db.serialize(function() {
// We need to delete anys existing entries
if(rows && rows.length > 0){
db.run("DELETE FROM mstream_playlists WHERE playlist_name = ?;", title);
}
// Now we add the new entries
var sql2 = "insert into mstream_playlists (playlist_name, filepath) values ";
var sqlParser = [];
while(songs.length > 0) {
var song = songs.shift();
sql2 += "(?, ?), ";
sqlParser.push(title);
sqlParser.push( fe.join(rootDir, song) );
}
sql2 = sql2.slice(0, -2);
sql2 += ";";
db.run(sql2, sqlParser, function(){
res.send('DONE');
});
});
});
});
mstream.get('/getallplaylists', function (req, res){
// TODO: In V2 we need to change this to ignore hidden playlists
// TODO: db.all("SELECT DISTINCT playlist_name FROM mstream_playlists WHERE hide=0;", function(err, rows){
db.all("SELECT DISTINCT playlist_name FROM mstream_playlists", function(err, rows){
var playlists = [];
// loop through files
for (var i = 0; i < rows.length; i++) {
if(rows[i].playlist_name){
playlists.push({name: rows[i].playlist_name});
}
}
res.send(JSON.stringify(playlists));
});
});
mstream.get('/loadplaylist', function (req, res){
var playlist = req.query.playlistname;
db.all("SELECT * FROM mstream_playlists WHERE playlist_name = ? ORDER BY id COLLATE NOCASE ASC", playlist, function(err, rows){
var returnThis = [];
for (var i = 0; i < rows.length; i++) {
// var tempName = rows[i].filepath.split('/').slice(-1)[0];
var tempName = fe.basename(rows[i].filepath);
var extension = getFileType(rows[i].filepath);
var filepath = slash(fe.relative(rootDir, rows[i].filepath));
returnThis.push({name: tempName, file: filepath, filetype: extension });
}
res.send(JSON.stringify(returnThis));
});
});
mstream.get('/deleteplaylist', function(req, res){
var playlistname = req.query.playlistname;
// Handle a soft delete
if(req.query.hide && parseInt(req.query.hide) === 1 ){
db.run("UPDATE mstream_playlists SET hide = 1 WHERE playlist_name = ?;", playlistname, function(){
res.send('DONE');
});
}else{ // Permentaly delete
// Delete playlist from DB
db.run("DELETE FROM mstream_playlists WHERE playlist_name = ?;", playlistname, function(){
res.send('DONE');
});
}
});
// Download a zip file of music
mstream.post('/download', function (req, res){
var archive = archiver('zip');
archive.on('error', function(err) {
console.log(err.message);
res.status(500).send('{error: err.message}');
res.status(500).send('{error: '+err.message+'}');
});
archive.on('end', function() {
// TODO: add logging
console.log('Archive wrote %d bytes', archive.pointer());
});
//set the archive name
@ -502,256 +356,48 @@ mstream.post('/download', function (req, res){
// Get the POSTed files
var fileArray = JSON.parse(req.body.fileArray);
////////////////////////////////////////////////////////////
// TODO: Confirm each item in posted data is a real file //
///////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
for(var i in fileArray) {
var fileString = fileArray[i];
archive.file(fe.normalize( fileString), { name: fe.basename(fileString) });
}
// TODO: Recursivly download a posted directory //
//////////////////////////////////////////////////
// SEE: https://github.com/archiverjs/node-archiver/tree/master/examples
// var directory = req.body.directory;
archive.finalize();
});
const mstreamDB = require('./modules/database-'+program.databaseplugin+'.js');
mstreamDB.setup(mstream, program, rootDir, db);
mstream.post('/db/search', function(req, res){
var searchTerm = "%" + req.body.search + "%" ;
var returnThis = {"albums":[], "artists":[]};
// TODO: Combine SQL calls into one
db.serialize(function() {
var sqlAlbum = "SELECT DISTINCT album FROM items WHERE items.album LIKE ? ORDER BY album COLLATE NOCASE ASC;";
db.all(sqlAlbum, searchTerm, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
// rows.splice(i, 1);
returnThis.albums.push(rows[i].album);
}
}
});
var sqlAlbum = "SELECT DISTINCT artist FROM items WHERE items.artist LIKE ? ORDER BY artist COLLATE NOCASE ASC;";
db.all(sqlAlbum, searchTerm, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
for (var i = 0; i < rows.length; i++) {
if(rows[i].artist){
// rows.splice(i, 1);
returnThis.artists.push(rows[i].artist);
}
}
res.send(JSON.stringify(returnThis));
});
});
});
mstream.get('/db/artists', function (req, res) {
var sql = "SELECT DISTINCT artist FROM items ORDER BY artist COLLATE NOCASE ASC;";
var artists = {"artists":[]};
db.all(sql, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].artist){
// rows.splice(i, 1);
artists.artists.push(rows[i].artist);
}
}
res.send(JSON.stringify(artists));
});
});
mstream.post('/db/artists-albums', function (req, res) {
var sql = "SELECT DISTINCT album FROM items WHERE artist = ? ORDER BY album COLLATE NOCASE ASC;";
var searchTerm = req.body.artist ;
var albums = {"albums":[]};
// TODO: Make a list of all songs without null albums and add them to the response
db.all(sql, searchTerm, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
// rows.splice(i, 1);
albums.albums.push(rows[i].album);
}
}
res.send(JSON.stringify(albums));
});
});
mstream.get('/db/albums', function (req, res) {
var sql = "SELECT DISTINCT album FROM items ORDER BY album COLLATE NOCASE ASC;";
var albums = {"albums":[]};
db.all(sql, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
var returnArray = [];
for (var i = 0; i < rows.length; i++) {
if(rows[i].album){
albums.albums.push(rows[i].album);
}
}
console.log(JSON.stringify(albums));
res.send(JSON.stringify(albums));
});
});
mstream.post('/db/album-songs', function (req, res) {
var sql = "SELECT title, artist, album, format, year, cast(path as TEXT), track FROM items WHERE album = ? ORDER BY track ASC;";
var searchTerm = req.body.album ;
db.all(sql, searchTerm, function(err, rows) {
if(err){
res.status(500).json({ error: 'DB Error' });
return;
}
// Format data for API
rows = setLocalFileLocation(rows);
res.send(JSON.stringify(rows));
});
});
function setLocalFileLocation(rows){
for(var i in rows ){
var path = String(rows[i]['cast(path as TEXT)']);
rows[i].format = rows[i].format.toLowerCase(); // make sure the format is lowecase
rows[i].file_location = slash(fe.relative(rootDir, path)); // Get the local file location
rows[i].filename = fe.basename( path ); // Ge the filname
}
return rows;
}
// ============================================================================
// // New Way
// // TODO: We need to pull this from the program var
var dbSettings = program.database_plugin;
const mstreamDB = require('./modules/db-management/database-master.js');
mstreamDB.setup(mstream, program);
// ============================================================================
// TODO: Add individual song
// mstream.get('/db/add-songs', function(req, res){
// // deseralize json array
// // Add all files
// });
// Download the database
mstream.get('/db/download-db', function(req, res){
var file = program.database;
res.download(file); // Set disposition and send it.
mstream.get('/db/add-songs', function(req, res){
res.send('Coming Soon!');
});
// Get hash of database
mstream.get( '/db/hash', function(req, res){
var hash = crypto.createHash('sha256');
var fileStream = fs.createReadStream(program.database);
hash.setEncoding('hex');
fileStream.pipe(hash, { end: false });
fileStream.on('end', function () {
hash.end();
var returnThis = {
hash:String(hash.read())
};
res.send(JSON.stringify(returnThis));
});
});
// TODO: Get Album Art calls
mstream.post( '/get-album-art', function(req, res){
// Get filepath from post
// Check if album art is in DB
// Return If So
// Pull album art from file stream
// ??? Lookup album art via 3rd party ???
res.send('Coming Soon!');
}
const server = mstream.listen(port, function () {
// var host = server.address().address;
// var port = server.address().port;
// console.log('Example app listening at http://%s:%s', host, port);
});
// Start the server!
// TODO: Check if port is in use befoe firing up server
const server = mstream.listen(program.port, function () {});

View File

@ -1,6 +1,6 @@
{
"name": "mstream",
"version": "1.28.0",
"version": "2.0.0",
"description": "music streaming server",
"main": "mstream.js",
"bin": {
@ -21,12 +21,13 @@
"body-parser": "^1.15.1",
"commander": "^2.9.0",
"express": "^4.13.4",
"graceful-fs": "^4.1.4",
"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

@ -37,6 +37,7 @@ $(document).ready(function(){
// Add the token the URL calls
accessKey = token;
virtualDirectory = parsedResponse.vPath;
loadFileExplorer();
// Remove the overlay
@ -57,7 +58,7 @@ $(document).ready(function(){
var accessKey = '';
var virtualDirectory = '';
$.ajaxPrefilter(function( options ) {
options.beforeSend = function (xhr) {
xhr.setRequestHeader('x-access-token', accessKey);
@ -67,7 +68,7 @@ $(document).ready(function(){
// Determine if the user needs to log sin
// Determine if the user needs to log in
function testIt(){
var token = Cookies.get('token');
if(token){
@ -82,6 +83,9 @@ $(document).ready(function(){
request.done(function( msg ) {
// Remove login screen
// set virtualDirectory
var decoded = msg;
virtualDirectory = decoded.vPath;
});
request.fail(function( jqXHR, textStatus ) {
@ -95,6 +99,12 @@ $(document).ready(function(){
testIt();
// TODO: This var nees to be appened to the beginning of any music fileapath
// This var will either be the username or the value returned by the ping API call
var vPath = '';
////////////////////////////// Initialization code
// Supported file types
@ -185,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());
}
@ -195,6 +208,9 @@ $(document).ready(function(){
function addFile2(that){
var filename = $(that).attr("id");
var file_location = $(that).data("file_location");
if(accessKey){
file_location += '?token=' + accessKey;
}
var filetype = $(that).data("filetype");
var title = $(that).find('span.title').html();