d-fi.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const fs = require('fs-extra');
  4. const axios = require('axios');
  5. const chalk = require('chalk');
  6. const nodePath = require('path');
  7. const signale = require('signale');
  8. const { prompt } = require('prompts');
  9. const commandLineArgs = require('command-line-args');
  10. const commandLineUsage = require('command-line-usage');
  11. // import components
  12. const log = require('./src/components/logger');
  13. const downloadState = require('./src/components/downloadState');
  14. const { ensureDir } = require('./src/components/utils');
  15. // import service
  16. const defaults = require('./src/defaults');
  17. const musicQualities = require('./src/musicQualities');
  18. const downloadTypes = require('./src/downloadTypes');
  19. const downloadMultiple = require('./src/downloadMultiple');
  20. // check and notify about update
  21. const updateNotifier = require('./src/updateCheck');
  22. const pkg = require('./package.json');
  23. updateNotifier(pkg);
  24. // init deezer api
  25. const config = require('./src/config');
  26. const deezerApi = require('deezer-request2');
  27. const isCli = process.argv.length > 2;
  28. const cliOptionDefinitions = [
  29. {
  30. name: 'help',
  31. alias: 'h',
  32. description: 'Print this usage guide :)',
  33. },
  34. {
  35. name: 'quality',
  36. alias: 'q',
  37. type: String,
  38. description: 'The quality of the files to download: 128/320/FLAC',
  39. },
  40. {
  41. name: 'path',
  42. alias: 'p',
  43. type: String,
  44. description: 'The path to download the files to: path with / in the end',
  45. },
  46. {
  47. name: 'url',
  48. alias: 'u',
  49. type: String,
  50. defaultOption: true,
  51. description: 'Downloads single deezer url: album/artist/playlist/track url',
  52. },
  53. {
  54. name: 'downloadmode',
  55. alias: 'd',
  56. type: String,
  57. description: 'Downloads multiple urls from list: "all" for downloadLinks.txt',
  58. },
  59. {
  60. name: 'concurrency',
  61. alias: 'c',
  62. type: Number,
  63. description: 'Download concurrency for album, artists and playlist',
  64. },
  65. {
  66. name: 'no-progress',
  67. alias: 'n',
  68. type: Boolean,
  69. description: 'Hide download progress bar',
  70. },
  71. {
  72. name: 'set-arl',
  73. alias: 'a',
  74. type: String,
  75. description: 'Set arl cookie',
  76. },
  77. ];
  78. const onCancel = (prompt) => {
  79. console.log('Abort!');
  80. process.exit();
  81. };
  82. global.bar = true;
  83. /**
  84. * Application init.
  85. */
  86. const initApp = async () => {
  87. // App info
  88. console.log(chalk.cyan('╔══════════════════════════════════════════════════════════╗'));
  89. console.log(
  90. chalk.cyan('║') +
  91. chalk.bold.yellow(` d-fi (${pkg.version}) `) +
  92. chalk.cyan('║')
  93. );
  94. console.log(chalk.cyan('╠══════════════════════════════════════════════════════════╣'));
  95. console.log(
  96. chalk.redBright(' ♥ REPO ') +
  97. chalk.cyan('║') +
  98. ' https://notabug.org/sayem314/d-fi ' +
  99. chalk.cyan('║')
  100. );
  101. console.log(chalk.cyan('╠══════════════════════════════════════════════════════════╣'));
  102. console.log(
  103. chalk.redBright(' ♥ DONATE ') +
  104. chalk.cyan('║') +
  105. ' https://sayem.eu.org/donate ' +
  106. chalk.cyan('║')
  107. );
  108. console.log(chalk.cyan('╚══════════════════════════════════════════════════════════╝\n'));
  109. // console.log(chalk.yellow('Please read the latest manual thoroughly before asking for help!\n'));
  110. if (isCli) {
  111. try {
  112. let cliOptions = commandLineArgs(cliOptionDefinitions);
  113. for (let [key, value] of Object.entries(cliOptions)) {
  114. switch (key) {
  115. case 'url':
  116. defaults.DOWNLOAD_URL = value;
  117. break;
  118. case 'quality':
  119. defaults.DOWNLOAD_QUALITY = value;
  120. break;
  121. case 'path':
  122. defaults.DOWNLOAD_DIR = value;
  123. break;
  124. case 'downloadmode':
  125. defaults.DOWNLOAD_MODE = value;
  126. break;
  127. case 'concurrency':
  128. defaults.CONCURRENCY = value;
  129. break;
  130. case 'no-progress':
  131. global.bar = false;
  132. break;
  133. case 'set-arl':
  134. if (value) {
  135. config.set('cookies.arl', value);
  136. signale.info('arl cookie set to: ' + value);
  137. }
  138. process.exit();
  139. break;
  140. default:
  141. const helpSections = [
  142. {
  143. header: 'CLI Options',
  144. optionList: cliOptionDefinitions,
  145. },
  146. {
  147. content: 'More info here: https://notabug.org/sayem314/d-fi',
  148. },
  149. ];
  150. console.log(commandLineUsage(helpSections));
  151. process.exit();
  152. }
  153. }
  154. } catch (err) {
  155. log.debug(err.message || err);
  156. signale.fatal(err.message || err);
  157. process.exit(1);
  158. }
  159. }
  160. try {
  161. // init api
  162. signale.info('Initializing session..');
  163. await deezerApi.initDeezerApi(config.get('cookies.arl'));
  164. defaults.DOWNLOAD_DIR = nodePath.normalize(nodePath.resolve(defaults.DOWNLOAD_DIR.replace(/\/$|\\$/, '')));
  165. selectMusicQuality();
  166. } catch (err) {
  167. log.debug(err);
  168. process.exit(1);
  169. }
  170. };
  171. /**
  172. * Show user selection for the music download quality.
  173. */
  174. const selectMusicQuality = async () => {
  175. if (isCli) {
  176. switch (defaults.DOWNLOAD_QUALITY) {
  177. case '128':
  178. case 'MP3_128':
  179. musicQualities.selectedQuality = musicQualities.qualities.MP3_128;
  180. break;
  181. case '320':
  182. case 'MP3_320':
  183. musicQualities.selectedQuality = musicQualities.qualities.MP3_320;
  184. break;
  185. case 'flac':
  186. case 'Flac':
  187. case 'FLAC':
  188. musicQualities.selectedQuality = musicQualities.qualities.FLAC;
  189. break;
  190. default:
  191. musicQualities.selectedQuality = musicQualities.qualities.MP3_320;
  192. }
  193. if (defaults.DOWNLOAD_MODE == 'all') {
  194. downloadLinksFromFile();
  195. } else {
  196. try {
  197. await startDownload(defaults.DOWNLOAD_URL);
  198. process.exit();
  199. } catch (err) {
  200. log.debug(err.message);
  201. signale.fatal(err);
  202. downloadState.finish();
  203. process.exit(1);
  204. }
  205. }
  206. } else {
  207. let answers = await prompt(
  208. [
  209. {
  210. type: 'select',
  211. name: 'musicQuality',
  212. message: 'Select music quality:',
  213. choices: [{ title: 'MP3 - 128 kbps' }, { title: 'MP3 - 320 kbps' }, { title: 'FLAC - 1411 kbps' }],
  214. initial: 1,
  215. },
  216. ],
  217. {
  218. onCancel,
  219. }
  220. );
  221. switch (answers.musicQuality) {
  222. case 0:
  223. musicQualities.selectedQuality = musicQualities.qualities.MP3_128;
  224. break;
  225. case 2:
  226. musicQualities.selectedQuality = musicQualities.qualities.FLAC;
  227. break;
  228. default:
  229. musicQualities.selectedQuality = musicQualities.qualities.MP3_320;
  230. }
  231. log.debug('Selected music quality: ' + answers.musicQuality);
  232. selectDownloadMode();
  233. }
  234. };
  235. /**
  236. * Ask for download mode (single or all).
  237. */
  238. const selectDownloadMode = async () => {
  239. let answers = await prompt(
  240. [
  241. {
  242. type: 'select',
  243. name: 'downloadMode',
  244. message: 'Select download mode:',
  245. choices: [
  246. { title: 'Single (Download single link)' },
  247. { title: 'All (Download all links in "' + defaults.DOWNLOAD_LINKS_FILE + '")' },
  248. ],
  249. initial: 0,
  250. },
  251. ],
  252. {
  253. onCancel,
  254. }
  255. );
  256. if (answers.downloadMode === 1) {
  257. downloadLinksFromFile();
  258. } else {
  259. askForNewDownload();
  260. }
  261. };
  262. /**
  263. * Download all links from file
  264. */
  265. const downloadLinksFromFile = async () => {
  266. if (!fs.existsSync(defaults.DOWNLOAD_LINKS_FILE)) {
  267. ensureDir(defaults.DOWNLOAD_LINKS_FILE);
  268. fs.writeFileSync(defaults.DOWNLOAD_LINKS_FILE, '');
  269. }
  270. const lines = fs
  271. .readFileSync(defaults.DOWNLOAD_LINKS_FILE, 'utf-8')
  272. .split(/^(.*)[\r|\n]/)
  273. .filter(Boolean);
  274. if (lines[0]) {
  275. const firstLine = lines[0].trim();
  276. if ('' === firstLine) {
  277. removeFirstLineFromFile(defaults.DOWNLOAD_LINKS_FILE);
  278. await downloadLinksFromFile();
  279. } else {
  280. try {
  281. await startDownload(firstLine, defaults.DOWNLOAD_DIR, musicQualities.selectedQuality.id, true);
  282. removeFirstLineFromFile(defaults.DOWNLOAD_LINKS_FILE);
  283. await downloadLinksFromFile();
  284. } catch (err) {
  285. log.debug(err);
  286. signale.fatal(err.message || err);
  287. downloadState.finish(false);
  288. removeFirstLineFromFile(defaults.DOWNLOAD_LINKS_FILE);
  289. await downloadLinksFromFile();
  290. }
  291. }
  292. } else {
  293. signale.success('Finished downloading from text file');
  294. if (isCli) {
  295. process.exit();
  296. } else {
  297. console.log('\n');
  298. selectDownloadMode();
  299. }
  300. }
  301. };
  302. /**
  303. * Remove the first line from the given file.
  304. *
  305. * @param {String} filePath
  306. */
  307. const removeFirstLineFromFile = (filePath) => {
  308. const lines = fs
  309. .readFileSync(filePath, 'utf-8')
  310. .split(/^(.*)[\r|\n]/)
  311. .filter(Boolean);
  312. let contentToWrite = '';
  313. if (lines[1]) {
  314. contentToWrite = lines[1].trim();
  315. }
  316. fs.writeFileSync(filePath, contentToWrite);
  317. };
  318. /**
  319. * Ask for a album, playlist or track link to start the download.
  320. */
  321. const askForNewDownload = async () => {
  322. let questions = [
  323. {
  324. type: 'text',
  325. name: 'deezerUrl',
  326. message: 'Query:',
  327. },
  328. ];
  329. let answers = await prompt(questions, { onCancel });
  330. if (!answers.deezerUrl) {
  331. process.exit();
  332. }
  333. try {
  334. await startDownload(answers.deezerUrl);
  335. askForNewDownload();
  336. } catch (err) {
  337. log.debug(err.message || err);
  338. signale.fatal(err.message || err);
  339. downloadState.finish();
  340. askForNewDownload();
  341. }
  342. };
  343. /**
  344. * Start a deezer download.
  345. *
  346. * @param {String} deezerUrl
  347. * @param {Boolean} downloadFromFile
  348. */
  349. const startDownload = async (
  350. deezerUrl,
  351. path = defaults.DOWNLOAD_DIR,
  352. quality = musicQualities.selectedQuality.id,
  353. downloadFromFile = false
  354. ) => {
  355. log.debug('Started download task: ' + deezerUrl);
  356. try {
  357. let downloadType = await downloadTypes(deezerUrl);
  358. if (downloadType.type == 'unknown') {
  359. const { data } = await axios.get('https://api.deezer.com/search?q=' + encodeURIComponent(deezerUrl));
  360. const answer = await prompt(
  361. [
  362. {
  363. type: 'select',
  364. name: 'url',
  365. message: 'Select a song:',
  366. choices: data.data.map((item) => {
  367. item.description = `${item.artist.name} - ${item.link} - ${item.duration}s`;
  368. item.value = item.link;
  369. return item;
  370. }),
  371. initial: 0,
  372. },
  373. ],
  374. {
  375. onCancel,
  376. }
  377. );
  378. downloadType = await downloadTypes(answer.url);
  379. }
  380. downloadState.start(downloadType.type, downloadType.id);
  381. const value = await downloadMultiple(downloadType, path, quality);
  382. downloadState.finish(!downloadFromFile);
  383. } catch (err) {
  384. log.debug(err);
  385. signale.fatal(err.message || err);
  386. }
  387. };
  388. initApp();