Skip to content

Instantly share code, notes, and snippets.

@5shekel
Last active February 7, 2026 09:26
Show Gist options
  • Select an option

  • Save 5shekel/fd87ea27b86d243bf894635a783239c2 to your computer and use it in GitHub Desktop.

Select an option

Save 5shekel/fd87ea27b86d243bf894635a783239c2 to your computer and use it in GitHub Desktop.
uv yt-dlp setup scripts (unix + windows pwsh)

Use yt-dlp impersonation (wrapper-based, uv underneath)

Two setup scripts are provided:

  • Unix/macOS/Linux: ./setup-ytdlp-uv-unix.sh
  • Windows PowerShell: ./setup-ytdlp-uv-windows.ps1

Both scripts:

  1. Detect whether yt-dlp already exists in PATH
  2. Create a wrapper that runs uvx --with 'yt-dlp[curl-cffi]' yt-dlp ...
  3. Write a default yt-dlp config with impersonation enabled

The Windows script additionally:

  • reports where.exe yt-dlp and where.exe yt-dlp.exe results
  • detects Scoop-installed yt-dlp / yt-dlp-nightly
  • prompts to uninstall Scoop packages before wrapper setup
  • writes persistent aliases for both yt-dlp and yt-dlp.exe

1) Run setup

Unix/macOS/Linux

chmod +x ./setup-ytdlp-uv-unix.sh
./setup-ytdlp-uv-unix.sh

Windows (PowerShell)

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\setup-ytdlp-uv-windows.ps1

If you run from a different directory, use the full/relative path to the script file location.

If Scoop packages are detected, the script will ask:

Uninstall Scoop yt-dlp package(s) now? [y/N]

Type y to remove Scoop packages and shims, or press Enter to keep them.


2) Wrapper behavior

The wrapper executes:

uvx --with 'yt-dlp[curl-cffi]' yt-dlp "$@"

On Windows, the wrapper command is equivalent in yt-dlp.cmd:

uvx --with "yt-dlp[curl-cffi]" yt-dlp %*

3) Default config written by scripts

--remote-components ejs:github
--impersonate chrome-136
-o %(id)s.%(ext)s

Config file locations:

  • Unix/macOS/Linux: $HOME/.config/yt-dlp/config
  • Windows: %APPDATA%\yt-dlp\config

4) Check available impersonation targets

yt-dlp --list-impersonate-targets

5) Normal usage (global defaults already applied)

yt-dlp \
  --skip-download \
  --write-auto-subs --write-subs \
  --sub-langs 'en.*' \
  'https://www.youtube.com/watch?v=D7hkFh4uhMo'

6) Override defaults for one run

yt-dlp --impersonate 'firefox-135' -o '%(title)s.%(ext)s' URL

7) Optional: let yt-dlp pick any supported target

yt-dlp --impersonate '' URL

8) Why --remote-components ejs:github is set

For YouTube specifically, this is a practical fix for:

n challenge solving failed: Some formats may be missing

Use:

yt-dlp --remote-components ejs:github <your-normal-args>

9) Confirm wrapper, config, and version

Unix/macOS/Linux

which yt-dlp
cat "$HOME/.config/yt-dlp/config"
yt-dlp --version

Windows (PowerShell)

Get-Command yt-dlp | Format-List CommandType,Source,Definition
Get-Command yt-dlp.exe | Format-List CommandType,Source,Definition
Get-Content "$env:APPDATA\yt-dlp\config"
yt-dlp --version
yt-dlp.exe --version

On Windows, command resolution can look confusing:

  • where.exe yt-dlp.exe only checks executable resolution on PATH.
  • PowerShell command precedence can resolve aliases first.
  • This setup intentionally aliases both yt-dlp and yt-dlp.exe to the wrapper.

Recommended checks:

Get-Command yt-dlp | Format-List CommandType,Source,Definition
Get-Command yt-dlp.exe | Format-List CommandType,Source,Definition
where.exe yt-dlp
where.exe yt-dlp.exe

The setup script writes a persistent alias block into your PowerShell profile so both names resolve to the wrapper in new sessions.

