-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.ts
144 lines (131 loc) · 3.58 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { setMaxListeners } from 'node:events';
import path from 'node:path';
import PQueue from 'p-queue';
import { DEFAULT_INTERVAL, DEFAULT_STEP } from './constanta.js';
import {
DownloadOptions,
ImageOptions,
download,
parseImageParams,
} from './downloader.js';
export type Image = {
/**
* The URL of the image.
*/
url: URL;
/**
* The name of the image file, without the extension.
*/
name: string;
/**
* The extension of the image without the dot. Example: `jpg`.
*/
extension: string;
/**
* The path of the directory where the image is saved.
*/
directory: string;
/**
* The original name of the image file, without the extension.
*/
originalName?: string;
/**
* The original extension of the image file, without the dot. Example: `jpg`.
*/
originalExtension?: string;
/**
* The absolute path of the image, including the directory, name, and extension.
*/
path: string;
};
export type Options = (ImageOptions & DownloadOptions) & {
/**
* Do something when an image is successfully downloaded.
*
* @param image The downloaded image.
*/
onSuccess?: (image: Image) => void;
/**
* Do something when an image fails to download.
*
* @param error The error that caused the download to fail.
* @param url The URL of the image that failed to download.
*/
onError?: (error: Error, url: string) => void;
/**
* The number of requests to make at the same time.
*
* @default 5
*/
step?: number;
/**
* The interval between each batch of requests in milliseconds.
*
* @default 100
*/
interval?: number;
/**
* The signal which can be used to abort requests.
*/
signal?: AbortSignal;
};
async function imgdl(
url: string | (string | ({ url: string } & ImageOptions))[],
options?: Options,
): Promise<void> {
const {
directory,
name,
extension,
onSuccess,
onError,
step,
interval,
...downloadOptions
} = options ?? {};
const urls = Array.isArray(url) ? url : [url];
const queue = new PQueue({
concurrency: step ?? DEFAULT_STEP,
interval: interval ?? DEFAULT_INTERVAL,
intervalCap: step ?? DEFAULT_STEP,
});
// Set max listeners to infinity to prevent memory leak warning
if (downloadOptions?.signal) {
setMaxListeners(Number.POSITIVE_INFINITY, downloadOptions.signal);
}
const countNames = new Map<string, number>();
for (const _url of urls) {
// Get image URL and options
const { url: imgUrl, ...imgOptions } =
typeof _url === 'string' ? { url: _url } : _url;
try {
// Validate and parse the image parameters
const img = parseImageParams(imgUrl, {
directory: imgOptions.directory || directory,
name: imgOptions.name || name,
extension: imgOptions.extension || extension,
});
// Make sure the name is unique in the destination directory
const nameKey = `${img.directory}/${img.name}.${img.extension}`;
const count = countNames.get(nameKey);
if (count) {
img.name = `${img.name} (${count})`;
img.path = path.resolve(img.directory, `${img.name}.${img.extension}`);
}
countNames.set(nameKey, (count || 0) + 1);
// Add the download task to queue
const image = await queue.add(
({ signal }) => download(img, { ...downloadOptions, signal }),
{ signal: downloadOptions?.signal },
);
if (image) {
onSuccess?.(image);
}
} catch (error) {
onError?.(error as Error, imgUrl);
}
}
await queue.onIdle();
}
export default imgdl;
export { ImageOptions, DownloadOptions };