@shotapi/sdk 1.0.0 → 1.0.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.
- package/dist/index.d.mts +27 -9
- package/dist/index.d.ts +27 -9
- package/dist/index.js +104 -27
- package/dist/index.mjs +104 -27
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -147,6 +147,19 @@ interface ShotAPIConfig {
|
|
|
147
147
|
* Number of retry attempts on failure (default: 2)
|
|
148
148
|
*/
|
|
149
149
|
retries?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Maximum concurrent requests for batch operations (default: 5)
|
|
152
|
+
*/
|
|
153
|
+
maxConcurrent?: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Device preset dimensions
|
|
157
|
+
*/
|
|
158
|
+
interface DevicePresetConfig {
|
|
159
|
+
width: number;
|
|
160
|
+
height: number;
|
|
161
|
+
mobile: boolean;
|
|
162
|
+
scale: number;
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
/**
|
|
@@ -163,7 +176,7 @@ declare class ShotAPIException extends Error {
|
|
|
163
176
|
*
|
|
164
177
|
* @example
|
|
165
178
|
* ```typescript
|
|
166
|
-
* import ShotAPI from 'shotapi';
|
|
179
|
+
* import ShotAPI from '@shotapi/sdk';
|
|
167
180
|
*
|
|
168
181
|
* const client = new ShotAPI({ apiKey: 'your-api-key' });
|
|
169
182
|
*
|
|
@@ -185,6 +198,7 @@ declare class ShotAPI {
|
|
|
185
198
|
private readonly baseUrl;
|
|
186
199
|
private readonly timeout;
|
|
187
200
|
private readonly retries;
|
|
201
|
+
private readonly maxConcurrent;
|
|
188
202
|
constructor(config: ShotAPIConfig);
|
|
189
203
|
/**
|
|
190
204
|
* Capture a screenshot of a URL
|
|
@@ -218,11 +232,20 @@ declare class ShotAPI {
|
|
|
218
232
|
*/
|
|
219
233
|
screenshotToFile(options: ScreenshotOptions, filePath: string): Promise<ScreenshotResponse>;
|
|
220
234
|
/**
|
|
221
|
-
* Capture multiple screenshots in batch
|
|
235
|
+
* Capture multiple screenshots in batch with rate limiting
|
|
222
236
|
*
|
|
223
237
|
* @param urls - Array of URLs to capture
|
|
224
238
|
* @param options - Common options for all screenshots
|
|
225
239
|
* @returns Array of screenshot responses
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const results = await client.batch([
|
|
244
|
+
* 'https://example.com',
|
|
245
|
+
* 'https://google.com',
|
|
246
|
+
* 'https://github.com'
|
|
247
|
+
* ], { width: 1280 });
|
|
248
|
+
* ```
|
|
226
249
|
*/
|
|
227
250
|
batch(urls: string[], options?: Omit<ScreenshotOptions, 'url'>): Promise<ScreenshotResponse[]>;
|
|
228
251
|
/**
|
|
@@ -231,12 +254,7 @@ declare class ShotAPI {
|
|
|
231
254
|
* @param device - Device preset name
|
|
232
255
|
* @returns Device dimensions
|
|
233
256
|
*/
|
|
234
|
-
static getDevicePreset(device: DevicePreset):
|
|
235
|
-
width: number;
|
|
236
|
-
height: number;
|
|
237
|
-
mobile: boolean;
|
|
238
|
-
scale: number;
|
|
239
|
-
};
|
|
257
|
+
static getDevicePreset(device: DevicePreset): DevicePresetConfig;
|
|
240
258
|
/**
|
|
241
259
|
* List all available device presets
|
|
242
260
|
*/
|
|
@@ -259,4 +277,4 @@ declare class ShotAPI {
|
|
|
259
277
|
private sleep;
|
|
260
278
|
}
|
|
261
279
|
|
|
262
|
-
export { type DevicePreset, type ImageFormat, type ScreenshotOptions, type ScreenshotResponse, ShotAPI, type ShotAPIConfig, type ShotAPIError, ShotAPIException, ShotAPI as default };
|
|
280
|
+
export { type DevicePreset, type DevicePresetConfig, type ImageFormat, type ScreenshotOptions, type ScreenshotResponse, ShotAPI, type ShotAPIConfig, type ShotAPIError, ShotAPIException, ShotAPI as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -147,6 +147,19 @@ interface ShotAPIConfig {
|
|
|
147
147
|
* Number of retry attempts on failure (default: 2)
|
|
148
148
|
*/
|
|
149
149
|
retries?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Maximum concurrent requests for batch operations (default: 5)
|
|
152
|
+
*/
|
|
153
|
+
maxConcurrent?: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Device preset dimensions
|
|
157
|
+
*/
|
|
158
|
+
interface DevicePresetConfig {
|
|
159
|
+
width: number;
|
|
160
|
+
height: number;
|
|
161
|
+
mobile: boolean;
|
|
162
|
+
scale: number;
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
/**
|
|
@@ -163,7 +176,7 @@ declare class ShotAPIException extends Error {
|
|
|
163
176
|
*
|
|
164
177
|
* @example
|
|
165
178
|
* ```typescript
|
|
166
|
-
* import ShotAPI from 'shotapi';
|
|
179
|
+
* import ShotAPI from '@shotapi/sdk';
|
|
167
180
|
*
|
|
168
181
|
* const client = new ShotAPI({ apiKey: 'your-api-key' });
|
|
169
182
|
*
|
|
@@ -185,6 +198,7 @@ declare class ShotAPI {
|
|
|
185
198
|
private readonly baseUrl;
|
|
186
199
|
private readonly timeout;
|
|
187
200
|
private readonly retries;
|
|
201
|
+
private readonly maxConcurrent;
|
|
188
202
|
constructor(config: ShotAPIConfig);
|
|
189
203
|
/**
|
|
190
204
|
* Capture a screenshot of a URL
|
|
@@ -218,11 +232,20 @@ declare class ShotAPI {
|
|
|
218
232
|
*/
|
|
219
233
|
screenshotToFile(options: ScreenshotOptions, filePath: string): Promise<ScreenshotResponse>;
|
|
220
234
|
/**
|
|
221
|
-
* Capture multiple screenshots in batch
|
|
235
|
+
* Capture multiple screenshots in batch with rate limiting
|
|
222
236
|
*
|
|
223
237
|
* @param urls - Array of URLs to capture
|
|
224
238
|
* @param options - Common options for all screenshots
|
|
225
239
|
* @returns Array of screenshot responses
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const results = await client.batch([
|
|
244
|
+
* 'https://example.com',
|
|
245
|
+
* 'https://google.com',
|
|
246
|
+
* 'https://github.com'
|
|
247
|
+
* ], { width: 1280 });
|
|
248
|
+
* ```
|
|
226
249
|
*/
|
|
227
250
|
batch(urls: string[], options?: Omit<ScreenshotOptions, 'url'>): Promise<ScreenshotResponse[]>;
|
|
228
251
|
/**
|
|
@@ -231,12 +254,7 @@ declare class ShotAPI {
|
|
|
231
254
|
* @param device - Device preset name
|
|
232
255
|
* @returns Device dimensions
|
|
233
256
|
*/
|
|
234
|
-
static getDevicePreset(device: DevicePreset):
|
|
235
|
-
width: number;
|
|
236
|
-
height: number;
|
|
237
|
-
mobile: boolean;
|
|
238
|
-
scale: number;
|
|
239
|
-
};
|
|
257
|
+
static getDevicePreset(device: DevicePreset): DevicePresetConfig;
|
|
240
258
|
/**
|
|
241
259
|
* List all available device presets
|
|
242
260
|
*/
|
|
@@ -259,4 +277,4 @@ declare class ShotAPI {
|
|
|
259
277
|
private sleep;
|
|
260
278
|
}
|
|
261
279
|
|
|
262
|
-
export { type DevicePreset, type ImageFormat, type ScreenshotOptions, type ScreenshotResponse, ShotAPI, type ShotAPIConfig, type ShotAPIError, ShotAPIException, ShotAPI as default };
|
|
280
|
+
export { type DevicePreset, type DevicePresetConfig, type ImageFormat, type ScreenshotOptions, type ScreenshotResponse, ShotAPI, type ShotAPIConfig, type ShotAPIError, ShotAPIException, ShotAPI as default };
|
package/dist/index.js
CHANGED
|
@@ -57,15 +57,24 @@ var DEVICE_PRESETS = {
|
|
|
57
57
|
"galaxy-s23": { width: 360, height: 780, mobile: true, scale: 3 },
|
|
58
58
|
"pixel-7": { width: 412, height: 915, mobile: true, scale: 2.625 }
|
|
59
59
|
};
|
|
60
|
+
function isValidUrl(url) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = new URL(url);
|
|
63
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
60
68
|
var ShotAPI = class {
|
|
61
69
|
constructor(config) {
|
|
62
70
|
if (!config.apiKey) {
|
|
63
|
-
throw new ShotAPIException("API key is required");
|
|
71
|
+
throw new ShotAPIException("API key is required", "MISSING_API_KEY");
|
|
64
72
|
}
|
|
65
73
|
this.apiKey = config.apiKey;
|
|
66
74
|
this.baseUrl = config.baseUrl || "https://shotapi.dev";
|
|
67
75
|
this.timeout = config.timeout || 3e4;
|
|
68
76
|
this.retries = config.retries ?? 2;
|
|
77
|
+
this.maxConcurrent = config.maxConcurrent ?? 5;
|
|
69
78
|
}
|
|
70
79
|
/**
|
|
71
80
|
* Capture a screenshot of a URL
|
|
@@ -85,7 +94,29 @@ var ShotAPI = class {
|
|
|
85
94
|
*/
|
|
86
95
|
async screenshot(options) {
|
|
87
96
|
if (!options.url) {
|
|
88
|
-
throw new ShotAPIException("URL is required");
|
|
97
|
+
throw new ShotAPIException("URL is required", "MISSING_URL");
|
|
98
|
+
}
|
|
99
|
+
if (!isValidUrl(options.url)) {
|
|
100
|
+
throw new ShotAPIException(
|
|
101
|
+
`Invalid URL: ${options.url}. URL must start with http:// or https://`,
|
|
102
|
+
"INVALID_URL"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (options.quality !== void 0) {
|
|
106
|
+
if (options.quality < 1 || options.quality > 100) {
|
|
107
|
+
throw new ShotAPIException(
|
|
108
|
+
"Quality must be between 1 and 100",
|
|
109
|
+
"INVALID_QUALITY"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (options.delay !== void 0) {
|
|
114
|
+
if (options.delay < 0 || options.delay > 1e4) {
|
|
115
|
+
throw new ShotAPIException(
|
|
116
|
+
"Delay must be between 0 and 10000 milliseconds",
|
|
117
|
+
"INVALID_DELAY"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
89
120
|
}
|
|
90
121
|
const params = this.buildParams(options);
|
|
91
122
|
return this.request("/api/v1/screenshot", params);
|
|
@@ -98,12 +129,7 @@ var ShotAPI = class {
|
|
|
98
129
|
*/
|
|
99
130
|
async screenshotBuffer(options) {
|
|
100
131
|
const response = await this.screenshot(options);
|
|
101
|
-
|
|
102
|
-
if (!imageResponse.ok) {
|
|
103
|
-
throw new ShotAPIException("Failed to fetch screenshot image");
|
|
104
|
-
}
|
|
105
|
-
const arrayBuffer = await imageResponse.arrayBuffer();
|
|
106
|
-
return Buffer.from(arrayBuffer);
|
|
132
|
+
return this.fetchBuffer(response.url);
|
|
107
133
|
}
|
|
108
134
|
/**
|
|
109
135
|
* Capture a screenshot and save to a file
|
|
@@ -115,21 +141,42 @@ var ShotAPI = class {
|
|
|
115
141
|
const response = await this.screenshot(options);
|
|
116
142
|
const buffer = await this.fetchBuffer(response.url);
|
|
117
143
|
const fs = await import("fs/promises");
|
|
144
|
+
const path = await import("path");
|
|
145
|
+
const dir = path.dirname(filePath);
|
|
146
|
+
await fs.mkdir(dir, { recursive: true });
|
|
118
147
|
await fs.writeFile(filePath, buffer);
|
|
119
148
|
return response;
|
|
120
149
|
}
|
|
121
150
|
/**
|
|
122
|
-
* Capture multiple screenshots in batch
|
|
151
|
+
* Capture multiple screenshots in batch with rate limiting
|
|
123
152
|
*
|
|
124
153
|
* @param urls - Array of URLs to capture
|
|
125
154
|
* @param options - Common options for all screenshots
|
|
126
155
|
* @returns Array of screenshot responses
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const results = await client.batch([
|
|
160
|
+
* 'https://example.com',
|
|
161
|
+
* 'https://google.com',
|
|
162
|
+
* 'https://github.com'
|
|
163
|
+
* ], { width: 1280 });
|
|
164
|
+
* ```
|
|
127
165
|
*/
|
|
128
166
|
async batch(urls, options) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
const results = [];
|
|
168
|
+
for (let i = 0; i < urls.length; i += this.maxConcurrent) {
|
|
169
|
+
const chunk = urls.slice(i, i + this.maxConcurrent);
|
|
170
|
+
const promises = chunk.map(
|
|
171
|
+
(url) => this.screenshot({ ...options, url })
|
|
172
|
+
);
|
|
173
|
+
const chunkResults = await Promise.all(promises);
|
|
174
|
+
results.push(...chunkResults);
|
|
175
|
+
if (i + this.maxConcurrent < urls.length) {
|
|
176
|
+
await this.sleep(100);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return results;
|
|
133
180
|
}
|
|
134
181
|
/**
|
|
135
182
|
* Get device preset dimensions
|
|
@@ -168,11 +215,11 @@ var ShotAPI = class {
|
|
|
168
215
|
if (options.fullPage) params.set("full_page", "true");
|
|
169
216
|
if (options.format) params.set("format", options.format);
|
|
170
217
|
if (options.quality) params.set("quality", options.quality.toString());
|
|
171
|
-
if (options.scale) params.set("scale", options.scale.toString());
|
|
218
|
+
if (options.scale && !options.device) params.set("scale", options.scale.toString());
|
|
172
219
|
if (options.delay) params.set("delay", options.delay.toString());
|
|
173
220
|
if (options.waitForSelector) params.set("wait_for", options.waitForSelector);
|
|
174
221
|
if (options.darkMode) params.set("dark_mode", "true");
|
|
175
|
-
if (options.mobile) params.set("mobile", "true");
|
|
222
|
+
if (options.mobile && !options.device) params.set("mobile", "true");
|
|
176
223
|
if (options.selector) params.set("selector", options.selector);
|
|
177
224
|
if (options.css) params.set("css", options.css);
|
|
178
225
|
if (options.js) params.set("js", options.js);
|
|
@@ -188,36 +235,61 @@ var ShotAPI = class {
|
|
|
188
235
|
const url = `${this.baseUrl}${endpoint}?${params.toString()}`;
|
|
189
236
|
let lastError = null;
|
|
190
237
|
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
191
240
|
try {
|
|
192
|
-
const controller = new AbortController();
|
|
193
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
194
241
|
const response = await fetch(url, {
|
|
195
242
|
method: "GET",
|
|
196
243
|
signal: controller.signal
|
|
197
244
|
});
|
|
198
245
|
clearTimeout(timeoutId);
|
|
199
|
-
const data = await response.json();
|
|
200
246
|
if (!response.ok) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
247
|
+
let errorData = null;
|
|
248
|
+
try {
|
|
249
|
+
errorData = await response.json();
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
const error = new ShotAPIException(
|
|
253
|
+
errorData?.error || `HTTP ${response.status}: ${response.statusText}`,
|
|
254
|
+
errorData?.code || "HTTP_ERROR",
|
|
255
|
+
errorData?.details,
|
|
206
256
|
response.status
|
|
207
257
|
);
|
|
258
|
+
if (response.status >= 400 && response.status < 500) {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
lastError = error;
|
|
262
|
+
if (attempt < this.retries) {
|
|
263
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
208
266
|
}
|
|
267
|
+
const data = await response.json();
|
|
209
268
|
return data;
|
|
210
269
|
} catch (error) {
|
|
211
|
-
|
|
212
|
-
if (error instanceof ShotAPIException
|
|
270
|
+
clearTimeout(timeoutId);
|
|
271
|
+
if (error instanceof ShotAPIException) {
|
|
213
272
|
throw error;
|
|
214
273
|
}
|
|
274
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
275
|
+
lastError = new ShotAPIException(
|
|
276
|
+
`Request timeout after ${this.timeout}ms`,
|
|
277
|
+
"TIMEOUT"
|
|
278
|
+
);
|
|
279
|
+
} else if (error instanceof Error) {
|
|
280
|
+
lastError = new ShotAPIException(
|
|
281
|
+
error.message || "Network error",
|
|
282
|
+
"NETWORK_ERROR"
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
lastError = new ShotAPIException("Unknown error occurred", "UNKNOWN_ERROR");
|
|
286
|
+
}
|
|
215
287
|
if (attempt < this.retries) {
|
|
216
288
|
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
217
289
|
}
|
|
218
290
|
}
|
|
219
291
|
}
|
|
220
|
-
throw lastError || new ShotAPIException("Request failed after retries");
|
|
292
|
+
throw lastError || new ShotAPIException("Request failed after retries", "MAX_RETRIES");
|
|
221
293
|
}
|
|
222
294
|
/**
|
|
223
295
|
* Fetch a URL as Buffer
|
|
@@ -225,7 +297,12 @@ var ShotAPI = class {
|
|
|
225
297
|
async fetchBuffer(url) {
|
|
226
298
|
const response = await fetch(url);
|
|
227
299
|
if (!response.ok) {
|
|
228
|
-
throw new ShotAPIException(
|
|
300
|
+
throw new ShotAPIException(
|
|
301
|
+
`Failed to fetch image: HTTP ${response.status}`,
|
|
302
|
+
"IMAGE_FETCH_ERROR",
|
|
303
|
+
void 0,
|
|
304
|
+
response.status
|
|
305
|
+
);
|
|
229
306
|
}
|
|
230
307
|
const arrayBuffer = await response.arrayBuffer();
|
|
231
308
|
return Buffer.from(arrayBuffer);
|
package/dist/index.mjs
CHANGED
|
@@ -21,15 +21,24 @@ var DEVICE_PRESETS = {
|
|
|
21
21
|
"galaxy-s23": { width: 360, height: 780, mobile: true, scale: 3 },
|
|
22
22
|
"pixel-7": { width: 412, height: 915, mobile: true, scale: 2.625 }
|
|
23
23
|
};
|
|
24
|
+
function isValidUrl(url) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = new URL(url);
|
|
27
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
var ShotAPI = class {
|
|
25
33
|
constructor(config) {
|
|
26
34
|
if (!config.apiKey) {
|
|
27
|
-
throw new ShotAPIException("API key is required");
|
|
35
|
+
throw new ShotAPIException("API key is required", "MISSING_API_KEY");
|
|
28
36
|
}
|
|
29
37
|
this.apiKey = config.apiKey;
|
|
30
38
|
this.baseUrl = config.baseUrl || "https://shotapi.dev";
|
|
31
39
|
this.timeout = config.timeout || 3e4;
|
|
32
40
|
this.retries = config.retries ?? 2;
|
|
41
|
+
this.maxConcurrent = config.maxConcurrent ?? 5;
|
|
33
42
|
}
|
|
34
43
|
/**
|
|
35
44
|
* Capture a screenshot of a URL
|
|
@@ -49,7 +58,29 @@ var ShotAPI = class {
|
|
|
49
58
|
*/
|
|
50
59
|
async screenshot(options) {
|
|
51
60
|
if (!options.url) {
|
|
52
|
-
throw new ShotAPIException("URL is required");
|
|
61
|
+
throw new ShotAPIException("URL is required", "MISSING_URL");
|
|
62
|
+
}
|
|
63
|
+
if (!isValidUrl(options.url)) {
|
|
64
|
+
throw new ShotAPIException(
|
|
65
|
+
`Invalid URL: ${options.url}. URL must start with http:// or https://`,
|
|
66
|
+
"INVALID_URL"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (options.quality !== void 0) {
|
|
70
|
+
if (options.quality < 1 || options.quality > 100) {
|
|
71
|
+
throw new ShotAPIException(
|
|
72
|
+
"Quality must be between 1 and 100",
|
|
73
|
+
"INVALID_QUALITY"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (options.delay !== void 0) {
|
|
78
|
+
if (options.delay < 0 || options.delay > 1e4) {
|
|
79
|
+
throw new ShotAPIException(
|
|
80
|
+
"Delay must be between 0 and 10000 milliseconds",
|
|
81
|
+
"INVALID_DELAY"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
53
84
|
}
|
|
54
85
|
const params = this.buildParams(options);
|
|
55
86
|
return this.request("/api/v1/screenshot", params);
|
|
@@ -62,12 +93,7 @@ var ShotAPI = class {
|
|
|
62
93
|
*/
|
|
63
94
|
async screenshotBuffer(options) {
|
|
64
95
|
const response = await this.screenshot(options);
|
|
65
|
-
|
|
66
|
-
if (!imageResponse.ok) {
|
|
67
|
-
throw new ShotAPIException("Failed to fetch screenshot image");
|
|
68
|
-
}
|
|
69
|
-
const arrayBuffer = await imageResponse.arrayBuffer();
|
|
70
|
-
return Buffer.from(arrayBuffer);
|
|
96
|
+
return this.fetchBuffer(response.url);
|
|
71
97
|
}
|
|
72
98
|
/**
|
|
73
99
|
* Capture a screenshot and save to a file
|
|
@@ -79,21 +105,42 @@ var ShotAPI = class {
|
|
|
79
105
|
const response = await this.screenshot(options);
|
|
80
106
|
const buffer = await this.fetchBuffer(response.url);
|
|
81
107
|
const fs = await import("fs/promises");
|
|
108
|
+
const path = await import("path");
|
|
109
|
+
const dir = path.dirname(filePath);
|
|
110
|
+
await fs.mkdir(dir, { recursive: true });
|
|
82
111
|
await fs.writeFile(filePath, buffer);
|
|
83
112
|
return response;
|
|
84
113
|
}
|
|
85
114
|
/**
|
|
86
|
-
* Capture multiple screenshots in batch
|
|
115
|
+
* Capture multiple screenshots in batch with rate limiting
|
|
87
116
|
*
|
|
88
117
|
* @param urls - Array of URLs to capture
|
|
89
118
|
* @param options - Common options for all screenshots
|
|
90
119
|
* @returns Array of screenshot responses
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const results = await client.batch([
|
|
124
|
+
* 'https://example.com',
|
|
125
|
+
* 'https://google.com',
|
|
126
|
+
* 'https://github.com'
|
|
127
|
+
* ], { width: 1280 });
|
|
128
|
+
* ```
|
|
91
129
|
*/
|
|
92
130
|
async batch(urls, options) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
const results = [];
|
|
132
|
+
for (let i = 0; i < urls.length; i += this.maxConcurrent) {
|
|
133
|
+
const chunk = urls.slice(i, i + this.maxConcurrent);
|
|
134
|
+
const promises = chunk.map(
|
|
135
|
+
(url) => this.screenshot({ ...options, url })
|
|
136
|
+
);
|
|
137
|
+
const chunkResults = await Promise.all(promises);
|
|
138
|
+
results.push(...chunkResults);
|
|
139
|
+
if (i + this.maxConcurrent < urls.length) {
|
|
140
|
+
await this.sleep(100);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
97
144
|
}
|
|
98
145
|
/**
|
|
99
146
|
* Get device preset dimensions
|
|
@@ -132,11 +179,11 @@ var ShotAPI = class {
|
|
|
132
179
|
if (options.fullPage) params.set("full_page", "true");
|
|
133
180
|
if (options.format) params.set("format", options.format);
|
|
134
181
|
if (options.quality) params.set("quality", options.quality.toString());
|
|
135
|
-
if (options.scale) params.set("scale", options.scale.toString());
|
|
182
|
+
if (options.scale && !options.device) params.set("scale", options.scale.toString());
|
|
136
183
|
if (options.delay) params.set("delay", options.delay.toString());
|
|
137
184
|
if (options.waitForSelector) params.set("wait_for", options.waitForSelector);
|
|
138
185
|
if (options.darkMode) params.set("dark_mode", "true");
|
|
139
|
-
if (options.mobile) params.set("mobile", "true");
|
|
186
|
+
if (options.mobile && !options.device) params.set("mobile", "true");
|
|
140
187
|
if (options.selector) params.set("selector", options.selector);
|
|
141
188
|
if (options.css) params.set("css", options.css);
|
|
142
189
|
if (options.js) params.set("js", options.js);
|
|
@@ -152,36 +199,61 @@ var ShotAPI = class {
|
|
|
152
199
|
const url = `${this.baseUrl}${endpoint}?${params.toString()}`;
|
|
153
200
|
let lastError = null;
|
|
154
201
|
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
155
204
|
try {
|
|
156
|
-
const controller = new AbortController();
|
|
157
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
158
205
|
const response = await fetch(url, {
|
|
159
206
|
method: "GET",
|
|
160
207
|
signal: controller.signal
|
|
161
208
|
});
|
|
162
209
|
clearTimeout(timeoutId);
|
|
163
|
-
const data = await response.json();
|
|
164
210
|
if (!response.ok) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
211
|
+
let errorData = null;
|
|
212
|
+
try {
|
|
213
|
+
errorData = await response.json();
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
const error = new ShotAPIException(
|
|
217
|
+
errorData?.error || `HTTP ${response.status}: ${response.statusText}`,
|
|
218
|
+
errorData?.code || "HTTP_ERROR",
|
|
219
|
+
errorData?.details,
|
|
170
220
|
response.status
|
|
171
221
|
);
|
|
222
|
+
if (response.status >= 400 && response.status < 500) {
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
lastError = error;
|
|
226
|
+
if (attempt < this.retries) {
|
|
227
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
172
230
|
}
|
|
231
|
+
const data = await response.json();
|
|
173
232
|
return data;
|
|
174
233
|
} catch (error) {
|
|
175
|
-
|
|
176
|
-
if (error instanceof ShotAPIException
|
|
234
|
+
clearTimeout(timeoutId);
|
|
235
|
+
if (error instanceof ShotAPIException) {
|
|
177
236
|
throw error;
|
|
178
237
|
}
|
|
238
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
239
|
+
lastError = new ShotAPIException(
|
|
240
|
+
`Request timeout after ${this.timeout}ms`,
|
|
241
|
+
"TIMEOUT"
|
|
242
|
+
);
|
|
243
|
+
} else if (error instanceof Error) {
|
|
244
|
+
lastError = new ShotAPIException(
|
|
245
|
+
error.message || "Network error",
|
|
246
|
+
"NETWORK_ERROR"
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
lastError = new ShotAPIException("Unknown error occurred", "UNKNOWN_ERROR");
|
|
250
|
+
}
|
|
179
251
|
if (attempt < this.retries) {
|
|
180
252
|
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
181
253
|
}
|
|
182
254
|
}
|
|
183
255
|
}
|
|
184
|
-
throw lastError || new ShotAPIException("Request failed after retries");
|
|
256
|
+
throw lastError || new ShotAPIException("Request failed after retries", "MAX_RETRIES");
|
|
185
257
|
}
|
|
186
258
|
/**
|
|
187
259
|
* Fetch a URL as Buffer
|
|
@@ -189,7 +261,12 @@ var ShotAPI = class {
|
|
|
189
261
|
async fetchBuffer(url) {
|
|
190
262
|
const response = await fetch(url);
|
|
191
263
|
if (!response.ok) {
|
|
192
|
-
throw new ShotAPIException(
|
|
264
|
+
throw new ShotAPIException(
|
|
265
|
+
`Failed to fetch image: HTTP ${response.status}`,
|
|
266
|
+
"IMAGE_FETCH_ERROR",
|
|
267
|
+
void 0,
|
|
268
|
+
response.status
|
|
269
|
+
);
|
|
193
270
|
}
|
|
194
271
|
const arrayBuffer = await response.arrayBuffer();
|
|
195
272
|
return Buffer.from(arrayBuffer);
|