# Podcast Scraper: Shows, Episodes & RSS Feeds (`arman-bd/podcast-directory-scraper`) Actor

Search Apple's podcast directory, then parse each show's RSS feed for full episode lists: titles, show notes, durations, audio URLs, season and episode numbers, and publish dates.

- **URL**: https://apify.com/arman-bd/podcast-directory-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Automation, AI, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.56 / 1,000 record scrapeds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Podcast Scraper: Shows, Episodes & RSS Feeds

![Podcast Scraper: Show metadata from Apple's directory plus every episode's title, notes, duration and direct audio URL from the show's own RSS feed](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/podcast-directory-scraper.jpg)

**Podcast Scraper** finds shows in Apple's public podcast directory and then reads their RSS feeds directly, show metadata, artwork and genres, plus every episode's title, show notes, duration, season and episode numbers and the direct audio URL.

Two public sources, chained: Apple's directory for discovery, and the publisher's own RSS feed for the episode list. A discovery-only sweep is a handful of requests; turning episodes on adds one request per show.

**Every cap is a run total.** `maxShows` bounds the shows the whole run delivers across every term, ID and feed URL combined, and `maxRecords` bounds the rows — shows and episodes together — so a run cannot cost more than the number you set.

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/podcast-directory-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/podcast-directory-scraper.md
```

### What you get

Records come in two shapes, told apart by `recordType`. Every record carries the show fields; episode records add the episode fields on top.

| Output field | Meaning |
|---|---|
| `recordType` | `show` or `episode` |
| `source` | What produced the record, `search:<term>`, `id:<collectionId>` or `feed:<url>` |
| `collectionId`, `appleUrl` | Apple's ID for the show and its Apple Podcasts page (`null` for direct feeds) |
| `showName`, `artistName` | Show title and publisher |
| `feedUrl`, `feedTitle` | RSS feed Apple points at, and the title inside that feed |
| `genres`, `primaryGenre` | Apple's genre list and the primary one |
| `episodeCount` | Apple's episode count for the show |
| `artworkUrl`, `country`, `contentAdvisoryRating` | 600 px artwork, storefront and explicit rating |
| `title`, `link`, `author`, `publishedAt` | Episode title / show name, canonical link, byline, ISO-8601 publish time |
| `contentText` | Show notes (episodes) or the channel description (shows) as clean plain text |
| `episodeGuid` | The feed's own identity for the episode |
| `duration`, `durationSeconds` | `itunes:duration` verbatim, and parsed to seconds from `HH:MM:SS`, `MM:SS` or a bare count |
| `audioUrl`, `audioLengthBytes`, `audioType` | The enclosure, direct MP3 URL, byte size and MIME type |
| `episodeNumber`, `seasonNumber`, `episodeType` | `itunes:episode`, `itunes:season`, and `full` / `trailer` / `bonus` |
| `explicit`, `imageUrl` | Per-episode explicit flag (`true` / `false` / `null`) and episode artwork, falling back to show artwork |
| `scrapedAt` | Run timestamp |

A `RUN_SUMMARY` record in the key-value store holds per-run counts, the filters used, and any search, lookup or feed that failed.

### Common use cases

- **Build a podcast search engine.** Index shows by genre and episodes by title and show notes.
- **PR and guest sourcing.** Find every show in a niche, with the publisher name and a contact trail.
- **Transcription pipelines.** `audioUrl` is a direct MP3 link, ready to hand to a speech-to-text step.
- **Media monitoring.** Watch a set of shows and pick up new episodes on a schedule.
- **Market research.** Episode cadence, duration trends and genre mix across a category.

### Quick start

Discovery only, fast and cheap, one record per show:

```json
{
 "searchTerms": ["true crime", "software engineering"],
 "maxShows": 50
}
```

Specific shows with their recent episodes:

```json
{
 "podcastIds": [
 "1200361736",
 "https://podcasts.apple.com/us/podcast/serial/id917918570"
 ],
 "includeEpisodes": true,
 "maxEpisodesPerShow": 25
}
```

A feed you already know, skipping Apple entirely:

```json
{
 "feedUrls": ["https://feeds.simplecast.com/Sl5CSM3S"],
 "includeEpisodes": true,
 "maxEpisodesPerShow": 0,
 "maxRecords": 500
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `searchTerms` | array | `[]` | Keywords for Apple's directory. The show cap is shared out evenly between the terms. |
| `podcastIds` | array | `[]` | Apple collection IDs or Apple Podcasts show URLs. Looked up 50 per request. |
| `feedUrls` | array | `[]` | Podcast RSS URLs read directly, with no Apple lookup. |
| `country` | string | `US` | Two-letter storefront code. Results and ranking differ by market. |
| `maxShows` | integer | `20` | Show records for the **whole run**, 1 to 500, across every term, ID and feed URL. |
| `maxRecords` | integer | `1000` | Total rows for the whole run, shows and episodes together, 1 to 100000. |
| `includeEpisodes` | boolean | `false` | Fetch each show's feed and emit one record per episode. |
| `maxEpisodesPerShow` | integer | `10` | Episodes per show, newest first. `0` = the whole feed, still bounded by `maxRecords`. |

All three discovery inputs combine. IDs and feed URLs are read first and search terms share whatever is left of `maxShows`. With none of them set the Actor searches for `technology`, so an empty input still returns something useful — but an input whose every entry was unusable is refused outright rather than quietly turned into that fallback.

A value outside the stated range is an error, never a licence to fetch everything: `maxShows: -5` fails the run before a single row is delivered. `maxShowsPerTerm` is the old name for `maxShows` and is still accepted, with the same run-total meaning.

### Output example

An episode record:

```json
{
 "recordType": "episode",
 "source": "id:917918570",
 "collectionId": 917918570,
 "showName": "Serial",
 "artistName": "Serial Productions & The New York Times",
 "feedUrl": "https://feeds.simplecast.com/PpzWFGhg",
 "feedTitle": "Serial",
 "genres": ["News", "Podcasts", "True Crime"],
 "primaryGenre": "News",
 "episodeCount": 125,
 "artworkUrl": "https://is1-ssl.mzstatic.com/image/thumb/Podcasts221/v4/…/600x600bb.jpg",
 "country": "USA",
 "contentAdvisoryRating": "Clean",
 "appleUrl": "https://podcasts.apple.com/us/podcast/serial/id917918570?uo=4",
 "episodeGuid": "20ad2ce9-3088-449b-8001-2e3e739b54d8",
 "title": "The Last 12 Weeks - Ep. 5",
 "link": "https://serialpodcast.org",
 "author": "Serial Productions & The New York Times",
 "publishedAt": "2026-06-18T10:20:00.000Z",
 "contentText": "Days before the execution, the defense team scrambles to respond to an unexpected ruling …",
 "duration": "00:39:22",
 "durationSeconds": 2362,
 "audioUrl": "https://dts.podtrac.com/redirect.mp3/…/audio/128/default.mp3",
 "audioLengthBytes": 37796581,
 "audioType": "audio/mpeg",
 "episodeNumber": 5,
 "seasonNumber": 17,
 "episodeType": "full",
 "explicit": true,
 "imageUrl": "https://image.simplecastcdn.com/images/…/3000x3000/tl12walbum_art3000x3000v2.jpg",
 "scrapedAt": "2026-08-06T11:52:18.721Z"
}
```

### Finding an Apple podcast ID

Open the show on Apple Podcasts and read the number out of the URL:

| URL you see | ID |
|---|---|
| `podcasts.apple.com/us/podcast/the-daily/id1200361736` | `1200361736` |
| `podcasts.apple.com/gb/podcast/serial/id917918570?i=1000123` | `917918570` |

Paste the whole URL if you prefer, the Actor extracts the ID itself. If you only know the show's name, put it in `searchTerms` instead and read the ID off the result.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~podcast-directory-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "searchTerms": ["history"],
 "maxShows": 25,
 "includeEpisodes": true,
 "maxEpisodesPerShow": 5
 }'
```

### JavaScript example

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/podcast-directory-scraper').call({
 podcastIds: ['1200361736'],
 includeEpisodes: true,
 maxEpisodesPerShow: 20,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const rec of items.filter((r) => r.recordType === 'episode')) {
 console.log(`${rec.publishedAt}, ${rec.title} (${rec.durationSeconds}s) → ${rec.audioUrl}`);
}
```

### Limits and behaviour

- **A single search returns at most 100 shows** and cannot be paged. Use narrower terms, or supply IDs and feed URLs directly.
- **Caps bind the run, not the target.** `maxShows` is shared across every search term rather than granted to each, and `maxRecords` stops the run pushing once the total is reached. Whatever was left unread is counted in `RUN_SUMMARY.skipped`.
- **Unusable input is named, never guessed at.** A show name in `podcastIds`, a publisher (artist) page instead of a show, or a URL with no ID in it is listed in `RUN_SUMMARY.rejectedInputs` with the reason. If nothing usable is left, the run fails instead of substituting something else.
- **An ID that matches no show** is listed in `RUN_SUMMARY.idsNotFound`; a term that matched nothing is listed in `termsWithNoResults`.
- **The same feed spelled several ways is one feed.** `http`/`https`, `www.`, letter case, a trailing slash and campaign parameters all collapse, so an alias list is charged once.
- **Not every show has a feed.** Apple occasionally omits `feedUrl`. Those shows are still saved as show records; they simply cannot produce episodes.
- **Feeds vary wildly.** Missing enclosures, absent `itunes:duration`, no season numbers, RFC-822 dates with the wrong weekday, all of it is handled defensively and yields `null` rather than a wrong value.
- **Episodes are newest first.** That is the order feeds are published in, so `maxEpisodesPerShow` gives you the most recent N.
- **One failure never kills the run.** A failed search, a dead feed or a private feed is logged into `RUN_SUMMARY.failures` and the run continues. The Actor only errors out when nothing at all was saved.
- **Public data only.** No authentication, no personal data, no paywall circumvention. Audio URLs are the ones publishers put in their public feeds.

### FAQ

**Do I need an Apple developer account?** No.

**Can I get more than 100 shows for one topic?** Not from one search. Split the topic into several narrower terms, or supply IDs and feed URLs directly.

**Does it download the audio?** No. It gives you `audioUrl`, `audioLengthBytes` and `audioType` so you can fetch or stream the file yourself.

**Are transcripts included?** Only if the publisher puts them in the show notes, which most do not. There is no structured transcript field in podcast RSS.

**Why is `contentText` null on a show record?** The channel description comes from the feed, and the feed is only fetched when `includeEpisodes` is on or when you passed the feed URL directly. Everything else on the show record comes from Apple.

**Why did two inputs produce one show?** Because they resolved to the same feed, or to the same Apple ID. Deduplication uses both, and ignores the cosmetic differences between feed URLs, so you are never charged twice for the same catalogue.

**I asked for 20 shows across 4 terms and got 20, not 80.** That is the contract: `maxShows` is what the run delivers in total, shared evenly between the terms. Raise it if you want more.

**Can I integrate it with something else?** Yes, Apify API, client libraries, webhooks, scheduled runs, dataset exports (JSON/CSV/Excel) or MCP. Output is structured JSON.

# Actor input Schema

## `searchTerms` (type: `array`):

Keywords to look up in Apple's podcast directory: a topic ('true crime'), a show name ('The Daily') or a publisher ('NPR'). The 'Max shows' cap is shared out evenly between the terms, so two terms at a cap of 10 give you five shows each. Leave empty if you are using podcast IDs or feed URLs instead.

## `podcastIds` (type: `array`):

Exact shows by Apple ID: the number after 'id' in the Apple Podcasts URL (podcasts.apple.com/us/podcast/the-daily/id1200361736 → '1200361736'). Paste the whole show URL if you prefer; the ID is extracted. A show name, a publisher (artist) page or anything else with no show ID in it is rejected and listed in the run summary, never guessed at.

## `feedUrls` (type: `array`):

Podcast RSS feeds to read without going through Apple at all. Use this for shows that are not in the directory, or when you already know the feed. Show metadata then comes from the feed's own channel tags rather than from Apple. Aliases of one feed (http/https, www, letter case, a trailing slash, campaign parameters) count as one feed and are charged once.

## `country` (type: `string`):

Two-letter ISO country code for the Apple storefront to search. Results and ranking differ by market (US, GB, DE, IN, BR…). Has no effect on direct feed URLs.

## `maxShows` (type: `integer`):

How many show records the whole run may deliver, across every search term, ID and feed URL combined — not a per-term allowance. IDs and feed URLs are read first, then the remainder is shared evenly between the search terms. A single search cannot return more than 100 shows whatever you ask for.

## `maxRecords` (type: `integer`):

Hard ceiling on the total rows the run delivers, shows and episodes together. This is what bounds a run with episodes switched on, since a long-running show can carry thousands of them. The run stops pushing at this number and says so in the run summary.

## `includeEpisodes` (type: `boolean`):

Fetch each show's RSS feed and emit one record per episode alongside the show record. This is one extra HTTP request per show, so a 100-show search becomes a much longer run. Off by default.

## `maxEpisodesPerShow` (type: `integer`):

Cap on episodes saved per show, newest first, when 'Include episodes' is on. Long-running shows publish thousands, and feeds are ordered newest first, so a small cap gives you the recent catalogue cheaply. 0 = every episode in the feed, still bounded by 'Max records'.

## `maxShowsPerTerm` (type: `integer`):

Deprecated former name for 'Max shows'. It never was per-term in any meaningful sense and is now treated exactly as 'Max shows': a ceiling on the whole run. Use 'Max shows' instead; this is kept only so existing API callers keep a bounded run.

## Actor input object example

```json
{
  "searchTerms": [
    "history",
    "startup"
  ],
  "podcastIds": [
    "1200361736",
    "https://podcasts.apple.com/us/podcast/serial/id917918570"
  ],
  "feedUrls": [
    "https://feeds.simplecast.com/Sl5CSM3S"
  ],
  "country": "GB",
  "maxShows": 20,
  "maxRecords": 1000,
  "includeEpisodes": false,
  "maxEpisodesPerShow": 10
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN\_SUMMARY record from the run's key-value store.

# 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 = {
    "searchTerms": [
        "true crime",
        "software engineering"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/podcast-directory-scraper").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 = { "searchTerms": [
        "true crime",
        "software engineering",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/podcast-directory-scraper").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 '{
  "searchTerms": [
    "true crime",
    "software engineering"
  ]
}' |
apify call arman-bd/podcast-directory-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/podcast-directory-scraper"
        }
    }
}

```

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/kSlTzgziYGVdwsybK/builds/b8IUPwkqgw9K9fLLe/openapi.json
