Download a Playlist or Whole Channel with yt-dlp
Quick answer
A playlist is where yt-dlp stops being a command and becomes a job. Three flags make the difference between one that finishes and one that gets rate-limited halfway and has to start over:
yt-dlp --download-archive archive.txt --sleep-requests 2 --limit-rate 2M -i \
-o "%(playlist)s/%(playlist_index)02d - %(title)s.%(ext)s" \
"PLAYLIST_URL"
The flag everyone meets by accident
Clicking a video inside a playlist gives a URL with &list= in it. To yt-dlp
that is a playlist, so pasting it downloads all 400 videos:
yt-dlp --no-playlist "https://www.youtube.com/watch?v=VIDEO&list=PLAYLIST"
yt-dlp --yes-playlist "https://www.youtube.com/watch?v=VIDEO&list=PLAYLIST"
--no-playlist is the single most worthwhile line in a
config file. Set it as the default and pass
--yes-playlist on the rare occasions you mean the playlist.
The archive file
This is what makes a playlist job resumable. Every completed video's id is appended to the file, and a later run skips those without making a request:
yt-dlp --download-archive archive.txt "PLAYLIST_URL"
Three things follow from that, all of them useful:
- An interrupted run resumes instead of restarting.
- A weekly cron job downloads only what is new.
- Deleting a file locally does not cause it to be downloaded again — the archive says it was.
That last one surprises people. To re-download something, remove its line from the archive.
Ranges and subsets
yt-dlp -I 5-20 "PLAYLIST_URL" # items 5 to 20
yt-dlp -I 1,3,7-9 "PLAYLIST_URL" # a mixed selection
yt-dlp -I ::-1 "PLAYLIST_URL" # reverse order
yt-dlp -I -10: "PLAYLIST_URL" # the last ten
yt-dlp --max-downloads 5 "PLAYLIST_URL" # stop after five
--max-downloads 5 is how to test a command against a 900-video channel without
committing to it.
Whole channels
The channel URL works, and the tab suffixes restrict it:
yt-dlp "https://www.youtube.com/@channelname" # everything
yt-dlp "https://www.youtube.com/@channelname/videos" # long-form only
yt-dlp "https://www.youtube.com/@channelname/shorts" # shorts only
yt-dlp "https://www.youtube.com/@channelname/streams" # past livestreams
Before starting, find out what you are committing to. --flat-playlist lists entries
without resolving each one, so it takes seconds rather than minutes:
yt-dlp --flat-playlist --print "%(title)s" "https://www.youtube.com/@channelname/videos" | wc -l
Pacing, and why it is not optional
A 200-video playlist makes several hundred metadata requests before the first byte of video is
transferred. That burst is what trips
HTTP 429, and it is why
--limit-rate alone does not prevent it:
yt-dlp --sleep-requests 2 --sleep-interval 5 --max-sleep-interval 15 --limit-rate 2M "PLAYLIST_URL"
--sleep-requests targets the extraction burst.
--max-sleep-interval adds jitter, because an exact five-second gap between every
download is itself a machine signature. Two seconds per request costs a 200-video playlist about
seven minutes; a rate limit costs it a day.
Filtering what gets downloaded
yt-dlp --dateafter now-1month "CHANNEL_URL"
yt-dlp --match-filters "duration < 600" "CHANNEL_URL"
yt-dlp --match-filters "view_count > 10000 & duration > 120" "CHANNEL_URL"
yt-dlp --match-filters "!is_live" "CHANNEL_URL"
Filters are evaluated after metadata extraction, so they save disk and bandwidth but not
requests. For a rate-limit problem, -I is the better tool because it skips
extraction entirely.
Naming, so the result is usable
Padding the index is what makes a file manager sort the playlist correctly. Without it you get 1, 10, 11, 2:
yt-dlp -o "%(playlist)s/%(playlist_index)02d - %(title)s [%(id)s].%(ext)s" "PLAYLIST_URL"
Output templates covers the rest, including templates that produce something Plex or Jellyfin will index.
A complete archive job
Everything above, in the shape you would actually schedule:
#!/bin/sh
yt-dlp -U --update-to nightly || true
yt-dlp \
--download-archive /srv/media/archive.txt \
--sleep-requests 2 --limit-rate 4M -i \
-S res:1080,vcodec:h264 --merge-output-format mp4 \
--embed-metadata --embed-thumbnail --embed-chapters \
--sponsorblock-mark all \
-o "/srv/media/%(uploader)s/%(upload_date>%Y-%m-%d)s - %(title)s [%(id)s].%(ext)s" \
"https://www.youtube.com/@channelname/videos"
Updating first because a stale build fails silently for weeks;
|| true so a failed update does not abort the run;
--sponsorblock-mark rather than
--remove, so the file is not re-cut and the job stays fast.
If a run goes wrong
-
It stopped at the first error. Add
-i. Any playlist older than a few months contains deleted and private videos, and that is normal. - 429 partway through. Stop, wait an hour, and come back with the pacing flags — the 429 page explains why retrying immediately makes it worse.
- It re-downloads everything each run. No archive file, or a different path each time. Use an absolute path in anything scheduled.
-
Only the first video appears. A
--no-playlistline in your config.--yes-playlistoverrides it.
Frequently asked
Why did it download the whole playlist when I linked one video?
How do I download only new videos next time?
Can I download a whole channel?
It stopped halfway through. Do I lose everything?
Related
- Fix "HTTP Error 429: Too Many Requests" in yt-dlp A 429 is a rate limit with a timer attached, and retrying immediately restarts it. How long the block actually...
- yt-dlp Output Templates: Name and Sort Files Automatically The -o template turns a folder of random filenames into a library. Every field worth using, the numeric and da...
- yt-dlp SponsorBlock: Cut Sponsors Out Automatically yt-dlp has SponsorBlock built in — it can mark sponsor segments as chapters or cut them out of the file entire...