> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tornadoapi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Batch Downloads

> Download entire playlists and podcast shows with a single request

## Overview

Batch downloads allow you to download all videos from a YouTube playlist or all episodes from a Spotify podcast show with a single API call. Tornado automatically extracts all URLs and creates individual jobs for each -- **there is no separate batch endpoint**, this triggers automatically off the `url` you send to [`POST /jobs`](/api-reference/jobs/create).

<Note>
  Batch downloads are supported for:

  * **YouTube Playlists** (`list=PL...`, and channel-uploads/`UU...`/course-style `OL...` playlists)
  * **Spotify Shows** (podcast episodes)
</Note>

<Warning>
  YouTube's auto-generated **"Radio"/Mix** queues (`list=RD...`, the parameter YouTube's own UI attaches automatically when you open a video from an autoplay queue, the homepage, or related videos -- **not** a real playlist) are deliberately **excluded** from auto-detection, along with `Watch Later` (`list=WL`) and `Liked videos` (`list=LL`). A URL carrying one of these is downloaded as a single video, not a batch. See [Playlist Auto-Detection](/api-reference/jobs/create#playlist-auto-detection) for the full breakdown -- getting this wrong (treating a Mix queue as a real playlist) previously caused two production incidents where a single-video request silently turned into thousands of jobs.
</Warning>

## How It Works

<Steps>
  <Step title="Submit Show URL">
    Send a Spotify show URL to the `/jobs` endpoint
  </Step>

  <Step title="Episode Extraction">
    Tornado extracts all episode URLs from the show (can take 30-120 seconds for large shows)
  </Step>

  <Step title="Batch Creation">
    A batch job is created with individual jobs for each episode
  </Step>

  <Step title="Parallel Processing">
    Episodes are downloaded in parallel for maximum speed
  </Step>

  <Step title="Progress Tracking">
    Track overall progress via the batch status endpoint
  </Step>
</Steps>

## Create a Batch

### YouTube Playlist

```bash theme={null}
curl -X POST "https://api.tornadoapi.io/jobs" \
  -H "x-api-key: sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf",
    "folder": "my-playlist-2024"
  }'
```

### Spotify Show

```bash theme={null}
curl -X POST "https://api.tornadoapi.io/jobs" \
  -H "x-api-key: sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://open.spotify.com/show/7iQXmUT7XGuZSzAMjoNWlX",
    "folder": "my-podcast-2024",
    "webhook_url": "https://myapp.com/batch-complete"
  }'
```

## Batch Response

The response shape depends on the source. **Spotify shows** return episode metadata:

```json Spotify Show theme={null}
{
  "batch_id": "550e8400-e29b-41d4-a716-446655440001",
  "total_episodes": 142,
  "paused": false,
  "episodes": [
    {
      "job_id": "uuid-1",
      "url": "https://open.spotify.com/episode/abc",
      "title": "Episode 1 - Introduction"
    },
    {
      "job_id": "uuid-2",
      "url": "https://open.spotify.com/episode/def",
      "title": "Episode 2 - Deep Dive"
    }
  ],
  "episode_jobs": ["uuid-1", "uuid-2", "..."]
}
```

**YouTube playlists** return a simpler shape with `total_videos` and `video_jobs` (no `episodes`/`total_episodes`/`paused`):

```json YouTube Playlist theme={null}
{
  "batch_id": "550e8400-e29b-41d4-a716-446655440002",
  "total_videos": 25,
  "video_jobs": ["uuid-1", "uuid-2", "..."]
}
```

<Warning>
  The two shapes differ: read `total_episodes`/`episode_jobs` for Spotify shows, and `total_videos`/`video_jobs` for YouTube playlists. The Python example below targets Spotify shows.
</Warning>

## Check Batch Status

Poll the batch endpoint for progress:

```bash theme={null}
curl -X GET "https://api.tornadoapi.io/batch/550e8400-e29b-41d4-a716-446655440001"
```

### Response

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440001",
  "show_url": "https://open.spotify.com/show/7iQXmUT7XGuZSzAMjoNWlX",
  "status": "processing",
  "folder": "my-podcast-2024",
  "total_episodes": 142,
  "completed_episodes": 45,
  "failed_episodes": 2,
  "episode_jobs": ["job-uuid-1", "job-uuid-2", "..."]
}
```

## Batch Status Values

| Status       | Description                                                             |
| ------------ | ----------------------------------------------------------------------- |
| `paused`     | Batch created with `paused: true`, waiting for `POST /batch/{id}/start` |
| `processing` | Batch is being processed, some episodes may be complete                 |
| `completed`  | All episodes finished successfully                                      |
| `finished`   | All episodes done, but some failed or were skipped                      |

## Paused Mode (Rename Before Download)

Create a batch in paused mode to review and rename episodes before downloading:

### 1. Create Paused Batch

```bash theme={null}
curl -X POST "https://api.tornadoapi.io/jobs" \
  -H "x-api-key: sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://open.spotify.com/show/7iQXmUT7XGuZSzAMjoNWlX",
    "folder": "my-podcast",
    "paused": true
  }'
