grab-url 0.9.142 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/grab-api.cjs.js +1 -1
  2. package/dist/grab-api.cjs.js.map +1 -1
  3. package/dist/grab-api.d.ts +5 -5
  4. package/dist/grab-api.es.js.map +1 -1
  5. package/dist/icons.cjs.js +1 -1
  6. package/dist/icons.cjs.js.map +1 -1
  7. package/dist/icons.d.ts +0 -234
  8. package/dist/icons.es.js +1 -113
  9. package/dist/icons.es.js.map +1 -1
  10. package/package.json +12 -13
  11. package/readme.md +12 -18
  12. package/src/grab-api.ts +5 -5
  13. package/src/{grab-url-cli.js → grab-url.ts} +380 -354
  14. package/src/{spinners.json → icons/cli/spinners.json} +1 -482
  15. package/src/icons/index.ts +0 -255
  16. package/src/icons/svg/index.ts +313 -0
  17. package/src/grab-cli.js +0 -316
  18. /package/src/icons/{loading-bouncy-ball.svg → svg/loading-bouncy-ball.svg} +0 -0
  19. /package/src/icons/{loading-double-ring.svg → svg/loading-double-ring.svg} +0 -0
  20. /package/src/icons/{loading-eclipse.svg → svg/loading-eclipse.svg} +0 -0
  21. /package/src/icons/{loading-ellipsis.svg → svg/loading-ellipsis.svg} +0 -0
  22. /package/src/icons/{loading-floating-search.svg → svg/loading-floating-search.svg} +0 -0
  23. /package/src/icons/{loading-gears.svg → svg/loading-gears.svg} +0 -0
  24. /package/src/icons/{loading-infinity.svg → svg/loading-infinity.svg} +0 -0
  25. /package/src/icons/{loading-orbital.svg → svg/loading-orbital.svg} +0 -0
  26. /package/src/icons/{loading-pacman.svg → svg/loading-pacman.svg} +0 -0
  27. /package/src/icons/{loading-pulse-bars.svg → svg/loading-pulse-bars.svg} +0 -0
  28. /package/src/icons/{loading-red-blue-ball.svg → svg/loading-red-blue-ball.svg} +0 -0
  29. /package/src/icons/{loading-reload-arrow.svg → svg/loading-reload-arrow.svg} +0 -0
  30. /package/src/icons/{loading-ring.svg → svg/loading-ring.svg} +0 -0
  31. /package/src/icons/{loading-ripple.svg → svg/loading-ripple.svg} +0 -0
  32. /package/src/icons/{loading-spinner-oval.svg → svg/loading-spinner-oval.svg} +0 -0
  33. /package/src/icons/{loading-spinner.svg → svg/loading-spinner.svg} +0 -0
  34. /package/src/icons/{loading-square-blocks.svg → svg/loading-square-blocks.svg} +0 -0
@@ -7,6 +7,9 @@ import { Readable } from 'stream';
7
7
  import cliProgress from 'cli-progress';
8
8
  import chalk from 'chalk';
9
9
  import ora from 'ora';
10
+ import Table from 'cli-table3';
11
+ import grab, { log } from './grab-api.ts';
12
+ import readline from 'readline';
10
13
  import { readFileSync } from 'fs';
11
14
  import { fileURLToPath } from 'url';
12
15
  import { dirname, join } from 'path';
@@ -17,35 +20,151 @@ const __dirname = dirname(__filename);
17
20
 
18
21
  let spinners;
19
22
  try {
20
- // Try the local path first
21
- spinners = JSON.parse(readFileSync(join(__dirname, 'spinners.json'), 'utf8'));
23
+ // Try the icons/cli path first (where it actually exists)
24
+ spinners = JSON.parse(readFileSync(join(__dirname, 'icons', 'cli', 'spinners.json'), 'utf8'));
22
25
  } catch (error) {
23
26
  try {
24
- // Try the parent directory (src)
25
- spinners = JSON.parse(readFileSync(join(__dirname, '..', 'spinners.json'), 'utf8'));
27
+ // Try the local path
28
+ spinners = JSON.parse(readFileSync(join(__dirname, 'spinners.json'), 'utf8'));
26
29
  } catch (error2) {
27
30
  try {
28
- // Try the current working directory
29
- spinners = JSON.parse(readFileSync(join(process.cwd(), 'src', 'spinners.json'), 'utf8'));
31
+ // Try the parent directory (src)
32
+ spinners = JSON.parse(readFileSync(join(__dirname, '..', 'spinners.json'), 'utf8'));
30
33
  } catch (error3) {
31
- // Fallback to default spinners if file not found
32
- console.warn('Could not load spinners.json, using defaults');
33
- spinners = {
34
- dots: { frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] },
35
- line: { frames: ['-', '\\', '|', '/'] },
36
- arrow: { frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'] }
37
- };
34
+ try {
35
+ // Try the current working directory
36
+ spinners = JSON.parse(readFileSync(join(process.cwd(), 'src', 'spinners.json'), 'utf8'));
37
+ } catch (error4) {
38
+ // Fallback to default spinners if file not found
39
+ console.warn('Could not load spinners.json, using defaults');
40
+ spinners = {
41
+ dots: { frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] },
42
+ line: { frames: ['-', '\\', '|', '/'] },
43
+ arrow: { frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'] }
44
+ };
45
+ }
38
46
  }
39
47
  }
40
48
  }
41
- import readline from 'readline';
42
- import { GlobalKeyboardListener } from 'node-global-key-listener';
43
-
44
-
45
49
 
