# podcast-dl: a CLI for downloading and archiving podcasts

> podcast-dl is a small Node command line tool that pulls podcast episodes out of an RSS feed, keeps an archive file so you do not download the same episode twice, and writes episode files with names you control through templates. It runs through npx or as a standalone binary.

**lightpohl/podcast-dl** — A humble CLI for downloading and archiving podcasts.

- Repository: https://github.com/lightpohl/podcast-dl
- Stars: 589 · Forks: 48
- Language: JavaScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/lightpohl-podcast-dl

## Two ways to run it, one argument to learn

The usage is deliberately minimal, and the README calls the tool a humble CLI for downloading and archiving podcasts, which is an accurate description of the design. There are two ways to run it. The first is through npx, which requires Node, and the command takes the podcast RSS URL as its argument.

```bash
npx podcast-dl --url <PODCAST_RSS_URL>
```

The second is a binary, and the README points at the releases page to download the latest binary for your system, after which the same command line works.

```bash
podcast-dl --url <PODCAST_RSS_URL>
```

The argument vocabulary is one option deep, with either --url for the RSS feed or --file for a path to a local RSS file, and one of the two must be provided. The --file route is the one people miss, and it is the more useful of the pair for anybody who already has a feed saved, because it removes the network from the loop entirely and lets you test a parsing change against a fixed input. Beyond that, the options table is where the tool actually lives, and there is a link to more examples in the docs directory. The convention for options is worth noting, because it is the kind of detail that shows up in scripts, with values surrounded in square brackets usable as boolean options that take no argument.

## The archive file is what makes repeat runs cheap

The option that changes how you use the tool is --archive, and it is listed as accepting a String in square brackets, which means it can be used as a bare boolean or given a path. The README describes it as downloading or writing out items not listed in the archive file, and as generating that archive file at a path if it is not found, with the default being a path inside the podcast title template directory. Read carefully, that is the whole point of the tool. A podcast feed is an append-only list, and a feed that publishes three episodes a week is a feed you will re-read every time you run the command. The archive file is the record of what you already have, so a second run of the same command fetches only what is new. That single feature converts a downloader into an archiver, and it is also why the option exists as both a boolean and a path, since the path is what you would point at if you keep several feeds in one place, and the boolean is what you would use if you want the default layout. The related output options support the same goal, with --include-meta writing podcast metadata to JSON and --include-episode-meta writing individual episode metadata to JSON.

## Choosing what to download: dates, regex, limit and offset

The selection options make the tool usable on a feed with a long back catalogue, which is the situation where a naive downloader hurts most. --limit sets the maximum number of episodes to download, and downloads all by default, so a first run against a feed with two hundred episodes is a mistake you make once. --offset is the starting download position and defaults to 0, which lets you page through a feed in blocks. --after and --before take dates in the MM/DD/YYYY format and are inclusive, so they describe a window rather than a cut point, and the format is worth noting because it is not ISO and not the locale default. The regex options are the most precise of the set. --episode-regex matches episode titles against a provided regular expression before starting a download, and --episode-regex-exclude removes the titles that match, which is how you skip a recurring segment such as a daily news show inside a weekly feed. Two smaller options shape the numbering of what you keep, with --episode-digits setting the minimum number of digits used for episode numbering, so 3 would generate 001 instead of 1, and --episode-num-offset offsetting the acquired episode number, both defaulting to 1 and 0. Between them they are what make an archive sort correctly in a file manager.

## Filenames come from templates, and they get sanitised

The output directory and the episode file names are both templated, which is where this tool differs from a plain downloader. The --out-dir option sets where episodes and metadata are written, and its default is a template itself, the podcast title in double curly braces, so the default layout is already a directory named after the show. The --episode-template option sets the template for episode file names, and --episode-custom-template-options passes extra values into that template. The archive file default follows the same pattern, a path inside the podcast title directory. The fact that the defaults are templates rather than fixed strings is the design decision worth noticing, because it means the same tool can produce a flat directory, a per show directory, or a directory tree sorted by year, without a different tool or a rename script afterwards. The dependency list backs this up, with filenamify in the runtime dependencies, which is a library for turning arbitrary strings into safe file names, so a title with slashes, quotes or a colon in it does not produce an unusable path on your filesystem.

## Images, transcripts and the metadata that makes an archive useful

Beyond the audio, there are three opt-in extras, and they are all off by default. --include-episode-images downloads the episode images that are found, --include-episode-transcripts downloads the transcripts that are found, and the two metadata options write JSON. The word found in the descriptions is doing real work. These are all features of the RSS feed, not features the tool can create, so what you get depends entirely on what the publisher puts in the feed. That is a good property for an archivist, because it means the tool is not scraping a website and guessing at structure, it is reading the feed the publisher already provides, which is both more stable and more honest about what it does. It also sets the limit. A transcript delivered as a separate file is downloaded as a separate file, and there is no indication in the README that it is aligned to the audio timeline, so if you want a searchable transcript you have to do that work yourself. The images and the metadata, by contrast, are immediately useful, because they are what a media library or a note taking system can index.

