Back to skills
SkillHub ClubShip Full StackFull Stack

podcast-downloader

Search and download podcast episodes from Apple Podcasts. Use when user wants to find podcasts, download podcast episodes, get podcast information, or mentions Apple Podcasts, iTunes, podcast search, or audio downloads.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
182
Hot score
97
Updated
March 20, 2026
Overall rating
C3.0
Composite score
3.0
Best-practice grade
B75.6

Install command

npx @skill-hub/cli install crazynomad-tutorial-podcast-downloader

Repository

crazynomad/tutorial

Skill path: notebooklm/.claude/skills/podcast-downloader

Search and download podcast episodes from Apple Podcasts. Use when user wants to find podcasts, download podcast episodes, get podcast information, or mentions Apple Podcasts, iTunes, podcast search, or audio downloads.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: crazynomad.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install podcast-downloader into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/crazynomad/tutorial before adding podcast-downloader to shared team environments
  • Use podcast-downloader for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: podcast-downloader
description: Search and download podcast episodes from Apple Podcasts. Use when user wants to find podcasts, download podcast episodes, get podcast information, or mentions Apple Podcasts, iTunes, podcast search, or audio downloads.
allowed-tools: Bash(python3:*), Bash(curl:*), Read, Write
---

# Apple Podcast Downloader

A comprehensive skill for searching, browsing, and downloading podcast episodes from Apple Podcasts using the iTunes Search API.

## Core Capabilities

1. **Search Podcasts** - Find podcasts by keyword, author, or topic
2. **Browse Episodes** - List episodes from a specific podcast
3. **Download Audio** - Download podcast episodes as MP3 files
4. **Get Metadata** - Retrieve detailed information about podcasts and episodes

## Quick Start

### Search for Podcasts

When user asks to search for podcasts:

1. Use the helper script to search:
```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "keyword" [limit]
```

2. Display results in a clear table format showing:
   - Podcast name
   - Author/Publisher
   - Total episodes
   - Genres
   - Collection ID (for downloading episodes)

### List Episodes

When user wants to see episodes from a podcast:

1. Get the collection ID from search results
2. Fetch episodes:
```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py episodes <collection_id> [limit]
```

3. Show episode list with:
   - Episode title
   - Release date
   - Duration
   - Short description
   - Episode index number (for downloading)

### Download Episodes

When user wants to download podcast audio:

1. Ensure download directory exists:
```bash
mkdir -p downloads/podcasts
```

2. Download using the helper script:
```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download <collection_id> <episode_index> [output_path]
```

3. Confirm download completion with file size and location

## Workflow Examples

### Example 1: Search and Download Latest Episode

**User Request**: "Download the latest episode of The Daily podcast"

**Steps**:
1. Search for "The Daily"
2. Get the collection ID from results
3. Fetch episodes (limit 1)
4. Download the first episode
5. Confirm completion

### Example 2: Browse and Select

**User Request**: "Show me the latest 10 episodes of Python Bytes and let me choose which to download"

**Steps**:
1. Search for "Python Bytes"
2. Get collection ID
3. Fetch 10 latest episodes
4. Display numbered list
5. Wait for user selection
6. Download selected episode(s)

### Example 3: Batch Download

**User Request**: "Download the 5 latest episodes from All-In Podcast"

**Steps**:
1. Search for "All-In Podcast"
2. Get collection ID
3. Fetch 5 latest episodes
4. Download each episode sequentially
5. Report progress and completion

## Best Practices

### User Experience

1. **Always confirm before downloading** - Show episode details and ask for confirmation
2. **Display progress** - Show download progress and estimated time
3. **Handle errors gracefully** - Provide clear error messages and suggestions
4. **Organize downloads** - Create organized directory structure (e.g., `downloads/podcasts/podcast-name/`)

### Error Handling

Common errors and solutions:

- **No results found**: Suggest alternative search terms
- **Invalid collection ID**: Verify the ID or re-search
- **Download failed**: Check network connection, retry with error details
- **File exists**: Ask user whether to overwrite or skip

### Performance

- **Limit search results**: Default to 10 results, max 50
- **Batch downloads**: Use sequential downloads to avoid overwhelming the API
- **Cache metadata**: Reuse search results within the same conversation

## Command Reference

### Search Command
```bash
python3 scripts/itunes_api.py search <keyword> [limit]
```
**Parameters**:
- `keyword`: Search term (required)
- `limit`: Number of results (optional, default: 10)

**Output**: JSON array of podcast objects

### Episodes Command
```bash
python3 scripts/itunes_api.py episodes <collection_id> [limit]
```
**Parameters**:
- `collection_id`: Podcast ID from search results (required)
- `limit`: Number of episodes (optional, default: 10)

**Output**: JSON array of episode objects

### Download Command
```bash
python3 scripts/itunes_api.py download <collection_id> <episode_index> [output_path]
```
**Parameters**:
- `collection_id`: Podcast ID (required)
- `episode_index`: Episode number from list (0-based) (required)
- `output_path`: Save location (optional, default: downloads/podcasts/)

**Output**: Downloaded MP3 file path

## Data Structures

### Podcast Object
```json
{
  "collectionId": 1200361736,
  "collectionName": "The Daily",
  "artistName": "The New York Times",
  "trackCount": 2464,
  "feedUrl": "https://feeds.simplecast.com/...",
  "genres": ["Daily News", "Podcasts", "News"]
}
```

### Episode Object
```json
{
  "trackId": 1000742770142,
  "trackName": "Episode Title",
  "releaseDate": "2025-12-26T10:45:00Z",
  "trackTimeMillis": 1247000,
  "episodeUrl": "https://...",
  "description": "Full description...",
  "shortDescription": "Brief description..."
}
```

## Advanced Features

### RSS Feed Access

