Skip to content

Start typing. Errors match on their exact yt-dlp output.

to move, to open esc to close

Download a Playlist or Whole Channel with yt-dlp

Tested on yt-dlp 2026.07.28 Updated

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:

the command worth memorising
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:

bash
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:

bash
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

bash
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:

bash
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:

bash
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:

bash
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

bash
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:

bash
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:

archive-channel.sh
#!/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-playlist line in your config. --yes-playlist overrides it.

Frequently asked

Why did it download the whole playlist when I linked one video?
A YouTube watch URL containing &list= is a playlist URL as far as yt-dlp is concerned. --no-playlist takes only the video, and it belongs in your config file — this catches everybody at least once.
How do I download only new videos next time?
That is exactly what --download-archive does. It records each completed video id and skips it on later runs, without even making a request for it.
Can I download a whole channel?
Yes — the channel URL works directly, and /videos, /shorts or /streams restrict it to one tab. Expect it to take hours and to need pacing.
It stopped halfway through. Do I lose everything?
No, if you used an archive file. Rerun the same command and it resumes from where it stopped. Without one, it starts from the top and re-checks everything.