lamd.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. /*
  2. LiveMe Monitor CLI
  3. */
  4. const os = require('os'),
  5. fs = require('fs'),
  6. http = require('http'),
  7. LiveMe = require('liveme-api'),
  8. ffmpeg = require('fluent-ffmpeg'),
  9. m3u8stream = require('./modules/m3u8stream');
  10. var config = {
  11. downloaderFFMPEG: true,
  12. downloadPath: os.homedir() + '/Downloads',
  13. downloadChunks: 10,
  14. downloadTemplate: '%%replayid%%',
  15. loopCycle: 30,
  16. localPort: 8280,
  17. console_output: true
  18. },
  19. accounts = [],
  20. account_index = 0,
  21. download_list = [],
  22. errored_list = [],
  23. downloadActive = false,
  24. minuteTick = 0,
  25. APIVERSION = '1.1';
  26. main();
  27. function main() {
  28. /*
  29. Load configuration file
  30. */
  31. if (fs.existsSync('config.json')) {
  32. fs.readFile('config.json', 'utf8', (err,data) => {
  33. if (!err) {
  34. config = JSON.parse(data);
  35. // These options only come int play when using the stream downloader and not FFMPEG
  36. if ((config.downloaderFFMPEG == undefined) || (config.downloaderFFMPEG == null)) config.downloaderFFMPEG = true;
  37. if (config.downloadChunks < 2) config.downloadChunks = 2;
  38. if (config.downloadChunks > 250) config.downloadChunks = 250;
  39. if (config.loopCycle > 360) config.loopCycle = 360;
  40. if (config.loopCycle < 15) config.loopCycle = 15;
  41. if ((config.console_output == undefined) || (config.console_output == null)) config.console_output = false;
  42. }
  43. if (config.console_output) {
  44. process.stdout.write("\x1b[1;34mLiveMe Account Monitor Daemon (LAMD)\n\x1b[0;34mhttps://thecoderstoolbox.com/lamd\n");
  45. process.stdout.write("\x1b[1;30m------------------------------------------------------------------------------\n");
  46. process.stdout.write("\x1b[1;32m Scan Interval: \x1b[1;36m" + config.loopCycle + " \x1b[1;32mminutes\n\n");
  47. process.stdout.write("\x1b[1;32m Download Path: \x1b[1;36m" + config.downloadPath + "\n");
  48. process.stdout.write("\x1b[1;32m Download Template: \x1b[1;36m" + config.downloadTemplate + "\n\n");
  49. process.stdout.write("\x1b[1;32m Download Engine: \x1b[1;36m" + (config.downloaderFFMPEG ? 'FFMPEG' : 'Stream Downloader') + "\n");
  50. if (config.downloaderFFMPEG == false) {
  51. process.stdout.write("\x1b[1;32m Download Chunks: \x1b[1;36m" + config.downloadChunks + "\x1b[1;32m at a time\n");
  52. }
  53. process.stdout.write("\x1b[1;30m------------------------------------------------------------------------------\n");
  54. process.stdout.write("\x1b[0;37m");
  55. }
  56. });
  57. }
  58. for (var i = 0; i < process.argv.length; i++) {
  59. if (process.argv[i] == '--writecfg') {
  60. fs.writeFile(
  61. 'config.json',
  62. JSON.stringify(config, null, 2),
  63. () => {}
  64. );
  65. }
  66. }
  67. /*
  68. Load Account List
  69. */
  70. if (fs.existsSync('accounts.json')) {
  71. fs.readFile('accounts.json', 'utf8', (err,data) => {
  72. if (!err) {
  73. accounts = JSON.parse(data);
  74. if (config.console_output) {
  75. process.stdout.write("\x1b[1;33m" + accounts.length + " \x1b[1;34maccounts loaded in.\n");
  76. }
  77. }
  78. });
  79. }
  80. if (fs.existsSync('queued.json')) {
  81. fs.readFile('queued.json', 'utf8', (err,data) => {
  82. if (!err) {
  83. download_list = JSON.parse(data);
  84. if (download_list.length > 0) {
  85. if (config.console_output) process.stdout.write("\x1b[1;33mResuming existing download queue...\n");
  86. setTimeout(() => {
  87. downloadFile();
  88. }, 5000);
  89. }
  90. }
  91. });
  92. }
  93. /*
  94. Replay Check Interval - Runs every minute
  95. */
  96. setInterval(() => {
  97. minuteTick++;
  98. if (minuteTick == config.loopCycle) {
  99. minuteTick = 0;
  100. setImmediate(() => {
  101. account_index = 0;
  102. accountScanLoop();
  103. });
  104. }
  105. }, 60000);
  106. setTimeout(() => {
  107. account_index = 0;
  108. accountScanLoop();
  109. }, 5);
  110. /*
  111. Internal Web Server - Used for command interface
  112. */
  113. http.createServer( (req, res) => {
  114. var chunks = req.url.substr(1).split('/'),
  115. response = {
  116. api_version: APIVERSION,
  117. code: 500,
  118. message: '',
  119. data: null
  120. }
  121. switch (chunks[0]) {
  122. case 'add-user':
  123. case 'add-account':
  124. var add_this = true, i = 0, isnum = /^\d+$/.test(chunks[1]);
  125. for (i = 0; i < accounts.length; i++) {
  126. if (accounts[i].userid == chunks[1]) { add_this = false; }
  127. }
  128. if (add_this && isnum) {
  129. accounts.push({
  130. userid: chunks[1],
  131. scanned: Math.floor((new Date()).getTime() / 1000)
  132. });
  133. fs.writeFile(
  134. 'accounts.json',
  135. JSON.stringify(accounts),
  136. () => {}
  137. );
  138. response.message = 'Account added.';
  139. response.code = 200;
  140. if (config.console_output) process.stdout.write("\x1b1;36mAdded \x1b[1;33m" + chunks[1] + " \x1b[1;36mfor monitoring.\n");
  141. } else {
  142. response.message = 'Account already in list.';
  143. response.code = 302;
  144. if (config.console_output) process.stdout.write("\x1b[1;31mAccount \x1b[1;33m" + chunks[1] + " \x1b[1;31malready in database.\n");
  145. }
  146. break;
  147. case 'check-user':
  148. case 'check-account':
  149. var is_present = false;
  150. for (var i = 0; i < accounts.length; i++) {
  151. if (accounts[i].userid == chunks[1]) { is_present = true; }
  152. }
  153. response.message = is_present ? 'Account is in the list.' : 'Account not found in the list.';
  154. response.data = [];
  155. response.code = is_present ? 200 : 404;
  156. break;
  157. case 'remove-user':
  158. case 'remove-account':
  159. response.message = 'Account not in the list.';
  160. response.code = 404;
  161. for (var i = 0; i < accounts.length; i++) {
  162. if (accounts[i].userid == chunks[1]) {
  163. accounts.splice(i, 1);
  164. response.message = 'Account removed.';
  165. response.code = 200;
  166. if (config.console_output) process.stdout.write("\x1b[1;36mAccount \x1b[1;33m" + chunks[1] + " \x1b[1;36mremoved from list.\n");
  167. }
  168. }
  169. fs.writeFile(
  170. 'accounts.json',
  171. JSON.stringify(accounts),
  172. () => {}
  173. );
  174. break;
  175. case 'list-users':
  176. case 'list-accounts':
  177. response.message = 'Accounts in list';
  178. response.code = 200;
  179. response.data = [];
  180. for (var i = 0; i < accounts.length; i++) {
  181. response.data.push(accounts[i].userid);
  182. }
  183. break;
  184. case 'add-replay':
  185. case 'add-download':
  186. response.message = 'Replay added to queue.';
  187. response.code = 200;
  188. response.data = [];
  189. var isnum = /^\d+$/.test(chunks[1]);
  190. if (isnum) {
  191. if (config.console_output) process.stdout.write("\x1b[1;36mReplay \x1b[1;33m" + chunks[1] + " \x1b[1;36m- added to queue. \r");
  192. download_list.push(chunks[1]);
  193. downloadFile();
  194. }
  195. break;
  196. case 'ping':
  197. response.message = 'Pong';
  198. response.code = 200;
  199. break;
  200. case 'shutdown':
  201. if (config.console_output) process.stdout.write("\x1b[1;31mShutting down and storing information...\n");
  202. setTimeout(() => {
  203. process.exit(0);
  204. }, 250);
  205. break;
  206. default:
  207. response.message = 'Invalid command.';
  208. break;
  209. }
  210. res.writeHead(200, { 'Content-Type': 'text/javascript'});
  211. res.write(JSON.stringify(response, null, 2));
  212. res.end();
  213. }).listen(config.localPort);
  214. }
  215. /*
  216. Account Scan Loop
  217. */
  218. function accountScanLoop() {
  219. if (account_index < accounts.length) {
  220. setTimeout(() => {
  221. accountScanLoop();
  222. }, 250);
  223. }
  224. setImmediate(function(){
  225. if (account_index < accounts.length) { account_index++; scanForNewReplays(account_index); }
  226. });
  227. }
  228. /*
  229. Replay Scan
  230. */
  231. function scanForNewReplays(i) {
  232. if (accounts[i] == undefined) return;
  233. LiveMe.getUserReplays(accounts[i].userid, 1, 10).then(replays => {
  234. if (replays == undefined) return;
  235. if (replays.length < 1) return;
  236. var ii = 0,
  237. count = 0,
  238. userid = replays[0].userid,
  239. last_scanned = 0,
  240. dt = Math.floor((new Date()).getTime() / 1000);
  241. last_scanned = accounts[i].scanned;
  242. accounts[i].scanned = dt;
  243. fs.writeFile(
  244. 'accounts.json',
  245. JSON.stringify(accounts),
  246. () => {}
  247. );
  248. var replay_count = 0;
  249. for (ii = 0; ii < replays.length; ii++) {
  250. // If we take the video time and subtract the last time we scanned and its
  251. // greater than zero then its new and needs to be added
  252. if ((replays[ii].vtime - last_scanned) > 0) {
  253. var add_replay = true;
  254. for (var j = 0; j < download_list.length; j++) {
  255. if (download_list[j] == replays[ii].vid) add_replay = false;
  256. }
  257. if (add_replay == true) {
  258. replay_count++;
  259. download_list.push(replays[ii].vid);
  260. fs.writeFile(
  261. 'queued.json',
  262. JSON.stringify(download_list),
  263. () => {
  264. // Queue file was written
  265. }
  266. );
  267. }
  268. }
  269. }
  270. if (replay_count > 0) {
  271. if (config.console_output) process.stdout.write("\x1b[1;36mAdding \x1b[1;33m"+replay_count+" \x1b[1;36mreplays for \x1b[1;33m"+userid+" \n");
  272. downloadFile();
  273. } else {
  274. if (config.console_output) process.stdout.write("\x1b[1;36mNo new replays found for \x1b[1;33m"+userid+"\x1b[1;36m. \n");
  275. }
  276. });
  277. }
  278. /*
  279. Download Handler
  280. */
  281. function downloadFile() {
  282. if (downloadActive == true) return;
  283. if (download_list.length == 0) return;
  284. LiveMe.getVideoInfo(download_list[0]).then(video => {
  285. var dt = new Date(video.vtime * 1000), mm = dt.getMonth() + 1, dd = dt.getDate(), filename = '';
  286. filename = config.downloadTemplate
  287. .replace(/%%broadcaster%%/g, video.uname)
  288. .replace(/%%longid%%/g, video.userid)
  289. .replace(/%%replayid%%/g, video.vid)
  290. .replace(/%%replayviews%%/g, video.playnumber)
  291. .replace(/%%replaylikes%%/g, video.likenum)
  292. .replace(/%%replayshares%%/g, video.sharenum)
  293. .replace(/%%replaytitle%%/g, video.title ? video.title : 'untitled')
  294. .replace(/%%replayduration%%/g, video.videolength)
  295. .replace(/%%replaydatepacked%%/g, (dt.getFullYear() + (mm < 10 ? '0' : '') + mm + (dd < 10 ? '0' : '') + dd))
  296. .replace(/%%replaydateus%%/g, ((mm < 10 ? '0' : '') + mm + '-' + (dd < 10 ? '0' : '') + dd + '-' + dt.getFullYear()))
  297. .replace(/%%replaydateeu%%/g, ((dd < 10 ? '0' : '') + dd + '-' + (mm < 10 ? '0' : '') + mm + '-' + dt.getFullYear()));
  298. // Cleanup any illegal characters in the filename
  299. filename = filename.replace(/[/\\?%*:|"<>]/g, '-');
  300. filename = filename.replace(/([^a-z0-9\s]+)/gi, '-');
  301. filename = filename.replace(/[\u{0080}-\u{FFFF}]/gu, '');
  302. if (config.downloaderFFMPEG == true) {
  303. filename += '.mp4';
  304. ffmpeg(video.hlsvideosource)
  305. .outputOptions([
  306. '-c copy',
  307. '-bsf:a aac_adtstoasc',
  308. '-vsync 2',
  309. '-movflags faststart'
  310. ])
  311. .output(config.downloadPath + '/' + filename)
  312. .on('end', function(stdout, stderr) {
  313. if (config.console_output) process.stdout.write("\x1b[1;34mReplay \x1b[1;33m" + download_list[0] + " \x1b[1;34m- downloaded. \n");
  314. download_list.shift();
  315. downloadActive = false;
  316. // Update current queue file
  317. fs.writeFile(
  318. 'queued.json',
  319. JSON.stringify(download_list),
  320. () => {
  321. }
  322. );
  323. downloadFile();
  324. })
  325. .on('progress', function(progress) {
  326. if (config.console_output) process.stdout.write("\x1b[1;34mReplay \x1b[1;33m" + download_list[0] + " \x1b[1;34m- \x1b[1;33m" + progress.percent.toFixed(2) + "% \r");
  327. })
  328. .on('start', function(c) {
  329. downloadActive = true;
  330. })
  331. .on('error', function(err, stdout, exterr) {
  332. if (config.console_output) process.stdout.write("\x1b[1;34mReplay \x1b[1;33m" + download_list[0] + " \x1b[1;34m- \x1b[1;31mErrored \x1b[1;36m(\x1b[1;34mDetails: \x1b[1;37m"+err+"\x1b[1;36m) \n");
  333. errored_list.push(download_list[0]);
  334. download_list.shift();
  335. downloadActive = false;
  336. // Update current queue file
  337. fs.writeFile(
  338. 'queued.json',
  339. JSON.stringify(download_list),
  340. () => {
  341. // Queue file was written
  342. }
  343. );
  344. // Update errored file
  345. fs.writeFile(
  346. 'errored.json',
  347. errored_list.join("\n"),
  348. () => {
  349. // Errored file was written
  350. }
  351. );
  352. downloadFile();
  353. })
  354. .run();
  355. } else {
  356. filename += '.ts';
  357. m3u8stream(video, {
  358. chunkReadahead: config.downloadChunks,
  359. on_progress: (e) => {
  360. var p = Math.floor((e.index / e.total) * 10000) / 100;
  361. if (config.console_output) process.stdout.write("\x1b[1;34mReplay \x1b[1;33m" + download_list[0] + " \x1b[1;34m- \x1b[1;33m" + p + "% \r");
  362. },
  363. on_complete: (e) => {
  364. if (config.console_output) process.stdout.write("\x1b[1;34mReplay \x1b[1;33m" + download_list[0] + " \x1b[1;34m- downloaded. \n");
  365. download_list.shift();
  366. downloadActive = false;
  367. // Update current queue file
  368. fs.writeFile(
  369. 'queued.json',
  370. JSON.stringify(download_list),
  371. () => {
  372. }
  373. );
  374. downloadFile();
  375. },
  376. on_error: (e) => {
  377. // We ignore the timeout errors to avoid issues.
  378. if (e.error == 'Download timeout') return;
  379. if (config.console_output) process.stdout.write("\x1b[1;34mReplay \x1b[1;33m" + download_list[0] + " \x1b[1;34m- \x1b[1;31mErrored \x1b[1;36m(\x1b[1;34mDetails: \x1b[1;37m"+err+"\x1b[1;36m) \n");
  380. errored_list.push(download_list[0]);
  381. download_list.shift();
  382. downloadActive = false;
  383. // Update current queue file
  384. fs.writeFile(
  385. 'queued.json',
  386. JSON.stringify(download_list),
  387. () => {
  388. // Queue file was written
  389. }
  390. );
  391. // Update errored file
  392. fs.writeFile(
  393. 'errored.json',
  394. errored_list.join("\n"),
  395. () => {
  396. // Errored file was written
  397. }
  398. );
  399. downloadFile();
  400. }
  401. }).pipe(fs.createWriteStream(config.downloadPath + '/' + filename));
  402. }
  403. });
  404. }