d-fi.js 10 KB

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