jackwener/OpenCLI · error · CommandExecutionError

Pin ${id} has no downloadable image (it may be a video or st

Error message

Pin ${id} has no downloadable image (it may be a video or story pin)

What it means

The pin resolved but pickPinImage(pin.images) returned no usable image URL, so the command raises CommandExecutionError. Some Pinterest pins are videos or story (idea) pins whose images map contains no static downloadable rendition.

Source

Thrown at clis/pinterest/download.js:40

  func: async (page, kwargs) => {
    const id = parsePinId(kwargs.pin);
    const output = String(kwargs.output ?? './pinterest-downloads');

    const sourceUrl = `/pin/${id}/`;
    await page.goto(`${PINTEREST_BASE}${sourceUrl}`);

    const { data: pin } = await pinterestResourceFetch(
      page,
      'PinResource',
      { id, field_set_key: 'detailed' },
      sourceUrl,
    );
    if (!pin || !pin.id) {
      throw new EmptyResultError('pinterest download', `pin "${id}" not found`);
    }
    const imageUrl = pickPinImage(pin.images);
    if (!imageUrl) {
      throw new CommandExecutionError(`Pin ${id} has no downloadable image (it may be a video or story pin)`);
    }

    fs.mkdirSync(output, { recursive: true });
    const ext = path.extname(new URL(imageUrl).pathname) || '.jpg';
    const destPath = path.join(output, `${id}${ext}`);

    let result;
    try {
      result = await httpDownload(imageUrl, destPath, { timeout: 60000 });
    } catch (err) {
      throw new CommandExecutionError(`Failed to download pin ${id}: ${getErrorMessage(err)}`);
    }
    if (!result.success) {
      throw new CommandExecutionError(`Failed to download pin ${id}: ${result.error || 'unknown error'}`);
    }

    return [{
      pinId: id,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Only call download on image pins; detect video/story pins beforehand and skip them.
  2. If you need video content, use a pin video resource/URL instead of the image path.
  3. Catch CommandExecutionError and record the pin as skipped in your pipeline.
  4. Check pin.images in the API response — if it lacks renditions, the pin is not image-based.

Example fix

// before
await cli.download(videoPinId);
// after
if (pin.type === 'video' || pin.is_story) skip(pin); else await cli.download(pin.id);
Defensive patterns

Strategy: type-guard

Validate before calling

const pin = await fetchPin(id);
if (pin.type === 'video' || pin.is_story || !pin.images || Object.keys(pin.images).length === 0) {
  skip(id, 'non-image pin');
}

Type guard

function isDownloadableImagePin(pin) {
  return Boolean(pin && pin.id && pin.images && Object.keys(pin.images).length > 0);
}

Try / catch

try {
  await cli.download(pinId);
} catch (e) {
  if (/no downloadable image/.test(e.message)) {
    skipped.push({ id: pinId, reason: 'video-or-story-pin' });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `pinterest download <id>` on a video pin, story/idea pin, or a pin whose detailed field_set images payload lacks the expected renditions.

Common situations: Batch-download scripts iterating a user's pins and hitting video-heavy boards; newer Pinterest formats (idea pins) that older scrapers assume are image pins.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/60367bc051f09aaa. Report an issue: GitHub.