#!/usr/bin/env bash
set -euo pipefail
echo "==> uv + yt-dlp impersonation setup (Unix)"
if ! command -v uv >/dev/null 2>&1; then
echo "ERROR: uv is not installed or not in PATH."
echo "Install uv first: https://docs.astral.sh/uv/getting-started/installation/"
exit 1
fi
if command -v yt-dlp >/dev/null 2>&1; then
EXISTING_YTDLP="$(command -v yt-dlp)"
echo "Found existing yt-dlp in PATH: ${EXISTING_YTDLP}"
else
EXISTING_YTDLP=""
echo "No existing yt-dlp found in PATH."
fi
BIN_DIR="${HOME}/.local/bin"
CONFIG_DIR="${HOME}/.config/yt-dlp"
WRAPPER_PATH="${BIN_DIR}/yt-dlp"
CONFIG_PATH="${CONFIG_DIR}/config"
mkdir -p "${BIN_DIR}" "${CONFIG_DIR}"
if [[ -f "${WRAPPER_PATH}" ]]; then
if head -n 1 "${WRAPPER_PATH}" | grep -q "#!/usr/bin/env bash"; then
echo "Existing wrapper detected at ${WRAPPER_PATH}; replacing it."
else
BACKUP_PATH="${WRAPPER_PATH}.backup.$(date +%s)"
cp "${WRAPPER_PATH}" "${BACKUP_PATH}"
echo "Backed up non-wrapper file from ${WRAPPER_PATH} to ${BACKUP_PATH}."
fi
fi
cat > "${WRAPPER_PATH}" <<'EOF'
#!/usr/bin/env bash
exec uvx --with 'yt-dlp[curl-cffi]' yt-dlp "$@"
EOF
chmod +x "${WRAPPER_PATH}"
cat > "${CONFIG_PATH}" <<'EOF'
--remote-components ejs:github
--impersonate chrome-136
-o %(id)s.%(ext)s
EOF
case ":${PATH}:" in
*":${BIN_DIR}:"*)
echo "PATH already contains ${BIN_DIR}."
;;
*)
echo "WARNING: ${BIN_DIR} is not currently in PATH."
echo "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):"
echo " export PATH=\"${BIN_DIR}:\$PATH\""
;;
esac
echo
echo "==> Setup complete"
echo "Wrapper: ${WRAPPER_PATH}"
echo "Config: ${CONFIG_PATH}"
if [[ -n "${EXISTING_YTDLP}" ]]; then
echo "Previous yt-dlp was detected at: ${EXISTING_YTDLP}"
fi
echo
echo "Test with:"
echo " yt-dlp --version"
echo " yt-dlp --list-impersonate-targets"
$ErrorActionPreference = 'Stop'
Write-Host '==> uv + yt-dlp impersonation setup (Windows/PowerShell)'
$uvCmd = Get-Command uv -ErrorAction SilentlyContinue
if (-not $uvCmd) {
Write-Error 'uv is not installed or not in PATH. Install uv first: https://docs.astral.sh/uv/getting-started/installation/'
}
$existing = Get-Command yt-dlp -ErrorAction SilentlyContinue
if ($existing) {
$resolved = $null
if ($existing.Path) { $resolved = $existing.Path }
elseif ($existing.Source -and (Test-Path $existing.Source -ErrorAction SilentlyContinue)) { $resolved = $existing.Source }
elseif ($existing.Definition -and (Test-Path $existing.Definition -ErrorAction SilentlyContinue)) { $resolved = $existing.Definition }
$origin = 'PATH'
$probe = if ($resolved) { $resolved } else { "$($existing.CommandType):$($existing.Definition)" }
if ($probe -match '(?i)\\scoop\\') { $origin = 'Scoop' }
elseif ($probe -match '(?i)\\chocolatey\\') { $origin = 'Chocolatey' }
if ($resolved) {
Write-Host "Found existing yt-dlp in PATH: $resolved"
} else {
Write-Host "Found existing yt-dlp command: type=$($existing.CommandType), definition=$($existing.Definition)"
}
Write-Host "Detected source: $origin"
} else {
Write-Host 'No existing yt-dlp found in PATH.'
}
$allBareMatches = @()
try {
$allBareMatches = @(where.exe yt-dlp 2>$null)
} catch {}
$allExeMatches = @()
try {
$allExeMatches = @(where.exe yt-dlp.exe 2>$null)
} catch {}
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
$nodeRuntime = 'node'
if ($nodeCmd) {
$nodePath = $null
if ($nodeCmd.Path -and (Test-Path $nodeCmd.Path -ErrorAction SilentlyContinue)) {
$nodePath = $nodeCmd.Path
} elseif ($nodeCmd.Source -and (Test-Path $nodeCmd.Source -ErrorAction SilentlyContinue)) {
$nodePath = $nodeCmd.Source
}
if ($nodePath) {
$nodePathForConfig = $nodePath -replace '\\', '/'
if ($nodePathForConfig -match '\s') {
$nodeRuntime = ('"node:{0}"' -f $nodePathForConfig)
} else {
$nodeRuntime = "node:$nodePathForConfig"
}
Write-Host "Detected node runtime: $nodePath"
} else {
Write-Host 'Detected node command, but executable path was not resolved. Using --js-runtimes node.'
}
} else {
Write-Host 'node was not found in current PATH. Wrapper will try `nvm use lts` automatically when needed.'
}
if ($allBareMatches.Count -gt 0) {
Write-Host 'where.exe yt-dlp results:'
$allBareMatches | ForEach-Object { Write-Host " $_" }
}
if ($allExeMatches.Count -gt 0) {
Write-Host 'where.exe yt-dlp.exe results:'
$allExeMatches | ForEach-Object { Write-Host " $_" }
}
$scoopCmd = Get-Command scoop -ErrorAction SilentlyContinue
if ($scoopCmd) {
$scoopPkgs = @('yt-dlp', 'yt-dlp-nightly')
$installedViaScoop = @()
foreach ($pkg in $scoopPkgs) {
$pkgOut = & scoop list $pkg 2>$null | Out-String
if ($LASTEXITCODE -eq 0 -and $pkgOut -match "(?m)^$([Regex]::Escape($pkg))\s+") {
$installedViaScoop += $pkg
}
}
if ($installedViaScoop.Count -gt 0) {
Write-Host "Detected Scoop package(s): $($installedViaScoop -join ', ')"
$answer = Read-Host 'Uninstall Scoop yt-dlp package(s) now? [y/N]'
if ($answer -match '^(?i)y(?:es)?$') {
foreach ($pkg in $installedViaScoop) {
Write-Host "Uninstalling Scoop package: $pkg"
& scoop uninstall $pkg
}
Write-Host 'Scoop yt-dlp package cleanup complete.'
} else {
Write-Host 'Keeping Scoop package(s); PowerShell alias/wrapper will still take precedence.'
}
}
}
$binDir = Join-Path $env:USERPROFILE 'bin'
$configDir = Join-Path $env:APPDATA 'yt-dlp'
$xdgConfigDir = Join-Path $env:USERPROFILE '.config\yt-dlp'
$wrapperPath = Join-Path $binDir 'yt-dlp.cmd'
$configPath = Join-Path $configDir 'config'
$xdgConfigPath = Join-Path $xdgConfigDir 'config.txt'
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
New-Item -ItemType Directory -Force -Path $xdgConfigDir | Out-Null
function Set-ManagedBlock {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$StartMarker,
[Parameter(Mandatory = $true)]
[string]$EndMarker,
[Parameter(Mandatory = $true)]
[string]$Block
)
$existingContent = ''
if (Test-Path $Path) {
$existingContent = Get-Content -Path $Path -Raw -ErrorAction SilentlyContinue
if (-not $existingContent) { $existingContent = '' }
}
$escapedStart = [Regex]::Escape($StartMarker)
$escapedEnd = [Regex]::Escape($EndMarker)
$pattern = "(?s)$escapedStart.*?$escapedEnd"
if ($existingContent -match $pattern) {
$existingContent = [Regex]::Replace($existingContent, $pattern, $Block)
} else {
if ($existingContent.Length -gt 0 -and -not $existingContent.EndsWith("`n")) {
$existingContent += "`r`n"
}
$existingContent += $Block
}
Set-Content -Path $Path -Value $existingContent -NoNewline
}
if (Test-Path $wrapperPath) {
$header = Get-Content -Path $wrapperPath -TotalCount 5 -ErrorAction SilentlyContinue
if (($header -join "`n") -notmatch 'Roo-uv-ytdlp-wrapper') {
$backupPath = "$wrapperPath.backup.$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
Copy-Item $wrapperPath $backupPath -Force
Write-Host "Backed up existing non-wrapper file: $backupPath"
} else {
Write-Host "Existing wrapper detected at $wrapperPath; replacing it."
}
}
$cmdWrapper = @'
@echo off
REM Roo-uv-ytdlp-wrapper
where node >nul 2>nul
if errorlevel 1 (
where nvm >nul 2>nul
if not errorlevel 1 (
call nvm use lts >nul 2>nul
)
)
uvx --with "yt-dlp[curl-cffi]" yt-dlp %*
'@
Set-Content -Path $wrapperPath -Value $cmdWrapper -NoNewline
$ps1WrapperPath = Join-Path $binDir 'yt-dlp.ps1'
$ps1Wrapper = @'
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$ArgsFromCaller
)
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
$nvmCmd = Get-Command nvm -ErrorAction SilentlyContinue
if ($nvmCmd) {
& nvm use lts | Out-Null
}
}
& uvx --with "yt-dlp[curl-cffi]" yt-dlp @ArgsFromCaller
exit $LASTEXITCODE
'@
Set-Content -Path $ps1WrapperPath -Value $ps1Wrapper -NoNewline
$configStartMarker = '# >>> yt-dlp uv defaults >>>'
$configEndMarker = '# <<< yt-dlp uv defaults <<<'
$managedConfigBlock = @"
$configStartMarker
--remote-components ejs:github
--js-runtimes $nodeRuntime
--impersonate chrome-136
-o %(id)s.%(ext)s
$configEndMarker
"@
Set-ManagedBlock -Path $configPath -StartMarker $configStartMarker -EndMarker $configEndMarker -Block $managedConfigBlock
Set-ManagedBlock -Path $xdgConfigPath -StartMarker $configStartMarker -EndMarker $configEndMarker -Block $managedConfigBlock
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$pathEntries = @()
if ($userPath) { $pathEntries = $userPath -split ';' }
$hasUserBin = $pathEntries | Where-Object { $_.TrimEnd('\\') -ieq $binDir.TrimEnd('\\') }
if (-not $hasUserBin) {
$newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) { $binDir } else { "$binDir;$userPath" }
[Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User')
Write-Host "Added $binDir to USER PATH. Restart terminal/session to apply."
} else {
Write-Host "$binDir already exists in USER PATH."
}
$profilePath = $PROFILE.CurrentUserAllHosts
$profileDir = Split-Path -Path $profilePath -Parent
New-Item -ItemType Directory -Force -Path $profileDir | Out-Null
if (-not (Test-Path $profilePath)) {
New-Item -ItemType File -Force -Path $profilePath | Out-Null
}
$profileContent = Get-Content -Path $profilePath -Raw -ErrorAction SilentlyContinue
if (-not $profileContent) { $profileContent = '' }
$startMarker = '# >>> yt-dlp uv wrapper >>>'
$endMarker = '# <<< yt-dlp uv wrapper <<<'
$escapedStart = [Regex]::Escape($startMarker)
$escapedEnd = [Regex]::Escape($endMarker)
$pattern = "(?s)$escapedStart.*?$escapedEnd"
$aliasBlock = @"
$startMarker
if (Test-Path '$wrapperPath') {
Set-Alias -Name yt-dlp -Value '$wrapperPath' -Scope Global
Set-Alias -Name yt-dlp.exe -Value '$wrapperPath' -Scope Global
}
$endMarker
"@
if ($profileContent -match $pattern) {
$profileContent = [Regex]::Replace($profileContent, $pattern, $aliasBlock)
} else {
if ($profileContent.Length -gt 0 -and -not $profileContent.EndsWith("`n")) {
$profileContent += "`r`n"
}
$profileContent += $aliasBlock
}
Set-Content -Path $profilePath -Value $profileContent -NoNewline
Set-Alias -Name yt-dlp -Value $wrapperPath -Scope Global
Set-Alias -Name yt-dlp.exe -Value $wrapperPath -Scope Global
Write-Host "Configured PowerShell alias in profile: $profilePath"
Write-Host ''
Write-Host '==> Setup complete'
Write-Host "Wrapper: $wrapperPath"
Write-Host "PS1 wrapper: $ps1WrapperPath"
Write-Host "Config (AppData): $configPath"
Write-Host "Config (XDG): $xdgConfigPath"
Write-Host ''
Write-Host 'Test with:'
Write-Host ' Get-Command yt-dlp | Format-List CommandType,Source,Definition'
Write-Host ' Get-Command yt-dlp.exe | Format-List CommandType,Source,Definition'
Write-Host ' yt-dlp --version'
Write-Host ' yt-dlp.exe --version'
Write-Host ' node -v'
Write-Host ' yt-dlp --print-config'
Write-Host ' yt-dlp -v --simulate "https://www.youtube.com/watch?v=BaW_jenozKc"'
Write-Host ' yt-dlp --list-impersonate-targets'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment