import { Router } from "express"; import { takeScreenshot } from "../services/screenshot.js"; import { getUsageForKey, incrementUsage } from "../middleware/usage.js"; import { getTierLimit } from "../services/keys.js"; import logger from "../services/logger.js"; export const batchRouter = Router(); /** * @openapi * /v1/screenshots/batch: * post: * tags: [Screenshots] * summary: Take multiple screenshots in a single request * description: > * Capture multiple URLs in one API call. Each URL counts as one screenshot * toward your usage limits. All parameters except `urls` are shared across * all screenshots. Returns partial results if some URLs fail. * operationId: batchScreenshots * security: * - BearerAuth: [] * - ApiKeyAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [urls] * properties: * urls: * type: array * items: * type: string * format: uri * minItems: 1 * maxItems: 10 * description: Array of URLs to capture (1-10) * example: ["https://example.com", "https://example.org"] * format: * type: string * enum: [png, jpeg, webp] * default: png * width: * type: integer * minimum: 320 * maximum: 3840 * default: 1280 * height: * type: integer * minimum: 200 * maximum: 2160 * default: 800 * fullPage: * type: boolean * default: false * quality: * type: integer * minimum: 1 * maximum: 100 * default: 80 * darkMode: * type: boolean * default: false * css: * type: string * maxLength: 5000 * js: * type: string * maxLength: 5000 * selector: * type: string * maxLength: 200 * userAgent: * type: string * maxLength: 500 * delay: * type: integer * minimum: 0 * maximum: 5000 * waitForSelector: * type: string * hideSelectors: * oneOf: * - type: string * - type: array * items: * type: string * maxItems: 10 * clip: * type: object * properties: * x: * type: integer * y: * type: integer * width: * type: integer * height: * type: integer * examples: * basic: * summary: Two URLs * value: * urls: ["https://example.com", "https://example.org"] * with_options: * summary: With shared options * value: * urls: ["https://example.com", "https://example.org"] * format: jpeg * width: 1920 * height: 1080 * quality: 90 * responses: * 200: * description: Batch results (may include partial failures) * content: * application/json: * schema: * type: object * properties: * results: * type: array * items: * type: object * properties: * url: * type: string * status: * type: string * enum: [success, error] * image: * type: string * description: Base64-encoded image (only on success) * error: * type: string * description: Error message (only on error) * 400: * description: Invalid request * content: * application/json: * schema: { $ref: "#/components/schemas/Error" } * 401: * description: Missing API key * 429: * description: Usage limit exceeded */ batchRouter.post("/", async (req: any, res: any) => { const { urls, ...sharedParams } = req.body; // Validate urls if (!urls || !Array.isArray(urls) || urls.length === 0) { res.status(400).json({ error: "Missing required parameter: urls (array of 1-10 URLs)" }); return; } if (urls.length > 10) { res.status(400).json({ error: "Maximum 10 URLs per batch request" }); return; } // Check usage quota for all URLs before starting const keyInfo = req.apiKeyInfo; if (keyInfo) { const key = keyInfo.key; const limit = getTierLimit(keyInfo.tier); const currentUsage = getUsageForKey(key); const monthKey = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`; const currentCount = (currentUsage && currentUsage.monthKey === monthKey) ? currentUsage.count : 0; if (currentCount + urls.length > limit) { res.status(429).json({ error: `Monthly limit would be exceeded. Need ${urls.length} screenshots but only ${limit - currentCount} remaining (${limit} limit for ${keyInfo.tier} tier).`, usage: currentCount, limit, }); return; } } // Extract shared screenshot params const { format, width, height, fullPage, quality, darkMode, css, js, selector, userAgent, delay, waitForSelector, hideSelectors, clip, waitUntil, deviceScale, } = sharedParams; // Process all URLs concurrently const settled = await Promise.allSettled( urls.map((url: string) => takeScreenshot({ url, format: format || undefined, width: width ? parseInt(width, 10) || width : undefined, height: height ? parseInt(height, 10) || height : undefined, fullPage, quality: quality ? parseInt(quality, 10) || quality : undefined, darkMode, css, js, selector, userAgent, delay: delay ? parseInt(delay, 10) || delay : undefined, waitForSelector, hideSelectors, clip, waitUntil, deviceScale, }) ) ); // Build results and track usage const results = settled.map((result, i) => { if (result.status === "fulfilled") { // Increment usage for successful screenshot if (keyInfo) { incrementUsage(keyInfo.key); } return { url: urls[i], status: "success" as const, image: result.value.buffer.toString("base64"), }; } else { return { url: urls[i], status: "error" as const, error: result.reason?.message || "Unknown error", }; } }); res.json({ results }); });