Podcasts include RSS feed URLs that can be used for:
- Getting ALL episodes (not limited by API)
- Subscribing in podcast apps
- Accessing additional metadata

Access via `feedUrl` field in search results.

### Metadata Extraction

Extract rich metadata including:
- Artwork (multiple resolutions: 30px, 60px, 100px, 600px)
- Genres and categories
- Explicit content ratings
- Publisher information
- Episode descriptions

### Filtering and Sorting

When displaying results, consider:
- Sorting by release date (newest first)
- Filtering by duration
- Grouping by genre
- Showing only recent episodes (e.g., last 30 days)

## Troubleshooting

### Common Issues

**Issue**: "curl: command not found"
**Solution**: Install curl or use Python's requests library

**Issue**: "Invalid JSON response"
**Solution**: Check network connection and API availability

**Issue**: "Permission denied" when saving files
**Solution**: Check directory permissions or use different output path

**Issue**: "File too large"
**Solution**: Check available disk space, typical episodes are 20-100MB

## Additional Resources

- For detailed API documentation, see [reference.md](reference.md)
- For more usage examples, see [examples.md](examples.md)
- Helper script source: `scripts/itunes_api.py`

## Notes

- This skill uses the free Apple iTunes Search API (no authentication required)
- Audio files are downloaded directly from podcast CDNs
- Supports all podcasts available on Apple Podcasts
- Download speeds depend on network connection and CDN performance


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### reference.md

```markdown
# Podcast Downloader - API Reference

Complete reference documentation for the Apple iTunes Search API and helper script.

## Table of Contents

1. [iTunes Search API](#itunes-search-api)
2. [Helper Script Reference](#helper-script-reference)
3. [Data Structures](#data-structures)
4. [Error Codes](#error-codes)
5. [Performance Guidelines](#performance-guidelines)

---

## iTunes Search API

### Base URL
```
https://itunes.apple.com
```

### Endpoints

#### 1. Search Endpoint

**URL**: `/search`

**Method**: GET

**Purpose**: Search for podcasts or episodes by keyword

**Parameters**:

| Parameter | Required | Type | Default | Description |
|-----------|----------|------|---------|-------------|
| `term` | Yes | string | - | Search keyword (URL encoded) |
| `entity` | No | string | `all` | Search type: `podcast`, `podcastEpisode` |
| `limit` | No | integer | 50 | Number of results (max: 200) |
| `country` | No | string | `US` | Country code (US, CN, GB, etc.) |
| `lang` | No | string | `en_us` | Language code |
| `explicit` | No | string | - | Filter explicit content: `Yes`, `No` |

**Example Request**:
```
https://itunes.apple.com/search?term=python+programming&entity=podcast&limit=10
```

**Response Structure**:
```json
{
  "resultCount": 10,
  "results": [
    {
      "wrapperType": "track",
      "kind": "podcast",
      "collectionId": 979020229,
      "trackId": 979020229,
      "artistName": "Michael Kennedy",
      "collectionName": "Talk Python To Me",
      "trackName": "Talk Python To Me",
      "feedUrl": "https://talkpython.fm/episodes/rss",
      "trackCount": 445,
      "artworkUrl600": "https://...",
      "primaryGenreName": "Technology",
      "genres": ["Technology", "Podcasts"]
    }
  ]
}
```

---

#### 2. Lookup Endpoint

**URL**: `/lookup`

**Method**: GET

**Purpose**: Get specific podcast details or episode list by ID

**Parameters**:

| Parameter | Required | Type | Default | Description |
|-----------|----------|------|---------|-------------|
| `id` | Yes | integer | - | Collection ID or Track ID |
| `entity` | No | string | - | Return type: `podcast`, `podcastEpisode` |
| `limit` | No | integer | 50 | Number of episodes to return |

**Example Request**:
```
https://itunes.apple.com/lookup?id=979020229&entity=podcastEpisode&limit=10
```

**Response Structure**:
```json
{
  "resultCount": 11,
  "results": [
    {
      "wrapperType": "track",
      "kind": "podcast",
      "collectionId": 979020229,
      "...": "podcast details"
    },
    {
      "wrapperType": "podcastEpisode",
      "kind": "podcast-episode",
      "trackId": 1000742770142,
      "trackName": "Episode Title",
      "releaseDate": "2025-12-26T10:45:00Z",
      "trackTimeMillis": 1247000,
      "episodeUrl": "https://...",
      "episodeFileExtension": "mp3",
      "description": "Full description",
      "shortDescription": "Brief description"
    }
  ]
}
```

**Note**: First result is always the podcast itself, subsequent results are episodes.

---

## Helper Script Reference

### Installation

No installation required. Uses Python 3 standard library only.

### Usage

```bash
python3 scripts/itunes_api.py <command> [arguments]
```

### Commands

#### search

Search for podcasts by keyword.

**Syntax**:
```bash
python3 scripts/itunes_api.py search <keyword> [limit]
```

**Arguments**:
- `keyword` (required): Search term
- `limit` (optional): Number of results, default: 10, max: 200

**Output**: JSON array to stdout, progress to stderr

**Example**:
```bash
python3 scripts/itunes_api.py search "python programming" 5
```

**Output Format**:
```json
[
  {
    "collectionId": 979020229,
    "collectionName": "Talk Python To Me",
    "artistName": "Michael Kennedy",
    "trackCount": 445,
    "feedUrl": "https://talkpython.fm/episodes/rss",
    "genres": ["Technology", "Podcasts"]
  }
]
```

---

#### episodes

Get episode list for a podcast.

**Syntax**:
```bash
python3 scripts/itunes_api.py episodes <collection_id> [limit]
```

**Arguments**:
- `collection_id` (required): Podcast ID from search results
- `limit` (optional): Number of episodes, default: 10, max: 100*

**Output**: JSON array to stdout, progress to stderr

**Example**:
```bash
python3 scripts/itunes_api.py episodes 979020229 10
```

**Output Format**:
```json
[
  {
    "trackId": 1000742770142,
    "trackName": "Episode Title",
    "releaseDate": "2025-12-26T10:45:00Z",
    "trackTimeMillis": 1247000,
    "episodeUrl": "https://...",
    "collectionName": "Talk Python To Me",
    "description": "Full description",
    "shortDescription": "Brief description"
  }
]
```

*Note: iTunes API has a practical limit around 100 episodes per request.

---

#### download

Download a podcast episode.

**Syntax**:
```bash
python3 scripts/itunes_api.py download <collection_id> <episode_index> [output_dir]
```

**Arguments**:
- `collection_id` (required): Podcast ID
- `episode_index` (required): 0-based index from episode list
- `output_dir` (optional): Download directory, default: `downloads/podcasts`

**Output**: JSON result to stdout, progress to stderr

**Example**:
```bash
python3 scripts/itunes_api.py download 979020229 0 ~/Downloads
```

**Output Format** (Success):
```json
{
  "success": true,
  "path": "/Users/username/Downloads/Talk Python To Me/Talk Python To Me - Episode Title.mp3",
  "episode": "Episode Title",
  "podcast": "Talk Python To Me"
}
```

**Output Format** (Error):
```json
{
  "success": false,
  "error": "Error message"
}
```

---

## Data Structures

### Podcast Object

Complete podcast metadata from iTunes API.

```typescript
interface Podcast {
  // Identification
  wrapperType: "track"
  kind: "podcast"
  collectionId: number          // Unique podcast ID
  trackId: number               // Same as collectionId
  artistId: number              // Publisher/creator ID

