index.js 12 KB

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