Metadata-Version: 2.4
Name: podcast-helper
Version: 1.1.3
Summary: Podcast Helper — universal audio stream consumer exposed as library, argparse CLI, click CLI, and FastAPI HTTP surface. URL-in → PCM-out for local files, direct audio URLs, RSS feed enclosures, and yt-dlp-supported sources (YouTube, Vimeo, Twitch, SoundCloud, …). Built on youtube-helper + ffmpeg + feedparser + podcastparser; Shannon-correct resampling, mono downmix or native channels preserved.
Author-email: Warith HARCHAOUI <warithmetics@deraison.ai>
License: BSD-3-Clause
Project-URL: Homepage, https://github.com/warith-harchaoui/podcast-helper
Project-URL: Issues, https://github.com/warith-harchaoui/podcast-helper/issues
Keywords: podcast,audio,streaming,pcm,yt-dlp,youtube,rss,feedparser
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio
Requires-Python: <3.14,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: youtube-helper<3,>=2.0.0
Requires-Dist: os-helper<3,>=2.0.0
Requires-Dist: feedparser<7,>=6.0
Requires-Dist: podcastparser<1,>=0.6
Requires-Dist: requests<3,>=2.28
Requires-Dist: numpy>=1.24
Provides-Extra: cli
Requires-Dist: click<9,>=8.1; extra == "cli"
Provides-Extra: api
Requires-Dist: fastapi<1,>=0.111; extra == "api"
Requires-Dist: uvicorn<1,>=0.30; extra == "api"
Requires-Dist: python-multipart<1,>=0.0.9; extra == "api"
Provides-Extra: mcp
Requires-Dist: podcast-helper[api]; extra == "mcp"
Requires-Dist: fastapi-mcp<1,>=0.3; extra == "mcp"
Requires-Dist: mcp<2,>=1.12; extra == "mcp"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: click<9,>=8.1; extra == "dev"
Requires-Dist: fastapi<1,>=0.111; extra == "dev"
Requires-Dist: python-multipart<1,>=0.0.9; extra == "dev"
Requires-Dist: fastapi-mcp<1,>=0.3; extra == "dev"
Requires-Dist: mcp<2,>=1.12; extra == "dev"
Dynamic: license-file

# Podcast Helper