50
+ // --- ArgParser from grab-cli.js ---
51
+ class ArgParser {
52
+ constructor() {
53
+ this.commands = {};
54
+ this.options = {};
55
+ this.examples = [];
56
+ this.helpText = '';
57
+ this.versionText = '1.0.0';
58
+ }
59
+ usage(text) { this.helpText = text; return this; }
60
+ command(pattern, desc, handler) {
61
+ const match = pattern.match(/\$0 <(\w+)>/);
62
+ if (match) this.commands[match[1]] = { desc, handler, required: true };
63
+ return this;
64
+ }
65
+ option(name, opts = {}) { this.options[name] = opts; return this; }
66
+ example(cmd, desc) { this.examples.push({ cmd, desc }); return this; }
67
+ help() { return this; }
68
+ alias(short, long) { if (this.options[long]) this.options[long].alias = short; return this; }
69
+ version(v) { if (v) this.versionText = v; return this; }
70
+ strict() { return this; }
71
+ parseSync() {
72
+ const args = process.argv.slice(2);
73
+ const result = {};
74
+ const positional = [];
75
+ if (args.includes('--help') || args.includes('-h')) { this.showHelp(); process.exit(0); }
76
+ if (args.includes('--version')) { console.log(this.versionText); process.exit(0); }
77
+ for (let i = 0; i < args.length; i++) {
78
+ const arg = args[i];
79
+ if (arg.startsWith('--')) {
80
+ const [key, value] = arg.split('=');
81
+ const optName = key.slice(2);
82
+ if (value !== undefined) {
83
+ result[optName] = this.coerceValue(optName, value);
84
+ } else if (this.options[optName]?.type === 'boolean') {
85
+ result[optName] = true;
86
+ } else {
87
+ const nextArg = args[i + 1];
88
+ if (nextArg && !nextArg.startsWith('-')) {
89
+ result[optName] = this.coerceValue(optName, nextArg);
90
+ i++;
91
+ } else {
92
+ result[optName] = true;
93
+ }
94
+ }
95
+ } else if (arg.startsWith('-') && arg.length === 2) {
96
+ const shortFlag = arg[1];
97
+ const longName = this.findLongName(shortFlag);
98
+ if (longName) {
99
+ if (this.options[longName]?.type === 'boolean') {
100
+ result[longName] = true;
101
+ } else {
102
+ const nextArg = args[i + 1];
103
+ if (nextArg && !nextArg.startsWith('-')) {
104
+ result[longName] = this.coerceValue(longName, nextArg);
105
+ i++;
106
+ }
107
+ }
108
+ }
109
+ } else {
110
+ positional.push(arg);
111
+ }
112
+ }
113
+ if (positional.length > 0) result.urls = positional;
114
+ Object.keys(this.options).forEach(key => {
115
+ if (result[key] === undefined && this.options[key].default !== undefined) {
116
+ result[key] = this.options[key].default;
117
+ }
118
+ });
119
+ if ((!result.urls || result.urls.length === 0) && this.commands.url?.required) {
120
+ console.error('Error: Missing required argument: url');
121
+ this.showHelp();
122
+ process.exit(1);
123
+ }
124
+ return result;
125
+ }
126
+ coerceValue(optName, value) {
127
+ const opt = this.options[optName];
128
+ if (!opt) return value;
129
+ if (opt.coerce) return opt.coerce(value);
130
+ switch (opt.type) {
131
+ case 'number': return Number(value);
132
+ case 'boolean': return value === 'true' || value === '1';
133
+ default: return value;
134
+ }
135
+ }
136
+ findLongName(shortFlag) {
137
+ return Object.keys(this.options).find(key => this.options[key].alias === shortFlag);
138
+ }
139
+ showHelp() {
140
+ console.log(this.helpText || 'Usage: grab <url> [options]');
141
+ console.log('\nPositional arguments:');
142
+ Object.keys(this.commands).forEach(cmd => {
143
+ console.log(` ${cmd.padEnd(20)} ${this.commands[cmd].desc}`);
144
+ });
145
+ console.log('\nOptions:');
146
+ Object.keys(this.options).forEach(key => {
147
+ const opt = this.options[key];
148
+ const flags = opt.alias ? `-${opt.alias}, --${key}` : `--${key}`;
149
+ console.log(` ${flags.padEnd(20)} ${opt.describe || ''}`);
150
+ });
151
+ if (this.examples.length > 0) {
152
+ console.log('\nExamples:');
153
+ this.examples.forEach(ex => {
154
+ console.log(` ${ex.cmd}`);
155
+ console.log(` ${ex.desc}`);
156
+ });
157
+ }
158
+ }
159
+ }
46
160
 
161
+ // --- Helper: Detect if a URL is a file download ---
162
+ function isFileUrl(url) {
163
+ // Heuristic: ends with a file extension (e.g., .zip, .mp4, .tar.gz, .pdf, etc)
164
+ return /\.[a-zA-Z0-9]{1,5}(?:\.[a-zA-Z0-9]{1,5})*$/.test(url.split('?')[0]);
165
+ }
47
166
 
48
- class ColorfulFileDownloader {
167
+ export class ColorFileDownloader {
49
168
  constructor() {
50
169
  this.progressBar = null;
51
170
  this.multiBar = null;
@@ -109,7 +228,7 @@ constructor() {
109
228
  ];
110
229
 
111
230
  // Available spinner types for random selection (from spinners.json)
112
- this.spinnerTypes = Object.keys(spinners);
231
+ this.spinnerTypes = Object.keys(spinners.default || spinners);
113
232
 
114
233
  // Initialize state directory
115
234
  this.stateDir = this.getStateDirectory();
@@ -141,7 +260,7 @@ ensureStateDirectoryExists() {
141
260
  fs.mkdirSync(this.stateDir, { recursive: true });
142
261
  }
143
262
  } catch (error) {
144
- console.log(this.colors.warning('Could not create state directory, using current directory'));
263
+ console.log(this.colors.warning('⚠️ Could not create state directory, using current directory'));
145
264
  this.stateDir = process.cwd();
146
265
  }
147
266
  }
@@ -166,7 +285,7 @@ cleanupStateFile(stateFilePath) {
166
285
  fs.unlinkSync(stateFilePath);
167
286
  }
168
287
  } catch (error) {
169
- console.log(this.colors.warning('Could not clean up state file'));
288
+ console.log(this.colors.warning('⚠️ Could not clean up state file'));
170
289
  }
171
290
  }
172
291
 
