Public/Search-PatMedia.ps1
|
function Search-PatMedia { <# .SYNOPSIS Searches for media items across Plex libraries. .DESCRIPTION Searches for media items (movies, TV shows, music, etc.) across all or specific Plex library sections using the Plex search API. Returns flattened results with type information for easy filtering and pipeline operations. .PARAMETER Query The search term to find matching media items. .PARAMETER ServerUri The base URI of the Plex server (e.g., http://plex.example.com:32400). If not specified, uses the default stored server. .PARAMETER Token The Plex authentication token. Required when using -ServerUri to authenticate with the server. If not specified with -ServerUri, requests may fail with 401. .PARAMETER SectionName Limit search to a specific library section by name (e.g., "Movies", "TV Shows"). .PARAMETER SectionId Limit search to a specific library section by ID. .PARAMETER Type Filter results by media type(s). Valid values: movie, show, season, episode, artist, album, track, photo, collection. .PARAMETER Limit Maximum number of results to return per media type. Defaults to 10. .EXAMPLE Search-PatMedia -Query "matrix" Searches for "matrix" across all libraries on the default server. .EXAMPLE Search-PatMedia -Query "action" -SectionName "Movies" Searches for "action" only in the Movies library. .EXAMPLE Search-PatMedia -Query "beatles" -Type artist,album Searches for "beatles" and returns only artist and album results. .EXAMPLE Search-PatMedia -Query "star" -Limit 5 Searches for "star" with a maximum of 5 results per type. .EXAMPLE Search-PatMedia -Query "favorites" -Type movie | Get-PatMediaInfo Searches for movies matching "favorites" and gets detailed media info. .OUTPUTS PSCustomObject[] Returns an array of search result objects with properties: - Type: The media type (movie, show, episode, artist, etc.) - RatingKey: Unique identifier for the media item - Title: Title of the media item - Year: Release year (if applicable) - Summary: Description of the media item - Thumb: Thumbnail image path - LibraryId: ID of the library section containing this item - LibraryName: Name of the library section - ServerUri: URI of the Plex server #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', 'commandName', Justification = 'Standard ArgumentCompleter parameter, not always used' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', 'parameterName', Justification = 'Standard ArgumentCompleter parameter, not always used' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', 'commandAst', Justification = 'Standard ArgumentCompleter parameter, not always used' )] [CmdletBinding(DefaultParameterSetName = 'All')] [OutputType([PSCustomObject[]])] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string] $Query, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-PatServerUri -Uri $_ })] [string] $ServerUri, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $Token, [Parameter(Mandatory = $false, ParameterSetName = 'ByName')] [ValidateNotNullOrEmpty()] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) # Strip leading quotes for matching (case-insensitive) $quoteChar = '' $strippedWord = $wordToComplete if ($wordToComplete -match "^([`"'])(.*)$") { $quoteChar = $Matches[1] $strippedWord = $Matches[2] } if ($fakeBoundParameters.ContainsKey('ServerUri')) { try { $sections = Get-PatLibrary -ServerUri $fakeBoundParameters['ServerUri'] -ErrorAction 'SilentlyContinue' foreach ($sectionTitle in $sections.Directory.title) { if ($sectionTitle -ilike "$strippedWord*") { if ($quoteChar) { $completionText = "$quoteChar$sectionTitle$quoteChar" } elseif ($sectionTitle -match '\s') { $completionText = "'$sectionTitle'" } else { $completionText = $sectionTitle } [System.Management.Automation.CompletionResult]::new($completionText, $sectionTitle, 'ParameterValue', $sectionTitle) } } } catch { Write-Debug "Tab completion failed for SectionName: $($_.Exception.Message)" } } else { try { $sections = Get-PatLibrary -ErrorAction 'SilentlyContinue' foreach ($sectionTitle in $sections.Directory.title) { if ($sectionTitle -ilike "$strippedWord*") { if ($quoteChar) { $completionText = "$quoteChar$sectionTitle$quoteChar" } elseif ($sectionTitle -match '\s') { $completionText = "'$sectionTitle'" } else { $completionText = $sectionTitle } [System.Management.Automation.CompletionResult]::new($completionText, $sectionTitle, 'ParameterValue', $sectionTitle) } } } catch { Write-Debug "Tab completion failed for SectionName (default server): $($_.Exception.Message)" } } })] [string] $SectionName, [Parameter(Mandatory = $false, ParameterSetName = 'ById')] [ValidateRange(1, [int]::MaxValue)] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) # Strip leading quotes for matching $strippedWord = $wordToComplete -replace "^[`"']", '' if ($fakeBoundParameters.ContainsKey('ServerUri')) { try { $sections = Get-PatLibrary -ServerUri $fakeBoundParameters['ServerUri'] -ErrorAction 'SilentlyContinue' $sections.Directory | ForEach-Object { $sectionId = ($_.key -replace '.*/(\d+)$', '$1') if ($sectionId -ilike "$strippedWord*") { [System.Management.Automation.CompletionResult]::new($sectionId, "$sectionId - $($_.title)", 'ParameterValue', "$($_.title) (ID: $sectionId)") } } } catch { Write-Debug "Tab completion failed for SectionId: $($_.Exception.Message)" } } else { try { $sections = Get-PatLibrary -ErrorAction 'SilentlyContinue' $sections.Directory | ForEach-Object { $sectionId = ($_.key -replace '.*/(\d+)$', '$1') if ($sectionId -ilike "$strippedWord*") { [System.Management.Automation.CompletionResult]::new($sectionId, "$sectionId - $($_.title)", 'ParameterValue', "$($_.title) (ID: $sectionId)") } } } catch { Write-Debug "Tab completion failed for SectionId (default server): $($_.Exception.Message)" } } })] [int] $SectionId, [Parameter(Mandatory = $false)] [ValidateSet('movie', 'show', 'season', 'episode', 'artist', 'album', 'track', 'photo', 'collection')] [string[]] $Type, [Parameter(Mandatory = $false)] [ValidateRange(1, 1000)] [int] $Limit = 10 ) begin { $script:serverContext = Resolve-PatServerContext -ServerUri $ServerUri -Token $Token $effectiveUri = $script:serverContext.Uri $headers = $script:serverContext.Headers } process { try { # Resolve SectionName to SectionId if needed $resolvedSectionId = $null $resolvedSectionName = $null if ($SectionId) { $resolvedSectionId = $SectionId # Get section name for output if ($script:serverContext.WasExplicitUri) { $sections = Get-PatLibrary -ServerUri $effectiveUri -ErrorAction 'SilentlyContinue' } else { $sections = Get-PatLibrary -ErrorAction 'SilentlyContinue' } $matchingSection = $sections.Directory | Where-Object { ($_.key -replace '.*/(\d+)$', '$1') -eq $resolvedSectionId.ToString() } $resolvedSectionName = $matchingSection.title } elseif ($SectionName) { if ($script:serverContext.WasExplicitUri) { $sections = Get-PatLibrary -ServerUri $effectiveUri -ErrorAction 'Stop' } else { $sections = Get-PatLibrary -ErrorAction 'Stop' } $matchingSection = $sections.Directory | Where-Object { $_.title -eq $SectionName } if (-not $matchingSection) { throw "Library section '$SectionName' not found" } $resolvedSectionId = [int]($matchingSection.key -replace '.*/(\d+)$', '$1') $resolvedSectionName = $SectionName Write-Verbose "Resolved section name '$SectionName' to ID $resolvedSectionId" } # Build query string $queryParams = @( "query=$([System.Uri]::EscapeDataString($Query))" "limit=$Limit" ) if ($resolvedSectionId) { $queryParams += "sectionId=$resolvedSectionId" } $queryString = $queryParams -join '&' $endpoint = '/hubs/search' Write-Verbose "Searching for '$Query' (limit: $Limit)" $uri = Join-PatUri -BaseUri $effectiveUri -Endpoint $endpoint -QueryString $queryString $result = Invoke-PatApi -Uri $uri -Headers $headers -ErrorAction 'Stop' # Process and flatten hub results if ($result.Hub) { foreach ($hub in $result.Hub) { # Skip if type filter is specified and this hub doesn't match if ($Type -and $hub.type -notin $Type) { continue } # Skip empty hubs if (-not $hub.Metadata) { continue } foreach ($item in $hub.Metadata) { # Extract library info from the item if available $itemLibraryId = if ($item.librarySectionID) { [int]$item.librarySectionID } elseif ($resolvedSectionId) { $resolvedSectionId } else { $null } $itemLibraryName = if ($item.librarySectionTitle) { $item.librarySectionTitle } elseif ($resolvedSectionName) { $resolvedSectionName } else { $null } [PSCustomObject]@{ PSTypeName = 'PlexAutomationToolkit.SearchResult' Type = $hub.type RatingKey = if ($item.ratingKey) { [int]$item.ratingKey } else { $null } Title = $item.title Year = if ($item.year) { [int]$item.year } else { $null } Summary = $item.summary Thumb = $item.thumb LibraryId = $itemLibraryId LibraryName = $itemLibraryName ServerUri = $effectiveUri } } } } else { Write-Verbose "No search results found for '$Query'" } } catch { throw "Search failed: $($_.Exception.Message)" } } } |