diff --git a/.gitignore b/.gitignore index 443ea9e..ee23fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ node_modules/* image-cache/* !image-cache/default.png !image-cache/README.md + +save/* +!save/README.md + mstreamdb.lite mstream.db package-lock.json diff --git a/docs/json_config.md b/docs/json_config.md index bb927c1..ecf8860 100644 --- a/docs/json_config.md +++ b/docs/json_config.md @@ -2,17 +2,20 @@ Using a JSON config with mStream allows for more advanced configurations. This example contains all configurable params for reference purposes. -``` +```json { "port": 3030, "userinterface":"public", "secret": "b6j7j5e6u5g36ubn536uyn536unm5m67u5365vby435y54ymn", - "database_plugin":{ - "dbPath":"/path/to/loki.db", - "interval": 2, - "saveInterval": 500, + "scanOptions":{ "skipImg": true, - "pause": 50 + "scanInterval": 1.5, + "pause": 50, + "saveInterval": 500, + "bootScanDelay": 15 + }, + "database_plugin":{ + "dbPath":"/path/to/loki.db" }, "albumArtDir": "/media/album-art", "folders": { @@ -60,19 +63,34 @@ Folder that contains the frontend for mStream. Defaults to `public` if not set Sets the secret key used for the login system. If this is not set, mStream will generate a random secret key on boot and previous login sessions will be voided -## Database +## Scan Options -* `dbPath`: path to save the database file to -* `interval`: The interval which controls how often file system will be scanned for changes (in hours) * `skipImg`: (boolean) whether to skip scanning for album art. Speeds up the scan time +* `bootScanDelay`: delay between server boot and first file scan (in seconds) +* `scanInterval`: The interval which controls how often file system will be scanned for changes (in hours) * `saveInterval`: interval which to refresh the DB on scan. Defaults to 250. Can be set to a higher number for large collections to avoid hogging the CPU thread * `pause` (in milliseconds): During the scan, there is an optional pause that is aded between file parsing. This can prevent mStream from hogging system resources during the initial scan +```json +{ + "scanOptions":{ + "skipImg": true, + "scanInterval": 1.5, + "pause": 50, + "saveInterval": 500, + "bootScanDelay": 15 + } +} ``` + +## Database + +* `dbPath`: path to save the database file to + +```json "database_plugin":{ "dbPath":"/path/to/loki.db", - "interval": "1.5", - ""pause": 50, + } ``` diff --git a/modules/config/config-inquirer.js b/modules/config/config-inquirer.js index bbdcbc1..c30f50c 100644 --- a/modules/config/config-inquirer.js +++ b/modules/config/config-inquirer.js @@ -172,7 +172,7 @@ function editPort(port = 3000) { } }]) .then(answers => { - return answers.port; + return Number(answers.port); }); } @@ -605,14 +605,18 @@ function addNewFolder() { type: 'directory', name: 'from', message: 'Choose your music directory:', - basePath: '.' + basePath: require('os').homedir() }]).then((answers) => { return answers.from; }); } - async function doTheThing(filepath) { + if (typeof filepath !== 'string') { + filepath = path.join(__dirname, '../../save/default.json'); + } + filepath = path.resolve(filepath); + console.clear(); console.log(); console.log(colors.blue.bold('Welcome To The mStream Setup Wizard')); @@ -634,7 +638,16 @@ async function doTheThing(filepath) { } } - await initFile(filepath); + try { + await initFile(filepath); + } catch (err) { + console.log(); + console.log(colors.red('Failed to save file')); + console.log(colors.yellow('Check that you have write access to this directory')); + console.log(); + process.exit(0); + } + try { var loadJson = JSON.parse(fs.readFileSync(filepath, 'utf8')); } catch (error) { @@ -683,7 +696,7 @@ async function doTheThing(filepath) { console.log(colors.blue.bold('Welcome To The mStream Setup Wizard')); console.log(colors.magenta('User Configuration')); console.log(); - var addUser = false; + var shouldAdd = false; if (!loadJson.users || typeof loadJson.users !== 'object') { loadJson.users = {}; } @@ -691,11 +704,12 @@ async function doTheThing(filepath) { console.log('There are currently no users'); console.log(colors.yellow('With no users, mStream will be publicly available and the login system will be disabled')); console.log(); + shouldAdd = true; } else { printUsers(loadJson.users); } - addUser = await confirmThis("Would you like to add a user?"); - while (addUser) { + while (await confirmThis("Would you like to add a user?", shouldAdd)) { + shouldAdd = false; await addOneUser(loadJson); console.clear(); console.log(); @@ -703,7 +717,6 @@ async function doTheThing(filepath) { console.log(colors.magenta('User Configuration')); console.log(); printUsers(loadJson.users); - addUser = await confirmThis("Would you like to add a user?"); } // Port @@ -741,6 +754,48 @@ async function doTheThing(filepath) { // console.log('BLAH BLAH'); // console.log(); + // Test write access in mStream directory + // If it works, suggest this one + // if not, suggest another one + + // Scan Options + if (!loadJson.scanOptions) { + loadJson.scanOptions = {}; + } + var editDb = { dbList: true }; + while (editDb.dbList !== 'finished') { + console.clear(); + console.log(); + console.log(colors.blue.bold('Welcome To The mStream Setup Wizard')); + console.log(colors.magenta('File Scan Options')); + console.log(); + + switch (editDb.dbList) { + case 'dbpause': + loadJson.scanOptions.pause = await setScanPause(); + break; + case 'interval': + loadJson.scanOptions.scanInterval = await setScanInterval(); + break; + case 'bootpause': + loadJson.scanOptions.bootScanDelay = await setBootDelay(); + break; + case 'saveinterval': + loadJson.scanOptions.saveInterval = await setSaveInterval(); + break; + case 'skipimg': + const shouldSkip = await skipImg(); + if (shouldSkip) { + loadJson.scanOptions.skipImg = true + } + break; + default: + console.log('How did you get here???'); + } + + editDb = await chooseDirOption(); + } + // Save fs.writeFileSync( filepath, JSON.stringify(loadJson, null, 2), 'utf8'); console.clear(); @@ -755,6 +810,139 @@ async function doTheThing(filepath) { // Print a Help Text explaining basic usage things } +function chooseDirOption() { + return inquirer + .prompt([{ + message: 'Choose your DB Option', + type: "list", + name: "dbList", + choices: [{ name: 'finished', value: 'finished' }, + new inquirer.Separator(), + { name: 'Pause Between Files', value: 'dbpause' }, + { name: 'Scan Interval', value: 'interval' }, + { name: 'Boot Scan Pause', value: 'bootpause' }, + { name: 'Skip Image Scan', value: 'skipimg' }, + { name: 'Save Interval', value: 'saveinterval' } + ] + }]) + .then(answers => { + return answers; + }); +} + +function skipImg() { + console.log(colors.yellow('Skipping images while scanning will reduce the scan time and lower the memory usage during scan')); + console.log(); + + return inquirer + .prompt([{ + message: 'Would you like to skip Album Art images when scanning?', + type: "confirm", + name: "confirm", + default: false + }]) + .then(answers => { + if(answers.confirm === true) { + return true; + } + return false; + }); +} + +function setSaveInterval() { + console.log(colors.yellow('Sets how often a DB update should happen during a file scan')); + console.log('Large libraries (4TB+) can see some performance gains during scan by increasing this'); + console.log(); + + return inquirer + .prompt([{ + message: "Save DB every __ files: ", + type: "input", + name: "interval", + default: 250, + validate: answer => { + if (!Number.isInteger(Number(answer)) || Number(answer) < 100) { + return 'Save Interval must be a an integer greater than 100!'; + } + return true; + } + }]) + .then(answers => { + return Number(answers.interval); + }); +} + +function setScanPause() { + console.log(colors.yellow('Sets a pause interval between each file that is scanned (in milliseconds)')); + console.log('Scanning large libraries can eat up disk and CPU resources on slower system'); + console.log('Setting a pause between files will increase the scan time but reduce system resource usage'); + console.log(); + + return inquirer + .prompt([{ + message: "Set pause (milliseconds): ", + type: "input", + name: "pause", + default: 0, + validate: answer => { + if (!Number.isInteger(Number(answer)) || Number(answer) < 0) { + return 'Pause cannot be less than 0'; + } + return true; + } + }]) + .then(answers => { + return Number(answers.pause); + }); +} + +function setBootDelay() { + console.log(colors.yellow('Sets a delay between server boot and the initial file scan (in seconds)')); + console.log('Scanning large libraries can cause a spike in memory usage. And booting the server causes a spike in memory usage'); + console.log('By adding a delay between boot and scan, you can reduce the max memory use'); + console.log(); + + return inquirer + .prompt([{ + message: "Set boot scan delay (seconds): ", + type: "input", + name: "delay", + default: 0, + validate: answer => { + if (!Number.isInteger(Number(answer)) || Number(answer) < 0) { + return 'Delay cannot be less than 0'; + } + return true; + } + }]) + .then(answers => { + return Number(answers.delay); + }); +} + +function setScanInterval() { + console.log(colors.yellow('Sets how often a scan should happen (in hours)')); + console.log('Scans happen every 24 hours by default'); + console.log(); + + return inquirer + .prompt([{ + message: "Scan every __ hours: ", + type: "input", + name: "interval", + default: 24, + validate: answer => { + if (!Number.isInteger(Number(answer)) || Number(answer) < 0) { + return 'Scan Interval cannot be less than 0'; + } + return true; + } + }]) + .then(answers => { + return Number(answers.interval); + }); +} + function generateSecret() { return new Promise((resolve, reject) => { require('crypto').randomBytes(48, function (err, buffer) { diff --git a/modules/config/configure-commander.js b/modules/config/configure-commander.js index e350f34..4c5b299 100644 --- a/modules/config/configure-commander.js +++ b/modules/config/configure-commander.js @@ -35,10 +35,11 @@ exports.setup = function (args) { // DB .option('-d, --database ', 'Specify Database Filepath', 'mstream.db') - .option('-E, --interval ', 'Specify Database Scan Interval (In Hours)', /^\d+$/i, 24) + .option('-E, --scaninterval ', 'Specify Database Scan Interval (In Hours)', /^\d+$/i, 24) .option('-D, --saveinterval ', 'Specify Database Save Interval', /^\d+$/i, 250) .option('-S, --skipimg', 'While skip parsing album art if flagged') - .option('-P, --dbpause ', 'Specify File Scan Pause Interval', /^\d+$/i, 0) + .option('-B, --bootdelay ', 'Specify Boot Scan Pause (In Seconds)', /^\d+$/i, 0) + .option('-P, --dbpause ', 'Specify File Scan Pause Interval (in Milliseconds)', /^\d+$/i, 0) // Logs .option('-L, --logs ', 'Specify Database Filepath') @@ -56,9 +57,11 @@ exports.setup = function (args) { .option("--makesecret", "Add an SSL Cert") .option("--removeuser", "Delete User From Config") .option("--removepath", "Remove Folder From Config") - .option("--wizard ", "Setup Wizard") + .option("--wizard [file]", "Setup Wizard") .parse(args); + + // TODO: If no params are supplied, try to use default.json if (program.init) { require('./config-inquirer').init(program.init).then((didWrite) => { @@ -194,15 +197,19 @@ exports.setup = function (args) { program3['lastfm-password'] = program.lpass; } + program3.scanOptions = { + scanInterval: Number(program.scaninterval), + saveInterval: Number(program.saveinterval), + pause: Number(program.dbpause), + bootScanDelay: Number(program.bootdelay) + } + // db plugins program3.database_plugin = { - dbPath: program.database, - interval: Number(program.interval), - saveInterval: Number(program.saveinterval), - pause: Number(program.dbpause) + dbPath: program.database } if (program.skipimg) { - program3.database_plugin.skipImg = true; + program3.scanOptions.skipImg = true; } // port forwarding diff --git a/modules/config/configure-json-file.js b/modules/config/configure-json-file.js index 50a96e9..8e31f27 100644 --- a/modules/config/configure-json-file.js +++ b/modules/config/configure-json-file.js @@ -17,27 +17,59 @@ exports.setup = function (loadJson) { loadJson.database_plugin = {}; } + if (!loadJson.scanOptions) { + loadJson.scanOptions = {}; + } + + if (loadJson.database_plugin && loadJson.database_plugin.skipImg) { + console.log('The database plugin is being deprecated. Please move `skipImg` to the `scanOptions` object'); + console.log('Or you can use `mstream --wizard` to automatically handle your config file'); + loadJson.scanOptions.skipImg = loadJson.database_plugin.skipImg; + } + + if (loadJson.database_plugin && loadJson.database_plugin.pause) { + console.log('The database plugin is being deprecated. Please move `skipImg` to the `pause` object'); + console.log('Or you can use `mstream --wizard` to automatically handle your config file'); + loadJson.scanOptions.pause = loadJson.database_plugin.pause; + } + + if (loadJson.database_plugin && loadJson.database_plugin.saveInterval) { + console.log('The database plugin is being deprecated. Please move `saveInterval` to the `scanOptions` object'); + console.log('Or you can use `mstream --wizard` to automatically handle your config file'); + loadJson.scanOptions.saveInterval = loadJson.database_plugin.saveInterval; + } + + if (loadJson.database_plugin && loadJson.database_plugin.interval) { + console.log('The database plugin is being deprecated. Please move `interval` to the `scanOptions` object under the name `scanInterval`'); + console.log('Or you can use `mstream --wizard` to automatically handle your config file'); + loadJson.scanOptions.scanInterval = loadJson.database_plugin.interval; + } + if (!loadJson.database_plugin.dbPath) { loadJson.database_plugin.dbPath = 'mstream.db'; } - if (loadJson.database_plugin.interval === false) { - loadJson.database_plugin.interval = 0; + if (loadJson.scanOptions.scanInterval === false) { + loadJson.scanOptions.scanInterval = 0; } - loadJson.database_plugin.interval = Number(loadJson.database_plugin.interval); - if (typeof loadJson.database_plugin.interval !== 'number' || isNaN(loadJson.database_plugin.interval) || loadJson.database_plugin.interval < 0) { - loadJson.database_plugin.interval = 24; + loadJson.scanOptions.scanInterval = Number(loadJson.scanOptions.scanInterval); + if (typeof loadJson.scanOptions.scanInterval !== 'number' || isNaN(loadJson.scanOptions.scanInterval) || loadJson.scanOptions.scanInterval < 0) { + loadJson.scanOptions.scanInterval = 24; } - loadJson.database_plugin.saveInterval = Number(loadJson.database_plugin.saveInterval); - if (typeof loadJson.database_plugin.saveInterval !== 'number' || isNaN(loadJson.database_plugin.saveInterval) || loadJson.database_plugin.saveInterval < 0) { - loadJson.database_plugin.saveInterval = 250; + loadJson.scanOptions.saveInterval = Number(loadJson.scanOptions.saveInterval); + if (typeof loadJson.scanOptions.saveInterval !== 'number' || isNaN(loadJson.scanOptions.saveInterval) || loadJson.scanOptions.saveInterval < 0) { + loadJson.scanOptions.saveInterval = 250; } - loadJson.database_plugin.pause = Number(loadJson.database_plugin.pause); - if (typeof loadJson.database_plugin.pause !== 'number' || isNaN(loadJson.database_plugin.pause) || loadJson.database_plugin.pause < 0) { - loadJson.database_plugin.pause = 0; + loadJson.scanOptions.pause = Number(loadJson.scanOptions.pause); + if (typeof loadJson.scanOptions.pause !== 'number' || isNaN(loadJson.scanOptions.pause) || loadJson.scanOptions.pause < 0) { + loadJson.scanOptions.pause = 0; + } + + if(!loadJson.scanOptions) { + loadJson.scanOptions = {}; } if (!loadJson.folders || typeof loadJson.folders !== 'object') { diff --git a/modules/db-management/database-default-manager.js b/modules/db-management/database-default-manager.js index 7836ff5..c159b34 100644 --- a/modules/db-management/database-default-manager.js +++ b/modules/db-management/database-default-manager.js @@ -151,7 +151,7 @@ function parseFile(thisSong) { return metadata.parseFile(thisSong, opt).then(thisMetadata => { return thisMetadata.common; }).catch(err => { - console.error(`Warning: metadata parse error on${thisSong}: ${err.message}`); + console.error(`Warning: metadata parse error on ${thisSong}: ${err.message}`); return {track: { no: null, of: null }, disk: { no: null, of: null }}; }).then(songInfo => { songInfo.filesize = filestat.size; diff --git a/modules/db-management/database-master.js b/modules/db-management/database-master.js index e32e980..abee81c 100644 --- a/modules/db-management/database-master.js +++ b/modules/db-management/database-master.js @@ -15,9 +15,9 @@ function scanIt(directory, vpath, program, callback) { vpath: vpath, dbSettings: program.database_plugin, albumArtDir: program.albumArtDir, - skipImg: program.database_plugin.skipImg ? true : false, - saveInterval: program.database_plugin.saveInterval ? program.database_plugin.saveInterval : 250, - pause: program.database_plugin.pause ? program.database_plugin.pause : false + skipImg: program.scanOptions.skipImg ? true : false, + saveInterval: program.scanOptions.saveInterval ? program.scanOptions.saveInterval : 250, + pause: program.scanOptions.pause ? program.scanOptions.pause : false } const forkedScan = child.fork(fe.join(__dirname, 'database-default-manager.js'), [JSON.stringify(jsonLoad)], { silent: true }); @@ -94,9 +94,15 @@ exports.setup = function (mstream, program) { } exports.runAfterBoot = function (program) { - runScan(program); - - if (program.database_plugin.interval) { - setInterval(() => runScan(program), program.database_plugin.interval * 60 * 60 * 1000); + var scanDelay = 0; + if (program.scanOptions.bootScanDelay && Number.isInteger(program.scanOptions.bootScanDelay) && program.scanOptions.bootScanDelay > 0) { + scanDelay = program.scanOptions.bootScanDelay } + + setTimeout(() => { + runScan(program); + if (program.scanOptions.scanInterval) { + setInterval(() => runScan(program), program.scanOptions.scanInterval * 60 * 60 * 1000); + } + }, scanDelay * 1000); } \ No newline at end of file diff --git a/save/README.md b/save/README.md new file mode 100644 index 0000000..407e374 --- /dev/null +++ b/save/README.md @@ -0,0 +1 @@ +DB and Config files go here \ No newline at end of file