@@ -175,15 +294,15 @@ cleanupStateFile(stateFilePath) {
175
294
  */
176
295
  printHeaderRow() {
177
296
  console.log(
178
- this.colors.success('%'.padEnd(this.COL_PERCENT)) +
179
- this.colors.yellow('File'.padEnd(this.COL_FILENAME)) +
180
- this.colors.cyan(''.padEnd(this.COL_SPINNER)) +
297
+ this.colors.success('📈 %'.padEnd(this.COL_PERCENT)) +
298
+ this.colors.yellow('📁 Files'.padEnd(this.COL_FILENAME)) +
299
+ this.colors.cyan('🔄'.padEnd(this.COL_SPINNER)) +
181
300
  ' ' +
182
- this.colors.green('Progress'.padEnd(this.COL_BAR + 1)) +
183
- this.colors.info('Downloaded'.padEnd(this.COL_DOWNLOADED)) +
184
- this.colors.info('Total'.padEnd(this.COL_TOTAL)) +
185
- this.colors.purple('Speed'.padEnd(this.COL_SPEED)) +
186
- this.colors.pink('ETA'.padEnd(this.COL_ETA))
301
+ this.colors.green('📊 Progress'.padEnd(this.COL_BAR + 1)) +
302
+ this.colors.info('📥 Downloaded'.padEnd(this.COL_DOWNLOADED)) +
303
+ this.colors.info('📦 Total'.padEnd(this.COL_TOTAL)) +
304
+ this.colors.purple('Speed'.padEnd(this.COL_SPEED)) +
305
+ this.colors.pink('⏱️ ETA'.padEnd(this.COL_ETA))
187
306
  );
188
307
  }
189
308
 
@@ -224,14 +343,15 @@ getRandomSpinner() {
224
343
  * @returns {array} Array of spinner frame characters
225
344
  */
226
345
  getSpinnerFrames(spinnerType) {
227
- const spinner = spinners[spinnerType];
346
+ const spinnerData = spinners.default || spinners;
347
+ const spinner = spinnerData[spinnerType];
228
348
 
229
349
  if (spinner && spinner.frames) {
230
350
  return spinner.frames;
231
351
  }
232
352
 
233
353
  // Fallback to dots if spinner not found
234
- return spinners.dots?.frames || ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
354
+ return spinnerData.dots?.frames || ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
235
355
  }
236
356
 
237
357
  /**
@@ -291,17 +411,11 @@ calculateBarSize(spinnerFrame, baseBarSize = 20) {
291
411
  */
292
412
  async checkServerSupport(url) {
293
413
  try {
294
- // Create a timeout controller
295
- const timeoutController = new AbortController();
296
- const timeoutId = setTimeout(() => timeoutController.abort(), 3000); // 3 second timeout
297
-
298
414
  const response = await fetch(url, {
299
415
  method: 'HEAD',
300
- signal: timeoutController.signal
416
+ signal: this.abortController?.signal
301
417
  });
302
418
 
303
- clearTimeout(timeoutId);
304
-
305
419
  if (!response.ok) {
306
420
  throw new Error(`HTTP error! status: ${response.status}`);
307
421
  }
@@ -319,11 +433,7 @@ async checkServerSupport(url) {
319
433
  headers: response.headers
320
434
  };
321
435
  } catch (error) {
322
- if (error.name === 'AbortError') {
323
- console.log(this.colors.warning('Server check timed out, proceeding with regular download'));
324
- } else {
325
- console.log(this.colors.warning('Could not check server resume support, proceeding with regular download'));
326
- }
436
+ console.log(this.colors.warning('⚠️ Could not check server resume support, proceeding with regular download'));
327
437
  return {
328
438
  supportsResume: false,
329
439
  totalSize: 0,
@@ -346,7 +456,7 @@ loadDownloadState(stateFilePath) {
346
456
  return JSON.parse(stateData);
347
457
  }
348
458
  } catch (error) {
349
- console.log(this.colors.warning('Could not load download state, starting fresh'));
459
+ console.log(this.colors.warning('⚠️ Could not load download state, starting fresh'));
350
460
  }
351
461
  return null;
352
462
  }
@@ -360,7 +470,7 @@ saveDownloadState(stateFilePath, state) {
360
470
  try {
361
471
  fs.writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
362
472
  } catch (error) {
363
- console.log(this.colors.warning('Could not save download state'));
473
+ console.log(this.colors.warning('⚠️ Could not save download state'));
364
474
  }
365
475
  }
366
476
 
@@ -376,7 +486,7 @@ getPartialFileSize(filePath) {
376
486
  return stats.size;
377
487
  }
378
488
  } catch (error) {
379
- console.log(this.colors.warning('Could not read partial file size'));
489
+ console.log(this.colors.warning('⚠️ Could not read partial file size'));
380
490
  }
381
491
  return 0;
382
492
  }
@@ -401,11 +511,11 @@ formatBytes(bytes, decimals = 2) {
401
511
  const dm = decimals < 0 ? 0 : decimals;
402
512
  const sizes = [
403
513
  { unit: 'B', color: this.colors.info },
404
- { unit: 'K', color: this.colors.cyan },
405
- { unit: 'M', color: this.colors.yellow },
406
- { unit: 'G', color: this.colors.purple },
407
- { unit: 'T', color: this.colors.pink },
408
- { unit: 'P', color: this.colors.primary }
514
+ { unit: 'KB', color: this.colors.cyan },
515
+ { unit: 'MB', color: this.colors.yellow },
516
+ { unit: 'GB', color: this.colors.purple },
517
+ { unit: 'TB', color: this.colors.pink },
518
+ { unit: 'PB', color: this.colors.primary }
409
519
  ];
410
520
 
411
521
  const i = Math.floor(Math.log(bytes) / Math.log(k));
@@ -631,16 +741,16 @@ formatTotalSpeed(bytesPerSecond) {
631
741
  */
632
742
  async downloadMultipleFiles(downloads) {
633
743
  try {
634
- console.log(this.colors.primary(`Starting ${downloads.length} downloads...\n`));
744
+ console.log(this.colors.primary(`🚀 Starting download of ${downloads.length} files...\n`));
635
745
 
636
746
  // Set up global keyboard listener for pause/resume and add URL BEFORE starting downloads
637
747
  this.setupGlobalKeyboardListener();
638
748
 
639
- // Print header row
749
+ // Print header row with emojis
640
750
  this.printHeaderRow();
641
751
 
642
- // Show keyboard shortcuts
643
- console.log(this.colors.info('Press p to pause/resume, a to add URL'));
752
+ // Show keyboard shortcut info for pause/resume in multibar view
753
+ console.log(this.colors.info('💡 Press p to pause/resume downloads, a to add URL.'));
644
754
 
645
755
  // Get random colors for the multibar
646
756
  const masterBarColor = this.getRandomBarColor();
@@ -701,11 +811,10 @@ async downloadMultipleFiles(downloads) {
701
811
  individualSpeeds[i] = incrementalDownloaded / timeSinceLastUpdate;
702
812
 
703
813
  if (fileBars[i] && fileBars[i].bar) {
704
- const speed = this.isPaused ? this.formatSpeed('0B') : this.formatSpeed(this.formatSpeedDisplay(individualSpeeds[i]));
705
- const eta = this.isPaused ? '⏸️ [P]AUSED' :
706
- (individualSizes[i] > 0 ?
707
- this.formatETA((individualSizes[i] - individualDownloaded[i]) / individualSpeeds[i]) :
708
- this.formatETA(0));
814
+ const speed = this.formatSpeed(this.formatSpeedDisplay(individualSpeeds[i]));
815
+ const eta = individualSizes[i] > 0 ?
816
+ this.formatETA((individualSizes[i] - individualDownloaded[i]) / individualSpeeds[i]) :
817
+ this.formatETA(0);
709
818
 
710
819
  fileBars[i].bar.update(individualDownloaded[i], {
711
820
  speed: speed,
@@ -722,14 +831,14 @@ async downloadMultipleFiles(downloads) {
722
831
  lastSpeedUpdate = now;
723
832
  lastIndividualDownloaded = [...individualDownloaded];
724
833
 
725
- // Calculate total speed (0 when paused)
726
- const totalSpeedBps = this.isPaused ? 0 : individualSpeeds.reduce((sum, speed) => sum + speed, 0);
834
+ // Calculate total speed
835
+ const totalSpeedBps = individualSpeeds.reduce((sum, speed) => sum + speed, 0);
727
836
 
728
837
  // Calculate total downloaded from individual files
729
838
  const totalDownloadedFromFiles = individualDownloaded.reduce((sum, downloaded) => sum + downloaded, 0);
730
839
 
731
- // Calculate time elapsed since start (only when not paused)
732
- const timeElapsed = this.isPaused ? 0 : (now - individualStartTimes[0]) / 1000; // seconds since first download started
840
+ // Calculate time elapsed since start
841
+ const timeElapsed = (now - individualStartTimes[0]) / 1000; // seconds since first download started
733
842
 
734
843
  // Update master bar
735
844
  const totalEta = totalSize > 0 && totalSpeedBps > 0 ?
@@ -743,15 +852,12 @@ async downloadMultipleFiles(downloads) {
743
852
  const discoveredTotalSize = individualSizes.reduce((sum, size) => sum + size, 0);
744
853
  const displayTotalSize = discoveredTotalSize > 0 ? discoveredTotalSize : totalSize;
745
854
 
746
- // Show paused status or time elapsed
747
- const etaDisplay = this.isPaused ? '⏸️ [P]AUSED' : this.formatETA(timeElapsed);
748
-
749
855
  masterBar.update(totalDownloadedFromFiles, {
750
856
  speed: this.formatTotalSpeed(totalSpeedBps),
751
857
  progress: this.formatMasterProgress(totalDownloadedFromFiles, displayTotalSize),
752
858
  downloadedDisplay: this.formatBytesCompact(totalDownloadedFromFiles),
753
859
  totalDisplay: this.formatTotalDisplay(displayTotalSize),
754
- etaFormatted: etaDisplay,
860
+ etaFormatted: this.formatETA(timeElapsed), // Show time elapsed instead of ETA
755
861
  percentage: displayTotalSize > 0 ?
756
862
  Math.round((totalDownloadedFromFiles / displayTotalSize) * 100) : 0
757
863
  });
@@ -813,10 +919,10 @@ async downloadMultipleFiles(downloads) {
813
919
  etaFormatted: this.formatETA(0),
814
920
  percentage: ' 0'.padStart(3)
815
921
  }, {
816
- format: this.colors.success('{percentage}%') + ' ' +
817
- this.colors.yellow('{filename}') + ' ' +
922
+ format: this.colors.yellow('{filename}') + ' ' +
818
923
  this.colors.cyan('{spinner}') + ' ' +
819
924
  fileBarColor + '{bar}\u001b[0m' + ' ' +
925
+ this.colors.success('{percentage}%') + ' ' +
820
926
  this.colors.info('{downloadedDisplay}') + ' ' +
821
927
  this.colors.info('{totalDisplay}') + ' ' +
822
928
  this.colors.purple('{speed}') + ' ' +
@@ -867,20 +973,23 @@ async downloadMultipleFiles(downloads) {
867
973
  const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
868
974
  const failed = results.length - successful;
869
975
 
870
- console.log(this.colors.green(`Success: ${successful}/${downloads.length}`));
976
+ console.log(this.colors.green(`✅ Successful: ${successful}/${downloads.length}`));
871
977
  if (failed > 0) {
872
- console.log(this.colors.error(`Failed: ${failed}/${downloads.length}`));
978
+ console.log(this.colors.error(`❌ Failed: ${failed}/${downloads.length}`));
873
979
 
874
980
  results.forEach((result, index) => {
875
981
  if (result.status === 'rejected' || !result.value.success) {
876
982
  const filename = downloads[index].filename;
877
983
  const error = result.reason || result.value?.error || 'Unknown error';
878
- console.log(this.colors.error(` ${filename}: ${error.message || error}`));
984
+ console.log(this.colors.error(` ${filename}: ${error.message || error}`));
879
985
  }
880
986
  });
881
987
  }
882
988
 
883
- console.log(this.colors.success('Download completed'));
989
+ // Random celebration emoji
990
+ const celebrationEmojis = ['🥳', '🎊', '🎈', '🌟', '💯', '🚀', '✨', '🔥'];
991
+ const randomEmoji = celebrationEmojis[Math.floor(Math.random() * celebrationEmojis.length)];
992
+ console.log(this.colors.success(`${randomEmoji} Batch download completed! ${randomEmoji}`));
884
993
 
885
994
  this.clearAbortControllers();
886
995
 
@@ -889,14 +998,14 @@ async downloadMultipleFiles(downloads) {
889
998
  this.setPauseCallback(() => {
890
999
  if (!pausedMessageShown) {
891
1000
  this.multiBar.stop();
892
- console.log(this.colors.warning('Paused. Press p to resume, a to add URL'));
1001
+ console.log(this.colors.warning('⏸️ Paused. Press p to resume, a to add URL.'));
893
1002
  pausedMessageShown = true;
894
1003
  }
895
1004
  });
896
1005
 
897
1006
  this.setResumeCallback(() => {
898
1007
  if (pausedMessageShown) {
899
- console.log(this.colors.success('Resumed. Press p to pause, a to add URL'));
1008
+ console.log(this.colors.success('▶️ Resumed. Press p to pause, a to add URL.'));
900
1009
  pausedMessageShown = false;
901
1010
  }
902
1011
  });
@@ -970,20 +1079,12 @@ async downloadSingleFileWithBar(fileBar, masterBar, totalFiles, totalTracking) {
970
1079
  headers['Range'] = `bytes=${startByte}-`;
971
1080
  }
972
1081
 
973
- // Make the fetch request with timeout
974
- const timeoutController = new AbortController();
975
- const timeoutId = setTimeout(() => timeoutController.abort(), 30000); // 30 second timeout
976
-
977
- // Combine abort and timeout signals
978
- const combinedSignal = abortController.signal;
979
-
1082
+ // Make the fetch request
980
1083
  const response = await fetch(url, {
981
1084
  headers,
982
- signal: combinedSignal
1085
+ signal: abortController.signal
983
1086
  });
984
1087
 
985
- clearTimeout(timeoutId);
986
-
987
1088
  if (!response.ok) {
988
1089
  throw new Error(`HTTP error! status: ${response.status}`);
989
1090
  }
@@ -1140,7 +1241,7 @@ async downloadSingleFileWithBar(fileBar, masterBar, totalFiles, totalTracking) {
1140
1241
  progress: this.formatMasterProgress(totalTracking.totalDownloaded, displayTotalSize),
1141
1242
  downloadedDisplay: this.formatBytesCompact(totalTracking.totalDownloaded),
1142
1243
  totalDisplay: this.formatTotalDisplay(displayTotalSize),
1143
- etaFormatted: this.formatETA((Date.now() - individualStartTimes[0]) / 1000) // Show time elapsed
1244
+ etaFormatted: this.formatETA((Date.now() - (totalTracking.individualStartTimes?.[0] || Date.now())) / 1000) // Show time elapsed
1144
1245
  });
1145
1246
 
1146
1247
  } catch (error) {
@@ -1153,7 +1254,7 @@ async downloadSingleFileWithBar(fileBar, masterBar, totalFiles, totalTracking) {
1153
1254
  });
1154
1255
 
1155
1256
  // Don't clean up partial file on error - allow resume
1156
- console.log(this.colors.info(`Partial download saved for ${filename}. Restart to resume.`));
1257
+ console.log(this.colors.info(`💾 Partial download saved for ${filename}. Restart to resume.`));
1157
1258
  throw error;
1158
1259
  }
1159
1260
  }
@@ -1174,7 +1275,7 @@ async downloadFile(url, outputPath) {
1174
1275
  // Start with a random ora spinner animation
1175
1276
  const randomOraSpinner = this.getRandomOraSpinner();
1176
1277
  this.loadingSpinner = ora({
1177
- text: this.colors.primary('Checking server capabilities...'),
1278
+ text: this.colors.primary('🌐 Checking server capabilities...'),
1178
1279
  spinner: randomOraSpinner,
1179
1280
  color: 'cyan'
1180
1281
  }).start();
@@ -1201,10 +1302,10 @@ async downloadFile(url, outputPath) {
1201
1302
  if (fileUnchanged && partialSize < serverInfo.totalSize) {
1202
1303
  startByte = partialSize;
1203
1304
  resuming = true;
1204
- this.loadingSpinner.succeed(this.colors.success(`Found partial download: ${this.formatBytes(partialSize)} of ${this.formatTotal(serverInfo.totalSize)}`));
1205
- console.log(this.colors.info(`Resuming download from ${this.formatBytes(startByte)}`));
1305
+ this.loadingSpinner.succeed(this.colors.success(`✅ Found partial download: ${this.formatBytes(partialSize)} of ${this.formatTotal(serverInfo.totalSize)}`));
1306
+ console.log(this.colors.info(`🔄 Resuming download from ${this.formatBytes(startByte)}`));
1206
1307
  } else {
1207
- this.loadingSpinner.warn(this.colors.warning('File changed on server, starting fresh download'));
1308
+ this.loadingSpinner.warn(this.colors.warning('⚠️ File changed on server, starting fresh download'));
1208
1309
  // Clean up partial file and state
1209
1310
  if (fs.existsSync(tempFilePath)) {
1210
1311
  fs.unlinkSync(tempFilePath);
@@ -1214,7 +1315,7 @@ async downloadFile(url, outputPath) {
1214
1315
  } else {
1215
1316
  this.loadingSpinner.stop();
1216
1317
  if (partialSize > 0) {
1217
- console.log(this.colors.warning('Server does not support resumable downloads, starting fresh'));
1318
+ console.log(this.colors.warning('⚠️ Server does not support resumable downloads, starting fresh'));
1218
1319
  // Clean up partial file
1219
1320
  if (fs.existsSync(tempFilePath)) {
1220
1321
  fs.unlinkSync(tempFilePath);
@@ -1228,17 +1329,12 @@ async downloadFile(url, outputPath) {
1228
1329
  headers['Range'] = `bytes=${startByte}-`;
1229
1330
  }
1230
1331
 
1231
- // Make the fetch request with timeout
1232
- const timeoutController = new AbortController();
1233
- const timeoutId = setTimeout(() => timeoutController.abort(), 30000); // 30 second timeout
1234
-
1332
+ // Make the fetch request
1235
1333
  const response = await fetch(url, {
1236
1334
  headers,
1237
1335
  signal: this.abortController.signal
1238
1336
  });
1239
1337
 
1240
- clearTimeout(timeoutId);
1241
-
1242
1338
  if (!response.ok) {
1243
1339
  throw new Error(`HTTP error! status: ${response.status}`);
1244
1340
  }
@@ -1250,9 +1346,9 @@ async downloadFile(url, outputPath) {
1250
1346
 
1251
1347
  if (!resuming) {
1252
1348
  if (totalSize === 0) {
1253
- console.log(this.colors.warning('Warning: Content-Length not provided, progress will be estimated'));
1349
+ console.log(this.colors.warning('⚠️ Warning: Content-Length not provided, progress will be estimated'));
1254
1350
  } else {
1255
- console.log(this.colors.info(`File size: ${this.formatTotal(totalSize)}`));
1351
+ console.log(this.colors.info(`📦 File size: ${this.formatTotal(totalSize)}`));
1256
1352
  }
1257
1353
  }
1258
1354
 
@@ -1439,31 +1535,36 @@ async downloadFile(url, outputPath) {
1439
1535
  // Clean up state file
1440
1536
  this.cleanupStateFile(stateFilePath);
1441
1537
 
1442
- // Success message
1443
- console.log(this.colors.success('Download completed!'));
1444
- console.log(this.colors.primary('File saved to: ') + chalk.underline(outputPath));
1445
- console.log(this.colors.purple('Total size: ') + this.formatBytes(downloaded));
1538
+ // Success celebration
1539
+ console.log(this.colors.success('Download completed!'));
1540
+ console.log(this.colors.primary('📁 File saved to: ') + chalk.underline(outputPath));
1541
+ console.log(this.colors.purple('📊 Total size: ') + this.formatBytes(downloaded));
1446
1542
 
1447
1543
  if (resuming) {
1448
- console.log(this.colors.info('Resumed from: ') + this.formatBytes(startByte));
1449
- console.log(this.colors.info('Downloaded this session: ') + this.formatBytes(sessionDownloaded));
1544
+ console.log(this.colors.info('🔄 Resumed from: ') + this.formatBytes(startByte));
1545
+ console.log(this.colors.info('📥 Downloaded this session: ') + this.formatBytes(sessionDownloaded));
1450
1546
  }
1547
+
1548
+ // Random success emoji
1549
+ const celebrationEmojis = ['🥳', '🎊', '🎈', '🌟', '💯', '🚀', '✨', '🔥'];
1550
+ const randomEmoji = celebrationEmojis[Math.floor(Math.random() * celebrationEmojis.length)];
1551
+ console.log(this.colors.success(`${randomEmoji} Successfully downloaded! ${randomEmoji}`));
1451
1552
 
1452
1553
  } catch (error) {
1453
1554
  if (this.loadingSpinner && this.loadingSpinner.isSpinning) {
1454
- this.loadingSpinner.fail(this.colors.error('Connection failed'));
1555
+ this.loadingSpinner.fail(this.colors.error('Connection failed'));
1455
1556
  }
1456
1557
  if (this.progressBar) {
1457
1558
  this.progressBar.stop();
1458
1559
  }
1459
1560
 
1460
1561
  // Don't clean up partial file on error - allow resume
1461
- console.error(this.colors.error.bold('Download failed: ') + this.colors.warning(error.message));
1562
+ console.error(this.colors.error.bold('💥 Download failed: ') + this.colors.warning(error.message));
1462
1563
 
1463
1564
  if (error.name === 'AbortError') {
1464
- console.log(this.colors.info('Download state saved. You can resume later by running the same command.'));
1565
+ console.log(this.colors.info('💾 Download state saved. You can resume later by running the same command.'));
1465
1566
  } else {
1466
- console.log(this.colors.info('Partial download saved. Restart to resume from where it left off.'));
1567
+ console.log(this.colors.info('💾 Partial download saved. Restart to resume from where it left off.'));
1467
1568
  }
1468
1569
 
1469
1570
  throw error;
@@ -1718,7 +1819,7 @@ setupFallbackKeyboardListener() {
1718
1819
  };
1719
1820
 
1720
1821
  process.stdin.on('data', handleKeypress);
1721
- console.log(this.colors.info('Keyboard active: p=pause/resume, a=add URL'));
1822
+ console.log(this.colors.info('💡 Keyboard listener active: Press p to pause/resume, a to add URL'));
1722
1823
  }
1723
1824
  }
1724
1825
 
@@ -1805,253 +1906,178 @@ resumeAll() {
1805
1906
  }
1806
1907
  }
1807
1908
 
1808
-
1809
-
1810
- /**
1811
- * Display help information
1812
- */
1813
- function displayHelp() {
1814
- console.log('\n' + chalk.blue.bold('🚀 COLORFUL FILE DOWNLOADER 🚀'));
1815
- console.log(chalk.magenta(''.repeat(50)));
1816
-
1817
- console.log(chalk.cyan.bold('\n📋 Usage:') + chalk.white(' node downloader.js <url> [output-path] [url2] [output-path2] ...'));
1818
-
1819
- console.log(chalk.yellow.bold('\n✨ Examples:'));
1820
- console.log(chalk.gray(' # Single download:'));
1821
- console.log(chalk.gray(' node downloader.js https://example.com/file.iso'));
1822
- console.log(chalk.gray(' node downloader.js https://example.com/file.iso ./custom-name.iso'));
1823
- console.log(chalk.gray('\n # Multiple downloads:'));
1824
- console.log(chalk.gray(' node downloader.js https://url1.com/file1.iso https://url2.com/file2.iso'));
1825
- console.log(chalk.gray(' node downloader.js https://url1.com/file1.iso ./file1.iso https://url2.com/file2.iso ./file2.iso'));
1826
-
1827
- console.log(chalk.magenta.bold('\n🔧 Environment Variables:'));
1828
- console.log(chalk.gray(' GRAB_DOWNLOAD_STATE_DIR - Directory to store download state files'));
1829
- console.log(chalk.gray(' (default: ./.grab-downloads)'));
1830
- console.log(chalk.blue('═'.repeat(50)) + '\n');
1831
- }
1832
-
1833
- /**
1834
- * Generate output filename from URL
1835
- * @param {string} url - Download URL
1836
- * @returns {string} - Generated filename
1837
- */
1838
- function generateFilename(url) {
1839
- try {
1840
- const filename = path.basename(new URL(url).pathname);
1841
- return filename || 'downloaded-file';
1842
- } catch (error) {
1843
- return 'downloaded-file';
1844
- }
1845
- }
1846
-
1847
- /**
1848
- * Display file statistics
1849
- * @param {string} filepath - Path to the downloaded file
1850
- * @param {ColorfulFileDownloader} downloader - Downloader instance for formatting
1851
- */
1852
- function displayFileStats(filepath, downloader) {
1853
- try {
1854
- const stats = fs.statSync(filepath);
1855
- console.log(chalk.green('📈 Final file size: ') + downloader.formatBytes(stats.size));
1856
- console.log(chalk.cyan('📅 Created: ') + chalk.bold(stats.birthtime.toLocaleString()));
1857
- console.log(chalk.blue('═'.repeat(50)));
1858
- } catch (error) {
1859
- console.log(chalk.yellow('⚠️ Could not read file statistics'));
1860
- }
1861
- }
1862
-
1863
- // CLI Interface with colorful output
1864
- async function main() {
1865
- const args = process.argv.slice(2);
1866
-
1867
- if (args.length < 1) {
1868
- displayHelp();
1869
- process.exit(1);
1870
- }
1871
-
1872
- const downloader = new ColorfulFileDownloader();
1873
-
1874
- // First pass: identify all URLs and count them
1875
- const urlIndices = [];
1876
- for (let i = 0; i < args.length; i++) {
1877
- if (downloader.isValidUrl(args[i])) {
1878
- urlIndices.push(i);
1879
- }
1880
- }
1881
-
1882
- if (urlIndices.length === 0) {
1883
- console.error(chalk.red.bold('❌ No valid URLs provided'));
1884
- process.exit(1);
1885
- }
1886
-
1887
- // Parse URLs and their corresponding output paths
1888
- const downloads = [];
1889
-
1890
- for (let i = 0; i < urlIndices.length; i++) {
1891
- const urlIndex = urlIndices[i];
1892
- const url = args[urlIndex];
1893
- let outputPath = null;
1894
-
1895
- // Check if there's a potential output path after this URL
1896
- const nextUrlIndex = urlIndices[i + 1];
1897
- if (nextUrlIndex !== undefined) {
1898
- // If there's exactly one argument between this URL and the next URL, it's an output path
1899
- if (nextUrlIndex - urlIndex === 2) {
1900
- outputPath = args[urlIndex + 1];
1909
+ // --- Main CLI logic ---
1910
+ // Only run CLI when this file is executed directly, not when imported
1911
+ if (import.meta.url === `file://${process.argv[1]}`) {
1912
+ const argv = new ArgParser()
1913
+ .usage('Usage: grab-url <url> [options]')
1914
+ .command('$0 <url>', 'Fetch data from API endpoint or download files')
1915
+ .option('no-save', {
1916
+ type: 'boolean',
1917
+ default: false,
1918
+ describe: 'Don\'t save output to file, just print to console'
1919
+ })
1920
+ .option('output', {
1921
+ alias: 'o',
1922
+ type: 'string',
1923
+ describe: 'Output filename (default: output.json)',
1924
+ default: null
1925
+ })
1926
+ .option('params', {
1927
+ alias: 'p',
1928
+ type: 'string',
1929
+ describe: 'JSON string of query parameters (e.g., \'{"key":"value"}\')',
1930
+ coerce: (arg) => {
1931
+ if (!arg) return {};
1932
+ try { return JSON.parse(arg); } catch (e) { throw new Error(`Invalid JSON in params: ${arg}`); }
1933
+ }
1934
+ })
1935
+ .help()
1936
+ .alias('h', 'help')
1937
+ .version('1.0.0')
1938
+ .strict()
1939
+ .parseSync();
1940
+
1941
+ const urls = argv.urls || [];
1942
+ const params = argv.params || {};
1943
+ const outputFile = argv.output;
1944
+ const noSave = argv['no-save'];
1945
+
1946
+ // --- Mode detection ---
1947
+ const anyFileUrl = urls.some(isFileUrl);
1948
+ const isDownloadMode = urls.length > 1 || anyFileUrl;
1949
+
1950
+ (async () => {
1951
+ if (isDownloadMode) {
1952
+ // --- Download Mode ---
1953
+ const downloader = new ColorFileDownloader();
1954
+ // Prepare download objects
1955
+ const downloads = urls.map((url, i) => {
1956
+ let filename = null;
1957
+ // If user provided output, use it for the first file
1958
+ if (i === 0 && outputFile) filename = outputFile;
1959
+ return { url, outputPath: filename };
1960
+ });
1961
+ // Show detected downloads in a table
1962
+ const detectedTable = new Table({
1963
+ head: ['#', 'URL'],
1964
+ colWidths: [4, 80],
1965
+ colAligns: ['right', 'left'],
1966
+ style: { 'padding-left': 1, 'padding-right': 1, head: [], border: [] }
1967
+ });
1968
+ downloads.forEach((download, index) => {
1969
+ detectedTable.push([
1970
+ (index + 1).toString(),
1971
+ download.url
1972
+ ]);
1973
+ });
1974
+ console.log(chalk.cyan.bold(`Detected ${downloads.length} download(s):`));
1975
+ console.log(detectedTable.toString());
1976
+ console.log('');
1977
+ // Prepare download objects with filenames
1978
+ const downloadObjects = downloads.map((download, index) => {
1979
+ let actualUrl = download.url;
1980
+ let filename = download.outputPath;
1981
+ if (!filename) filename = downloader.generateFilename(actualUrl);
1982
+ const outputPath = path.isAbsolute(filename) ? filename : path.join(process.cwd(), filename);
1983
+ const outputDir = path.dirname(outputPath);
1984
+ try { if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); } catch (error) {
1985
+ console.error(chalk.red.bold('❌ Could not create output directory: ') + error.message);
1986
+ process.exit(1);
1987
+ }
1988
+ return {
1989
+ url: actualUrl,
1990
+ outputPath,
1991
+ filename: path.basename(filename)
1992
+ };
1993
+ });
1994
+ // Show download queue in a table
1995
+ const queueTable = new Table({
1996
+ head: ['#', 'Filename', 'Output Path'],
1997
+ colWidths: [4, 32, 54],
1998
+ colAligns: ['right', 'left', 'left'],
1999
+ style: { 'padding-left': 1, 'padding-right': 1, head: [], border: [] }
2000
+ });
2001
+ downloadObjects.forEach((downloadObj, index) => {
2002
+ queueTable.push([
2003
+ (index + 1).toString(),
2004
+ downloadObj.filename,
2005
+ downloadObj.outputPath
2006
+ ]);
2007
+ });
2008
+ console.log(chalk.cyan.bold('\nDownload Queue:'));
2009
+ console.log(queueTable.toString());
2010
+ console.log('');
2011
+ try {
2012
+ await downloader.downloadMultipleFiles(downloadObjects);
2013
+ // Display individual file stats in a table
2014
+ const statsTable = new Table({
2015
+ head: ['Filename', 'Size', 'Created'],
2016
+ colWidths: [32, 14, 25],
2017
+ colAligns: ['left', 'right', 'left'],
2018
+ style: { 'padding-left': 1, 'padding-right': 1, head: [], border: [] }
2019
+ });
2020
+ downloadObjects.forEach((downloadObj) => {
2021
+ try {
2022
+ const stats = fs.statSync(downloadObj.outputPath);
2023
+ statsTable.push([
2024
+ downloadObj.filename,
2025
+ downloader.formatBytes(stats.size),
2026
+ stats.birthtime.toLocaleString()
2027
+ ]);
2028
+ } catch (error) {
2029
+ statsTable.push([
2030
+ downloadObj.filename,
2031
+ 'Error',
2032
+ 'Could not read'
2033
+ ]);
2034
+ }
2035
+ });
2036
+ console.log(chalk.cyan.bold('\nFile Details:'));
2037
+ console.log(statsTable.toString());
2038
+ } catch (error) {
2039
+ console.error(chalk.red.bold('Failed to download files: ') + chalk.yellow(error.message));
2040
+ process.exit(1);
1901
2041
  }
2042
+ downloader.cleanup();
1902
2043
  } else {
1903
- // This is the last URL, check if there's an argument after it
1904
- if (urlIndex + 1 < args.length) {
1905
- outputPath = args[urlIndex + 1];
1906
- }
1907
- }
1908
-
1909
- downloads.push({ url, outputPath });
1910
- }
1911
-
1912
- console.log(chalk.cyan.bold(`Detected ${downloads.length} download(s):`));
1913
- downloads.forEach((download, index) => {
1914
- console.log(` ${index + 1}. ${chalk.yellow(download.url)} (URL)`);
1915
- });
1916
- console.log('');
1917
-
1918
- // Handle multiple downloads
1919
- if (downloads.length > 1) {
1920
- console.log(chalk.blue.bold(`Multiple downloads detected: ${downloads.length} files\n`));
1921
-
1922
- // Prepare download objects
1923
- const downloadObjects = downloads.map((download, index) => {
1924
- let actualUrl = download.url;
1925
- let filename = download.outputPath;
1926
-
1927
- // Generate filename if not provided
1928
- if (!filename) {
1929
- filename = generateFilename(actualUrl);
1930
- }
1931
-
1932
- const outputPath = path.isAbsolute(filename) ? filename : path.join(process.cwd(), filename);
1933
-
1934
- // Ensure output directory exists
1935
- const outputDir = path.dirname(outputPath);
2044
+ // --- API Mode ---
2045
+ const url = urls[0];
2046
+ const startTime = process.hrtime();
1936
2047
  try {
1937
- if (!fs.existsSync(outputDir)) {
1938
- fs.mkdirSync(outputDir, { recursive: true });
2048
+ const res = await grab(url, params);
2049
+ if (res.error) log(`\n\nStatus: ${res.error}`);
2050
+ let filePath = null;
2051
+ let outputData;
2052
+ let isTextData = false;
2053
+ if (typeof res.data === 'string') { outputData = res.data; isTextData = true; }
2054
+ else if (Buffer.isBuffer(res.data) || res.data instanceof Uint8Array) { outputData = res.data; isTextData = false; }
2055
+ else if (res.data instanceof Blob) { const arrayBuffer = await res.data.arrayBuffer(); outputData = Buffer.from(arrayBuffer); isTextData = false; }
2056
+ else if (res.data && typeof res.data === 'object') { outputData = JSON.stringify(res.data, null, 2); isTextData = true; }
2057
+ else { outputData = String(res.data); isTextData = true; }
2058
+ if (!noSave) {
2059
+ const urlPath = new URL(url).pathname;
2060
+ const urlExt = path.extname(urlPath);
2061
+ const defaultExt = isTextData ? '.json' : (urlExt || '.bin');
2062
+ filePath = outputFile ? path.resolve(outputFile) : path.resolve(process.cwd(), `output${defaultExt}`);
2063
+ if (isTextData) fs.writeFileSync(filePath, outputData, 'utf8');
2064
+ else fs.writeFileSync(filePath, outputData);
2065
+ const [seconds, nanoseconds] = process.hrtime(startTime);
2066
+ const elapsedMs = (seconds + nanoseconds / 1e9).toFixed(2);
2067
+ const stats = fs.statSync(filePath);
2068
+ const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(1);
2069
+ log(`⏱️ ${elapsedMs}s 📦 ${fileSizeMB}MB ✅ Saved to: ${filePath}`);
2070
+ } else {
2071
+ if (isTextData) {
2072
+ log(outputData);
2073
+ } else {
2074
+ log(`Binary data received (${outputData.length} bytes). Use --output to save to file.`);
2075
+ }
1939
2076
  }
1940
2077
  } catch (error) {
1941
- console.error(chalk.red.bold('❌ Could not create output directory: ') + error.message);
2078
+ log(`Error: ${error.message}`, {color: 'red'});
1942
2079
  process.exit(1);
1943
2080
  }
1944
-
1945
- return {
1946
- url: actualUrl,
1947
- outputPath,
1948
- filename: path.basename(filename)
1949
- };
1950
- });
1951
-
1952
- // Show download queue
1953
- console.log(chalk.cyan.bold('\nDownload Queue:'));
1954
- downloadObjects.forEach((downloadObj, index) => {
1955
- console.log(` ${chalk.yellow((index + 1).toString().padStart(2))}. ${chalk.green(downloadObj.filename)} ${chalk.gray('→')} ${downloadObj.outputPath}`);
1956
- });
1957
- console.log('');
1958
-
1959
- try {
1960
- await downloader.downloadMultipleFiles(downloadObjects);
1961
-
1962
- // Display individual file stats
1963
- console.log(chalk.cyan.bold('\nFile Details:'));
1964
- downloadObjects.forEach((downloadObj) => {
1965
- displayFileStats(downloadObj.outputPath, downloader);
1966
- });
1967
-
1968
- } catch (error) {
1969
- console.error(chalk.red.bold('Failed to download files: ') + chalk.yellow(error.message));
1970
- process.exit(1);
1971
- }
1972
-
1973
- } else {
1974
- // Single download (existing logic)
1975
- let url = downloads[0].url;
1976
- let outputPath = downloads[0].outputPath;
1977
-
1978
- // Generate filename if not provided
1979
- if (!outputPath) {
1980
- outputPath = generateFilename(url);
1981
- }
1982
-
1983
- // Ensure output directory exists
1984
- const outputDir = path.dirname(outputPath);
1985
- try {
1986
- if (!fs.existsSync(outputDir)) {
1987
- fs.mkdirSync(outputDir, { recursive: true });
1988
- }
1989
- } catch (error) {
1990
- console.error(chalk.red.bold('❌ Could not create output directory: ') + error.message);
1991
- process.exit(1);
1992
- }
1993
-
1994
- // Check if file already exists
1995
- if (fs.existsSync(outputPath)) {
1996
- console.log(chalk.yellow('File already exists: ') + outputPath);
1997
- console.log(chalk.gray(' Continuing will overwrite the existing file...'));
1998
- }
1999
-
2000
- console.log(chalk.green('\nTarget: ') + chalk.bold(outputPath));
2001
-
2002
- try {
2003
- await downloader.downloadFile(url, outputPath);
2004
- displayFileStats(outputPath, downloader);
2005
-
2006
- } catch (error) {
2007
- console.error(chalk.red.bold('Failed to download file: ') + chalk.yellow(error.message));
2008
-
2009
- // Clean up partial file if it exists
2010
- if (fs.existsSync(outputPath)) {
2011
- try {
2012
- fs.unlinkSync(outputPath);
2013
- console.log(chalk.gray('Cleaned up partial download'));
2014
- } catch (cleanupError) {
2015
- console.log(chalk.yellow('Could not clean up partial file: ') + cleanupError.message);
2016
- }
2017
- }
2018
-
2019
- process.exit(1);
2020
2081
  }
2021
- }
2022
-
2023
- downloader.cleanup();
2024
- }
2025
-
2026
- // Handle graceful shutdown with colors
2027
- process.on('SIGINT', () => {
2028
- console.log(chalk.yellow.bold('\n🛑 Download cancelled by user'));
2029
- process.exit(0);
2030
- });
2031
-
2032
- process.on('SIGTERM', () => {
2033
- console.log(chalk.yellow.bold('\n🛑 Download terminated'));
2034
- process.exit(0);
2035
- });
2036
-
2037
- // Handle uncaught exceptions
2038
- process.on('uncaughtException', (error) => {
2039
- console.error(chalk.red.bold('💥 Uncaught exception: ') + error.message);
2040
- process.exit(1);
2041
- });
2042
-
2043
- process.on('unhandledRejection', (reason, promise) => {
2044
- console.error(chalk.red.bold('💥 Unhandled rejection at: ') + promise);
2045
- console.error(chalk.red('Reason: ') + reason);
2046
- process.exit(1);
2047
- });
2048
-
2049
- // Run the CLI if there are command line arguments (indicating it's being run as a CLI tool)
2050
- if (process.argv.length > 2) {
2051
- main().catch((error) => {
2052
- console.error(chalk.red.bold('💥 Fatal error: ') + error.message);
2053
- process.exit(1);
2054
- });
2055
- }
2056
-
2057
- export default ColorfulFileDownloader;
2082
+ })();
2083
+ }