mirror of
https://github.com/IrosTheBeggar/mStream.git
synced 2025-10-27 07:31:02 +00:00
719 lines
18 KiB
JavaScript
Executable File
719 lines
18 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const express = require('express');
|
|
const mstream = express();
|
|
const fs = require('graceful-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();
|
|
|
|
|
|
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');
|
|
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
|
|
|
|
|
|
// Handle ports. Default is 3000
|
|
const port = program.port;
|
|
console.log('Access mStream locally: http://localhost:' + 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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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()){
|
|
secretIsFile = true;
|
|
}
|
|
}catch(error){}
|
|
|
|
|
|
if(secretIsFile === true){
|
|
secret = fs.readFileSync(program.secret, 'utf8');
|
|
}else if(program.secret){
|
|
secret = String(program.secret);
|
|
}else{
|
|
require('crypto').randomBytes(48, function(err, buffer) {
|
|
secret = buffer.toString('hex');
|
|
});
|
|
}
|
|
|
|
// Create the user array
|
|
var Users = {};
|
|
|
|
Users[program.user] = {
|
|
'guest': false,
|
|
'password':'',
|
|
}
|
|
|
|
// 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;
|
|
});
|
|
});
|
|
|
|
// Handle guest account
|
|
if(program.guest && program.guestpassword){
|
|
Users[program.guest] = {
|
|
'guest': true,
|
|
'password':'',
|
|
}
|
|
|
|
// Encrypt the password
|
|
bcrypt.genSalt(10, function(err, salt) {
|
|
bcrypt.hash(program.guestpassword, salt, function(err, hash) {
|
|
// Store hash in your password DB.
|
|
Users[program.guest]['password'] = hash;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Failed Login Attempt
|
|
mstream.get('/login-failed', function (req, res) {
|
|
// Wait before sending the response
|
|
setTimeout((function() {
|
|
res.status(599).send(JSON.stringify({'Error':'Try Again'}))
|
|
}), 800);
|
|
});
|
|
|
|
mstream.get('/access-denied', function (req, res) {
|
|
res.status(598).send(JSON.stringify({'Error':'Access Denied'}));
|
|
});
|
|
|
|
mstream.get('/guest-access-denied', function (req, res) {
|
|
res.status(597).send(JSON.stringify({'Error':'Access Denied'}));
|
|
});
|
|
|
|
// route to authenticate a user (POST http://localhost:8080/api/authenticate)
|
|
mstream.post('/login', function(req, res) {
|
|
let username = req.body.username;
|
|
let password = req.body.password;
|
|
|
|
// Check is user is in array
|
|
if(typeof Users[username] === 'undefined') {
|
|
// user does not exist
|
|
return res.redirect('/login-failed');
|
|
}
|
|
|
|
// Check is password is correct
|
|
bcrypt.compare(password, Users[username]['password'], function(err, match) {
|
|
if(match == false){
|
|
// Password does not match
|
|
return res.redirect('/login-failed');
|
|
}
|
|
|
|
var user = Users[username];
|
|
user['id'] = username;
|
|
|
|
// Make a token for the user
|
|
var token = jwt.sign(user, secret);
|
|
|
|
// return the information including token as JSON
|
|
var sendThis = {
|
|
success: true,
|
|
message: 'Welcome To mStream',
|
|
token: token };
|
|
|
|
res.send(JSON.stringify(sendThis));
|
|
|
|
});
|
|
});
|
|
|
|
// Guest Users are not allowed to access these functions
|
|
const forbiddenFunctions = ['/db/recursive-scan', '/saveplaylist', '/deleteplaylist'];
|
|
|
|
// Middleware that checks for token
|
|
mstream.use(function(req, res, next) {
|
|
// check header or url parameters or post parameters for token
|
|
var token = req.body.token || req.query.token || req.headers['x-access-token'];
|
|
|
|
// decode token
|
|
if (!token) {
|
|
return res.redirect('/access-denied');
|
|
}
|
|
|
|
// verifies secret and checks exp
|
|
jwt.verify(token, secret, function(err, decoded) {
|
|
if (err) {
|
|
return res.redirect('/access-denied');
|
|
}
|
|
|
|
// Deny guest access
|
|
req.decoded = decoded;
|
|
if(decoded.guest === true && forbiddenFunctions.indexOf(req.path) != -1){
|
|
return res.redirect('/guest-access-denied');
|
|
}
|
|
|
|
next();
|
|
});
|
|
});
|
|
|
|
|
|
// TODO: Authenticate all HTTP requests for music files (mp3 and other formats)
|
|
}
|
|
|
|
|
|
|
|
// Test function
|
|
// Used to determine the user has a working login token
|
|
mstream.get('/ping', function(req, res){
|
|
res.send('pong');
|
|
});
|
|
|
|
|
|
|
|
// parse directories
|
|
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);
|
|
}
|
|
|
|
// Will only show these files. Prevents people from snooping around
|
|
// TODO: Move to global vairable
|
|
var masterFileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a"];
|
|
var fileTypesArray;
|
|
|
|
if(req.body.filetypes){
|
|
fileTypesArray = JSON.parse(req.body.filetypes);
|
|
}else{
|
|
fileTypesArray = masterFileTypesArray;
|
|
}
|
|
|
|
|
|
// 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 = {};
|
|
|
|
var filePath = fe.join(path, files[i]);
|
|
try{
|
|
var stat = fs.statSync(filePath);
|
|
}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);
|
|
}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);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
var returnPath = slash( fe.relative(rootDir, 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));
|
|
|
|
});
|
|
|
|
|
|
|
|
function getFileType(filename){
|
|
return filename.split(".").pop();
|
|
}
|
|
|
|
|
|
|
|
|
|
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}');
|
|
});
|
|
|
|
archive.on('end', function() {
|
|
// TODO: add logging
|
|
console.log('Archive wrote %d bytes', archive.pointer());
|
|
});
|
|
|
|
//set the archive name
|
|
// TODO: Rename this
|
|
res.attachment('zipped-playlist.zip');
|
|
|
|
//streaming magic
|
|
archive.pipe(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;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
});
|
|
|
|
|
|
// 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));
|
|
|
|
});
|
|
});
|
|
|
|
|
|
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);
|
|
});
|