index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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 = [
  41. 'A',
  42. 'An',
  43. 'The',
  44. 'And',
  45. 'But',
  46. 'Or',
  47. 'For',
  48. 'Nor',
  49. 'As',
  50. 'At',
  51. 'By',
  52. 'For',
  53. 'From',
  54. 'In',
  55. 'Into',
  56. 'Near',
  57. 'Of',
  58. 'On',
  59. 'Onto',
  60. 'To',
  61. 'With',
  62. ];
  63. for (i = 0, j = lowers.length; i < j; i++)
  64. str = str.replace(new RegExp('\\s' + lowers[i] + '\\s', 'g'), function(txt) {
  65. return txt.toLowerCase();
  66. });
  67. // Certain words such as initialisms or acronyms should be left uppercase
  68. uppers = ['Id', 'Tv'];
  69. for (i = 0, j = uppers.length; i < j; i++) str = str.replace(new RegExp('\\b' + uppers[i] + '\\b', 'g'), uppers[i].toUpperCase());
  70. return str;
  71. };
  72. // get the db from the path
  73. const getDbForDir = (dir, opts) => {
  74. let options = opts || Object.assign({}, defaults, paths[dir]); // baseline the options
  75. let adapter = new FileSync(options.database); // init the db adapter
  76. let db = low(adapter); // connect the db
  77. return db;
  78. };
  79. // find a file in the db
  80. const findFile = (db, file) => {
  81. return db
  82. .get('files')
  83. .find({
  84. input: file,
  85. })
  86. .value(); // does it already exist?
  87. };
  88. // set a file in the db
  89. const setFile = (db, file, payload) => {
  90. if (!payload && typeof file === 'object')
  91. return db
  92. .get('files')
  93. .push(file)
  94. .write();
  95. return db
  96. .get('files')
  97. .find({
  98. input: file,
  99. })
  100. .assign(payload)
  101. .write();
  102. };
  103. // remove a file from the db
  104. const removeFile = (db, file) => {
  105. return db
  106. .get('files')
  107. .remove({
  108. input: file,
  109. })
  110. .write(); // remove file from database
  111. };
  112. // write process output
  113. const processOutput = str => {
  114. process.stdout.clearLine();
  115. process.stdout.cursorTo(0);
  116. process.stdout.write(str);
  117. };
  118. const getFilesize = async file => {
  119. let stats = fs.statSync(file);
  120. let bytes = stats['size'];
  121. return (bytes) ? bytes/1024 : 0;
  122. };
  123. // spawn a handbrake
  124. const processWithHandbrake = (input, output, preset) => {
  125. return new Promise((resolve, reject) => {
  126. const inputName = path.basename(input);
  127. const outputName = path.basename(output);
  128. hbjs
  129. .spawn({
  130. input: input,
  131. output: output,
  132. preset: preset,
  133. })
  134. .on('start', err => {
  135. processOutput(` -> processing "${inputName}" to "${outputName}" with "${preset}"`);
  136. })
  137. .on('error', err => {
  138. processOutput(` -> processing ${outputName} error: ${err.message || err}.\n`);
  139. reject(err);
  140. })
  141. .on('progress', progress => {
  142. processOutput(` -> processing ${outputName} - ${progress.percentComplete}%, ETA: ${progress.eta}`);
  143. })
  144. .on('cancelled', () => {
  145. processOutput(` -> processing ${outputName} cancelled\n`);
  146. reject(new Error(`Processing ${outputName} cancelled`));
  147. })
  148. .on('complete', () => {
  149. processOutput(` -> processing ${outputName} complete\n`);
  150. resolve(true);
  151. });
  152. });
  153. };
  154. const processFile = async file => {
  155. let db,
  156. options,
  157. preset = 'Fast 1080p30',
  158. clean = {
  159. '.': ' ',
  160. },
  161. ext = 'm4v',
  162. filename = path.parse(file).name,
  163. input = file,
  164. output = filename,
  165. titlecase = false,
  166. folder = false,
  167. destination = '';
  168. // determine the presets and clean object
  169. for (let i = 0, l = dirs.length; i < l; i++) {
  170. let dir = dirs[i]; // pointer to the dir
  171. if (file && dir && file.indexOf(dir) > -1) {
  172. // is this in this path?
  173. options = Object.assign({}, defaults, paths[dir]); // baseline the options
  174. db = getDbForDir(dir, options); // init the db connection
  175. let found = findFile(db, file); // does it already exist?
  176. if (found && found.status && found.status === 'success') {
  177. // was it already processed?
  178. return false; // break this loop
  179. } else if (!found) {
  180. // was it found? .. nope
  181. processOutput(` -> ${path.basename(file)} [processing]`);
  182. setFile(db, {
  183. input: file,
  184. output: '',
  185. status: '',
  186. date: new Date(),
  187. }); // push onto the list an entry
  188. } else {
  189. processOutput(` -> ${path.basename(file)} [re-processing]`);
  190. setFile(db, file, {
  191. status: '',
  192. date: new Date(),
  193. }); // set the status to blank
  194. }
  195. preset = options.preset; // apply the specifics ...
  196. clean = options.clean; // ...
  197. ext = options.ext; // ...
  198. titlecase = !!options.titlecase; // ...
  199. folder = !!options.folder; // ...
  200. destination = options.destination; // ...
  201. break; // break the loop
  202. }
  203. }
  204. // clean the output name
  205. let cleanKeys = Object.keys(clean);
  206. for (let c in cleanKeys) {
  207. let key = cleanKeys[c];
  208. let val = clean[key];
  209. let re = new RegExp(key, 'gi');
  210. output = output.replace(re, val);
  211. }
  212. // baseline the output name
  213. output = output.trim(); // trim the whitespace
  214. output = output.replace(/(\d{4})$/, `($1)`); // if there is a date at the end wrap it
  215. // do we have a sub folder option?
  216. if (folder) {
  217. let match = output.match(/^(.*?)?\./gm); // get the name for the file before the first .
  218. folder = match && match.length > 0 ? match[0].slice(0, -1) : false; // get just the stuff before the dot ... or false
  219. }
  220. // do we have title case enabled?
  221. if (options.titlecase) output = output.toTitleCase(); // titlecase that string
  222. // baseline the target
  223. let target = destination + (folder ? '/' + folder : ''); // setup the target location
  224. output = target + '/' + output + '.' + ext; // update the new name
  225. let outputSize = await getFilesize(output);
  226. // do we already have an existing output that matches? ... make sure its bigger than 100k
  227. if (fs.existsSync(output) && outputSize > 100) {
  228. processOutput(` -> ${path.basename(file)} [skipping ... already processed]\n`);
  229. setFile(db, file, {
  230. output: output,
  231. status: 'success',
  232. date: new Date(),
  233. }); // update database with status
  234. return false;
  235. } else {
  236. process.stdout.write('\n'); // send a new line
  237. }
  238. // update database with output name
  239. setFile(db, file, {
  240. output: output,
  241. });
  242. // create parent if required
  243. if (target && !fs.existsSync(target)) {
  244. processOutput(` -> creating parent directory: ${target}\n`);
  245. fs.mkdirSync(target, {
  246. recursive: true,
  247. });
  248. }
  249. // spawn handbrake
  250. if (!test) {
  251. try {
  252. await processWithHandbrake(input, output, preset);
  253. setFile(db, file, {
  254. status: 'success',
  255. date: new Date(),
  256. }); // update database with status
  257. } catch (err) {
  258. setFile(db, file, {
  259. status: 'failure',
  260. date: new Date(),
  261. }); // update database with status
  262. }
  263. }
  264. return true; // when complete return true
  265. };
  266. // cleanup removes from the db
  267. const cleanup = file => {
  268. let db;
  269. for (let i = 0, l = dirs.length; i < l; i++) {
  270. let dir = dirs[i]; // pointer to the dir
  271. if (file && dir && file.indexOf(dir) > -1) {
  272. // is this in this path?
  273. db = getDbForDir(dir); // init the db connection
  274. removeFile(db, file); // remove file form database
  275. }
  276. }
  277. };
  278. // handle args
  279. if (args.version) return version(); // show version
  280. if (args.help || !args.config) return help(); // show help
  281. const ignoreInitial = args.hasOwnProperty('ignoreInitial') ? args.ignoreInitial : false; // ignore initial files
  282. const test = args.hasOwnProperty('test') ? args.test : false; // do a dry run ... just log
  283. // get our paths
  284. const paths = require(args.config);
  285. // init our defaults
  286. const defaults = {
  287. preset: 'Fast 1080p30',
  288. clean: {},
  289. titlecase: false,
  290. folder: false,
  291. database: 'data/db.json',
  292. };
  293. // setup watcher options
  294. const opts = {
  295. ignored: /(^|[\/\\])\..|([s|S]ample\.*)/,
  296. ignoreInitial: ignoreInitial,
  297. persistent: true,
  298. usePolling: true,
  299. interval: 10000,
  300. depth: 1,
  301. awaitWriteFinish: {
  302. stabilityThreshold: 3000,
  303. pollInterval: 1000,
  304. },
  305. ignorePermissionErrors: false,
  306. atomic: true,
  307. };
  308. // parse the paths to dirs
  309. const dirs = Object.keys(paths);
  310. // initialize watches and db then start the watcher
  311. const main = async () => {
  312. var queue = new Queue(
  313. async (input, cb) => {
  314. // init the queue
  315. let result = await processFile(input); // process the queue
  316. cb(null, result);
  317. },
  318. {
  319. batchSize: 1,
  320. concurrent: 1,
  321. maxRetries: 3,
  322. retryDelay: 5000,
  323. }
  324. );
  325. var watches = []; // array of things to watch
  326. for (let d in dirs) {
  327. // loop the dirs
  328. let dir = dirs[d]; // pointer
  329. let options = Object.assign({}, defaults, paths[dir]); // baseline the options
  330. let db = getDbForDir(dir);
  331. db.defaults({
  332. files: [],
  333. }).write(); // init the database
  334. for (let e in options.exts) {
  335. // loop the exts to watch
  336. let ext = options.exts[e]; // alias the ext
  337. watches.push(`${dir}/**/*.${ext}`); // push the watch
  338. }
  339. }
  340. const watcher = chokidar.watch(watches, opts); // init our watcher
  341. console.log('Watching:', watches);
  342. watcher
  343. .on('add', async file => {
  344. // when a file is added ...
  345. queue.push(file); // push the file onto the queue to be processed
  346. })
  347. .on('change', file => {
  348. // when a file changes ...
  349. console.log(` -> ${file} has been changed`);
  350. })
  351. .on('unlink', async file => {
  352. // when a file is removed ...
  353. console.log(` -> ${file} has been removed`);
  354. cleanup(file);
  355. })
  356. .on('error', error => {
  357. console.error(` -> Error: ${error.message || error}`);
  358. });
  359. };
  360. (async () => {
  361. main();
  362. })().catch(err => {
  363. console.log('Error:', err.message || err);
  364. });