Download a whole YouTube playlist on a Mac — and re-run it safely
The command, the archive file that makes a second run skip what you already have, numbering that sorts correctly, and how to survive a playlist that fails halfway through.
Downloading one video is easy. A 200-video playlist is a different problem, because everything that can go wrong over three hours will: a private video in the middle, a rate limit, a closed laptop lid.
The difference between a command that works and one that survives is about four flags.
The whole thing
yt-dlp --yes-playlist \
--download-archive done.txt \
-o "%(playlist_index)03d - %(title)s.%(ext)s" \
-f "bestvideo[vcodec^=avc1]+bestaudio[ext=m4a]/best" \
--sleep-requests 1 \
--ignore-errors \
"PLAYLIST_URL"
Run it again after it dies and it picks up where it stopped. That is the only property that matters here, and it comes from one flag.
The flags, and why each is there
--download-archive done.txt
The important one. After each successful download, yt-dlp appends the video’s ID to that file. On every future run, anything listed is skipped without even fetching its metadata.
That single behaviour turns the playlist from something you must complete in one sitting into something you can run whenever. Interrupted at video 60? Re-run the same command; it resumes at 61. It also means you can re-run it weekly to pick up new additions and nothing else.
Keep the file next to the videos. Delete it and yt-dlp will happily download all 200 again.
--yes-playlist
A YouTube link often points at a video inside a playlist:
https://www.youtube.com/watch?v=VIDEO_ID&list=PLAYLIST_ID
It is genuinely ambiguous — you might mean either. --yes-playlist says take
the whole list; --no-playlist says take only this video. Being explicit is
what stops one video arriving when you wanted two hundred, and the reverse.
-o "%(playlist_index)03d - %(title)s.%(ext)s"
Without an index, your folder sorts alphabetically and a course in twelve parts becomes unreadable.
%(playlist_index)03d writes 001, 002, 013. The 03d matters: with
plain %(playlist_index)d, 10 sorts before 2 in Finder. Use 04d past a
thousand videos.
--sleep-requests 1
A 200-video playlist means at least 200 metadata requests before much video
arrives. Firing those as fast as your connection allows is the reliable way to
earn HTTP Error 429, which locks you out for hours.
One second between requests adds around three minutes to a run that was going to take two hours. See 403, 429 and downloads that crawl for what happens if you skip it.
--ignore-errors
Long playlists contain deleted, private and region-blocked videos. By default
one of them stops everything. --ignore-errors logs the failure and continues.
Failures are not written to the archive file, so a later run retries them — which is what you want for a temporary geo-block and harmless for a video that is genuinely gone.
Taking part of a playlist
yt-dlp --playlist-items 1-25 "URL" # a range
yt-dlp --playlist-items 3,7,11-14 "URL" # a selection
yt-dlp --playlist-items -10 "URL" # the last ten
Look before you commit:
yt-dlp --flat-playlist --print "%(playlist_index)s %(title)s" "URL"
--flat-playlist lists without touching each video’s page — a couple of
seconds for a list that would otherwise take minutes to enumerate.
Filter by title instead of position:
yt-dlp --match-filter "title ~= '(?i)part [0-9]+'" "URL"
And by length, which is the quickest way to drop trailers and shorts:
yt-dlp --match-filter "duration > 120" "URL"
Audio-only playlists
An album or a podcast back catalogue, without touching the audio:
yt-dlp --yes-playlist -f bestaudio[ext=m4a] \
--download-archive done.txt \
-o "%(playlist_index)03d - %(title)s.%(ext)s" \
--embed-thumbnail --embed-metadata \
"URL"
--embed-thumbnail and --embed-metadata matter more here than for video:
they are what makes the files legible in Music rather than a folder of untitled
tracks. Why m4a rather than MP3 is
its own piece — short version, MP3
means re-encoding something that was already compressed.
Keeping a playlist synced
Because the archive file makes re-runs cheap, “sync” is just the same command again. Save it:
#!/usr/bin/env bash
cd ~/Media/Course || exit 1
yt-dlp --yes-playlist \
--download-archive done.txt \
-o "%(playlist_index)03d - %(title)s.%(ext)s" \
-f "bestvideo[vcodec^=avc1]+bestaudio[ext=m4a]/best" \
--sleep-requests 1 --ignore-errors \
"PLAYLIST_URL"
chmod +x sync.sh, run it whenever. One caveat worth knowing: on a playlist
that gets reordered, playlist_index shifts, so old files keep their original
numbers while new ones get current positions. For a stable ordering, use
%(upload_date)s - %(title)s instead.
Where this gets tedious
The command handles all of it or a range you can name. What it does not do well is the thing people most often actually want: look at the list, pick nine of the forty, download those.
Doing that on the command line means running --flat-playlist, reading titles,
noting indices, and assembling --playlist-items 3,7,11-14. It works. It is
also twenty minutes of clerical work that a list of checkboxes solves in
twenty seconds, which is exactly why the app I build opens a picker when you
paste a playlist link: everything, a selection, or just the one video the link
pointed at. It runs this same yt-dlp underneath, with the same archive
behaviour — you are choosing an interface, not an engine.
The short version
| Flag | Why |
|---|---|
--download-archive done.txt |
Re-runs skip what is done. The one that makes it survivable |
--yes-playlist |
Removes the video-or-list ambiguity |
-o "%(playlist_index)03d - ..." |
Sorts correctly in Finder |
--sleep-requests 1 |
Avoids the 429 that costs you hours |
--ignore-errors |
One private video does not stop 199 others |
--playlist-items 1-25 |
A range |
--flat-playlist --print |
Read the list before committing |