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.
- package/dist/grab-api.cjs.js +1 -1
- package/dist/grab-api.cjs.js.map +1 -1
- package/dist/grab-api.d.ts +5 -5
- package/dist/grab-api.es.js.map +1 -1
- package/dist/icons.cjs.js +1 -1
- package/dist/icons.cjs.js.map +1 -1
- package/dist/icons.d.ts +0 -234
- package/dist/icons.es.js +1 -113
- package/dist/icons.es.js.map +1 -1
- package/package.json +12 -13
- package/readme.md +12 -18
- package/src/grab-api.ts +5 -5
- package/src/{grab-url-cli.js → grab-url.ts} +380 -354
- package/src/{spinners.json → icons/cli/spinners.json} +1 -482
- package/src/icons/index.ts +0 -255
- package/src/icons/svg/index.ts +313 -0
- package/src/grab-cli.js +0 -316
- /package/src/icons/{loading-bouncy-ball.svg → svg/loading-bouncy-ball.svg} +0 -0
- /package/src/icons/{loading-double-ring.svg → svg/loading-double-ring.svg} +0 -0
- /package/src/icons/{loading-eclipse.svg → svg/loading-eclipse.svg} +0 -0
- /package/src/icons/{loading-ellipsis.svg → svg/loading-ellipsis.svg} +0 -0
- /package/src/icons/{loading-floating-search.svg → svg/loading-floating-search.svg} +0 -0
- /package/src/icons/{loading-gears.svg → svg/loading-gears.svg} +0 -0
- /package/src/icons/{loading-infinity.svg → svg/loading-infinity.svg} +0 -0
- /package/src/icons/{loading-orbital.svg → svg/loading-orbital.svg} +0 -0
- /package/src/icons/{loading-pacman.svg → svg/loading-pacman.svg} +0 -0
- /package/src/icons/{loading-pulse-bars.svg → svg/loading-pulse-bars.svg} +0 -0
- /package/src/icons/{loading-red-blue-ball.svg → svg/loading-red-blue-ball.svg} +0 -0
- /package/src/icons/{loading-reload-arrow.svg → svg/loading-reload-arrow.svg} +0 -0
- /package/src/icons/{loading-ring.svg → svg/loading-ring.svg} +0 -0
- /package/src/icons/{loading-ripple.svg → svg/loading-ripple.svg} +0 -0
- /package/src/icons/{loading-spinner-oval.svg → svg/loading-spinner-oval.svg} +0 -0
- /package/src/icons/{loading-spinner.svg → svg/loading-spinner.svg} +0 -0
- /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
|
|
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
|
|
25
|
-
spinners = JSON.parse(readFileSync(join(__dirname, '
|
|
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
|
|
29
|
-
spinners = JSON.parse(readFileSync(join(
|
|
31
|
+
// Try the parent directory (src)
|
|
32
|
+
spinners = JSON.parse(readFileSync(join(__dirname, '..', 'spinners.json'), 'utf8'));
|
|
30
33
|
} catch (error3) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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: '
|
|
405
|
-
{ unit: '
|
|
406
|
-
{ unit: '
|
|
407
|
-
{ unit: '
|
|
408
|
-
{ unit: '
|
|
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(
|
|
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
|
|
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.
|
|
705
|
-
const eta =
|
|
706
|
-
(individualSizes[i]
|
|
707
|
-
|
|
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
|
|
726
|
-
const totalSpeedBps =
|
|
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
|
|
732
|
-
const timeElapsed =
|
|
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:
|
|
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.
|
|
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(
|
|
976
|
+
console.log(this.colors.green(`✅ Successful: ${successful}/${downloads.length}`));
|
|
871
977
|
if (failed > 0) {
|
|
872
|
-
console.log(this.colors.error(
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
1205
|
-
console.log(this.colors.info(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
//
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
-
|
|
1938
|
-
|
|
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
|
-
|
|
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
|
+
}
|