Improve error handling

This commit is contained in:
amd64fox
2026-03-26 04:42:00 +03:00
parent 691e24fa2e
commit 6070bbcf1b

455
run.ps1
View File

@@ -661,6 +661,225 @@ function Format-DownloadSizeMb {
return ('{0:N2} MB' -f ($Bytes / 1MB))
}
function Convert-CommandOutputToString {
param(
[object[]]$Output
)
if ($null -eq $Output) {
return ''
}
$lines = foreach ($item in @($Output)) {
if ($null -eq $item) {
continue
}
if ($item -is [System.Management.Automation.ErrorRecord]) {
$item.Exception.Message.TrimEnd()
continue
}
$item.ToString().TrimEnd()
}
return (@($lines) -join [Environment]::NewLine).Trim()
}
function Get-CurlHttpStatus {
param(
[string]$Output
)
$match = [regex]::Match([string]$Output, 'HTTP_STATUS:(\d{3})')
if ($match.Success) {
return $match.Groups[1].Value
}
return ''
}
function Get-CurlDiagnosticDetails {
param(
[string]$Output
)
if ([string]::IsNullOrWhiteSpace($Output)) {
return ''
}
$cleanLines = foreach ($line in ($Output -split '\r?\n')) {
$currentLine = [string]$line
if ([string]::IsNullOrWhiteSpace($currentLine)) {
continue
}
if ($currentLine -match '^\s*HTTP_STATUS:\d{3}\s*$') {
continue
}
if ($currentLine -match '^\s*[#O=\-]+\s*$') {
continue
}
$curlMessageIndex = $currentLine.IndexOf('curl:')
if ($curlMessageIndex -gt 0) {
$currentLine = $currentLine.Substring($curlMessageIndex)
}
$currentLine = $currentLine.Trim()
if ($currentLine) {
$currentLine
}
}
return (@($cleanLines) -join [Environment]::NewLine).Trim()
}
function Format-CurlFailureMessage {
param(
[string]$Url,
[string]$Stage,
[int]$ExitCode,
[string]$HttpStatus,
[string]$Details,
[string]$ResponseText
)
$lines = @(
"curl $Stage failed",
"URL: $Url"
)
if ($ExitCode -ne 0) {
$lines += "Exit code: $ExitCode"
}
if ($HttpStatus) {
$lines += "HTTP status: $HttpStatus"
}
if ($Details) {
$lines += "Details: $Details"
}
if ($ResponseText) {
$lines += "Server response: $ResponseText"
}
return ($lines -join [Environment]::NewLine)
}
function New-DownloadFailureException {
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[string]$DownloadMethod,
[string]$FailureKind,
[string]$HttpStatus,
[int]$ExitCode = 0,
[System.Exception]$InnerException
)
if ($InnerException) {
$exception = New-Object System.Exception($Message, $InnerException)
}
else {
$exception = New-Object System.Exception($Message)
}
if ($DownloadMethod) {
$exception.Data['DownloadMethod'] = $DownloadMethod
}
if ($FailureKind) {
$exception.Data['FailureKind'] = $FailureKind
}
if ($HttpStatus) {
$exception.Data['HttpStatus'] = $HttpStatus
}
if ($ExitCode -ne 0) {
$exception.Data['ExitCode'] = $ExitCode
}
return $exception
}
function Write-DownloadFailureDetails {
param(
[Parameter(Mandatory = $true)]
[string]$Method,
[System.Exception]$Exception,
[string]$Title
)
if ($Title) {
Write-Host $Title -ForegroundColor Red
}
Write-Host "Download method: $Method" -ForegroundColor Yellow
if ($Exception) {
Write-Host $Exception.Message -ForegroundColor Yellow
}
Write-Host
}
function Invoke-DownloadMethodWithRetries {
param(
[Parameter(Mandatory = $true)]
[string]$Url,
[Parameter(Mandatory = $true)]
[string]$DestinationPath,
[Parameter(Mandatory = $true)]
[System.Net.WebClient]$WebClient,
[Parameter(Mandatory = $true)]
[ValidateSet('curl', 'webclient')]
[string]$DownloadMethod,
[Parameter(Mandatory = $true)]
[string]$FileName
)
$lastError = $null
for ($attempt = 1; $attempt -le 2; $attempt++) {
try {
if ($attempt -gt 1) {
Write-Host ("Download method: {0} (attempt {1}/2)" -f $DownloadMethod, $attempt) -ForegroundColor Yellow
}
Invoke-SpotifyDownloadAttempt `
-Url $Url `
-DestinationPath $DestinationPath `
-WebClient $WebClient `
-DownloadMethod $DownloadMethod
return [PSCustomObject]@{
Success = $true
Error = $null
Method = $DownloadMethod
}
}
catch {
$lastError = $_.Exception
Write-Host
if ($attempt -eq 1) {
Write-Host ($lang).Download $FileName -ForegroundColor RED
Write-DownloadFailureDetails -Method $DownloadMethod -Exception $lastError
Write-Host ($lang).Download2`n
Start-Sleep -Milliseconds 5000
}
}
}
return [PSCustomObject]@{
Success = $false
Error = $lastError
Method = $DownloadMethod
}
}
function Invoke-WebClientDownloadWithProgress {
param(
[Parameter(Mandatory = $true)]
@@ -756,16 +975,143 @@ function Invoke-SpotifyDownloadAttempt {
switch ($DownloadMethod) {
'curl' {
$stcode = curl.exe -Is -w "%{http_code}" -o NUL -k $Url --retry 2 --ssl-no-revoke
if ($stcode.trim() -ne "200") {
throw "Unexpected HTTP status: $($stcode.Trim())"
if (Test-Path -LiteralPath $DestinationPath) {
Remove-Item -LiteralPath $DestinationPath -Force -ErrorAction SilentlyContinue
}
if ($null -eq $script:curlSupportsFailWithBody) {
try {
$helpOutput = curl.exe --help all 2>$null
$script:curlSupportsFailWithBody = [bool]($helpOutput -match '--fail-with-body')
}
catch {
$script:curlSupportsFailWithBody = $false
}
}
$curlFailOption = if ($script:curlSupportsFailWithBody) { '--fail-with-body' } else { '--fail' }
$curlOutput = $null
$null = & curl.exe `
-q `
-L `
-k `
$curlFailOption `
--connect-timeout 15 `
--ssl-no-revoke `
--progress-bar `
-o $DestinationPath `
-w "`nHTTP_STATUS:%{http_code}`n" `
$Url `
2>&1 | Tee-Object -Variable curlOutput | Out-Host
$curlExitCode = $LASTEXITCODE
$curlOutputText = Convert-CommandOutputToString -Output $curlOutput
$httpStatus = Get-CurlHttpStatus -Output $curlOutputText
$curlDetails = Get-CurlDiagnosticDetails -Output $curlOutputText
$responseText = ''
if ([string]::IsNullOrWhiteSpace($curlDetails)) {
$curlDetails = "curl exited with code $curlExitCode"
}
if ($httpStatus) {
try {
if (Test-Path -LiteralPath $DestinationPath) {
$responseFile = Get-Item -LiteralPath $DestinationPath -ErrorAction Stop
if ($responseFile.Length -gt 0) {
$bytesToRead = [Math]::Min([int]$responseFile.Length, 4096)
$stream = [System.IO.File]::OpenRead($DestinationPath)
try {
$buffer = New-Object byte[] $bytesToRead
$bytesRead = $stream.Read($buffer, 0, $buffer.Length)
}
finally {
$stream.Dispose()
}
if ($bytesRead -gt 0) {
$responseText = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $bytesRead)
$responseText = $responseText -replace "[`0-\b\v\f\x0E-\x1F]", " "
$responseText = ($responseText -replace '\s+', ' ').Trim()
}
}
}
}
catch {
$responseText = ''
}
}
$curlFailureKind = if ($httpStatus) { 'http' } else { 'network' }
if ($curlExitCode -ne 0) {
$message = Format-CurlFailureMessage -Url $Url -Stage 'download request' -ExitCode $curlExitCode -HttpStatus $httpStatus -Details $curlDetails -ResponseText $responseText
throw (New-DownloadFailureException -Message $message -DownloadMethod 'curl' -FailureKind $curlFailureKind -HttpStatus $httpStatus -ExitCode $curlExitCode)
}
if ($httpStatus -ne '200') {
$message = Format-CurlFailureMessage -Url $Url -Stage 'download request' -ExitCode $curlExitCode -HttpStatus $httpStatus -Details $curlDetails -ResponseText $responseText
throw (New-DownloadFailureException -Message $message -DownloadMethod 'curl' -FailureKind 'http' -HttpStatus $httpStatus -ExitCode $curlExitCode)
}
if (!(Test-Path -LiteralPath $DestinationPath)) {
throw "curl download failed`nURL: $Url`nDestination file was not created: $DestinationPath"
}
$downloadedFile = Get-Item -LiteralPath $DestinationPath -ErrorAction SilentlyContinue
if ($null -eq $downloadedFile -or $downloadedFile.Length -le 0) {
throw "curl download failed`nURL: $Url`nDownloaded file is empty: $DestinationPath"
}
curl.exe -q -k $Url -o $DestinationPath --progress-bar --retry 3 --ssl-no-revoke
return
}
'webclient' {
Invoke-WebClientDownloadWithProgress -WebClient $WebClient -Url $Url -DestinationPath $DestinationPath
try {
Invoke-WebClientDownloadWithProgress -WebClient $WebClient -Url $Url -DestinationPath $DestinationPath
}
catch {
$webException = $_.Exception
$httpStatus = ''
$details = $webException.Message
$failureKind = 'network'
if ($webException -is [System.Net.WebException]) {
$details = "WebException status: $($webException.Status)"
$httpResponse = $webException.Response -as [System.Net.HttpWebResponse]
if ($httpResponse) {
$httpStatus = [string][int]$httpResponse.StatusCode
$statusDescription = [string]$httpResponse.StatusDescription
$failureKind = 'http'
if ([string]::IsNullOrWhiteSpace($statusDescription)) {
$details = $webException.Message
}
else {
$details = $statusDescription
}
}
elseif ($webException.Message) {
$details = "WebException status: $($webException.Status)`n$($webException.Message)"
}
}
$lines = @(
'webclient download request failed',
"URL: $Url"
)
if ($httpStatus) {
$lines += "HTTP status: $httpStatus"
}
if ($details) {
$lines += "Details: $details"
}
$message = $lines -join [Environment]::NewLine
throw (New-DownloadFailureException -Message $message -DownloadMethod 'webclient' -FailureKind $failureKind -HttpStatus $httpStatus -InnerException $webException)
}
return
}
}
@@ -792,41 +1138,88 @@ function downloadSp([string]$DownloadFolder) {
Stop-Script
}
for ($attempt = 1; $attempt -le 2; $attempt++) {
try {
Invoke-SpotifyDownloadAttempt `
-Url $web_Url `
-DestinationPath $local_Url `
-WebClient $webClient `
-DownloadMethod $selectedDownloadMethod
$lastDownloadError = $null
$lastDownloadMethod = $selectedDownloadMethod
if ($selectedDownloadMethod -eq 'curl') {
$curlResult = Invoke-DownloadMethodWithRetries `
-Url $web_Url `
-DestinationPath $local_Url `
-WebClient $webClient `
-DownloadMethod 'curl' `
-FileName $web_name_file
if ($curlResult.Success) {
return
}
catch {
$lastDownloadError = $curlResult.Error
$lastDownloadMethod = $curlResult.Method
Write-DownloadFailureDetails -Method 'curl' -Exception $lastDownloadError -Title 'Curl download failed again'
$httpStatus = if ($lastDownloadError) { [string]$lastDownloadError.Data['HttpStatus'] } else { '' }
$shouldUseWebClientFallback = $httpStatus -ne '429'
if ($shouldUseWebClientFallback) {
Write-Host "Switching to WebClient fallback..." -ForegroundColor Yellow
Write-Host
if ($attempt -eq 1) {
Write-Host ($lang).Download $web_name_file -ForegroundColor RED
$_.Exception
if (Test-Path -LiteralPath $local_Url) {
Remove-Item -LiteralPath $local_Url -Force -ErrorAction SilentlyContinue
}
try {
$lastDownloadMethod = 'webclient'
Write-Host "Download method: webclient (fallback)" -ForegroundColor Yellow
Invoke-SpotifyDownloadAttempt `
-Url $web_Url `
-DestinationPath $local_Url `
-WebClient $webClient `
-DownloadMethod 'webclient'
return
}
catch {
$lastDownloadError = $_.Exception
Write-Host
Write-Host ($lang).Download2`n
Start-Sleep -Milliseconds 5000
continue
Write-DownloadFailureDetails -Method 'webclient' -Exception $lastDownloadError -Title 'WebClient fallback failed'
}
Write-Host ($lang).Download3 -ForegroundColor RED
$_.Exception
Write-Host
Write-Host ($lang).Download4`n
if ($DownloadFolder -and (Test-Path $DownloadFolder)) {
Start-Sleep -Milliseconds 200
Remove-Item -Recurse -LiteralPath $DownloadFolder -ErrorAction SilentlyContinue
}
else {
if ($httpStatus) {
Write-Host ("Skipping WebClient fallback because the server returned HTTP {0}." -f $httpStatus) -ForegroundColor Yellow
Write-Host
}
Stop-Script
}
}
}
else {
$downloadResult = Invoke-DownloadMethodWithRetries `
-Url $web_Url `
-DestinationPath $local_Url `
-WebClient $webClient `
-DownloadMethod $selectedDownloadMethod `
-FileName $web_name_file
if ($downloadResult.Success) {
return
}
$lastDownloadError = $downloadResult.Error
$lastDownloadMethod = $downloadResult.Method
}
Write-Host ($lang).Download3 -ForegroundColor RED
if ($lastDownloadError) {
Write-DownloadFailureDetails -Method $lastDownloadMethod -Exception $lastDownloadError
}
Write-Host ($lang).Download4`n
if ($DownloadFolder -and (Test-Path $DownloadFolder)) {
Start-Sleep -Milliseconds 200
Remove-Item -Recurse -LiteralPath $DownloadFolder -ErrorAction SilentlyContinue
}
Stop-Script
}
function Remove-TempDirectory {
param(