yt-dlp.exe (standalone build) or Python 3 + pip, an ffmpeg Windows build, and a URL of media you are allowed to download.
The whole setup is three pieces that must find each other: the downloader (yt-dlp.exe), the muxer (ffmpeg.exe) and the shell (PowerShell). The diagram of what you are building:
[yt-dlp.exe] --needs--> [ffmpeg.exe in PATH] --writes--> [output folder]
^ ^
| |
[PowerShell] --runs--> [config.txt: default flags] -----> [yt-dlp-gui.ps1]
yt-dlp.exe — the standalone release build from the official releases page (https://github.com/yt-dlp/yt-dlp/releases/latest).ffmpeg.exe being on PATH.C:\tools.Placeholders used below: <your-URL> — the real media link, <your-user> — your Windows account name.
What to do: pick one installation route for yt-dlp, then add ffmpeg.
Route A, pip (needs Python):
python -m pip install -U yt-dlp
Route B, standalone exe (no Python). Create a folder and drop the downloaded yt-dlp.exe into it:
mkdir C:\tools\yt-dlp
Then install ffmpeg: unpack the build so that ffmpeg.exe sits in C:\tools\ffmpeg\bin, and add both folders to PATH — press Win+R, run sysdm.cpl, open *Advanced → Environment Variables → User variables → Path → New*, and add:
C:\tools\yt-dlp
C:\tools\ffmpeg\bin
Close and reopen the terminal — PATH is read at process start.
Why: yt-dlp calls ffmpeg.exe as a separate program, so it must be resolvable by name. The PATH entry is what turns yt-dlp from a file path into a command.
How to check:
yt-dlp --version
ffmpeg -version
Example output: a date-like version string such as the release date for yt-dlp, and a banner starting with ffmpeg version ... followed by the build configuration. Two version strings = success. If PowerShell answers with The term 'yt-dlp' is not recognized..., jump to Troubleshooting, case 1.

What to do: run the four commands you will use 90% of the time, then move the recurring flags into a config file so you stop retyping them.
| Flag | What it does | Example |
|---|---|---|
| `-F` | lists every available format | `yt-dlp -F "<your-URL>"` |
| `-f` | selects a format | `-f "bv*[height<=1080]+ba/b[height<=1080]"` |
| `-P` | sets the output folder | `-P "D:\Downloads\yt-dlp"` |
| `-x` | extracts audio only | `-x --audio-format mp3` |
| `--write-subs` | saves subtitles | `--write-subs --sub-langs "en.*"` |
| `-o` | filename template | `-o "%(uploader)s/%(title)s.%(ext)s"` |
Example 1 — inspect before downloading:
yt-dlp -F "<your-URL>"
You get a numbered table of formats with columns for ID, extension, resolution and codecs. bv* means "best video", ba means "best audio", and bv*+ba asks for both, which is why ffmpeg is required.
Example 2 — cap quality at 1080p and choose a folder:
yt-dlp -f "bv*[height<=1080]+ba/b[height<=1080]" -P "D:\Downloads\yt-dlp" "<your-URL>"
The part after / is a fallback: if no separate stream pair exists, yt-dlp takes the best single file that already fits the height limit.
Example 3 — audio only:
yt-dlp -x --audio-format mp3 -P "D:\Downloads\yt-dlp\audio" "<your-URL>"
Example 4 — subtitles embedded into the file:
yt-dlp --write-subs --sub-langs "en.*" --embed-subs -P "D:\Downloads\yt-dlp" "<your-URL>"
If you want to summarise those subtitle files with a model afterwards, see How to use AI models for free in 2026.
Now create the config file at %APPDATA%\yt-dlp\config.txt (create the yt-dlp folder if it is missing). One option per line, # starts a comment:
# defaults applied to every yt-dlp run
-P "D:\Downloads\yt-dlp"
-o "%(uploader)s/%(title)s.%(ext)s"
-f bv*[height<=1080]+ba/b[height<=1080]
--write-subs
--sub-langs en.*
--embed-subs
Why: the config is parsed before the command line, and command-line options win. That gives you sane defaults plus per-run overrides — the GUI in Step 3 relies on exactly this.
How to check:
yt-dlp -v --simulate "<your-URL>"
Example: among the debug lines you will see one beginning with [debug] User config followed by the full path to config.txt and the options it supplied. That line proves the file is being read. To see where a file would land:
yt-dlp --simulate --print filename "<your-URL>"
It prints something like D:\Downloads\yt-dlp\<channel name>\<video title>.webm — your folder plus the -o template, resolved.

What to do: save the script below as C:\tools\yt-dlp\yt-dlp-gui.ps1. It is a thin Windows Forms wrapper — a window with a URL box, a folder box, a Browse button and a log — that calls the same yt-dlp.exe you just tested.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = 'yt-dlp shell'
$form.Size = New-Object System.Drawing.Size(600, 360)
$form.StartPosition = 'CenterScreen'
$lblUrl = New-Object System.Windows.Forms.Label
$lblUrl.Text = 'Video URL'; $lblUrl.Location = New-Object System.Drawing.Point(12, 12)
$form.Controls.Add($lblUrl)
$txtUrl = New-Object System.Windows.Forms.TextBox
$txtUrl.Location = New-Object System.Drawing.Point(12, 34)
$txtUrl.Size = New-Object System.Drawing.Size(560, 24)
$form.Controls.Add($txtUrl)
$lblDir = New-Object System.Windows.Forms.Label
$lblDir.Text = 'Save to folder'; $lblDir.Location = New-Object System.Drawing.Point(12, 70)
$form.Controls.Add($lblDir)
$txtDir = New-Object System.Windows.Forms.TextBox
$txtDir.Location = New-Object System.Drawing.Point(12, 92)
$txtDir.Size = New-Object System.Drawing.Size(460, 24)
$txtDir.Text = 'D:\Downloads\yt-dlp'
$form.Controls.Add($txtDir)
$btnBrowse = New-Object System.Windows.Forms.Button
$btnBrowse.Text = 'Browse...'
$btnBrowse.Location = New-Object System.Drawing.Point(480, 90)
$btnBrowse.Size = New-Object System.Drawing.Size(92, 26)
$form.Controls.Add($btnBrowse)
$btnGo = New-Object System.Windows.Forms.Button
$btnGo.Text = 'Download'
$btnGo.Location = New-Object System.Drawing.Point(12, 130)
$btnGo.Size = New-Object System.Drawing.Size(140, 32)
$form.Controls.Add($btnGo)
$txtLog = New-Object System.Windows.Forms.TextBox
$txtLog.Location = New-Object System.Drawing.Point(12, 172)
$txtLog.Size = New-Object System.Drawing.Size(560, 130)
$txtLog.Multiline = $true
$txtLog.ScrollBars = 'Vertical'
$txtLog.ReadOnly = $true
$form.Controls.Add($txtLog)
$btnBrowse.Add_Click({
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
if ($dlg.ShowDialog() -eq 'OK') { $txtDir.Text = $dlg.SelectedPath }
})
$btnGo.Add_Click({
$txtLog.Clear()
if ([string]::IsNullOrWhiteSpace($txtUrl.Text)) {
$txtLog.AppendText("Paste a URL first.`r`n"); return
}
$tmpOut = Join-Path $env:TEMP 'ytdlp-out.txt'
$tmpErr = Join-Path $env:TEMP 'ytdlp-err.txt'
$ytArgs = @(
'-P', ('"' + $txtDir.Text + '"'),
'-f', 'bv*[height<=1080]+ba/b[height<=1080]',
('"' + $txtUrl.Text + '"')
)
$txtLog.AppendText('yt-dlp ' + ($ytArgs -join ' ') + "`r`n")
try {
$p = Start-Process -FilePath 'yt-dlp.exe' -ArgumentList $ytArgs -NoNewWindow `
-Wait -PassThru -RedirectStandardOutput $tmpOut -RedirectStandardError $tmpErr
} catch {
$txtLog.AppendText('Could not start yt-dlp.exe: ' + $_.Exception.Message); return
}
$txtLog.AppendText((Get-Content $tmpOut -Raw) + (Get-Content $tmpErr -Raw))
if ($p.ExitCode -eq 0) { $txtLog.AppendText("`r`nDone. Exit code 0.`r`n") }
else { $txtLog.AppendText("`r`nFailed. Exit code $($p.ExitCode). Read the log above.`r`n") }
})
$form.ShowDialog() | Out-Null
Run it:
powershell -ExecutionPolicy Bypass -File "C:\tools\yt-dlp\yt-dlp-gui.ps1"
Why it works: the shell adds no new download logic — it builds one argument list and hands it to the same executable. Arguments are quoted by hand because Start-Process -ArgumentList joins an array with spaces and does not quote paths containing spaces. Because the -P you type here is on the command line, it overrides the -P from config.txt.
Example run: paste a link, set the folder to D:\Downloads\yt-dlp\demo, click Download. The log shows the reconstructed command line, then [download] progress lines, then Done. Exit code 0.
How to check:
Done. Exit code 0.%(uploader)s/%(title)s.%(ext)s template.To find the new files quickly in Explorer, use Windows search or the built-in AI search covered in How to Use AI-Powered Search in Windows 11.
Problem. Every command fails immediately with "not recognized"
'yt-dlp' is not recognized as an internal or external command, operable program or batch file.yt-dlp.exe is not in PATH, or the terminal was opened before the PATH edit.C:\tools\yt-dlp\yt-dlp.exe --version. If that prints a version, re-add C:\tools\yt-dlp under *User variables → Path*, close every terminal window and open a new one. Check with Get-Command yt-dlp.Problem. High-quality downloads abort right after the format selection
ERROR: You have requested merging of multiple formats but ffmpeg is not installed. Aborting due to --abort-on-errorbv*+ba produces two streams that must be merged, and ffmpeg.exe is not on PATH.ffmpeg -version; if it fails, add C:\tools\ffmpeg\bin to PATH and reopen the terminal. Temporary workaround: -f "b[height<=1080]" downloads a single pre-merged file, no ffmpeg needed.Problem. The site refuses to serve the video
ERROR: [youtube] <video id>: Sign in to confirm you're not a bot. Use --cookies-from-browser or --cookies for the authentication.yt-dlp -U for the standalone exe, python -m pip install -U yt-dlp for pip), then pass browser cookies — yt-dlp --cookies-from-browser firefox "<your-URL>", with the browser closed. If you get ERROR: Failed to decrypt with DPAPI, the Chromium cookie store is protected by app-bound encryption: use Firefox, or export a cookies.txt file and pass --cookies cookies.txt.You now have yt-dlp and ffmpeg on PATH, a %APPDATA%\yt-dlp\config.txt with your defaults, and a working GUI wrapper that calls the same executable. Next, extend the shell with an audio-only checkbox or a subtitle toggle — each is one more element in the $ytArgs array.
Do I need Python to run yt-dlp on Windows?
No. The standalone yt-dlp.exe from the releases page is self-contained; Python is