@nghiavuive/random-image 1.1.0 → 1.2.1

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 (4) hide show
  1. package/README.md +103 -3
  2. package/dist/cli.js +465 -0
  3. package/dist/cli.mjs +527 -0
  4. package/package.json +10 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # random-image
2
2
 
3
- A generic utility library to fetch random images from various providers like Unsplash and Pexels, with built-in download capabilities.
3
+ A generic utility library to fetch random images from various providers like Unsplash and Pexels, with built-in download capabilities and CLI support.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,7 +8,99 @@ A generic utility library to fetch random images from various providers like Uns
8
8
  npm install @nghiavuive/random-image
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## CLI Usage
12
+
13
+ You can use the CLI without installation via `npx`:
14
+
15
+ ```bash
16
+ npx @nghiavuive/random-image fetch --query "nature" --download
17
+ ```
18
+
19
+ Or install globally:
20
+
21
+ ```bash
22
+ npm install -g @nghiavuive/random-image
23
+ random-image fetch --query "mountains" --download
24
+ ```
25
+
26
+ ### CLI Setup
27
+
28
+ Set environment variables for the providers you want to use:
29
+
30
+ ```bash
31
+ export UNSPLASH_KEY="your_unsplash_access_key"
32
+ export PEXELS_KEY="your_pexels_api_key"
33
+ export PIXABAY_KEY="your_pixabay_api_key"
34
+ ```
35
+
36
+ Or create a `.env` file in your project:
37
+
38
+ ```env
39
+ UNSPLASH_KEY=your_unsplash_access_key
40
+ PEXELS_KEY=your_pexels_api_key
41
+ PIXABAY_KEY=your_pixabay_api_key
42
+ ```
43
+
44
+ ### CLI Commands
45
+
46
+ #### Fetch Command
47
+
48
+ ```bash
49
+ # Basic usage - uses random provider from available API keys
50
+ random-image fetch
51
+
52
+ # Fetch with search query
53
+ random-image fetch --query "nature"
54
+
55
+ # Fetch and download
56
+ random-image fetch --query "mountains" --download
57
+
58
+ # Specify provider
59
+ random-image fetch --provider unsplash --query "ocean"
60
+
61
+ # Custom dimensions
62
+ random-image fetch --width 1920 --height 1080 --query "sunset"
63
+
64
+ # Download to custom directory
65
+ random-image fetch --query "cats" --download ./my-images
66
+
67
+ # Download with custom filename
68
+ random-image fetch --query "dogs" --download --filename "my-dog.jpg"
69
+
70
+ # Download with overwrite
71
+ random-image fetch --download --overwrite
72
+
73
+ # Generate random UUID filename
74
+ random-image fetch --download --no-keep-original-name
75
+
76
+ # Full example with all options
77
+ random-image fetch \
78
+ --provider pexels \
79
+ --query "abstract art" \
80
+ --width 2560 \
81
+ --height 1440 \
82
+ --orientation landscape \
83
+ --download ./wallpapers \
84
+ --filename "wallpaper.jpg" \
85
+ --overwrite
86
+ ```
87
+
88
+ #### CLI Flags
89
+
90
+ - `-q, --query <search>`: Search query for the image
91
+ - `-w, --width <number>`: Width of the image
92
+ - `-h, --height <number>`: Height of the image
93
+ - `--quality <number>`: Quality of the image (0-100)
94
+ - `--orientation <type>`: Image orientation (`landscape` or `portrait`)
95
+ - `-p, --provider <name>`: Provider to use (`unsplash`, `pexels`, `pixabay`, or `random`)
96
+ - `-d, --download [path]`: Download the image (default: `./downloads`)
97
+ - `--filename <name>`: Custom filename for downloaded image
98
+ - `--overwrite`: Overwrite existing files
99
+ - `--no-keep-original-name`: Generate random UUID filename instead of keeping original
100
+
101
+ **Random Provider Mode**: If you don't specify `--provider` or use `--provider random`, the CLI will randomly select from providers that have API keys configured. This is useful for distributing requests across multiple services.
102
+
103
+ ## Programmatic Usage
12
104
 
13
105
  You need to obtain API keys from the respective providers:
