index.js 11 KB

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