  // Names and URLs
  artistName: string            // Publisher name
  collectionName: string        // Podcast name
  trackName: string             // Same as collectionName
  artistViewUrl: string         // Publisher's page
  collectionViewUrl: string     // Podcast page on Apple Podcasts
  feedUrl: string               // RSS feed URL

  // Artwork
  artworkUrl30: string          // 30x30px thumbnail
  artworkUrl60: string          // 60x60px thumbnail
  artworkUrl100: string         // 100x100px thumbnail
  artworkUrl600: string         // 600x600px cover art

  // Metadata
  trackCount: number            // Total number of episodes
  primaryGenreName: string      // Main genre
  genres: string[]              // All genres
  releaseDate: string           // Latest episode date (ISO 8601)
  country: string               // Country code
  currency: string              // Currency code

  // Ratings
  collectionExplicitness: "notExplicit" | "explicit" | "cleaned"
  contentAdvisoryRating: string // Rating (e.g., "Clean")

  // Pricing
  collectionPrice: number       // Usually 0 (free)
  trackPrice: number            // Usually 0 (free)
}
```

---

### Episode Object

Complete episode metadata from iTunes API.

```typescript
interface Episode {
  // Identification
  wrapperType: "podcastEpisode"
  kind: "podcast-episode"
  trackId: number               // Unique episode ID
  collectionId: number          // Parent podcast ID
  episodeGuid: string           // Episode GUID from RSS

  // Names
  trackName: string             // Episode title
  collectionName: string        // Podcast name
  artistIds: number[]           // Creator IDs

  // URLs
  trackViewUrl: string          // Episode page on Apple Podcasts
  collectionViewUrl: string     // Podcast page
  episodeUrl: string            // โญ Direct audio file URL
  feedUrl: string               // Podcast RSS feed

  // Audio info
  episodeFileExtension: "mp3"   // File format
  episodeContentType: "audio"   // Content type

  // Artwork
  artworkUrl60: string
  artworkUrl160: string
  artworkUrl600: string

  // Metadata
  releaseDate: string           // ISO 8601 date
  trackTimeMillis: number       // Duration in milliseconds
  description: string           // Full HTML description
  shortDescription: string      // Plain text summary

  // Ratings
  contentAdvisoryRating: string
  closedCaptioning: "none" | "available"

  // Categories
  genres: Array<{
    name: string
    id: string
  }>
}
```

---

## Error Codes

### HTTP Status Codes

| Code | Meaning | Handling |
|------|---------|----------|
| 200 | Success | Parse JSON response |
| 302 | Redirect | Follow redirect (automatic) |
| 400 | Bad Request | Check parameters |
| 403 | Forbidden | Check terms of service compliance |
| 404 | Not Found | Invalid ID or deleted content |
| 429 | Too Many Requests | Implement rate limiting |
| 500 | Server Error | Retry with exponential backoff |
| 503 | Service Unavailable | Retry later |

### Script Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Error (see stderr for details) |

---

## Performance Guidelines

### Rate Limiting

**iTunes API**:
- No official rate limit published
- Recommended: Max 20 requests per second
- Implement exponential backoff on errors

**Best Practices**:
```python
import time

def with_rate_limit(func):
    """Decorator to rate limit API calls"""
    last_call = [0]

    def wrapper(*args, **kwargs):
        now = time.time()
        time_since_last = now - last_call[0]

        if time_since_last < 0.05:  # 20 req/sec = 0.05s between
            time.sleep(0.05 - time_since_last)

        result = func(*args, **kwargs)
        last_call[0] = time.time()
        return result

    return wrapper
```

---

### Caching Strategy

**Search Results**:
- Cache duration: 1 hour
- Key: `search:{term}:{limit}`
- Invalidate: Manual or on error

**Episode Lists**:
- Cache duration: 15 minutes
- Key: `episodes:{collection_id}:{limit}`
- Invalidate: On new episode detection

**Example Implementation**:
```python
import json
import time
from pathlib import Path

class SimpleCache:
    def __init__(self, cache_dir=".cache"):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(exist_ok=True)

    def get(self, key, max_age=3600):
        cache_file = self.cache_dir / f"{key}.json"

        if not cache_file.exists():
            return None

        # Check age
        age = time.time() - cache_file.stat().st_mtime
        if age > max_age:
            return None

        with open(cache_file) as f:
            return json.load(f)

    def set(self, key, data):
        cache_file = self.cache_dir / f"{key}.json"
        with open(cache_file, 'w') as f:
            json.dump(data, f)
```

---

### Download Optimization

**Parallel Downloads**:
```python
from concurrent.futures import ThreadPoolExecutor

def download_multiple(episodes, max_workers=3):
    """Download multiple episodes in parallel"""
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [
            executor.submit(download_episode, ep)
            for ep in episodes
        ]

        for future in futures:
            try:
                result = future.result()
                print(f"Downloaded: {result}")
            except Exception as e:
                print(f"Error: {e}")
```

**Recommendations**:
- Max 3 concurrent downloads
- Use streaming for large files
- Show progress for downloads >10MB
- Implement resume capability for failed downloads

---

### Memory Management

**Large Episode Lists**:
```python
def get_all_episodes(collection_id):
    """Get all episodes using pagination"""
    episodes = []
    batch_size = 100
    offset = 0

    while True:
        batch = get_episodes(collection_id, limit=batch_size, offset=offset)

        if not batch:
            break

        episodes.extend(batch)
        offset += batch_size

        # Prevent infinite loop
        if len(batch) < batch_size:
            break

    return episodes
```

---

## Advanced Usage

### Using RSS Feeds Directly

For getting ALL episodes (not limited by API):

```python
import xml.etree.ElementTree as ET
from urllib.request import urlopen

def get_all_episodes_from_rss(feed_url):
    """Parse RSS feed to get all episodes"""
    response = urlopen(feed_url)
    xml_data = response.read()

    root = ET.fromstring(xml_data)
    episodes = []

    for item in root.findall('.//item'):
        episode = {
            'title': item.find('title').text,
            'description': item.find('description').text,
            'pub_date': item.find('pubDate').text,
            'audio_url': item.find('enclosure').get('url'),
            'duration': item.find('{http://www.itunes.com/dtds/podcast-1.0.dtd}duration').text
        }
        episodes.append(episode)

    return episodes
```

---

### Filtering and Sorting

```python
def filter_episodes(episodes, **criteria):
    """Filter episodes by criteria"""
    filtered = episodes

    # Filter by date range
    if 'after' in criteria:
        after = criteria['after']
        filtered = [e for e in filtered if e['releaseDate'] >= after]

    if 'before' in criteria:
        before = criteria['before']
        filtered = [e for e in filtered if e['releaseDate'] <= before]

    # Filter by duration
    if 'min_duration' in criteria:
        min_dur = criteria['min_duration']
        filtered = [e for e in filtered if e.get('trackTimeMillis', 0) >= min_dur]

    if 'max_duration' in criteria:
        max_dur = criteria['max_duration']
        filtered = [e for e in filtered if e.get('trackTimeMillis', 0) <= max_dur]

    return filtered

# Usage
recent_short_episodes = filter_episodes(
    episodes,
    after='2025-12-01T00:00:00Z',
    max_duration=30 * 60 * 1000  # 30 minutes in milliseconds
)
```

---

## Troubleshooting

### Common Issues

**Issue**: API returns empty results
**Causes**:
- Search term too specific
- Content not available in specified country
- Podcast removed from Apple Podcasts

**Solutions**:
- Broaden search terms
- Try different country codes
- Search with artist name instead

---

**Issue**: Download fails with 403/404
**Causes**:
- Episode removed or moved
- CDN link expired
- Geo-restrictions

**Solutions**:
- Re-fetch episode list to get fresh URL
- Try different network/VPN
- Contact podcast publisher

---

**Issue**: Slow downloads
**Causes**:
- Network congestion
- CDN routing
- Large file size

**Solutions**:
- Use parallel downloads (limit 3)
- Download during off-peak hours
- Implement resume capability

---

## API Limits and Quotas

| Resource | Limit | Notes |
|----------|-------|-------|
| Search results | 200 per request | Use pagination for more |
| Episode list | ~100 per request | Use RSS for full list |
| Request rate | ~20/second (unofficial) | Implement rate limiting |
| File size | No limit | Typical: 20-200 MB |

---

## Legal and Terms of Service

- iTunes Search API is provided by Apple Inc.
- Content accessed through API is subject to copyright
- Downloaded content for personal use only
- Respect publisher rights and terms
- See: [Apple Developer Terms](https://developer.apple.com/terms/)

---

**Last Updated**: 2025-12-27
**API Version**: iTunes Search API v2
**Helper Script Version**: 1.0.0

```

### examples.md

```markdown
# Podcast Downloader - Usage Examples

This document provides detailed examples of using the podcast-downloader skill.

## Example 1: Simple Search and Download

**User Request**: "Find and download the latest episode of The Daily"

**Claude's Response**:

```
I'll search for "The Daily" podcast and download the latest episode.

๐Ÿ” Searching for podcasts...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "the daily" 5
```

```
โœ… Found 5 podcasts:

1. The Daily (ID: 1200361736)
   - Author: The New York Times
   - Episodes: 2464
   - Genre: Daily News

2. The Journal. (ID: 1469394914)
   - Author: The Wall Street Journal
   - Episodes: 311
   - Genre: Daily News

3. Today, Explained (ID: 1346207297)
   - Author: Vox
   - Episodes: 2000
   - Genre: Daily News

I'll select #1 "The Daily" from The New York Times.

๐Ÿ“ป Getting latest episode...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py episodes 1200361736 1
```

```
Latest episode:
- Title: "Marriage and Sex in the Age of Ozempic: An Update"
- Released: 2025-12-26
- Duration: 20m 47s

Would you like me to download this episode?
```

**User**: "Yes, please"

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download 1200361736 0
```

```
๐Ÿ“ฅ Downloading episode...
โณ Progress: 100.0% (19.0/19.0 MB)
โœ… Download complete!

File saved to: downloads/podcasts/The Daily/The Daily - Marriage and Sex in the Age of Ozempic An Update.mp3
```

---

## Example 2: Browse Episodes Before Downloading

**User Request**: "Show me the last 5 episodes of Python Bytes"

**Claude's Response**:

```
I'll search for "Python Bytes" and show you the latest episodes.

๐Ÿ” Searching...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "python bytes" 3
```

```
Found podcast: Python Bytes (ID: 1173690032)
- Author: Michael Kennedy & Brian Okken
- Episodes: 400+
- Genre: Technology

๐Ÿ“ป Fetching latest 5 episodes...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py episodes 1173690032 5
```

```
Latest 5 episodes:

[0] Episode #367: Fastest Python Editor
    Released: 2025-12-20 | Duration: 52m
    Description: Discussing the fastest Python code editors and IDE performance...

[1] Episode #366: Python 3.13 Features
    Released: 2025-12-13 | Duration: 48m
    Description: New features in Python 3.13 including performance improvements...

[2] Episode #365: Type Hints Deep Dive
    Released: 2025-12-06 | Duration: 55m
    Description: Advanced type hinting patterns and best practices...

[3] Episode #364: Async Python Patterns
    Released: 2025-11-29 | Duration: 51m
    Description: Modern asyncio patterns and common pitfalls...

[4] Episode #363: Testing Strategies
    Released: 2025-11-22 | Duration: 49m
    Description: Comprehensive testing strategies for Python projects...

Which episode(s) would you like to download? (Provide the number, e.g., 0 or 0,2,4)
```

**User**: "Download 0 and 1"

```
Downloading 2 episodes...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download 1173690032 0
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download 1173690032 1
```

```
โœ… Downloaded:
1. downloads/podcasts/Python Bytes/Python Bytes - Episode #367 Fastest Python Editor.mp3
2. downloads/podcasts/Python Bytes/Python Bytes - Episode #366 Python 3.13 Features.mp3
```

---

## Example 3: Batch Download Multiple Episodes

**User Request**: "Download the 3 most recent episodes of All-In Podcast"

**Claude's Response**:

```
I'll find "All-In Podcast" and download the 3 latest episodes.

๐Ÿ” Searching...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "all-in podcast" 3
```

```
Found: All-In with Chamath, Jason, Sacks & Friedberg (ID: 1502871393)

๐Ÿ“ป Getting latest 3 episodes...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py episodes 1502871393 3
```

```
Found 3 episodes:

[0] E163: Tech predictions 2026
[1] E162: Year in review 2025
[2] E161: Trump's second term begins

Downloading all 3 episodes...

Episode 1/3: E163: Tech predictions 2026
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download 1502871393 0
```

```
โณ Progress: 100.0%
โœ… Complete

Episode 2/3: E162: Year in review 2025
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download 1502871393 1
```

```
โณ Progress: 100.0%
โœ… Complete

Episode 3/3: E161: Trump's second term begins
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download 1502871393 2
```

```
โณ Progress: 100.0%
โœ… Complete

โœ… All 3 episodes downloaded successfully!
Location: downloads/podcasts/All-In with Chamath Jason Sacks & Friedberg/
```

---

## Example 4: Search with Specific Criteria

**User Request**: "Find technology podcasts about AI and show me the top 3"

**Claude's Response**:

```
I'll search for technology podcasts related to AI.

๐Ÿ” Searching for: "AI technology"
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "AI technology" 10
```

```
Found 10 podcasts. Here are the top 3:

1. AI Breakdown
   - Author: NLX.ai
   - Episodes: 245
   - Genre: Technology
   - Description: Daily AI news and analysis

2. The AI Podcast
   - Author: NVIDIA
   - Episodes: 180
   - Genre: Technology
   - Description: Conversations with AI researchers and practitioners

3. Hard Fork
   - Author: The New York Times
   - Episodes: 156
   - Genre: Technology
   - Description: Tech news including AI developments

Would you like to:
- See episodes from any of these? (Specify number)
- Download latest episodes? (Specify number and how many)
- Search for something else?
```

---

## Example 5: Error Handling

**User Request**: "Download episode 100 from The Daily"

**Claude's Response**:

```
I'll fetch episode #100 from The Daily podcast.

๐Ÿ” Getting podcast info...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "the daily" 1
```

```
Found: The Daily (ID: 1200361736)

โš ๏ธ Note: The API limits episode fetching. To get episode #100, I'll need to fetch 101 episodes.

๐Ÿ“ป Fetching episodes...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py episodes 1200361736 101
```

```
โš ๏ธ API returned only 100 episodes (API limit: 100)

The episode you requested (#100, 0-indexed) is:
- Title: "[Episode from ~3 months ago]"
- Date: 2025-09-20
- Duration: 28m

Would you like to download this episode, or would you prefer a more recent one?
```

---

## Example 6: Custom Download Location

**User Request**: "Download latest episode of Lex Fridman to my Desktop/Podcasts folder"

**Claude's Response**:

```
I'll download the latest Lex Fridman podcast to your Desktop/Podcasts folder.

๐Ÿ” Searching...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "lex fridman" 1
```

```
Found: Lex Fridman Podcast (ID: 1434243584)

๐Ÿ“ป Getting latest episode...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py episodes 1434243584 1
```

```
Latest episode:
- Title: "#410 - Elon Musk on AI, Mars, and the Future"
- Released: 2025-12-15
- Duration: 3h 12m

๐Ÿ“ฅ Downloading to Desktop/Podcasts...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py download 1434243584 0 "/Users/username/Desktop/Podcasts"
```

```
โœ… Downloaded to:
/Users/username/Desktop/Podcasts/Lex Fridman Podcast/Lex Fridman Podcast - #410 - Elon Musk on AI Mars and the Future.mp3

File size: 185 MB
```

---

## Example 7: Getting Podcast Information

**User Request**: "What's the RSS feed URL for The Daily podcast?"

**Claude's Response**:

```
I'll get the RSS feed information for The Daily.

๐Ÿ” Searching...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "the daily" 1
```

```
The Daily - by The New York Times

๐Ÿ“Š Podcast Information:
- Collection ID: 1200361736
- Total Episodes: 2464
- Genre: Daily News
- RSS Feed: https://feeds.simplecast.com/Sl5CSM3S
- Apple Podcasts URL: https://podcasts.apple.com/us/podcast/the-daily/id1200361736

You can use this RSS feed URL to:
- Subscribe in any podcast app
- Get all episodes (not just recent ones)
- Access episode metadata
```

---

## Example 8: Search Refinement

**User Request**: "Find podcasts about Python programming"

**Claude's Response**:

```
๐Ÿ” Searching for Python programming podcasts...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py search "python programming" 10
```

```
Found 10 podcasts:

Top 5:
1. Talk Python To Me
2. Python Bytes
3. Real Python Podcast
4. Teaching Python
5. Test & Code

Would you like to:
- See more details about any of these? (Specify number)
- List episodes from any? (Specify number)
- Download latest episodes? (Specify which and how many)
- Refine search? (Provide new keywords)
```

**User**: "Show me episodes from Talk Python To Me"

```
๐Ÿ“ป Fetching episodes from Talk Python To Me (ID: 979020229)...
```

```bash
python3 .claude/skills/podcast-downloader/scripts/itunes_api.py episodes 979020229 10
```

```
Latest 10 episodes:

[0] #445: Django performance improvements
[1] #444: Python packaging in 2025
[2] #443: FastAPI best practices
[3] #442: Data science with Polars
[4] #441: Testing strategies
[5] #440: Python 3.13 deep dive
[6] #439: Building CLI tools
[7] #438: Async Python patterns
[8] #437: Web scraping techniques
[9] #436: ML deployment strategies

Which would you like to download?
```

---

## Common Patterns

### Pattern: "Quick Download"
User: "Download latest [podcast name]"
โ†’ Search โ†’ Get episodes (limit 1) โ†’ Download

### Pattern: "Browse and Choose"
User: "Show me recent [podcast name] episodes"
โ†’ Search โ†’ Get episodes (limit 10) โ†’ Display โ†’ Wait for selection โ†’ Download

### Pattern: "Batch Download"
User: "Download [N] latest episodes of [podcast]"
โ†’ Search โ†’ Get episodes (limit N) โ†’ Download all sequentially

### Pattern: "Information Lookup"
User: "What's the RSS feed for [podcast]?"
โ†’ Search โ†’ Display metadata including feedUrl

### Pattern: "Explore Topic"
User: "Find podcasts about [topic]"
โ†’ Search โ†’ Display results โ†’ Offer next steps

```

### scripts/itunes_api.py

```python
#!/usr/bin/env python3
"""
Apple iTunes Podcast API Helper
Provides command-line interface for searching and downloading podcasts
"""

import sys
import json
import re
from pathlib import Path
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
from urllib.parse import quote


class iTunesAPI:
    """Apple iTunes Search API wrapper"""

    BASE_URL = "https://itunes.apple.com"

    def search_podcasts(self, term, limit=10):
        """
        Search for podcasts by keyword

        Args:
            term: Search keyword
            limit: Number of results (max 200)

        Returns:
            List of podcast dictionaries
        """
        url = f"{self.BASE_URL}/search?term={self._encode(term)}&entity=podcast&limit={limit}"
        data = self._fetch_json(url)
        return data.get("results", [])

    def get_episodes(self, collection_id, limit=10):
        """
        Get episodes for a podcast

        Args:
            collection_id: Podcast collection ID
            limit: Number of episodes to fetch

        Returns:
            List of episode dictionaries
        """
        url = f"{self.BASE_URL}/lookup?id={collection_id}&entity=podcastEpisode&limit={limit}"
        data = self._fetch_json(url)
        results = data.get("results", [])

        # Filter out the podcast itself (first result)
        return [r for r in results if r.get("kind") == "podcast-episode"]

    def download_episode(self, episode_url, output_path):
        """
        Download podcast episode audio file

        Args:
            episode_url: Direct URL to audio file
            output_path: Where to save the file

        Returns:
            Path to downloaded file
        """
        print(f"๐Ÿ“ฅ Downloading to: {output_path}", file=sys.stderr)

        try:
            request = Request(episode_url, headers={'User-Agent': 'Mozilla/5.0'})
            response = urlopen(request)

            # Get file size
            content_length = response.headers.get('Content-Length')
            total_size = int(content_length) if content_length else 0

            # Download with progress
            downloaded = 0
            chunk_size = 8192

            output_path.parent.mkdir(parents=True, exist_ok=True)

            with open(output_path, 'wb') as f:
                while True:
                    chunk = response.read(chunk_size)
                    if not chunk:
                        break

                    f.write(chunk)
                    downloaded += len(chunk)

                    # Show progress
                    if total_size > 0:
                        progress = (downloaded / total_size) * 100
                        mb_downloaded = downloaded / (1024 * 1024)
                        mb_total = total_size / (1024 * 1024)
                        print(f"\rโณ Progress: {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)",
                              end='', file=sys.stderr)

            print(f"\nโœ… Download complete: {output_path}", file=sys.stderr)
            return str(output_path)

        except (URLError, HTTPError) as e:
            print(f"\nโŒ Download failed: {e}", file=sys.stderr)
            raise

    def _fetch_json(self, url):
        """Fetch and parse JSON from URL"""
        try:
            request = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            response = urlopen(request)
            return json.loads(response.read().decode('utf-8'))
        except (URLError, HTTPError) as e:
            print(f"โŒ API request failed: {e}", file=sys.stderr)
            raise

    def _encode(self, text):
        """URL encode text (supports Unicode)"""
        return quote(text, safe='')


def safe_filename(name, max_length=100):
    """Create safe filename from text"""
    # Remove unsafe characters
    safe = re.sub(r'[<>:"/\\|?*]', '', name)
    # Replace spaces and multiple dashes
    safe = re.sub(r'\s+', ' ', safe)
    safe = re.sub(r'-+', '-', safe)
    # Limit length
    return safe[:max_length].strip()


def format_duration(milliseconds):
    """Convert milliseconds to readable duration"""
    if not milliseconds:
        return "Unknown"

    seconds = milliseconds // 1000
    minutes = seconds // 60
    hours = minutes // 60

    if hours > 0:
        return f"{hours}h {minutes % 60}m"
    else:
        return f"{minutes}m {seconds % 60}s"


def format_date(iso_date):
    """Format ISO date to readable format"""
    if not iso_date:
        return "Unknown"

    # Simple format: 2025-12-26T10:45:00Z -> 2025-12-26
    return iso_date.split('T')[0]


def cmd_search(args):
    """Search command handler"""
    if len(args) < 1:
        print("Usage: search <keyword> [limit]", file=sys.stderr)
        return 1

    keyword = args[0]
    limit = int(args[1]) if len(args) > 1 else 10

    api = iTunesAPI()
    print(f"๐Ÿ” Searching for: '{keyword}'", file=sys.stderr)

    results = api.search_podcasts(keyword, limit)

    if not results:
        print("โŒ No results found", file=sys.stderr)
        return 1

    print(f"โœ… Found {len(results)} podcasts\n", file=sys.stderr)

    # Output JSON to stdout for Claude to parse
    print(json.dumps(results, indent=2))
    return 0


def cmd_episodes(args):
    """Episodes command handler"""
    if len(args) < 1:
        print("Usage: episodes <collection_id> [limit]", file=sys.stderr)
        return 1

    collection_id = args[0]
    limit = int(args[1]) if len(args) > 1 else 10

    api = iTunesAPI()
    print(f"๐Ÿ“ป Fetching episodes for podcast ID: {collection_id}", file=sys.stderr)

    episodes = api.get_episodes(collection_id, limit)

    if not episodes:
        print("โŒ No episodes found", file=sys.stderr)
        return 1

    print(f"โœ… Found {len(episodes)} episodes\n", file=sys.stderr)

    # Output JSON to stdout
    print(json.dumps(episodes, indent=2))
    return 0


def cmd_download(args):
    """Download command handler"""
    if len(args) < 2:
        print("Usage: download <collection_id> <episode_index> [output_dir]", file=sys.stderr)
        return 1

    collection_id = args[0]
    episode_index = int(args[1])
    output_dir = Path(args[2]) if len(args) > 2 else Path("downloads/podcasts")

    # Fetch episodes
    api = iTunesAPI()
    print(f"๐Ÿ“ป Fetching episodes for podcast ID: {collection_id}", file=sys.stderr)

    episodes = api.get_episodes(collection_id, limit=episode_index + 1)

    if not episodes or episode_index >= len(episodes):
        print(f"โŒ Episode index {episode_index} not found", file=sys.stderr)
        return 1

    episode = episodes[episode_index]

    # Get episode details
    episode_name = episode.get('trackName', 'Unknown Episode')
    episode_url = episode.get('episodeUrl')
    podcast_name = episode.get('collectionName', 'Unknown Podcast')

    if not episode_url:
        print("โŒ No download URL found for this episode", file=sys.stderr)
        return 1

    # Create safe filename
    filename = safe_filename(f"{podcast_name} - {episode_name}") + ".mp3"
    output_path = output_dir / safe_filename(podcast_name) / filename

    print(f"๐ŸŽง Episode: {episode_name}", file=sys.stderr)
    print(f"๐Ÿ“ Saving to: {output_path}", file=sys.stderr)

    # Download
    try:
        result_path = api.download_episode(episode_url, output_path)

        # Output result as JSON
        result = {
            "success": True,
            "path": result_path,
            "episode": episode_name,
            "podcast": podcast_name
        }
        print(json.dumps(result, indent=2))
        return 0

    except Exception as e:
        result = {
            "success": False,
            "error": str(e)
        }
        print(json.dumps(result, indent=2))
        return 1


def main():
    """Main entry point"""
    if len(sys.argv) < 2:
        print("Apple iTunes Podcast API Helper", file=sys.stderr)
        print("\nUsage:", file=sys.stderr)
        print("  search <keyword> [limit]              - Search for podcasts", file=sys.stderr)
        print("  episodes <collection_id> [limit]      - Get podcast episodes", file=sys.stderr)
        print("  download <collection_id> <index> [dir] - Download episode", file=sys.stderr)
        return 1

    command = sys.argv[1]
    args = sys.argv[2:]

    commands = {
        'search': cmd_search,
        'episodes': cmd_episodes,
        'download': cmd_download
    }

    if command not in commands:
        print(f"โŒ Unknown command: {command}", file=sys.stderr)
        return 1

    return commands[command](args)


if __name__ == "__main__":
    sys.exit(main())

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# Apple Podcast Downloader - Claude Skill

A comprehensive Claude Code skill for searching, browsing, and downloading podcast episodes from Apple Podcasts.

## ๐ŸŽฏ Features

- ๐Ÿ” **Search Podcasts** - Find podcasts by keyword, author, or topic
- ๐Ÿ“‹ **Browse Episodes** - List episodes with detailed metadata
- ๐Ÿ“ฅ **Download Audio** - Download episodes as MP3 files
- ๐Ÿ“Š **Get Metadata** - Access RSS feeds, artwork, and detailed information
- โšก **Fast & Simple** - No authentication required, uses Apple's free API

