YouTube β Programmatic Download & Automation
Last reviewed: 2026-09-17
Purpose: Reference for downloading and post-processing YouTube media from the command line and from Python β yt-dlp usage, FFmpeg post-processing, subtitle and metadata extraction, and configuration patterns.
Overview
YouTube exposes no official download API, so the practical tooling is a small, stable set of open-source components:
| Tool | Role |
|---|---|
| yt-dlp | Actively maintained fork of youtube-dl β downloads video, audio, subtitles, playlists and metadata |
| pytube | Older pure-Python client; simpler API, but breaks whenever YouTube changes internals |
| FFmpeg | Merges separate video/audio streams, transcodes, extracts audio and embeds metadata |
| python-dotenv | Loads URLs and flags from the environment instead of hardcoding them |
Respect copyright and each site's terms of service: download your own content, public-domain or explicitly licensed material, and anything you have permission to archive.
Topics Covered
- Choosing video vs audio-only output
- Format selection and stream merging with FFmpeg
- Subtitles and metadata without the media
- Playlist, channel and time-range selection
- Configuration through environment variables
- Driving downloads from Python with the
yt-dlpAPI - Comparison of
yt-dlpandpytube
Installing the Tools
# yt-dlp (recommended)
python3 -m pip install -U yt-dlp # or: pipx install yt-dlp
# FFmpeg β required for merging streams and audio extraction
sudo apt install -y ffmpeg # Debian/Ubuntu
brew install ffmpeg # macOS
# Verify
yt-dlp --version
ffmpeg -version
Common yt-dlp Commands
# Best video+audio, merged by FFmpeg
yt-dlp -f "bv*+ba/b" -o "%(title)s.%(ext)s" "https://www.youtube.com/watch?v=VIDEO_ID"
# Audio only -> MP3 at 192 kbps
yt-dlp -x --audio-format mp3 --audio-quality 192K "https://www.youtube.com/watch?v=VIDEO_ID"
# List available formats without downloading
yt-dlp -F "https://www.youtube.com/watch?v=VIDEO_ID"
# Subtitles only, no media
yt-dlp --write-auto-subs --skip-download --sub-langs "en.*" "URL"
# Metadata as JSON (title, duration, chapters, uploader, formats)
yt-dlp --dump-json "URL"
# A single item from a playlist (indexes are 1-based)
yt-dlp --playlist-items 3 "https://www.youtube.com/playlist?list=PLAYLIST_ID"
# Embed thumbnail and metadata into the output file
yt-dlp --embed-thumbnail --embed-metadata "URL"
| Flag | Effect |
|---|---|
-f |
Format selector (bv*+ba/b = best video + best audio, fallback to combined) |
-x / --extract-audio |
Audio-only output |
--audio-format |
Target codec (mp3, m4a, opus, wav) |
--audio-quality |
Bitrate, e.g. 192K |
-o |
Output filename template |
--playlist-items |
Subset of a playlist |
--download-archive |
Skip items already downloaded |
--cookies-from-browser |
Use an authenticated browser session for restricted media |
-N |
Number of concurrent fragment downloads |
Downloading from Python
from yt_dlp import YoutubeDL
opts = {
"format": "bv*+ba/b",
"outtmpl": "%(title)s.%(ext)s",
"postprocessors": [
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "192",
}
],
}
with YoutubeDL(opts) as ydl:
info = ydl.extract_info("https://www.youtube.com/watch?v=VIDEO_ID", download=True)
print(info["title"], info["duration"])
A small reusable runner with error handling:
import os
from dotenv import load_dotenv
from yt_dlp import YoutubeDL
from yt_dlp.utils import DownloadError
def download(url: str, audio_only: bool = False) -> None:
"""Download one URL as video, or as MP3 when audio_only is set."""
opts = {
"outtmpl": "%(title)s.%(ext)s",
"format": "bestaudio/best" if audio_only else "bv*+ba/b",
"quiet": False,
}
if audio_only:
opts["postprocessors"] = [
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "192",
}
]
try:
with YoutubeDL(opts) as ydl:
ydl.download([url])
except DownloadError as exc: # network, geo-blocking, removed video, ...
print(f"download failed: {exc}")
if __name__ == "__main__":
load_dotenv()
download(
os.environ["VIDEO_URL"],
audio_only=os.environ.get("ONLY_AUDIO", "FALSE").upper() == "TRUE",
)
The postprocessors list is what replaces manual FFmpeg calls β FFmpegExtractAudio, FFmpegMetadata, EmbedThumbnail cover the common cases.
Library Comparison
| Feature | pytube | yt-dlp |
|---|---|---|
| Maintenance | Archived/limited | Active development |
| Format support | Basic | Advanced (HLS, DASH, subtitles, chapters) |
| Post-processing | None | FFmpeg integration (merge, convert, thumbnail) |
| Reliability | Moderate (breaks with YouTube changes) | High (frequently updated) |
| Audio extraction | Manual rename hack | Native FFmpeg integration |
Recommendation: use yt-dlp for new projects. It is actively maintained, more reliable, and handles more edge cases.
Audio-Only Workflow
YouTube URL
β
yt-dlp downloads best audio stream
β
FFmpeg extracts audio
β
Converts to MP3 (192 kbps)
β
Saves as "title.mp3"
Configuration via Environment Variables
import os
from dotenv import load_dotenv
load_dotenv()
url = os.environ["VIDEO_URL"]
audio_only = os.environ.get("ONLY_AUDIO", "FALSE").upper() == "TRUE"
Keeping URLs and flags in the environment (or a .env file excluded from version control) means the same script runs unchanged in a container, a cron job or a notebook.
Automating Downloads
# Append one line per successful download, then skip those on the next run
yt-dlp --download-archive archive.txt -f "bv*+ba/b" "https://www.youtube.com/playlist?list=PLAYLIST_ID"
# Archive new uploads from a channel, keeping filenames stable
yt-dlp --download-archive archive.txt -o "%(upload_date)s - %(title)s.%(ext)s" \
"https://www.youtube.com/@CHANNEL_HANDLE"
For scheduled jobs, run the command from cron (or a systemd timer) with an absolute output directory and --no-progress, and treat a non-zero exit status as a failure to alert on.
Resources
- yt-dlp GitHub β Active fork of youtube-dl
- yt-dlp Documentation β Full option reference
- pytube Documentation
- FFmpeg β Audio/video processing tool
- python-dotenv β Environment variable loader