## The dependency set, the build and the release rhythm

The package manifest is where you can see what the tool actually does rather than what it claims. The runtime dependencies are small and specific. commander provides the option parsing, got is the HTTP client, xml2js parses the feed, dayjs handles the date arithmetic behind the --after and --before filters, p-limit and throttle-debounce control concurrency, filenamify sanitises names, global-agent is there for proxy handling, and command-exists and entities and pluralize handle small chores. The interesting one for a network tool is global-agent, because a downloader behind a corporate proxy is the normal case rather than the exception, and the --threads option, which sets the number of concurrent downloads and defaults to 1, is paired with --attempts, which sets the number of attempts per file and defaults to 3. The development side is modern and small. The build script is a Node file, build.cjs, and the packaging tool is pkg, which produces the standalone binaries the README offers. Formatting and linting are done by oxfmt and oxlint with their own configuration files at the root, the tests run on vitest with a vitest.config.js, and husky installs a pre-commit hook running lint-staged and a pre-push hook running the linter. The published files are only bin and lib. The release history is steady and recent, with 12.1.2 and 12.1.3 both on 2026-08-15 and 12.1.4 on 2026-08-17, and the last push on the same day, and the Node floor is 18.17.0.

## What this tool is, and what it is not

The comparison worth making is with the download button inside a podcast client, and the difference is one of scope. A client downloads an episode into its own application storage, with its own naming rules, and the file is coupled to that application. podcast-dl produces ordinary files in a directory you choose, with names you define, and an archive file that records what it has fetched, so the result is a folder you can put in a backup, a sync service or a media server. The trade is that everything the tool can do comes from the feed. It reads a URL or a local file, and the enclosures, images, transcripts and metadata it finds are what the publisher put there. It does not log in anywhere, it does not work around a feed that is missing something, and it has no concept of a subscription schedule beyond the options it exposes, so a regular run has to be scheduled by your operating system rather than by the tool. For a listener with a folder of files to manage, that is a good trade, because the tool stays out of the way. For a listener who wants a curated queue and automatic library management, the application it replaces is doing a different job.

## Conclusion

Adopt podcast-dl if you follow several feeds and want a local, orderly archive with consistent file names, because the archive file plus the templates give you something a podcast client does not, which is a directory you can back up, sync and diff. It does not fit a workflow that needs transcripts with timestamps, since the tool downloads transcripts as files but does not align them to the audio, and it does not fit an environment that cannot run Node, since the binary route is the one that avoids it. Before you set it up on a schedule, test a single feed with a small limit, then check that the archive file is written where you expect, because that file is what stops the same episode being fetched again.

## FAQ

### How do I run podcast-dl?

Either through npx, which requires Node, with npx podcast-dl --url <PODCAST_RSS_URL>, or by downloading the latest binary for your system from the releases page and running podcast-dl --url <PODCAST_RSS_URL>. Either --url or --file must be provided, with --file taking the path to a local RSS file.

### What does the archive option in podcast-dl do?

The --archive option downloads or writes out items that are not listed in the archive file, and generates the archive file at the given path if it is not found, with the default being ./{{podcast_title}}/archive.json. It is what makes repeated runs fetch only new episodes.

### How can I download only some episodes of a podcast?

There is --limit for the maximum number of episodes, --offset for the starting position, the inclusive --after and --before date filters in MM/DD/YYYY format, and the --episode-regex and --episode-regex-exclude options that match or exclude episode titles by regular expression before the download starts.

### Can podcast-dl name the files it downloads?

Yes. The --out-dir option sets the output directory and defaults to a template of the podcast title, the --episode-template option sets the template for episode file names, and --episode-custom-template-options passes extra values into that template. The archive file path is templated in the same way, and file names are sanitised with the filenamify library.

### What can podcast-dl fetch besides the audio file?

Podcast metadata and individual episode metadata can be written out as JSON with --include-meta and --include-episode-meta, and episode images and transcripts can be downloaded with --include-episode-images and --include-episode-transcripts. These come from the RSS feed itself, so what is available depends on what the publisher includes.

## Sources

- [Issues](https://github.com/lightpohl/podcast-dl/issues)
- [License: MIT](https://github.com/lightpohl/podcast-dl/blob/main/LICENSE)
- [lightpohl/podcast-dl on GitHub](https://github.com/lightpohl/podcast-dl)
- [README](https://github.com/lightpohl/podcast-dl/blob/main/README.md)
- [Releases](https://github.com/lightpohl/podcast-dl/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lightpohl-podcast-dl
