|
$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' |