```

The response includes episode metadata (title and URL) so you can decide how to rename them.

### 2. Rename Episodes (Optional)

```bash theme={null}
curl -X PATCH "https://api.tornadoapi.io/batch/{batch_id}/jobs" \
  -H "x-api-key: sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "renames": [
      {"job_id": "uuid-1", "filename": "01 - Custom Name"},
      {"job_id": "uuid-2", "filename": "02 - Another Name"}
    ]
  }'
```

### 3. Start the Batch

```bash theme={null}
curl -X POST "https://api.tornadoapi.io/batch/{batch_id}/start" \
  -H "x-api-key: sk_your_api_key"
```

## Batch Webhook

When a batch completes (all episodes done), a webhook is sent:

```json theme={null}
{
  "type": "batch_completed",
  "batch_id": "550e8400-e29b-41d4-a716-446655440001",
  "status": "finished",
  "show_url": "https://open.spotify.com/show/...",
  "folder": "my-podcast-2024",
  "total_episodes": 142,
  "completed_episodes": 140,
  "failed_episodes": 2
}
```

<Note>
  The batch webhook fires once when **all** episodes are done (completed + failed + skipped = total), not for each individual episode. Skipped episodes (e.g. audio-only episodes with Widevine DRM) count toward the `failed_episodes` counter. The `status` field is `"completed"` if all succeeded, or `"finished"` if some failed.
</Note>

## Folder Structure

All episodes are saved with the folder prefix you specify:

```
my-podcast-2024/
  ├── Episode 1 - Introduction.mp4
  ├── Episode 2 - Getting Started.mp4
  ├── Episode 3 - Deep Dive.mp4
  └── ...
```

## Python Example

```python theme={null}
import requests
import time

API_KEY = "sk_your_api_key"
BASE_URL = "https://api.tornadoapi.io"

def download_podcast(show_url, folder):
    # Create batch
    response = requests.post(
        f"{BASE_URL}/jobs",
        headers={"x-api-key": API_KEY},
        json={
            "url": show_url,
            "folder": folder
        }
    )

    data = response.json()
    batch_id = data["batch_id"]
    total = data["total_episodes"]

    print(f"Batch created: {batch_id}")
    print(f"Total episodes: {total}")

    # Poll for progress
    while True:
        status = requests.get(
            f"{BASE_URL}/batch/{batch_id}",
            headers={"x-api-key": API_KEY}
        ).json()

        completed = status["completed_episodes"]
        failed = status["failed_episodes"]

        print(f"Progress: {completed}/{total} ({failed} failed)")

        if status["status"] in ["completed", "finished"]:
            break

        time.sleep(10)

    print(f"Batch complete! Status: {status['status']}")

# Download a podcast
download_podcast(
    "https://open.spotify.com/show/7iQXmUT7XGuZSzAMjoNWlX",
    "huberman-lab-2024"
)
```

## Skipped Episodes

Some Spotify episodes are audio-only and protected by Widevine DRM — they have no video stream available. These episodes are automatically **skipped** instead of failing with retries:

* Job status is set to `Skipped` (not `Failed`)
* A `job_skipped` webhook is sent for each skipped episode
* Skipped episodes count toward the batch `failed` counter for completion tracking
* Skipped jobs are classified as **warnings**, not errors

You can identify skipped episodes by checking individual job statuses or listening for `job_skipped` webhooks.

## Limits & Performance

| Metric                 | Value                 |
| ---------------------- | --------------------- |
| Max episodes per batch | Unlimited             |
| Concurrent downloads   | \~100 per batch       |
| Typical speed          | 10-50 episodes/minute |
| Extraction timeout     | 120 seconds           |
