"use strict"; const fs = require("fs"); const path = require("path"); const minimist = require("minimist"); const low = require("lowdb"); const FileSync = require("lowdb/adapters/FileSync"); const chokidar = require("chokidar"); const hbjs = require("handbrake-js"); const args = minimist(process.argv.slice(2), { alias: { h: "help", v: "version", c: "config", i: "ignoreInitial", t: "test" } }); const version = () => { const pkg = require('./package.json'); return console.log(pkg.version); } const help = () => { let message = `Usage: \tnpm run movies \tnpm run tvshows \tnpm run pr0n \tnpm run all \tnpm run help \tnpm run version\n` return console.log(message); } String.prototype.toTitleCase = function() { var i, j, str, lowers, uppers; str = this.replace(/([^\W_]+[^\s-]*) */g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); // Certain minor words should be left lowercase unless // they are the first or last words in the string lowers = ['A', 'An', 'The', 'And', 'But', 'Or', 'For', 'Nor', 'As', 'At', 'By', 'For', 'From', 'In', 'Into', 'Near', 'Of', 'On', 'Onto', 'To', 'With']; for (i = 0, j = lowers.length; i < j; i++) str = str.replace(new RegExp('\\s' + lowers[i] + '\\s', 'g'), function(txt) { return txt.toLowerCase(); }); // Certain words such as initialisms or acronyms should be left uppercase uppers = ['Id', 'Tv']; for (i = 0, j = uppers.length; i < j; i++) str = str.replace(new RegExp('\\b' + uppers[i] + '\\b', 'g'), uppers[i].toUpperCase()); return str; }; if (args.version) return version(); if (args.help || !args.config) return help(); const ignoreInitial = (args.hasOwnProperty('ignoreInitial')) ? args.ignoreInitial : false; const test = (args.hasOwnProperty('test')) ? args.test : false; // get our paths const paths = require(args.config); // init our defaults const defaults = { "preset": "Fast 1080p30", "clean": {}, "titlecase": false, "folder": false, "database": "data/db.json" } // setup watcher options const opts = { ignored: /(^|[\/\\])\..|([s|S]ample\.*)/, ignoreInitial: ignoreInitial, persistent: true, usePolling: true, interval: 10000, depth: 1, awaitWriteFinish: { stabilityThreshold: 3000, pollInterval: 1000 }, ignorePermissionErrors: false }; // parse the paths to dirs const dirs = Object.keys(paths); // initialize watches and db let watches = []; // array of things to watch for (let d in dirs) { // loop the dirs let dir = dirs[d]; // pointer let options = Object.assign({}, defaults, paths[dir]); // baseline the options let adapter = new FileSync(options.database); // init our db adapter let db = low(adapter); // create a db connection db.defaults({ files: [] }).write(); // init the database for (let e in options.exts) { // loop the exts to watch let ext = options.exts[e]; // alias the ext watches.push(`${dir}/**/*.${ext}`); // push the watch } } const watcher = chokidar.watch(watches, opts); // init our watcher console.log('Watching', watches); // when a new file is added watcher.on('add', (file) => { let adapter, db, options, preset = 'Fast 1080p30', clean = { '.': ' ' }, ext = 'm4v', filename = path.parse(file).name, input = file, output = filename, titlecase = false, folder = false, destination = ''; // determine the presets and clean object for (let i = 0, l = dirs.length; i < l; i++) { let dir = dirs[i]; // pointer to the dir if (file && dir && file.indexOf(dir) > -1) { // is this in this path? options = Object.assign({}, defaults, paths[dir]); // baseline the options adapter = new FileSync(options.database); // init the db adapter db = low(adapter); // connect the db let found = db.get('files').find({ input:file }).value(); // does it already exist? if (found && found.status && found.status === 'success') { // was it already processed? //console.log(`File ${file} has already been successfully processed.`); return; // break this loop } else if (!found) { // was it found? console.log(`File ${file} has been added for processing.`); db.get('files').push({ input:file, output:'', status:'', date:new Date() }).write(); // push onto the list an entry } else { console.log(`File ${file} has been added for re-processing.`); db.get('files').find({ input: file }).assign({ status:'', date:new Date() }).write(); // set the status to blank } preset = options.preset; // apply the specifics ... clean = options.clean; // ... ext = options.ext; // ... titlecase = !!options.titlecase; // ... folder = !!options.folder; // ... destination = options.destination; // ... break; // break the loop } } // clean the output name let cleanKeys = Object.keys(clean); for (let c in cleanKeys) { let key = cleanKeys[c]; let val = clean[key]; let re = new RegExp(key, 'gi'); output = output.replace(re, val); } output = output.trim(); // trim the whitespace output = output.replace(/(\d{4})$/, `($1)`); // if there is a date at the end wrap it if (folder) { // do we have a sub folder option? let match = output.match(/^(.*?)?\./gm); // get the name for the file before the first . folder = (match && match.length > 0) ? match[0].slice(0, -1) : false; // get just the stuff before the dot ... or false } // do we have title case enabled? if (options.titlecase) output = output.toTitleCase(); // titlecase that string let target = destination + ((folder) ? '/' + folder : ''); // setup the target location output = target + '/' + output + '.' + ext; // update the new name db.get('files').find({ input: file }).assign({ output: output }).write(); // update database with output if (target && !fs.existsSync(target)) { console.log('Creating target directory:', target); fs.mkdirSync(target, { recursive: true }); } console.log(`Processing "${input}" to "${output}" with "${preset}"`); // spawn handbrake if (!test) { hbjs.spawn({ input: input, output: output, preset: preset }).on('start', (err) => { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`Processing "${input}" to "${output}" with "${preset}"`); }).on('error', (err) => { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`Processing ${output} error: ${err.message || err}.\n`); db.get('files').find({ input: file }).assign({ status: 'failure', date:new Date() }).write(); // update database with status }).on('progress', (progress) => { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`Processing ${output} - Percent complete: ${progress.percentComplete}, ETA: ${progress.eta}`); }).on('cancelled', () => { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`Processing ${output} cancelled.\n`); db.get('files').find({ input: file }).assign({ status: 'failure', date:new Date() }).write(); // update database with status }).on('complete', () => { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`Processing ${output} complete.\n`); db.get('files').find({ input: file }).assign({ status: 'success', date:new Date() }).write(); // update database with status }); } }).on('change', (file) => { console.log(`File ${file} has been changed.`); }).on('unlink', (file) => { console.log(`File ${file} has been removed.`); let adapter, db, options; for (let i = 0, l = dirs.length; i < l; i++) { let dir = dirs[i]; // pointer to the dir if (file && dir && file.indexOf(dir) > -1) { // is this in this path? options = Object.assign({}, defaults, paths[dir]); // baseline the options adapter = new FileSync(options.database); // init the db adapter db = low(adapter); // connect the db db.get('files').remove({ input: file }).write(); // remove file form database } } }).on('error', (error) => { console.error(`Error: ${error}.`); });