14
106
  - [Unsplash Developers](https://unsplash.com/developers)
@@ -149,8 +241,16 @@ Result object containing image information:
149
241
 
150
242
  ## Features
151
243
 
244
+ ### CLI Features
245
+ - ✅ Command-line interface for quick image fetching
246
+ - ✅ Random provider selection when multiple API keys are available
247
+ - ✅ Environment variable support for API keys
248
+ - ✅ Direct download from command line
249
+ - ✅ All library features available via CLI flags
250
+
251
+ ### Library Features
252
+
152
253
  ### Download Functionality
153
- - ✅ Automatic directory creation
154
254
  - ✅ Flexible filename options:
155
255
  - Custom filename
156
256
  - Keep original filename from URL (default)
package/dist/cli.js ADDED
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/providers/unsplash.ts
30
+ var import_axios = __toESM(require("axios"));
31
+ var UnsplashProvider = class {
32
+ constructor(accessKey) {
33
+ this.accessKey = accessKey;
34
+ }
35
+ async fetchRandomImage(options) {
36
+ const response = await import_axios.default.get("https://api.unsplash.com/photos/random", {
37
+ headers: {
38
+ Authorization: `Client-ID ${this.accessKey}`
39
+ },
40
+ params: {
41
+ query: options.query,
42
+ orientation: options.orientation
43
+ }
44
+ });
45
+ const data = response.data;
46
+ const photo = Array.isArray(data) ? data[0] : data;
47
+ const baseUrl = photo.urls.raw;
48
+ const sizeParams = new URLSearchParams();
49
+ if (options.height || options.width) {
50
+ sizeParams.append("fit", "crop");
51
+ sizeParams.append("crop", "entropy");
52
+ }
53
+ if (options.width) sizeParams.append("w", options.width.toString());
54
+ if (options.height) sizeParams.append("h", options.height.toString());
55
+ if (options.quality) sizeParams.append("q", options.quality.toString());
56
+ const finalUrl = `${baseUrl}&${sizeParams.toString()}`;
57
+ return {
58
+ url: finalUrl,
59
+ width: options.width || photo.width,
60
+ height: options.height || photo.height,
61
+ author: photo.user.name,
62
+ authorUrl: photo.user.links.html,
63
+ originalUrl: photo.links.html
64
+ // Link to photo page
65
+ };
66
+ }
67
+ };
68
+
69
+ // src/providers/pexels.ts
70
+ var import_axios2 = __toESM(require("axios"));
71
+ var PexelsProvider = class {
72
+ constructor(apiKey) {
73
+ this.apiKey = apiKey;
74
+ }
75
+ async fetchRandomImage(options) {
76
+ const endpoint = options.query ? "https://api.pexels.com/v1/search" : "https://api.pexels.com/v1/curated";
77
+ const randomPage = Math.floor(Math.random() * 100) + 1;
78
+ const params = {
79
+ per_page: 1,
80
+ page: randomPage
81
+ };
82
+ if (options.query) params.query = options.query;
83
+ const response = await import_axios2.default.get(endpoint, {
84
+ headers: {
85
+ Authorization: this.apiKey
86
+ },
87
+ params
88
+ });
89
+ const data = response.data;
90
+ if (!data.photos || data.photos.length === 0) {
91
+ throw new Error("No images found on Pexels");
92
+ }
93
+ const photo = data.photos[0];
94
+ const baseUrl = photo.src.original;
95
+ const sizeParams = new URLSearchParams();
96
+ sizeParams.append("auto", "compress");
97
+ sizeParams.append("cs", "tinysrgb");
98
+ if (options.width) sizeParams.append("w", options.width.toString());
99
+ if (options.height) sizeParams.append("h", options.height.toString());
100
+ const finalUrl = `${baseUrl}?${sizeParams.toString()}`;
101
+ return {
102
+ url: finalUrl,
103
+ width: options.width || photo.width,
104
+ height: options.height || photo.height,
105
+ author: photo.photographer,
106
+ authorUrl: photo.photographer_url,
107
+ originalUrl: photo.url
108
+ };
109
+ }
110
+ };
111
+
112
+ // src/providers/pixabay.ts
113
+ var import_axios3 = __toESM(require("axios"));
114
+ var PixabayProvider = class {
115
+ constructor(apiKey) {
116
+ this.apiKey = apiKey;
117
+ }
118
+ async fetchRandomImage(options) {
119
+ const params = {
120
+ key: this.apiKey,
121
+ q: options.query || "",
122
+ per_page: 20
123
+ // Fetch a few to pick randomly
124
+ };
125
+ if (options.orientation) {
126
+ params.orientation = options.orientation === "portrait" ? "vertical" : "horizontal";
127
+ }
128
+ const response = await import_axios3.default.get("https://pixabay.com/api/", {
129
+ params
130
+ });
131
+ const hits = response.data.hits;
132
+ if (!hits || hits.length === 0) {
133
+ throw new Error("No images found");
134
+ }
135
+ const randomHit = hits[Math.floor(Math.random() * hits.length)];
136
+ return {
137
+ url: randomHit.largeImageURL || randomHit.webformatURL,
138
+ width: randomHit.imageWidth || randomHit.webformatWidth,
139
+ height: randomHit.imageHeight || randomHit.webformatHeight,
140
+ author: randomHit.user,
141
+ authorUrl: `https://pixabay.com/users/${randomHit.user}-${randomHit.user_id}/`,
142
+ originalUrl: randomHit.pageURL
143
+ };
144
+ }
145
+ };
146
+
147
+ // src/image-fetcher.ts
148
+ var fs = __toESM(require("fs"));
149
+ var path = __toESM(require("path"));
150
+ var import_axios4 = __toESM(require("axios"));
151
+ var import_uuid = require("uuid");
152
+ var RandomImage = class {
153
+ constructor(provider) {
154
+ this.provider = provider;
155
+ }
156
+ /**
157
+ * Fetches a random image based on the provided options.
158
+ * @param options - Configuration options for the image (width, height, query, etc.)
159
+ * @returns A promise that resolves to an ImageResult object.
160
+ */
161
+ async getRandom(options = {}) {
162
+ return this.provider.fetchRandomImage(options);
163
+ }
164
+ /**
165
+ * Downloads an image to a specified directory.
166
+ * @param imageUrl - The URL of the image to download (can be a string or ImageResult object)
167
+ * @param destinationPath - The directory path where the image will be saved
168
+ * @param options - Download options (filename, overwrite, etc.)
169
+ * @returns A promise that resolves to the full path of the downloaded file
170
+ */
171
+ async download(imageUrl, destinationPath, options = {}) {
172
+ const url = typeof imageUrl === "string" ? imageUrl : imageUrl.url;
173
+ if (!fs.existsSync(destinationPath)) {
174
+ fs.mkdirSync(destinationPath, { recursive: true });
175
+ }
176
+ let finalFilename;
177
+ if (options.filename) {
178
+ finalFilename = options.filename;
179
+ } else if (options.keepOriginalName) {
180
+ const urlPath = new URL(url).pathname;
181
+ const urlFilename = path.basename(urlPath);
182
+ if (urlFilename && urlFilename.length > 0 && urlFilename !== "/") {
183
+ finalFilename = urlFilename;
184
+ } else {
185
+ const ext = this.getExtensionFromUrl(url) || ".jpg";
186
+ finalFilename = `${(0, import_uuid.v7)()}${ext}`;
187
+ }
188
+ } else {
189
+ const ext = this.getExtensionFromUrl(url) || ".jpg";
190
+ finalFilename = `${(0, import_uuid.v7)()}${ext}`;
191
+ }
192
+ if (!path.extname(finalFilename)) {
193
+ finalFilename += ".jpg";
194
+ }
195
+ const fullPath = path.join(destinationPath, finalFilename);
196
+ if (fs.existsSync(fullPath) && !options.overwrite) {
197
+ throw new Error(`File already exists: ${fullPath}. Set overwrite: true to replace it.`);
198
+ }
199
+ try {
200
+ const response = await import_axios4.default.get(url, {
201
+ responseType: "stream",
202
+ maxRedirects: 5
203
+ // Automatically handle redirects
204
+ });
205
+ const writer = fs.createWriteStream(fullPath);
206
+ response.data.pipe(writer);
207
+ return new Promise((resolve3, reject) => {
208
+ writer.on("finish", () => {
209
+ resolve3(fullPath);
210
+ });
211
+ writer.on("error", (err) => {
212
+ fs.unlink(fullPath, () => {
213
+ });
214
+ reject(err);
215
+ });
216
+ response.data.on("error", (err) => {
217
+ writer.close();
218
+ fs.unlink(fullPath, () => {
219
+ });
220
+ reject(err);
221
+ });
222
+ });
223
+ } catch (error) {
224
+ if (import_axios4.default.isAxiosError(error)) {
225
+ throw new Error(`Failed to download image: ${error.message}`);
226
+ }
227
+ throw error;
228
+ }
229
+ }
230
+ /**
231
+ * Helper method to extract file extension from URL
232
+ * @param url - The URL to extract extension from
233
+ * @returns The file extension (e.g., '.jpg', '.png') or null
234
+ */
235
+ getExtensionFromUrl(url) {
236
+ try {
237
+ const urlPath = new URL(url).pathname;
238
+ const ext = path.extname(urlPath);
239
+ return ext || null;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
244
+ };
245
+
246
+ // src/cli.ts
247
+ var import_chalk3 = __toESM(require("chalk"));
248
+
249
+ // src/cli/env-loader.ts
250
+ var path2 = __toESM(require("path"));
251
+ var dotenv = __toESM(require("dotenv"));
252
+ var fs2 = __toESM(require("fs"));
253
+ function loadEnvironment() {
254
+ const possibleEnvPaths = [
255
+ path2.resolve(process.cwd(), ".env"),
256
+ // Current working directory
257
+ path2.resolve(__dirname, ".env"),
258
+ // Same directory as the CLI script (dist/)
259
+ path2.resolve(__dirname, "..", ".env")
260
+ // Parent directory (root of project)
261
+ ];
262
+ for (const envPath of possibleEnvPaths) {
263
+ if (fs2.existsSync(envPath)) {
264
+ dotenv.config({ path: envPath });
265
+ break;
266
+ }
267
+ }
268
+ }
269
+ function getApiKeys() {
270
+ return {
271
+ unsplash: process.env.UNSPLASH_KEY,
272
+ pexels: process.env.PEXELS_KEY,
273
+ pixabay: process.env.PIXABAY_KEY
274
+ };
275
+ }
276
+
277
+ // src/cli/display.ts
278
+ var import_chalk = __toESM(require("chalk"));
279
+ var import_boxen = __toESM(require("boxen"));
280
+ function displayError(message) {
281
+ console.error(import_chalk.default.red(`
282
+ \u274C ${message}`));
283
+ }
284
+ function displayInfo(message) {
285
+ console.log(import_chalk.default.blue(`
286
+ \u2139\uFE0F ${message}`));
287
+ }
288
+ function displayImageInfo(image, providerName) {
289
+ const content = [
290
+ `${import_chalk.default.bold("\u{1F3A8} Provider:")} ${import_chalk.default.cyan(providerName)}`,
291
+ `${import_chalk.default.bold("\u{1F4D0} Size:")} ${import_chalk.default.yellow(`${image.width}x${image.height}`)}`,
292
+ `${import_chalk.default.bold("\u{1F464} Author:")} ${import_chalk.default.magenta(image.author)}`,
293
+ image.authorUrl ? `${import_chalk.default.bold("\u{1F517} Author URL:")} ${import_chalk.default.gray(image.authorUrl)}` : "",
294
+ `${import_chalk.default.bold("\u{1F310} Original:")} ${import_chalk.default.gray(image.originalUrl)}`,
295
+ `${import_chalk.default.bold("\u{1F4F7} Image URL:")} ${import_chalk.default.gray(image.url)}`
296
+ ].filter(Boolean).join("\n");
297
+ console.log("\n" + (0, import_boxen.default)(content, {
298
+ padding: 1,
299
+ margin: 1,
300
+ borderStyle: "round",
301
+ borderColor: "green",
302
+ title: "\u{1F5BC}\uFE0F Image Details",
303
+ titleAlignment: "center"
304
+ }));
305
+ }
306
+ function displayDownloadSuccess(filePath) {
307
+ const content = `${import_chalk.default.bold("\u{1F4C1} Saved to:")} ${import_chalk.default.cyan(filePath)}`;
308
+ console.log("\n" + (0, import_boxen.default)(content, {
309
+ padding: 1,
310
+ margin: 1,
311
+ borderStyle: "round",
312
+ borderColor: "blue",
313
+ title: "\u{1F4BE} Download Complete",
314
+ titleAlignment: "center"
315
+ }));
316
+ }
317
+
318
+ // src/cli/provider-manager.ts
319
+ function selectRandomProvider(providers) {
320
+ const availableProviders = Object.entries(providers).filter(([_, key]) => key).map(([name]) => name);
321
+ if (availableProviders.length === 0) {
322
+ return null;
323
+ }
324
+ return availableProviders[Math.floor(Math.random() * availableProviders.length)];
325
+ }
326
+ function createProvider(providerName, apiKey) {
327
+ switch (providerName) {
328
+ case "unsplash":
329
+ return new UnsplashProvider(apiKey);
330
+ case "pexels":
331
+ return new PexelsProvider(apiKey);
332
+ case "pixabay":
333
+ return new PixabayProvider(apiKey);
334
+ default:
335
+ return null;
336
+ }
337
+ }
338
+ function getProviderInstance(providerName, providers) {
339
+ const name = providerName.toLowerCase();
340
+ const apiKey = providers[name];
341
+ if (!apiKey) {
342
+ displayError(`API key for ${providerName} not found.`);
343
+ console.log(`Please set ${providerName.toUpperCase()}_KEY environment variable.
344
+ `);
345
+ return null;
346
+ }
347
+ const provider = createProvider(name, apiKey);
348
+ if (!provider) {
349
+ displayError(`Unknown provider "${providerName}"`);
350
+ console.log("Available providers: unsplash, pexels, pixabay, random\n");
351
+ return null;
352
+ }
353
+ return { provider, name };
354
+ }
355
+
356
+ // src/cli/options-builder.ts
357
+ function buildFetchOptions(options) {
358
+ const fetchOptions = {};
359
+ if (options.query) fetchOptions.query = options.query;
360
+ if (options.width) fetchOptions.width = options.width;
361
+ if (options.height) fetchOptions.height = options.height;
362
+ if (options.quality) fetchOptions.quality = options.quality;
363
+ if (options.orientation) fetchOptions.orientation = options.orientation;
364
+ return fetchOptions;
365
+ }
366
+ function buildDownloadOptions(options) {
367
+ const downloadOptions = {
368
+ overwrite: options.overwrite,
369
+ keepOriginalName: options.keepOriginalName
370
+ };
371
+ if (options.filename) {
372
+ downloadOptions.filename = options.filename;
373
+ }
374
+ return downloadOptions;
375
+ }
376
+ function getDownloadPath(options) {
377
+ return typeof options.download === "string" ? options.download : "./downloads";
378
+ }
379
+
380
+ // src/cli/actions.ts
381
+ var import_ora = __toESM(require("ora"));
382
+ var import_chalk2 = __toESM(require("chalk"));
383
+ var path3 = __toESM(require("path"));
384
+ async function fetchImage(fetcher, providerName, fetchOptions) {
385
+ const spinner = (0, import_ora.default)({
386
+ text: import_chalk2.default.cyan("\u{1F50D} Searching for the perfect image..."),
387
+ spinner: "dots"
388
+ }).start();
389
+ try {
390
+ const image = await fetcher.getRandom(fetchOptions);
391
+ spinner.succeed(import_chalk2.default.green("\u2728 Image found!"));
392
+ return image;
393
+ } catch (error) {
394
+ spinner.fail(import_chalk2.default.red("Failed to fetch image"));
395
+ throw error;
396
+ }
397
+ }
398
+ async function downloadImage(fetcher, image, downloadPath, downloadOptions) {
399
+ const resolvedPath = path3.resolve(downloadPath);
400
+ displayInfo(`Downloading to: ${import_chalk2.default.cyan(resolvedPath)}`);
401
+ const spinner = (0, import_ora.default)({
402
+ text: import_chalk2.default.cyan("\u{1F4BE} Downloading image..."),
403
+ spinner: "dots"
404
+ }).start();
405
+ try {
406
+ const filePath = await fetcher.download(image, downloadPath, downloadOptions);
407
+ spinner.succeed(import_chalk2.default.green("\u{1F4E6} Download complete!"));
408
+ return filePath;
409
+ } catch (error) {
410
+ spinner.fail(import_chalk2.default.red("Download failed"));
411
+ throw error;
412
+ }
413
+ }
414
+
415
+ // src/cli.ts
416
+ loadEnvironment();
417
+ var program = new import_commander.Command();
418
+ program.name("random-image").description("\u{1F3A8} CLI tool to fetch random images from various providers").version("1.2.0");
419
+ program.command("fetch").description("\u{1F5BC}\uFE0F Fetch a random image from image providers").option("-q, --query <search>", "Search query for the image").option("-w, --width <number>", "Width of the image", parseInt).option("-h, --height <number>", "Height of the image", parseInt).option("--quality <number>", "Quality of the image (0-100)", parseInt).option("--orientation <type>", "Image orientation (landscape or portrait)").option("-p, --provider <name>", "Provider to use (unsplash, pexels, pixabay, or random)", "random").option("-d, --download [path]", "Download the image to specified directory (default: ./downloads)").option("--filename <name>", "Custom filename for downloaded image").option("--overwrite", "Overwrite existing files", false).option("--no-keep-original-name", "Generate random UUID filename instead of keeping original").action(async (options) => {
420
+ try {
421
+ const providers = getApiKeys();
422
+ let providerName = options.provider.toLowerCase();
423
+ if (providerName === "random") {
424
+ const selectedProvider = selectRandomProvider(providers);
425
+ if (!selectedProvider) {
426
+ displayError("No API keys found in environment variables.");
427
+ console.log(import_chalk3.default.yellow("\n\u{1F4A1} Please set at least one of:"));
428
+ console.log(import_chalk3.default.cyan(" \u2022 UNSPLASH_KEY"));
429
+ console.log(import_chalk3.default.cyan(" \u2022 PEXELS_KEY"));
430
+ console.log(import_chalk3.default.cyan(" \u2022 PIXABAY_KEY\n"));
431
+ process.exit(1);
432
+ }
433
+ providerName = selectedProvider;
434
+ displayInfo(`Using random provider: ${import_chalk3.default.bold(providerName)}`);
435
+ }
436
+ const result = getProviderInstance(providerName, providers);
437
+ if (!result) {
438
+ process.exit(1);
439
+ }
440
+ const { provider, name } = result;
441
+ const fetcher = new RandomImage(provider);
442
+ const fetchOptions = buildFetchOptions(options);
443
+ const image = await fetchImage(fetcher, name, fetchOptions);
444
+ displayImageInfo(image, name);
445
+ if (options.download !== void 0) {
446
+ const downloadPath = getDownloadPath(options);
447
+ const downloadOptions = buildDownloadOptions(options);
448
+ const filePath = await downloadImage(
449
+ fetcher,
450
+ image,
451
+ downloadPath,
452
+ downloadOptions
453
+ );
454
+ displayDownloadSuccess(filePath);
455
+ }
456
+ } catch (error) {
457
+ if (error instanceof Error) {
458
+ displayError(error.message);
459
+ } else {
460
+ displayError("An unexpected error occurred");
461
+ }
462
+ process.exit(1);
463
+ }
464
+ });
465
+ program.parse(process.argv);
package/dist/cli.mjs ADDED
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __commonJS = (cb, mod) => function __require() {
7
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
8
+ };
9
+
10
+ // src/types.ts
11
+ var init_types = __esm({
12
+ "src/types.ts"() {
13
+ "use strict";
14
+ }
15
+ });
16
+
17
+ // src/providers/unsplash.ts
18
+ import axios from "axios";
19
+ var UnsplashProvider;
20
+ var init_unsplash = __esm({
21
+ "src/providers/unsplash.ts"() {
22
+ "use strict";
23
+ UnsplashProvider = class {
24
+ constructor(accessKey) {
25
+ this.accessKey = accessKey;
26
+ }
27
+ async fetchRandomImage(options) {
28
+ const response = await axios.get("https://api.unsplash.com/photos/random", {
29
+ headers: {
30
+ Authorization: `Client-ID ${this.accessKey}`
31
+ },
32
+ params: {
33
+ query: options.query,
34
+ orientation: options.orientation
35
+ }
36
+ });
37
+ const data = response.data;
38
+ const photo = Array.isArray(data) ? data[0] : data;
39
+ const baseUrl = photo.urls.raw;
40
+ const sizeParams = new URLSearchParams();
41
+ if (options.height || options.width) {
42
+ sizeParams.append("fit", "crop");
43
+ sizeParams.append("crop", "entropy");
44
+ }
45
+ if (options.width) sizeParams.append("w", options.width.toString());
46
+ if (options.height) sizeParams.append("h", options.height.toString());
47
+ if (options.quality) sizeParams.append("q", options.quality.toString());
48
+ const finalUrl = `${baseUrl}&${sizeParams.toString()}`;
49
+ return {
50
+ url: finalUrl,
51
+ width: options.width || photo.width,
52
+ height: options.height || photo.height,
53
+ author: photo.user.name,
54
+ authorUrl: photo.user.links.html,
55
+ originalUrl: photo.links.html
56
+ // Link to photo page
57
+ };
58
+ }
59
+ };
60
+ }
61
+ });
62
+
63
+ // src/providers/pexels.ts
64
+ import axios2 from "axios";
65
+ var PexelsProvider;
66
+ var init_pexels = __esm({
67
+ "src/providers/pexels.ts"() {
68
+ "use strict";
69
+ PexelsProvider = class {
70
+ constructor(apiKey) {
71
+ this.apiKey = apiKey;
72
+ }
73
+ async fetchRandomImage(options) {
74
+ const endpoint = options.query ? "https://api.pexels.com/v1/search" : "https://api.pexels.com/v1/curated";
75
+ const randomPage = Math.floor(Math.random() * 100) + 1;
76
+ const params = {
77
+ per_page: 1,
78
+ page: randomPage
79
+ };
80
+ if (options.query) params.query = options.query;
81
+ const response = await axios2.get(endpoint, {
82
+ headers: {
83
+ Authorization: this.apiKey
84
+ },
85
+ params
86
+ });
87
+ const data = response.data;
88
+ if (!data.photos || data.photos.length === 0) {
89
+ throw new Error("No images found on Pexels");
90
+ }
91
+ const photo = data.photos[0];
92
+ const baseUrl = photo.src.original;
93
+ const sizeParams = new URLSearchParams();
94
+ sizeParams.append("auto", "compress");
95
+ sizeParams.append("cs", "tinysrgb");
96
+ if (options.width) sizeParams.append("w", options.width.toString());
97
+ if (options.height) sizeParams.append("h", options.height.toString());
98
+ const finalUrl = `${baseUrl}?${sizeParams.toString()}`;
99
+ return {
100
+ url: finalUrl,
101
+ width: options.width || photo.width,
102
+ height: options.height || photo.height,
103
+ author: photo.photographer,
104
+ authorUrl: photo.photographer_url,
105
+ originalUrl: photo.url
106
+ };
107
+ }
108
+ };
109
+ }
110
+ });
111
+
112
+ // src/providers/pixabay.ts
113
+ import axios3 from "axios";
114
+ var PixabayProvider;
115
+ var init_pixabay = __esm({
116
+ "src/providers/pixabay.ts"() {
117
+ "use strict";
118
+ PixabayProvider = class {
119
+ constructor(apiKey) {
120
+ this.apiKey = apiKey;
121
+ }
122
+ async fetchRandomImage(options) {
123
+ const params = {
124
+ key: this.apiKey,
125
+ q: options.query || "",
126
+ per_page: 20
127
+ // Fetch a few to pick randomly
128
+ };
129
+ if (options.orientation) {
130
+ params.orientation = options.orientation === "portrait" ? "vertical" : "horizontal";
131
+ }
132
+ const response = await axios3.get("https://pixabay.com/api/", {
133
+ params
134
+ });
135
+ const hits = response.data.hits;
136
+ if (!hits || hits.length === 0) {
137
+ throw new Error("No images found");
138
+ }
139
+ const randomHit = hits[Math.floor(Math.random() * hits.length)];
140
+ return {
141
+ url: randomHit.largeImageURL || randomHit.webformatURL,
142
+ width: randomHit.imageWidth || randomHit.webformatWidth,
143
+ height: randomHit.imageHeight || randomHit.webformatHeight,
144
+ author: randomHit.user,
145
+ authorUrl: `https://pixabay.com/users/${randomHit.user}-${randomHit.user_id}/`,
146
+ originalUrl: randomHit.pageURL
147
+ };
148
+ }
149
+ };
150
+ }
151
+ });
152
+
153
+ // src/image-fetcher.ts
154
+ import * as fs from "fs";
155
+ import * as path from "path";
156
+ import axios4 from "axios";
157
+ import { v7 as uuidv4 } from "uuid";
158
+ var RandomImage;
159
+ var init_image_fetcher = __esm({
160
+ "src/image-fetcher.ts"() {
161
+ "use strict";
162
+ RandomImage = class {
163
+ constructor(provider) {
164
+ this.provider = provider;
165
+ }
166
+ /**
167
+ * Fetches a random image based on the provided options.
168
+ * @param options - Configuration options for the image (width, height, query, etc.)
169
+ * @returns A promise that resolves to an ImageResult object.
170
+ */
171
+ async getRandom(options = {}) {
172
+ return this.provider.fetchRandomImage(options);
173
+ }
174
+ /**
175
+ * Downloads an image to a specified directory.
176
+ * @param imageUrl - The URL of the image to download (can be a string or ImageResult object)
177
+ * @param destinationPath - The directory path where the image will be saved
178
+ * @param options - Download options (filename, overwrite, etc.)
179
+ * @returns A promise that resolves to the full path of the downloaded file
180
+ */
181
+ async download(imageUrl, destinationPath, options = {}) {
182
+ const url = typeof imageUrl === "string" ? imageUrl : imageUrl.url;
183
+ if (!fs.existsSync(destinationPath)) {
184
+ fs.mkdirSync(destinationPath, { recursive: true });
185
+ }
186
+ let finalFilename;
187
+ if (options.filename) {
188
+ finalFilename = options.filename;
189
+ } else if (options.keepOriginalName) {
190
+ const urlPath = new URL(url).pathname;
191
+ const urlFilename = path.basename(urlPath);
192
+ if (urlFilename && urlFilename.length > 0 && urlFilename !== "/") {
193
+ finalFilename = urlFilename;
194
+ } else {
195
+ const ext = this.getExtensionFromUrl(url) || ".jpg";
196
+ finalFilename = `${uuidv4()}${ext}`;
197
+ }
198
+ } else {
199
+ const ext = this.getExtensionFromUrl(url) || ".jpg";
200
+ finalFilename = `${uuidv4()}${ext}`;
201
+ }
202
+ if (!path.extname(finalFilename)) {
203
+ finalFilename += ".jpg";
204
+ }
205
+ const fullPath = path.join(destinationPath, finalFilename);
206
+ if (fs.existsSync(fullPath) && !options.overwrite) {
207
+ throw new Error(`File already exists: ${fullPath}. Set overwrite: true to replace it.`);
208
+ }
209
+ try {
210
+ const response = await axios4.get(url, {
211
+ responseType: "stream",
212
+ maxRedirects: 5
213
+ // Automatically handle redirects
214
+ });
215
+ const writer = fs.createWriteStream(fullPath);
216
+ response.data.pipe(writer);
217
+ return new Promise((resolve3, reject) => {
218
+ writer.on("finish", () => {
219
+ resolve3(fullPath);
220
+ });
221
+ writer.on("error", (err) => {
222
+ fs.unlink(fullPath, () => {
223
+ });
224
+ reject(err);
225
+ });
226
+ response.data.on("error", (err) => {
227
+ writer.close();
228
+ fs.unlink(fullPath, () => {
229
+ });
230
+ reject(err);
231
+ });
232
+ });
233
+ } catch (error) {
234
+ if (axios4.isAxiosError(error)) {
235
+ throw new Error(`Failed to download image: ${error.message}`);
236
+ }
237
+ throw error;
238
+ }
239
+ }
240
+ /**
241
+ * Helper method to extract file extension from URL
242
+ * @param url - The URL to extract extension from
243
+ * @returns The file extension (e.g., '.jpg', '.png') or null
244
+ */
245
+ getExtensionFromUrl(url) {
246
+ try {
247
+ const urlPath = new URL(url).pathname;
248
+ const ext = path.extname(urlPath);
249
+ return ext || null;
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ };
255
+ }
256
+ });
257
+
258
+ // src/index.ts
259
+ var init_index = __esm({
260
+ "src/index.ts"() {
261
+ "use strict";
262
+ init_types();
263
+ init_unsplash();
264
+ init_pexels();
265
+ init_pixabay();
266
+ init_image_fetcher();
267
+ }
268
+ });
269
+
270
+ // src/cli/env-loader.ts
271
+ import * as path2 from "path";
272
+ import * as dotenv from "dotenv";
273
+ import * as fs2 from "fs";
274
+ function loadEnvironment() {
275
+ const possibleEnvPaths = [
276
+ path2.resolve(process.cwd(), ".env"),
277
+ // Current working directory
278
+ path2.resolve(__dirname, ".env"),
279
+ // Same directory as the CLI script (dist/)
280
+ path2.resolve(__dirname, "..", ".env")
281
+ // Parent directory (root of project)
282
+ ];
283
+ for (const envPath of possibleEnvPaths) {
284
+ if (fs2.existsSync(envPath)) {
285
+ dotenv.config({ path: envPath });
286
+ break;
287
+ }
288
+ }
289
+ }
290
+ function getApiKeys() {
291
+ return {
292
+ unsplash: process.env.UNSPLASH_KEY,
293
+ pexels: process.env.PEXELS_KEY,
294
+ pixabay: process.env.PIXABAY_KEY
295
+ };
296
+ }
297
+ var init_env_loader = __esm({
298
+ "src/cli/env-loader.ts"() {
299
+ "use strict";
300
+ }
301
+ });
302
+
303
+ // src/cli/display.ts
304
+ import chalk from "chalk";
305
+ import boxen from "boxen";
306
+ function displayError(message) {
307
+ console.error(chalk.red(`
308
+ \u274C ${message}`));
309
+ }
310
+ function displayInfo(message) {
311
+ console.log(chalk.blue(`
312
+ \u2139\uFE0F ${message}`));
313
+ }
314
+ function displayImageInfo(image, providerName) {
315
+ const content = [
316
+ `${chalk.bold("\u{1F3A8} Provider:")} ${chalk.cyan(providerName)}`,
317
+ `${chalk.bold("\u{1F4D0} Size:")} ${chalk.yellow(`${image.width}x${image.height}`)}`,
318
+ `${chalk.bold("\u{1F464} Author:")} ${chalk.magenta(image.author)}`,
319
+ image.authorUrl ? `${chalk.bold("\u{1F517} Author URL:")} ${chalk.gray(image.authorUrl)}` : "",
320
+ `${chalk.bold("\u{1F310} Original:")} ${chalk.gray(image.originalUrl)}`,
321
+ `${chalk.bold("\u{1F4F7} Image URL:")} ${chalk.gray(image.url)}`
322
+ ].filter(Boolean).join("\n");
323
+ console.log("\n" + boxen(content, {
324
+ padding: 1,
325
+ margin: 1,
326
+ borderStyle: "round",
327
+ borderColor: "green",
328
+ title: "\u{1F5BC}\uFE0F Image Details",
329
+ titleAlignment: "center"
330
+ }));
331
+ }
332
+ function displayDownloadSuccess(filePath) {
333
+ const content = `${chalk.bold("\u{1F4C1} Saved to:")} ${chalk.cyan(filePath)}`;
334
+ console.log("\n" + boxen(content, {
335
+ padding: 1,
336
+ margin: 1,
337
+ borderStyle: "round",
338
+ borderColor: "blue",
339
+ title: "\u{1F4BE} Download Complete",
340
+ titleAlignment: "center"
341
+ }));
342
+ }
343
+ var init_display = __esm({
344
+ "src/cli/display.ts"() {
345
+ "use strict";
346
+ }
347
+ });
348
+
349
+ // src/cli/provider-manager.ts
350
+ function selectRandomProvider(providers) {
351
+ const availableProviders = Object.entries(providers).filter(([_, key]) => key).map(([name]) => name);
352
+ if (availableProviders.length === 0) {
353
+ return null;
354
+ }
355
+ return availableProviders[Math.floor(Math.random() * availableProviders.length)];
356
+ }
357
+ function createProvider(providerName, apiKey) {
358
+ switch (providerName) {
359
+ case "unsplash":
360
+ return new UnsplashProvider(apiKey);
361
+ case "pexels":
362
+ return new PexelsProvider(apiKey);
363
+ case "pixabay":
364
+ return new PixabayProvider(apiKey);
365
+ default:
366
+ return null;
367
+ }
368
+ }
369
+ function getProviderInstance(providerName, providers) {
370
+ const name = providerName.toLowerCase();
371
+ const apiKey = providers[name];
372
+ if (!apiKey) {
373
+ displayError(`API key for ${providerName} not found.`);
374
+ console.log(`Please set ${providerName.toUpperCase()}_KEY environment variable.
375
+ `);
376
+ return null;
377
+ }
378
+ const provider = createProvider(name, apiKey);
379
+ if (!provider) {
380
+ displayError(`Unknown provider "${providerName}"`);
381
+ console.log("Available providers: unsplash, pexels, pixabay, random\n");
382
+ return null;
383
+ }
384
+ return { provider, name };
385
+ }
386
+ var init_provider_manager = __esm({
387
+ "src/cli/provider-manager.ts"() {
388
+ "use strict";
389
+ init_index();
390
+ init_display();
391
+ }
392
+ });
393
+
394
+ // src/cli/options-builder.ts
395
+ function buildFetchOptions(options) {
396
+ const fetchOptions = {};
397
+ if (options.query) fetchOptions.query = options.query;
398
+ if (options.width) fetchOptions.width = options.width;
399
+ if (options.height) fetchOptions.height = options.height;
400
+ if (options.quality) fetchOptions.quality = options.quality;
401
+ if (options.orientation) fetchOptions.orientation = options.orientation;
402
+ return fetchOptions;
403
+ }
404
+ function buildDownloadOptions(options) {
405
+ const downloadOptions = {
406
+ overwrite: options.overwrite,
407
+ keepOriginalName: options.keepOriginalName
408
+ };
409
+ if (options.filename) {
410
+ downloadOptions.filename = options.filename;
411
+ }
412
+ return downloadOptions;
413
+ }
414
+ function getDownloadPath(options) {
415
+ return typeof options.download === "string" ? options.download : "./downloads";
416
+ }
417
+ var init_options_builder = __esm({
418
+ "src/cli/options-builder.ts"() {
419
+ "use strict";
420
+ }
421
+ });
422
+
423
+ // src/cli/actions.ts
424
+ import ora from "ora";
425
+ import chalk2 from "chalk";
426
+ import * as path3 from "path";
427
+ async function fetchImage(fetcher, providerName, fetchOptions) {
428
+ const spinner = ora({
429
+ text: chalk2.cyan("\u{1F50D} Searching for the perfect image..."),
430
+ spinner: "dots"
431
+ }).start();
432
+ try {
433
+ const image = await fetcher.getRandom(fetchOptions);
434
+ spinner.succeed(chalk2.green("\u2728 Image found!"));
435
+ return image;
436
+ } catch (error) {
437
+ spinner.fail(chalk2.red("Failed to fetch image"));
438
+ throw error;
439
+ }
440
+ }
441
+ async function downloadImage(fetcher, image, downloadPath, downloadOptions) {
442
+ const resolvedPath = path3.resolve(downloadPath);
443
+ displayInfo(`Downloading to: ${chalk2.cyan(resolvedPath)}`);
444
+ const spinner = ora({
445
+ text: chalk2.cyan("\u{1F4BE} Downloading image..."),
446
+ spinner: "dots"
447
+ }).start();
448
+ try {
449
+ const filePath = await fetcher.download(image, downloadPath, downloadOptions);
450
+ spinner.succeed(chalk2.green("\u{1F4E6} Download complete!"));
451
+ return filePath;
452
+ } catch (error) {
453
+ spinner.fail(chalk2.red("Download failed"));
454
+ throw error;
455
+ }
456
+ }
457
+ var init_actions = __esm({
458
+ "src/cli/actions.ts"() {
459
+ "use strict";
460
+ init_display();
461
+ }
462
+ });
463
+
464
+ // src/cli.ts
465
+ import { Command } from "commander";
466
+ import chalk3 from "chalk";
467
+ var require_cli = __commonJS({
468
+ "src/cli.ts"() {
469
+ init_index();
470
+ init_env_loader();
471
+ init_display();
472
+ init_provider_manager();
473
+ init_options_builder();
474
+ init_actions();
475
+ loadEnvironment();
476
+ var program = new Command();
477
+ program.name("random-image").description("\u{1F3A8} CLI tool to fetch random images from various providers").version("1.2.0");
478
+ program.command("fetch").description("\u{1F5BC}\uFE0F Fetch a random image from image providers").option("-q, --query <search>", "Search query for the image").option("-w, --width <number>", "Width of the image", parseInt).option("-h, --height <number>", "Height of the image", parseInt).option("--quality <number>", "Quality of the image (0-100)", parseInt).option("--orientation <type>", "Image orientation (landscape or portrait)").option("-p, --provider <name>", "Provider to use (unsplash, pexels, pixabay, or random)", "random").option("-d, --download [path]", "Download the image to specified directory (default: ./downloads)").option("--filename <name>", "Custom filename for downloaded image").option("--overwrite", "Overwrite existing files", false).option("--no-keep-original-name", "Generate random UUID filename instead of keeping original").action(async (options) => {
479
+ try {
480
+ const providers = getApiKeys();
481
+ let providerName = options.provider.toLowerCase();
482
+ if (providerName === "random") {
483
+ const selectedProvider = selectRandomProvider(providers);
484
+ if (!selectedProvider) {
485
+ displayError("No API keys found in environment variables.");
486
+ console.log(chalk3.yellow("\n\u{1F4A1} Please set at least one of:"));
487
+ console.log(chalk3.cyan(" \u2022 UNSPLASH_KEY"));
488
+ console.log(chalk3.cyan(" \u2022 PEXELS_KEY"));
489
+ console.log(chalk3.cyan(" \u2022 PIXABAY_KEY\n"));
490
+ process.exit(1);
491
+ }
492
+ providerName = selectedProvider;
493
+ displayInfo(`Using random provider: ${chalk3.bold(providerName)}`);
494
+ }
495
+ const result = getProviderInstance(providerName, providers);
496
+ if (!result) {
497
+ process.exit(1);
498
+ }
499
+ const { provider, name } = result;
500
+ const fetcher = new RandomImage(provider);
501
+ const fetchOptions = buildFetchOptions(options);
502
+ const image = await fetchImage(fetcher, name, fetchOptions);
503
+ displayImageInfo(image, name);
504
+ if (options.download !== void 0) {
505
+ const downloadPath = getDownloadPath(options);
506
+ const downloadOptions = buildDownloadOptions(options);
507
+ const filePath = await downloadImage(
508
+ fetcher,
509
+ image,
510
+ downloadPath,
511
+ downloadOptions
512
+ );
513
+ displayDownloadSuccess(filePath);
514
+ }
515
+ } catch (error) {
516
+ if (error instanceof Error) {
517
+ displayError(error.message);
518
+ } else {
519
+ displayError("An unexpected error occurred");
520
+ }
521
+ process.exit(1);
522
+ }
523
+ });
524
+ program.parse(process.argv);
525
+ }
526
+ });
527
+ export default require_cli();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nghiavuive/random-image",
3
- "version": "1.1.0",
4
- "description": "",
3
+ "version": "1.2.1",
4
+ "description": "Fetch random images from Unsplash, Pexels, and Pixabay with CLI support",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
@@ -32,13 +32,20 @@
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.2.0",
34
34
  "@types/uuid": "^10.0.0",
35
- "dotenv": "^17.2.3",
36
35
  "tsup": "^8.5.1",
37
36
  "typescript": "^5.9.3",
38
37
  "vitest": "^4.0.18"
39
38
  },
40
39
  "dependencies": {
41
40
  "axios": "^1.13.4",
41
+ "boxen": "^8.0.1",
42
+ "chalk": "^5.6.2",
43
+ "commander": "^12.1.0",
44
+ "dotenv": "^17.2.3",
45
+ "ora": "^9.1.0",
42
46
  "uuid": "^13.0.0"
47
+ },
48
+ "bin": {
49
+ "random-image": "./dist/cli.js"
43
50
  }
44
51
  }