index.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. "use strict";
  2. const fs = require("fs");
  3. const path = require("path");
  4. const minimist = require("minimist");
  5. const low = require("lowdb");
  6. const Queue = require('better-queue');
  7. const FileSync = require("lowdb/adapters/FileSync");
  8. const chokidar = require("chokidar");
  9. const hbjs = require("handbrake-js");
  10. const args = minimist(process.argv.slice(2), {
  11. alias: {
  12. h: "help",
  13. v: "version",
  14. c: "config",
  15. i: "ignoreInitial",
  16. t: "test"
  17. }
  18. });
  19. const version = () => {
  20. const pkg = require('./package.json');
  21. return console.log(pkg.version);
  22. }
  23. const help = () => {
  24. let message = `Usage:
  25. \tnpm run movies
  26. \tnpm run tvshows
  27. \tnpm run pr0n
  28. \tnpm run all
  29. \tnpm run help
  30. \tnpm run version\n`
  31. return console.log(message);
  32. }
  33. String.prototype.toTitleCase = function() {
  34. var i, j, str, lowers, uppers;
  35. str = this.replace(/([^\W_]+[^\s-]*) */g, function(txt) {
  36. return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
  37. });
  38. // Certain minor words should be left lowercase unless
  39. // they are the first or last words in the string
  40. lowers = ['A', 'An', 'The', 'And', 'But', 'Or', 'For', 'Nor', 'As', 'At',
  41. 'By', 'For', 'From', 'In', 'Into', 'Near', 'Of', 'On', 'Onto', 'To', 'With'];
  42. for (i = 0, j = lowers.length; i < j; i++)
  43. str = str.replace(new RegExp('\\s' + lowers[i] + '\\s', 'g'),
  44. function(txt) {
  45. return txt.toLowerCase();
  46. });
  47. // Certain words such as initialisms or acronyms should be left uppercase
  48. uppers = ['Id', 'Tv'];
  49. for (i = 0, j = uppers.length; i < j; i++)
  50. str = str.replace(new RegExp('\\b' + uppers[i] + '\\b', 'g'),
  51. uppers[i].toUpperCase());
  52. return str;
  53. };
  54. // get the db from the path
  55. const getDbForDir = (dir, opts) => {
  56. let options = opts || Object.assign({}, defaults, paths[dir]); // baseline the options
  57. let adapter = new FileSync(options.database); // init the db adapter
  58. let db = low(adapter); // connect the db
  59. return db;
  60. };
  61. // find a file in the db
  62. const findFile = (db, file) => {
  63. return db.get('files').find({ input:file }).value(); // does it already exist?
  64. };
  65. // set a file in the db
  66. const setFile = (db, file, payload) => {
  67. if (!payload && typeof file === 'object')
  68. return db.get('files').push(file).write();
  69. return db.get('files').find({ input: file }).assign(payload).write();
  70. };
  71. // remove a file from the db
  72. const removeFile = (db, file) => {
  73. return db.get('files').remove({ input: file }).write(); // remove file from database
  74. };
  75. // write process output
  76. const processOutput = (str) => {
  77. process.stdout.clearLine();
  78. process.stdout.cursorTo(0);
  79. process.stdout.write(str);
  80. };
  81. // spawn a handbrake
  82. const processWithHandbrake = (input, output, preset) => {
  83. return new Promise((resolve, reject) => {
  84. const filename = path.basename(input);
  85. hbjs.spawn({
  86. input: input,
  87. output: output,
  88. preset: preset
  89. }).on('start', (err) => {
  90. processOutput(` --> processing "${filename}" to "${output}" with "${preset}"`);
  91. }).on('error', (err) => {
  92. processOutput(` --> processing ${output} error: ${err.message || err}.\n`);
  93. reject(err);
  94. }).on('progress', (progress) => {
  95. processOutput(` --> processing ${output} - ${progress.percentComplete}%, ETA: ${progress.eta}`);
  96. }).on('cancelled', () => {
  97. processOutput(` --> processing ${output} cancelled\n`);
  98. reject(new Error(`Processing ${output} cancelled`));
  99. }).on('complete', () => {
  100. processOutput(` --> processing ${output} complete\n`);
  101. resolve(true);
  102. });
  103. });
  104. };
  105. const processFile = async (file) => {
  106. let adapter,
  107. db,
  108. options,
  109. preset = 'Fast 1080p30',
  110. clean = {
  111. '.': ' '
  112. },
  113. ext = 'm4v',
  114. filename = path.parse(file).name,
  115. input = file,
  116. output = filename,
  117. titlecase = false,
  118. folder = false,
  119. destination = '';
  120. // determine the presets and clean object
  121. for (let i = 0, l = dirs.length; i < l; i++) {
  122. let dir = dirs[i]; // pointer to the dir
  123. if (file && dir && file.indexOf(dir) > -1) { // is this in this path?
  124. options = Object.assign({}, defaults, paths[dir]); // baseline the options
  125. db = getDbForDir(dir, options); // init the db connection
  126. let found = findFile(db, file); // does it already exist?
  127. if (found && found.status && found.status === 'success') { // was it already processed?
  128. //console.log(`File ${file} has already been successfully processed.`);
  129. return false; // break this loop
  130. } else if (!found) { // was it found?
  131. processOutput(`-> ${path.basename(file)} [processing]`);
  132. setFile(db, { input:file, output:'', status:'', date:new Date() }); // push onto the list an entry
  133. } else {
  134. processOutput(`-> ${path.basename(file)} [re-processing]`);
  135. setFile(db, file, { status:'', date:new Date() }); // set the status to blank
  136. }
  137. preset = options.preset; // apply the specifics ...
  138. clean = options.clean; // ...
  139. ext = options.ext; // ...
  140. titlecase = !!options.titlecase; // ...
  141. folder = !!options.folder; // ...
  142. destination = options.destination; // ...
  143. break; // break the loop
  144. }
  145. }
  146. // clean the output name
  147. let cleanKeys = Object.keys(clean);
  148. for (let c in cleanKeys) {
  149. let key = cleanKeys[c];
  150. let val = clean[key];
  151. let re = new RegExp(key, 'gi');
  152. output = output.replace(re, val);
  153. }
  154. // baseline the output name
  155. output = output.trim(); // trim the whitespace
  156. output = output.replace(/(\d{4})$/, `($1)`); // if there is a date at the end wrap it
  157. // do we have a sub folder option?
  158. if (folder) {
  159. let match = output.match(/^(.*?)?\./gm); // get the name for the file before the first .
  160. folder = (match && match.length > 0) ? match[0].slice(0, -1) : false; // get just the stuff before the dot ... or false
  161. }
  162. // do we have title case enabled?
  163. if (options.titlecase) output = output.toTitleCase(); // titlecase that string
  164. // baseline the target
  165. let target = destination + ((folder) ? '/' + folder : ''); // setup the target location
  166. output = target + '/' + output + '.' + ext; // update the new name
  167. // do we already have an existing output that matches?
  168. if (fs.existsSync(output)) {
  169. processOutput(`-> ${path.basename(file)} [skipping ... already processed]\n`);
  170. setFile(db, file, { output: output, status: 'success', date:new Date() }); // update database with status
  171. return false;
  172. } else {
  173. processOutput('\n'); // send a new line
  174. }
  175. // update database with output name
  176. setFile(db, file, { output: output });
  177. // create parent if required
  178. if (target && !fs.existsSync(target)) {
  179. console.log(' --> creating parent directory:', target);
  180. fs.mkdirSync(target, {
  181. recursive: true
  182. });
  183. }
  184. // spawn handbrake
  185. if (!test) {
  186. try {
  187. await processWithHandbrake(input, output, preset);
  188. setFile(db, file, { status: 'success', date:new Date() }); // update database with status
  189. } catch (err) {
  190. setFile(db, file, { status: 'failure', date:new Date() }); // update database with status
  191. }
  192. }
  193. return true; // when complete return true
  194. };
  195. // cleanup removes from the db
  196. const cleanup = (file) => {
  197. let adapter, db, options;
  198. for (let i = 0, l = dirs.length; i < l; i++) {
  199. let dir = dirs[i]; // pointer to the dir
  200. if (file && dir && file.indexOf(dir) > -1) { // is this in this path?
  201. db = getDbForDir(dir); // init the db connection
  202. removeFile(db, file); // remove file form database
  203. }
  204. }
  205. };
  206. // handle args
  207. if (args.version) return version(); // show version
  208. if (args.help || !args.config) return help(); // show help
  209. const ignoreInitial = (args.hasOwnProperty('ignoreInitial')) ? args.ignoreInitial : false; // ignore initial files
  210. const test = (args.hasOwnProperty('test')) ? args.test : true; // do a dry run ... just log
  211. // get our paths
  212. const paths = require(args.config);
  213. // init our defaults
  214. const defaults = {
  215. "preset": "Fast 1080p30",
  216. "clean": {},
  217. "titlecase": false,
  218. "folder": false,
  219. "database": "data/db.json"
  220. }
  221. // setup watcher options
  222. const opts = {
  223. ignored: /(^|[\/\\])\..|([s|S]ample\.*)/,
  224. ignoreInitial: ignoreInitial,
  225. persistent: true,
  226. usePolling: true,
  227. interval: 10000,
  228. depth: 1,
  229. awaitWriteFinish: {
  230. stabilityThreshold: 3000,
  231. pollInterval: 1000
  232. },
  233. ignorePermissionErrors: false,
  234. atomic: true
  235. };
  236. // parse the paths to dirs
  237. const dirs = Object.keys(paths);
  238. // initialize watches and db then start the watcher
  239. const main = () => {
  240. var queue = new Queue((input,cb) => { // init the queue
  241. let result = processFile(input); // process the queue
  242. cb(null,result);
  243. });
  244. var watches = []; // array of things to watch
  245. for (let d in dirs) { // loop the dirs
  246. let dir = dirs[d]; // pointer
  247. let options = Object.assign({}, defaults, paths[dir]); // baseline the options
  248. let db = getDbForDir(dir);
  249. db.defaults({ files: [] }).write(); // init the database
  250. for (let e in options.exts) { // loop the exts to watch
  251. let ext = options.exts[e]; // alias the ext
  252. watches.push(`${dir}/**/*.${ext}`); // push the watch
  253. }
  254. }
  255. const watcher = chokidar.watch(watches, opts); // init our watcher
  256. console.log('Watching', watches);
  257. watcher.on('add', async (file) => { // when a new file is added ...
  258. queue.push(file); // push the file onto the queue to be processed
  259. }).on('change', (file) => { // when a file changes ...
  260. console.log(` -> ${file} has been changed`);
  261. }).on('unlink', async (file) => { // when a file is removed ...
  262. console.log(` -> ${file} has been removed`);
  263. cleanup(file);
  264. }).on('error', (error) => { // on errors ..
  265. console.error(` -> Error: ${error.message || error}`);
  266. });
  267. }
  268. main();