Skip to content

Commit 3c38fd1

Browse files
committed
fix: existing image are overwritten in bulk download
1 parent cbfb7db commit 3c38fd1

File tree

2 files changed

+51
-23
lines changed

2 files changed

+51
-23
lines changed

src/downloader.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,13 @@ import ArgumentError from './errors/ArgumentError.js';
1212
import DirectoryError from './errors/DirectoryError.js';
1313
import { Image } from './index.js';
1414

15-
export type DownloadOptions = {
15+
export type ImageOptions = {
1616
/**
1717
* The directory to save the image to.
1818
*
1919
* If not provided, the current working directory will be used.
2020
*/
2121
directory?: string;
22-
/**
23-
* The headers to send with the request.
24-
*/
25-
headers?: Record<string, string | string[] | undefined>;
2622
/**
2723
* The name of the image file.
2824
*
@@ -33,12 +29,6 @@ export type DownloadOptions = {
3329
* If a name with same extension already exists, ` (1)`, ` (2)`, etc. will be added to the end of the name.
3430
*/
3531
name?: string;
36-
/**
37-
* Set the maximum number of times to retry the request if it fails.
38-
*
39-
* @default 2
40-
*/
41-
maxRetry?: number;
4232
/**
4333
* The extension of the image.
4434
*
@@ -47,6 +37,19 @@ export type DownloadOptions = {
4737
* If the URL doesn't have an extension, `jpg` will be used.
4838
*/
4939
extension?: string;
40+
};
41+
42+
export type DownloadOptions = {
43+
/**
44+
* The headers to send with the request.
45+
*/
46+
headers?: Record<string, string | string[] | undefined>;
47+
/**
48+
* Set the maximum number of times to retry the request if it fails.
49+
*
50+
* @default 2
51+
*/
52+
maxRetry?: number;
5053
/**
5154
* Set timeout for each request in milliseconds.
5255
*/
@@ -58,11 +61,15 @@ export type DownloadOptions = {
5861
};
5962

6063
/**
61-
* Set the options with the default values if they are not provided.
64+
* Parses and validates the image parameters.
65+
*
66+
* If image options are not provided, the default values will be used.
67+
*
68+
* See {@link ImageOptions} for more information.
6269
*
6370
* @throws {ArgumentError} If there is an invalid value.
6471
*/
65-
export function parseImageParams(url: string, options?: DownloadOptions) {
72+
export function parseImageParams(url: string, options?: ImageOptions) {
6673
const lowerImgExts = [...imageExtensions].map((ext) => ext.toLowerCase());
6774
const originalExt = path.extname(url).replace('.', '');
6875
const img: Image = {
@@ -135,16 +142,14 @@ export function parseImageParams(url: string, options?: DownloadOptions) {
135142
}
136143

137144
/**
138-
* Downloads an image from a URL.
139-
* @param url The URL of the image to download.
140-
* @param options The options to use.
145+
* Downloads an image.
146+
* @param img The validated image parameters. See {@link parseImageParams}.
147+
* @param options The download options.
141148
* @returns The file path.
142149
* @throws {DirectoryError} If the directory cannot be created.
143150
* @throws {Error} If there are any other errors.
144151
*/
145-
export async function download(url: string, options: DownloadOptions = {}) {
146-
const img = parseImageParams(url, options);
147-
152+
export async function download(img: Image, options: DownloadOptions = {}) {
148153
// Create the directory if it doesn't exist.
149154
if (!fs.existsSync(img.directory)) {
150155
try {

src/index.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import PQueue from 'p-queue';
22
import { setMaxListeners } from 'node:events';
33
import { CancelError } from 'got';
44
import { DEFAULT_INTERVAL, DEFAULT_STEP } from './constanta.js';
5-
import { DownloadOptions, download } from './downloader.js';
5+
import {
6+
DownloadOptions,
7+
ImageOptions,
8+
download,
9+
parseImageParams,
10+
} from './downloader.js';
11+
import path from 'node:path';
612

713
export type Image = {
814
/**
@@ -35,7 +41,7 @@ export type Image = {
3541
path: string;
3642
};
3743

38-
export type Options = DownloadOptions & {
44+
export type Options = (ImageOptions & DownloadOptions) & {
3945
/**
4046
* Do something when the image is successfully downloaded.
4147
* For example, counting the number of successful downloads.
@@ -97,13 +103,29 @@ async function imgdl(
97103

98104
return new Promise<Image[]>((resolve, reject) => {
99105
const images: Image[] = [];
106+
const countNames = new Map<string, number>();
100107

101108
url.forEach((u) => {
109+
const img = parseImageParams(u, options);
110+
111+
// Make sure the name is unique
112+
const nameKey = `${img.name}.${img.extension}`;
113+
const count = countNames.get(nameKey);
114+
if (count) {
115+
img.name = `${img.name} (${count})`;
116+
img.path = path.resolve(
117+
img.directory,
118+
`${img.name}.${img.extension}`,
119+
);
120+
}
121+
countNames.set(nameKey, (count || 0) + 1);
122+
123+
// Add the download task to queue
102124
queue
103125
.add(
104126
async ({ signal }) => {
105127
try {
106-
return await download(u, {
128+
return await download(img, {
107129
...options,
108130
signal,
109131
});
@@ -130,6 +152,7 @@ async function imgdl(
130152
});
131153
});
132154

155+
// Resolve/reject when all task is finished
133156
queue
134157
.onIdle()
135158
.then(() => {
@@ -141,7 +164,7 @@ async function imgdl(
141164
});
142165
}
143166

144-
return download(url, {
167+
return download(parseImageParams(url, options), {
145168
...options,
146169
signal: options?.signal,
147170
});

0 commit comments

Comments
 (0)