Private/Provider/Invoke-PSGalleryApiSearch.ps1
|
function Invoke-PSGalleryApiSearch { <# .SYNOPSIS Searches PSGallery directly via the NuGet v2 Search API using the fast searchTerm parameter. .DESCRIPTION Bypasses Find-PSResource which generates slow substringof() OData filters that cause PSGallery 500 errors. Uses the native searchTerm parameter instead, which leverages the server's full-text search index for fast, reliable results. Returns PSObjects compatible with ConvertTo-ModuleInfo -ProviderType PSResourceGet. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SearchTerm, [int]$TotalNeeded = 99 ) $galleryUrl = 'https://www.powershellgallery.com/api/v2' $apiPageSize = 100 $apiSkip = 0 $allEntries = [System.Collections.Generic.List[PSObject]]::new() $escapedTerm = $SearchTerm.Replace("'", "''") while ($allEntries.Count -lt $TotalNeeded) { $batchTop = [Math]::Min($apiPageSize, $TotalNeeded - $allEntries.Count) $uri = "${galleryUrl}/Search()?`$filter=IsLatestVersion&searchTerm='${escapedTerm}'&`$top=${batchTop}&`$skip=${apiSkip}" Write-PSMBLog -Message "PSGallery API: searchTerm='$SearchTerm' top=$batchTop skip=$apiSkip" -Level 'DEBUG' $resp = Invoke-WebRequest -Uri $uri -UseBasicParsing -ErrorAction Stop [xml]$feed = $resp.Content $nsm = [System.Xml.XmlNamespaceManager]::new($feed.NameTable) $nsm.AddNamespace('a', 'http://www.w3.org/2005/Atom') $nsm.AddNamespace('d', 'http://schemas.microsoft.com/ado/2007/08/dataservices') $nsm.AddNamespace('m', 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata') $entries = $feed.SelectNodes('//a:entry', $nsm) if (-not $entries -or $entries.Count -eq 0) { break } foreach ($entry in $entries) { $p = $entry.SelectSingleNode('m:properties', $nsm) if (-not $p) { continue } $tagStr = $p.SelectSingleNode('d:Tags', $nsm)?.InnerText $tags = if ($tagStr) { @($tagStr -split '\s+' | Where-Object { $_ }) } else { @() } if ('PSScript' -in $tags) { continue } $pubStr = $p.SelectSingleNode('d:Published', $nsm)?.InnerText $published = if ($pubStr) { try { [datetime]$pubStr } catch { [datetime]::MinValue } } else { [datetime]::MinValue } $dlStr = $p.SelectSingleNode('d:DownloadCount', $nsm)?.InnerText $downloads = if ($dlStr) { try { [long]$dlStr } catch { 0L } } else { 0L } $depStr = $p.SelectSingleNode('d:Dependencies', $nsm)?.InnerText $deps = if ($depStr) { @($depStr -split '\|' | ForEach-Object { ($_ -split ':')[0] } | Where-Object { $_ }) } else { @() } $preStr = $p.SelectSingleNode('d:IsPrerelease', $nsm)?.InnerText $allEntries.Add([PSCustomObject]@{ Name = $p.SelectSingleNode('d:Id', $nsm)?.InnerText Version = $p.SelectSingleNode('d:Version', $nsm)?.InnerText Author = $p.SelectSingleNode('d:Authors', $nsm)?.InnerText Description = $p.SelectSingleNode('d:Description', $nsm)?.InnerText ProjectUri = $p.SelectSingleNode('d:ProjectUrl', $nsm)?.InnerText LicenseUri = $p.SelectSingleNode('d:LicenseUrl', $nsm)?.InnerText Tags = $tags Repository = 'PSGallery' PublishedDate = $published Dependencies = $deps IsPrerelease = ($preStr -eq 'true') AdditionalMetadata = @{ downloadCount = $downloads } }) if ($allEntries.Count -ge $TotalNeeded) { break } } $apiSkip += $entries.Count if ($entries.Count -lt $batchTop) { break } } return $allEntries.ToArray() } |