# Bluesky Media Downloader: Images & Videos by Keyword (`accountable_eel/bluesky-media-downloader`) Actor

Bluesky video downloader and image downloader: download all photos and videos from a Bluesky profile (up to your post cap), download Bluesky images by keyword in full resolution, or save Bluesky videos as MP4 from a post URL. Good for digital art. Bulk download via API or n8n, SHA-256 checksums.

- **URL**: https://apify.com/accountable_eel/bluesky-media-downloader.md
- **Developed by:** [Adriana Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Social media, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.28 / 1,000 stored media files

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bluesky Media Downloader: Images & Videos by Keyword

Downloads Bluesky images and videos into your run's default key-value store by keyword, profile handle, or post URL. Full resolution photos, remuxed MP4 videos, SHA-256 checksums, and ffprobe technical metadata for every file. No login or cookies required.

### Who it's for

- **Social media managers & creators**: Archive posts, high-resolution imagery, and video clips from specific Bluesky accounts or topics.
- **Researchers & ML engineers**: Curate clean datasets of images and videos filtered by keywords, engagement (`top` vs `latest`), and date ranges (`since`).
- **Automation workflows**: Streamlined integration with n8n, Make, or custom pipelines using Apify's key-value store URLs and structured JSON metadata.

### Why this one

- **Three ways to search**: Query by keyword search, track profile handles (with `posts_with_media` filtering), or download exact posts via URL.
- **Full-resolution originals**: Downloads high-resolution images from Bluesky's CDN (`cdn.bsky.app`), preserving full image dimensions and alt text.
- **Native MP4 videos**: Bluesky serves video as HLS (`playlist.m3u8`); this actor automatically remuxes streams into MP4 files using `ffmpeg` without re-encoding quality loss.
- **Checksums & technical metadata**: Computes `sha256` checksums during streaming and extracts video duration, resolution, fps, codec, and audio channels using `ffprobe`.
- **Fair pay-per-event billing**: Only successfully stored files are billed (`file` + `megabyte`). Misses, NSFW/adult-content label skips, duplicates, and over-cap files are never charged.

### What you get

One row per stored file by default (or nested when unexpanded):

| Field | Type | Description |
|---|---|---|
| `query` | text | The input source prefixed with its type, e.g. `query:sunset photography` or `handle:bsky.app` |
| `source` / `sourceValue` | text | Source type (`query`, `handle`, `post`) and search query or handle |
| `postUri` / `postUrl` | text | Bluesky AT URI (`at://did:plc:.../app.bsky.feed.post/...`) and public web URL |
| `kind` | text | `image` or `video` |
| `mediaIndex` | number | Index of the media item within the post (starting at 0) |
| `kvKey` / `kvUrl` | text | Key name and direct download URL in this run's default key-value store |
| `originalMediaUrl` | text | Source CDN or video playlist URL |
| `altText` | text | Image or video accessibility alt text |
| `bytes` / `sha256` | number / text | File size in bytes and SHA-256 hash |
| `width` / `height` | number | Pixel dimensions from embed or ffprobe |
| `duration_s` / `fps` / `codec` / `hasAudio` | number / text / boolean | Video metadata from ffprobe (`null` for images) |
| `text` | text | Post text content |
| `authorHandle` / `authorDisplayName` | text | Post creator's handle and display name |
| `likeCount` / `repostCount` | number | Engagement counters at scrape time |
| `createdAt` | text | Post creation timestamp (ISO 8601) |
| `error` | text | Failure explanation if storage failed (`null` on success) |

### Scope and limits

- **Public content only**: Only public Bluesky posts can be downloaded; private or blocked accounts are not accessible.
- **Content moderation labels**: Respects Bluesky moderation labels (`porn`, `sexual`, `nudity`, `graphic-media`), skipping adult content by default unless `includeAdult` is set to `true`.
- **Deduplication**: Posts with identical URIs within the same run are processed and billed only once by default.
- **Key-value store access**: Downloaded media files are stored in the run's default Key-Value Store. Accessing `kvUrl` records requires your Apify API token if your store is private.

### Example runs

1. **Download sunset photography images and videos by keyword**
   ```json
   {
     "queries": ["sunset photography"],
     "sort": "top",
     "maxPostsPerSource": 5,
     "kinds": "all"
   }
   ```
2. **Download media from an official Bluesky handle**
   ```json
   {
     "handles": ["bsky.app"],
     "maxPostsPerSource": 10,
     "kinds": "all"
   }
   ```
3. **Download images and video from a specific post URL**
   ```json
   {
     "posts": ["https://bsky.app/profile/bsky.app/post/3mv3shqdfuc2e"],
     "kinds": "all"
   }
   ```

Use-case variant candidate: none

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `queries` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~bluesky-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"queries":["sunset photography"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "queries": [
    "sunset photography"
  ]
}
```

One search term per line, as you would search on Bluesky. Keywords are one of three ways in: use this field, or fill Bluesky handles or Post URLs below. At least one of the three is needed. Accepted formats: sunset photography, digital art, astrophotography.

### Output

| query | found | status | postUri | postUrl | source | sourceValue | kind | mediaIndex | kvKey | kvUrl | originalMediaUrl | altText | bytes | sha256 | width | height | duration_s | fps | codec | hasAudio | text | authorHandle | authorDisplayName | likeCount | repostCount | createdAt | error | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| sunset photography | true | OK | <post at uri> | <post url> | <source type> | <source> | <file kind> | <media index> | <storage key> | <storage url> | <original media url> | <image alt text> | \<size (bytes)> | <sha-256> | <width> | <height> | \<duration (s)> | <fps> | <codec> | <has audio> | <post text> | <author handle> | <author display name> | <likes> | <reposts> | <created at> | <error> | 1970-01-01T00:00:00.000Z |

### Pricing

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~bluesky-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"queries":["sunset photography"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~bluesky-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"queries":["sunset photography"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~bluesky-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"queries":["{{source}}"]}`, mapping the row's source into the `queries` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Bluesky Video Downloader: Save Media by Keyword" — the agent will find and run this actor.

# Actor input Schema

## `queries` (type: `array`):

One search term per line, as you would search on Bluesky. Keywords are one of three ways in: use this field, or fill Bluesky handles or Post URLs below. At least one of the three is needed. Accepted formats: sunset photography, digital art, astrophotography. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `handles` (type: `array`):

Bluesky handles or profile URLs, one per line, e.g. bsky.app or https://bsky.app/profile/bsky.app. Downloads recent media posts from their feed.

## `posts` (type: `array`):

Post URLs, one per line, e.g. https://bsky.app/profile/bsky.app/post/3mv3shqdfuc2e. Resolves handle to DID and downloads post media.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each file found gets its own row instead of being grouped under its source. You're still only charged once per source, no matter how many rows it produces.

## `sort` (type: `string`):

"top" sorts by engagement; "latest" sorts by recency.

## `maxPostsPerSource` (type: `integer`):

How many posts to download from each keyword or handle. Capped at 100.

## `since` (type: `string`):

Optional ISO date or YYYY-MM-DD. Only downloads posts created after this timestamp.

## `kinds` (type: `string`):

"All" downloads both images and videos. "Images only" skips video posts. "Videos only" skips image posts.

## `includeAdult` (type: `boolean`):

Off by default. When false, posts carrying sexual, porn, nudity, or graphic-media labels are skipped and never billed.

## `maxMb` (type: `integer`):

Aborts a single file's download once it exceeds this size. 1-300.

## `skipDuplicates` (type: `boolean`):

On by default. With this on, each post URI is processed and billed at most once per run.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

## `proxyConfiguration` (type: `object`):

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "queries": [
    "sunset photography"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "handles": [],
  "posts": [],
  "columns": [
    "postUri",
    "postUrl",
    "source",
    "sourceValue",
    "kind",
    "mediaIndex",
    "kvKey",
    "kvUrl",
    "originalMediaUrl",
    "altText",
    "bytes",
    "sha256",
    "width",
    "height",
    "duration_s",
    "fps",
    "codec",
    "hasAudio",
    "text",
    "authorHandle",
    "authorDisplayName",
    "likeCount",
    "repostCount",
    "createdAt",
    "error"
  ],
  "expandRows": true,
  "sort": "top",
  "maxPostsPerSource": 25,
  "since": "",
  "kinds": "all",
  "includeAdult": false,
  "maxMb": 100,
  "skipDuplicates": true,
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "sunset photography"
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
    "handles": [],
    "posts": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/bluesky-media-downloader").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["sunset photography"],
    "includeKeywords": [],
    "excludeKeywords": [],
    "handles": [],
    "posts": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/bluesky-media-downloader").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "sunset photography"
  ],
  "includeKeywords": [],
  "excludeKeywords": [],
  "handles": [],
  "posts": []
}' |
apify call accountable_eel/bluesky-media-downloader --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/bluesky-media-downloader"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/DeHgf4G4yGo2wYWiP/builds/jUCy9VnbTXOpf9cT6/openapi.json