## ๐Ÿ“ Project Structure

```
podcast-downloader/
โ”œโ”€โ”€ SKILL.md              # Main skill definition (Claude reads this)
โ”œโ”€โ”€ README.md             # This file
โ”œโ”€โ”€ examples.md           # Usage examples and common patterns
โ”œโ”€โ”€ reference.md          # Complete API documentation
โ””โ”€โ”€ scripts/
    โ””โ”€โ”€ itunes_api.py     # Python helper script
```

## ๐Ÿš€ Quick Start

### Installation

1. This skill is already installed in `.claude/skills/podcast-downloader/`
2. No additional dependencies required (uses Python 3 standard library)
3. Claude will automatically use this skill when you mention podcasts

### Basic Usage

Just ask Claude natural questions:

```
"Find and download the latest episode of The Daily"
```

```
"Show me the 5 most recent Python Bytes episodes"
```

```
"Download episodes 1, 3, and 5 from All-In Podcast"
```

## ๐Ÿ’ก Example Conversations

### Example 1: Quick Download

**You**: "Download the latest episode of The Daily podcast"

**Claude**:
- Searches for "The Daily"
- Shows you the result
- Gets the latest episode
- Downloads it to `downloads/podcasts/The Daily/`

### Example 2: Browse and Choose

**You**: "Show me the latest 10 episodes of Python Bytes"

**Claude**:
- Searches for "Python Bytes"
- Lists 10 latest episodes with titles and descriptions
- Waits for you to choose which to download

### Example 3: Get Information

**You**: "What's the RSS feed URL for Talk Python To Me?"

**Claude**:
- Searches for the podcast
- Returns the RSS feed URL and other metadata

## ๐Ÿ”ง Technical Details

### How It Works

1. **Search**: Uses iTunes Search API to find podcasts
2. **Episodes**: Fetches episode list using podcast ID
3. **Download**: Downloads MP3 files directly from CDN

### API Information

- **Provider**: Apple iTunes Search API
- **Authentication**: None required (free public API)
- **Rate Limits**: Reasonable use (recommended: <20 req/sec)
- **Documentation**: See `reference.md` for complete API docs

### Helper Script

The Python helper script (`scripts/itunes_api.py`) provides three commands:

```bash
# Search for podcasts
python3 scripts/itunes_api.py search "keyword" [limit]

# Get episodes
python3 scripts/itunes_api.py episodes <podcast_id> [limit]

# Download episode
python3 scripts/itunes_api.py download <podcast_id> <episode_index> [output_dir]
```

## ๐Ÿ“š Documentation

- **SKILL.md** - Main instructions for Claude
- **examples.md** - Real-world usage examples
- **reference.md** - Complete API reference
- **README.md** - This overview (for humans)

## โš™๏ธ Skill Configuration

From `SKILL.md` frontmatter:

```yaml
name: podcast-downloader
description: Search and download podcast episodes from Apple Podcasts
allowed-tools: Bash(python3:*), Bash(curl:*), Read, Write
```

Claude will automatically activate this skill when you:
- Mention podcasts, Apple Podcasts, or iTunes
- Ask to search for or download podcast episodes
- Request podcast information or metadata

## ๐ŸŽ“ Teaching Resources

This skill was created as a teaching example for the NotebookLM tutorial on creating Claude Skills.

### Learning Objectives

1. **Skill Structure**: Understanding Claude Skill file organization
2. **API Integration**: Working with REST APIs in skills
3. **Tool Usage**: Using allowed-tools for system operations
4. **Documentation**: Creating comprehensive skill documentation

### What Makes This a Good Example

- โœ… **Simple API**: No authentication, easy to understand
- โœ… **Clear Use Case**: Practical and useful functionality
- โœ… **Complete Documentation**: All files included
- โœ… **Tested**: Fully functional and tested
- โœ… **Extensible**: Easy to add features

## ๐Ÿงช Testing

To test the skill functionality:

```bash
# Test search
python3 scripts/itunes_api.py search "python" 3

# Test episodes (Real Python Podcast ID: 1501905538)
python3 scripts/itunes_api.py episodes 1501905538 5

# Test download (creates a small test download)
python3 scripts/itunes_api.py download 1501905538 0
```

## ๐Ÿค Usage Tips

### For Best Results

1. **Be specific** in your search terms
2. **Confirm before downloading** large batches
3. **Check available space** for downloads
4. **Use episode numbers** from the list Claude provides

### Common Patterns

- **Quick download**: "Download latest [podcast name]"
- **Browse first**: "Show me recent episodes of [podcast]"
- **Batch download**: "Download episodes 1-5 from [podcast]"
- **Get info**: "What's the RSS feed for [podcast]?"

## ๐Ÿ“ Notes

- Downloads are saved to `downloads/podcasts/` by default
- Files are organized by podcast name
- MP3 format, typical quality: 128kbps, 44.1kHz
- Episode files are named: `[Podcast] - [Episode Title].mp3`

## ๐Ÿ” Troubleshooting

**Skill not activating?**
- Make sure you mention "podcast" in your request
- Try being more explicit: "Use podcast-downloader to..."

**Download failing?**
- Check internet connection
- Verify disk space
- Try a different episode

**No results found?**
- Try different search terms
- Check podcast name spelling
- Try searching by author name

## ๐Ÿ“„ License

This skill uses the Apple iTunes Search API, which is free for reasonable use.
Downloaded content is subject to copyright - personal use only.

## ๐Ÿ™ Credits

Created for the NotebookLM tutorial on Claude Skills development.

Based on Apple iTunes Search API: https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/

---

**Version**: 1.0.0
**Last Updated**: 2025-12-27
**Status**: โœ… Fully Functional

```

podcast-downloader | SkillHub