d-fi.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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 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); // let the api initiate
  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. log.debug(err);
  132. process.exit(1);
  133. }
  134. };
  135. /**
  136. * Show user selection for the music download quality.
  137. */
  138. const selectMusicQuality = async () => {
  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 '320':
  146. case 'MP3_320':
  147. musicQualities.selectedQuality = musicQualities.qualities.MP3_320;
  148. break;
  149. case 'flac':
  150. case 'Flac':
  151. case 'FLAC':
  152. musicQualities.selectedQuality = musicQualities.qualities.FLAC;
  153. break;
  154. default:
  155. musicQualities.selectedQuality = musicQualities.qualities.MP3_320;
  156. }
  157. if (defaults.DOWNLOAD_MODE == 'all') {
  158. downloadLinksFromFile();
  159. } else {
  160. try {
  161. await startDownload(defaults.DOWNLOAD_URL);
  162. process.exit();
  163. } catch (err) {
  164. log.debug(err.message);
  165. signale.fatal(err);
  166. downloadState.finish();
  167. process.exit(1);
  168. }
  169. }
  170. } else {
  171. let answers = await prompt(
  172. [
  173. {
  174. type: 'select',
  175. name: 'musicQuality',
  176. message: 'Select music quality:',
  177. choices: [{ title: 'MP3 - 128 kbps' }, { title: 'MP3 - 320 kbps' }, { title: 'FLAC - 1411 kbps' }],
  178. initial: 1,
  179. },
  180. ],
  181. {
  182. onCancel,
  183. }
  184. );
  185. switch (answers.musicQuality) {
  186. case 0:
  187. musicQualities.selectedQuality = musicQualities.qualities.MP3_128;
  188. break;
  189. case 2:
  190. musicQualities.selectedQuality = musicQualities.qualities.FLAC;
  191. break;
  192. default:
  193. musicQualities.selectedQuality = musicQualities.qualities.MP3_320;
  194. }
  195. log.debug('Selected music quality: ' + answers.musicQuality);
  196. selectDownloadMode();
  197. }
  198. };
  199. /**
  200. * Ask for download mode (single or all).
  201. */
  202. const selectDownloadMode = async () => {
  203. let answers = await prompt(
  204. [
  205. {
  206. type: 'select',
  207. name: 'downloadMode',
  208. message: 'Select download mode:',
  209. choices: [
  210. { title: 'Single (Download single link)' },
  211. { title: 'All (Download all links in "' + defaults.DOWNLOAD_LINKS_FILE + '")' },
  212. ],
  213. initial: 0,
  214. },
  215. ],
  216. {
  217. onCancel,
  218. }
  219. );
  220. if (answers.downloadMode === 1) {
  221. downloadLinksFromFile();
  222. } else {
  223. askForNewDownload();
  224. }
  225. };
  226. /**
  227. * Download all links from file
  228. */
  229. const downloadLinksFromFile = async () => {
  230. if (!fs.existsSync(defaults.DOWNLOAD_LINKS_FILE)) {
  231. ensureDir(defaults.DOWNLOAD_LINKS_FILE);
  232. fs.writeFileSync(defaults.DOWNLOAD_LINKS_FILE, '');
  233. }
  234. const lines = fs
  235. .readFileSync(defaults.DOWNLOAD_LINKS_FILE, 'utf-8')
  236. .split(/^(.*)[\r|\n]/)
  237. .filter(Boolean);
  238. if (lines[0]) {
  239. const firstLine = lines[0].trim();
  240. if ('' === firstLine) {
  241. removeFirstLineFromFile(defaults.DOWNLOAD_LINKS_FILE);
  242. await downloadLinksFromFile();
  243. } else {
  244. try {
  245. await startDownload(firstLine, defaults.DOWNLOAD_DIR, musicQualities.selectedQuality.id, true);
  246. removeFirstLineFromFile(defaults.DOWNLOAD_LINKS_FILE);
  247. await downloadLinksFromFile();
  248. } catch (err) {
  249. log.debug(err);
  250. signale.fatal(err.message || err);
  251. downloadState.finish(false);
  252. removeFirstLineFromFile(defaults.DOWNLOAD_LINKS_FILE);
  253. await downloadLinksFromFile();
  254. }
  255. }
  256. } else {
  257. signale.success('Finished downloading from text file');
  258. if (isCli) {
  259. process.exit();
  260. } else {
  261. console.log('\n');
  262. selectDownloadMode();
  263. }
  264. }
  265. };
  266. /**
  267. * Remove the first line from the given file.
  268. *
  269. * @param {String} filePath
  270. */
  271. const removeFirstLineFromFile = (filePath) => {
  272. const lines = fs
  273. .readFileSync(filePath, 'utf-8')
  274. .split(/^(.*)[\r|\n]/)
  275. .filter(Boolean);
  276. let contentToWrite = '';
  277. if (lines[1]) {
  278. contentToWrite = lines[1].trim();
  279. }
  280. fs.writeFileSync(filePath, contentToWrite);
  281. };
  282. /**
  283. * Ask for a album, playlist or track link to start the download.
  284. */
  285. const askForNewDownload = async () => {
  286. let questions = [
  287. {
  288. type: 'text',
  289. name: 'deezerUrl',
  290. message: 'Query:',
  291. },
  292. ];
  293. let answers = await prompt(questions, { onCancel });
  294. if (!answers.deezerUrl) {
  295. process.exit();
  296. }
  297. try {
  298. await startDownload(answers.deezerUrl);
  299. askForNewDownload();
  300. } catch (err) {
  301. log.debug(err.message || err);
  302. signale.fatal(err.message || err);
  303. downloadState.finish();
  304. askForNewDownload();
  305. }
  306. };
  307. /**
  308. * Start a deezer download.
  309. *
  310. * @param {String} deezerUrl
  311. * @param {Boolean} downloadFromFile
  312. */
  313. const startDownload = async (
  314. deezerUrl,
  315. path = defaults.DOWNLOAD_DIR,
  316. quality = musicQualities.selectedQuality.id,
  317. downloadFromFile = false
  318. ) => {
  319. log.debug('Started download task: ' + deezerUrl);
  320. try {
  321. let downloadType = await downloadTypes(deezerUrl);
  322. if (downloadType.type == 'unknown') {
  323. const { data } = await axios.get('https://api.deezer.com/search?q=' + encodeURIComponent(deezerUrl));
  324. const answer = await prompt(
  325. [
  326. {
  327. type: 'select',
  328. name: 'url',
  329. message: 'Select a song:',
  330. choices: data.data.map((item) => {
  331. item.description = `${item.artist.name} - ${item.link} - ${item.duration}s`;
  332. item.value = item.link;
  333. return item;
  334. }),
  335. initial: 0,
  336. },
  337. ],
  338. {
  339. onCancel,
  340. }
  341. );
  342. downloadType = await downloadTypes(answer.url);
  343. }
  344. downloadState.start(downloadType.type, downloadType.id);
  345. const value = await downloadMultiple(downloadType, path, quality);
  346. downloadState.finish(!downloadFromFile);
  347. } catch (err) {
  348. log.debug(err);
  349. signale.fatal(err.message || err);
  350. }
  351. };
  352. initApp();