[🇫🇷](https://github.com/warith-harchaoui/podcast-helper/blob/main/LISEZMOI.md) · [🇬🇧](https://github.com/warith-harchaoui/podcast-helper/blob/main/README.md)

[![CI](https://github.com/warith-harchaoui/podcast-helper/actions/workflows/ci.yml/badge.svg)](https://github.com/warith-harchaoui/podcast-helper/actions/workflows/ci.yml) [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://github.com/warith-harchaoui/podcast-helper/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue.svg)](#) [![Local-first](https://img.shields.io/badge/privacy-local--first-2f6f5e.svg)](#the-promise)

`Podcast Helper` belongs to a collection of libraries called `AI Helpers` developed for building Artificial Intelligence.

## The Promise

**Local-first by design.** podcast-helper runs entirely on your machine: it fetches only the episodes and feeds you ask for and processes them locally. Your data is never uploaded to a third-party service, there is no telemetry, no account, no cloud lock-in. Part of the [AI Helpers](https://github.com/warith-harchaoui/ai-helpers) suite: sovereignty over your data through local-first open source.

Universal audio stream consumer for podcasts and any audio-bearing URL. **URL in, PCM out**, for local files, direct audio URLs (RSS enclosure MP3, M4A, Opus, WAV, HLS m3u8), RSS/Atom feed URLs (auto-picks the latest episode), and every `yt-dlp`-supported source (YouTube, Vimeo, SoundCloud, Twitch VOD or live, and more). Spotify's DRM-gated catalog and Apple Podcasts catalog URLs are refused upfront, with a clear hint toward the RSS-feed workaround.

[🌍 AI Helpers](https://harchaoui.org/warith/ai-helpers)

[![logo](https://raw.githubusercontent.com/warith-harchaoui/podcast-helper/main/assets/logo.png)](https://harchaoui.org/warith/ai-helpers)

## Documentation

[💻 Documentation](https://harchaoui.org/warith/ai-helpers/docs/podcast-helper-doc/)

[🗺️ Landscape](https://github.com/warith-harchaoui/podcast-helper/blob/main/LANDSCAPE.md)

[📋 Examples](https://github.com/warith-harchaoui/podcast-helper/blob/main/EXAMPLES.md)

## Why it exists

Podcast pipelines (ASR, diarization, summarization, search indexing) usually start with the same question: *"give me a stream of PCM frames from this URL, never mind whether it's a `.mp3` link, a feed, a YouTube video, or a podcast hosted on a CDN I've never heard of."* This library is that one function, plus the small extras around it (`feed`, `latest_episode`) that make working with RSS sources friendly.

## Installation

**Prerequisites:** **Python 3.10-3.13**, **git**, and **ffmpeg**, cross-platform:

- 🍎 **macOS** ([Homebrew](https://brew.sh)): `brew install python git ffmpeg`
- 🐧 **Ubuntu/Debian**: `sudo apt update && sudo apt install -y python3 python3-pip git ffmpeg`
- 🪟 **Windows** (PowerShell): `winget install Python.Python.3.12 Git.Git Gyan.FFmpeg`

We recommend using Python environments. Check this link if you're unfamiliar with setting one up: [🥸 Tech tips](https://harchaoui.org/warith/4ml/#install).

### From PyPI (recommended)

```bash
# Core library
pip install podcast-helper

# Optional surfaces
pip install "podcast-helper[cli]"
pip install "podcast-helper[api]"
pip install "podcast-helper[mcp]"
```

This pulls in [youtube-helper](https://github.com/warith-harchaoui/youtube-helper) (and transitively `yt-dlp`, [os-helper](https://github.com/warith-harchaoui/os-helper), [audio-helper](https://github.com/warith-harchaoui/audio-helper), [video-helper](https://github.com/warith-harchaoui/video-helper)), plus [feedparser](https://feedparser.readthedocs.io/) and [podcastparser](https://podcastparser.readthedocs.io/) for RSS.

### From source (no PyPI)

```bash
git clone https://github.com/warith-harchaoui/podcast-helper.git
cd podcast-helper
pip install -e .

# Optional surfaces
pip install -e ".[cli]"
pip install -e ".[api]"
pip install -e ".[mcp]"
```

## Quick start

```python
import asyncio
import podcast_helper as ph

async def main():
    # Pass *any* URL: file, direct mp3, RSS feed, YouTube, SoundCloud, Twitch VOD.
    async for frame in ph.extract_audio_stream(
        "https://feeds.npr.org/510289/podcast.xml",   # ← RSS, auto-pick latest episode
        target_sample_rate=16000,
        to_mono=True,
        frame_ms=20,
    ):
        # frame["pcm"]: np.float32 (320,) for 20ms @ 16kHz
        # frame["t_abs_s"]: 0.0, 0.02, 0.04, ...
        await asr.feed(frame["pcm"])

asyncio.run(main())
```

For the full catalog of recipes (RSS, yt-dlp sources, live streams, stereo / multichannel, anti-aliasing, downstream ASR / VAD / summarization pipelines), see [📋 EXAMPLES.md](https://github.com/warith-harchaoui/podcast-helper/blob/main/EXAMPLES.md).

## What URLs are accepted

| Source | Detection | What happens |
|---|---|---|
| **Local file** / `file://` | path exists on disk OR `file://` scheme | ffmpeg opens it directly. |
| **Direct audio URL** (`.mp3`, `.m4a`, `.opus`, `.wav`, `.m3u8`, …) | URL extension is a known audio container | ffmpeg opens it directly with your `headers=` if any. |
| **RSS / Atom feed** (`.xml`, `.rss`, `.atom`) | URL extension is a known feed container | `podcastparser` (fallback: `feedparser`) parses it; latest episode's enclosure is fetched. |
| **YouTube / Vimeo / SoundCloud / Twitch VOD / Twitch live / …** | yt-dlp's extractor identifies it | yt-dlp picks `bestaudio*`, hands the direct URL + headers to ffmpeg. |
| **Generic web URL** (anything else) | yt-dlp's `generic` extractor | URL used as-is. |
| **Spotify** (open.spotify.com) | hostname match | `NotImplementedError`: Spotify audio is DRM-gated. Use the show's RSS feed if it exists. |
| **Apple Podcasts** (podcasts.apple.com) | hostname match | `NotImplementedError`: Apple URLs point to the catalog, not the audio. Use the show's RSS feed (linked on the show's site, or via `getrssfeed.com` / Podcast Index). |

## Signal-processing correctness

When `target_sample_rate` differs from the source rate, the conversion is performed by ffmpeg's `libswresample` (default) or `libsoxr` (`resample_quality="high"`). Both apply an **anti-aliasing low-pass filter at the new Nyquist frequency** (`target_sample_rate / 2`) before decimation, satisfying the Shannon-Nyquist sampling theorem. Naïve subsampling is never used.

Channel handling has exactly two modes; there is no synthetic upmix:

| `to_mono` | Output shape | What ffmpeg does |
|---|---|---|
| `True` (default) | `(n_samples,)` | Standard downmix (stereo → L+R with -3 dB, 5.1 → ITU mix) |
| `False` | `(n_samples, n_channels)` interleaved | Preserves the source's native channel count |

## Working with RSS feeds explicitly

If you want to inspect or select episodes yourself:

```python
import podcast_helper as ph

# Full episode list, most-recent first
episodes = ph.feed("https://feeds.npr.org/510289/podcast.xml", max_episodes=20)
for ep in episodes:
    print(ep["published_at"], "|", ep["title"], "|", ep["duration_seconds"], "s")

# Or just the latest one
ep = ph.latest_episode("https://feeds.npr.org/510289/podcast.xml")
print(ep["title"], "→", ep["enclosure_url"])

# Then stream its audio
import asyncio
async def main():
    async for frame in ph.extract_audio_stream(ep["enclosure_url"]):
        ...
asyncio.run(main())
```

Each `Episode` dict has a normalized schema regardless of feed flavor:

```
{guid, title, description, link, published_at (ISO UTC),
 duration_seconds, enclosure_url, enclosure_type, enclosure_size_bytes,
 image_url}
```

## Multi-surface exposure

`podcast-helper` exposes the same public functions through five
interchangeable surfaces; pick the one that fits the caller.

| Surface | Entry point | Extra | Best for |
|---|---|---|---|
| Library (async iterator) | `import podcast_helper as ph` | none | Python code, notebooks, downstream ASR / VAD / summarization |
| argparse CLI | `podcast-helper` | none (stdlib only) | shell scripts, CI, ffmpeg pipelines |
| click CLI | `podcast-helper-click` | `[cli]` | click-native shells (bash / zsh completion, colored help) |
| FastAPI HTTP | `uvicorn podcast_helper.api:app` | `[api]` | HTTP microservices, cross-language callers |
| Browser GUI | `GET /gui` (served by the API) | `[api]` | drop-a-URL episode browser: list · preview · archive, no terminal |
| MCP | `podcast-helper-mcp` | `[mcp]` | any MCP-aware agent host (calls the same endpoints as tools) |

Install any combination of extras:

```bash
pip install 'podcast-helper[cli]'          # + click twin
pip install 'podcast-helper[api]'          # + FastAPI HTTP surface
pip install 'podcast-helper[mcp]'          # + MCP tools (needs [api] too, pulled in automatically)
pip install 'podcast-helper[cli,api,mcp]'  # everything
```

Every surface publishes the same verbs, `feed`, `latest`, `stream`,
`record`, `probe`, with identical argument names, so switching
between them is a copy-paste. The Dockerfile in this repo ships the
FastAPI surface on port 8000 out of the box (`docker build -t
podcast-helper . && docker run --rm -p 8000:8000 podcast-helper`).

### Browser GUI: the episode browser (`GET /gui`)

With the `[api]` extra, the FastAPI app serves a self-contained
single-page **episode browser** (Tailwind via CDN + vanilla JS, no
build step) that drives the very same endpoints:

```bash
pip install 'podcast-helper[api]'
uvicorn podcast_helper.api:app --port 8000
# open http://localhost:8000/gui  (or just http://localhost:8000/)
```

Paste a feed / RSS / audio / yt-dlp URL → **List episodes** (calls
`/feed`) → click an episode to see its metadata and play the enclosure
inline → **Record to file** (calls `/record`) to download a compressed
archive. **Probe** classifies any URL. Nothing is uploaded: playback
streams the enclosure straight to your browser, and archiving runs
ffmpeg on your own machine.

For the exhaustive catalog of triggers, phrasings, accepted URLs and
when *not* to reach for podcast-helper, see
[`TRIGGERS.md`](https://github.com/warith-harchaoui/podcast-helper/blob/main/TRIGGERS.md).

For an ambitious visual product on top, see [`GUI.md`](https://github.com/warith-harchaoui/podcast-helper/blob/main/GUI.md).

## Live streams

For YouTube / Twitch live URLs, the resolved direct URL is typically an HLS `.m3u8` manifest. `extract_audio_stream` detects this (`is_live=True`) and automatically disables `-re` real-time pacing (the source paces itself). The async iterator runs indefinitely until the live stream ends; callers should `break` when they're done.

`speed != 1.0` for live streams raises `ValueError`: you can't fast-forward beyond the live edge. Use `speed=...` on VOD only.

## Not built yet

- `start_instant` / `end_instant` for VOD seek.
- `apple_podcasts_to_rss(url)`, resolving an Apple Podcasts catalog URL to its RSS feed via the iTunes Search API.
- Podcast Index API integration.
- Chapters (ID3 CTOC/CHAP, Podcasting 2.0 `<podcast:chapters>`) and transcripts.
- OPML import/export.

See [CHANGELOG.md](https://github.com/warith-harchaoui/podcast-helper/blob/main/CHANGELOG.md) for what has already shipped.

## Author

 - [Warith HARCHAOUI](https://linkedin.com/in/warith-harchaoui)

## Acknowledgements

Special thanks to [Mohamed Chelali](https://mchelali.github.io) and [Bachir Zerroug](https://www.linkedin.com/in/bachirzerroug) for fruitful discussions.

## License

This project is licensed under the BSD-3-Clause License. See the [LICENSE](https://github.com/warith-harchaoui/podcast-helper/blob/main/LICENSE) file for details.
