Private/Resolve-PlexFilter.ps1

function Resolve-PlexFilter
{
    <#
        .SYNOPSIS
            Parses filter string into Plex API query.
        .DESCRIPTION
            Parses filter string into Plex API query. Filter syntax is '<Attribute> <Operator> <Value>'. Clauses are separated by semi-colons (;)
        .EXAMPLE
            Resolve-PlexFilter -MatchAll -Filter "Title BeginsWith Star Trek; Unplayed IsTrue"
        .EXAMPLE
            Resolve-PlexFilter -MatchAny -Filter "DateAdded IsNotInTheLast 2y; Unplayed IsTrue"
        .EXAMPLE
            Resolve-PlexFilter -MatchAll -Filter "Actor IsNot Robert Pattinson; Title BeginsWith bat" -LibraryId 1
    #>


    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string]
        $Filter,

        [Parameter()]
        [ValidateSet("MatchAll", "MatchAny")]
        [string]
        $MatchType = "MatchAll",

        [Parameter()]
        [string]
        $LibraryId,

        [Parameter(DontShow)]
        [switch]
        $IKnowWhatImDoing
    )

    end
    {
        # Copied from Plex web GUI.
        $Operators = @{
            # Class = @{
            # Operator1 = "..."
            # Operator2 = "..."
            # }
            String   = @{
                Contains       = "="
                DoesNotContain = "!="
                Is             = "=="
                IsNot          = "!=="
                BeginsWith     = "<="
                EndsWith       = ">="
            }
            Numeric  = @{
                Is            = "="
                IsNot         = "!="
                IsGreaterThan = ">>="
                IsLessThan    = "<<="
            }
            Exact    = @{
                Is    = "="
                IsNot = "!="
            }
            Bool     = @{
                IsTrue  = "=1"
                IsFalse = "!=1"
            }
            SemiBool = @{
                Is = "="
            }
            Date     = @{
                IsBefore       = "<<="
                IsAfter        = ">>="
                IsInTheLast    = ">>=-"
                IsNotInTheLast = "<<=-"
            }
        }

        # Copied from Plex web GUI and translated into Plex DB attributes.
        $Attributes = @{
            # NameinGUI = @{ Class = "ClassName" ; Name = "NameInDB" }
            Title            = @{ Class = "String" ; Name = "title" }
            Studio           = @{ Class = "String" ; Name = "studio" }
            Edition          = @{ Class = "String" ; Name = "editionTitle" }
            Rating           = @{ Class = "Numeric" ; Name = "userRating" }
            Year             = @{ Class = "Numeric" ; Name = "year" }
            Decade           = @{ Class = "Numeric" ; Name = "decade" }
            Plays            = @{ Class = "Numeric" ; Name = "viewCount" }
            ContentRating    = @{ Class = "Exact" ; Name = "contentRating" }
            Genre            = @{ Class = "Exact" ; Name = "genre" }
            Collection       = @{ Class = "Exact" ; Name = "collection" }
            Director         = @{ Class = "Exact" ; Name = "director" }
            Writer           = @{ Class = "Exact" ; Name = "writer" }
            Producer         = @{ Class = "Exact" ; Name = "producer" }
            Actor            = @{ Class = "Exact" ; Name = "actor" }
            Country          = @{ Class = "Exact" ; Name = "country" }
            SubtitleLanguage = @{ Class = "Exact" ; Name = "subtitleLanguage" }
            AudioLanguage    = @{ Class = "Exact" ; Name = "audioLanguage" }
            Label            = @{ Class = "Exact" ; Name = "label" }
            Unmatched        = @{ Class = "Bool" ; Name = "unmatched" }
            Duplicate        = @{ Class = "Bool" ; Name = "duplicate" }
            Unplayed         = @{ Class = "Bool" ; Name = "unwatched" }
            HDR              = @{ Class = "Bool" ; Name = "hdr" }
            InProgress       = @{ Class = "Bool" ; Name = "inProgress" }
            Trash            = @{ Class = "Bool" ; Name = "trash" }
            Resolution       = @{ Class = "SemiBool" ; Name = "resolution" }
            ReleaseDate      = @{ Class = "Date" ; Name = "originallyAvailableAt" }
            DateAdded        = @{ Class = "Date" ; Name = "addedAt" }
            LastPlayed       = @{ Class = "Date" ; Name = "lastViewedAt" }
        }

        try
        {
            $Query = foreach ($Clause in ($Filter -split ';'))
            {
                # Parse out values from clause
                if (-not ($Clause -match '(?<Attribute>\w+) (?<Operator>\w+)(?: (?<Value>.*))?'))
                {
                    throw "Unable to parse filter clause '$Clause'. Syntax is '<Attribute> <Operator> <Value>'."
                }

                # Translate clause into API query and verify that it makes sense.
                $Attribute = $Attributes[$Matches.Attribute]
                $Operator = $Operators[$Attribute.Class].($Matches.Operator)
                if (-not $Attribute)
                {
                    throw "Unable to parse filter clause '$Clause'. Attribute does not exist in Plex DB."
                }
                if (-not $Operator)
                {
                    throw "Unable to parse filter clause '$Clause'. Operator not supported by the attribute."
                }

                # Translate Exact class into Plex DB key
                if ($Attribute.Class -eq 'Exact')
                {
                    # Import PlexConfigData
                    if (!$script:PlexConfigData)
                    {
                        try
                        {
                            Import-PlexConfiguration -WhatIf:$False
                        }
                        catch
                        {
                            throw $_
                        }
                    }
                    $Uri = Get-PlexAPIUri -RestEndpoint "library/sections/$LibraryId/$($Attribute.Name)" -ErrorAction Stop
                    $Key = ((Invoke-RestMethod -Uri $Uri -Method Get -ErrorAction Stop).MediaContainer.Directory | Where-Object { $_.title -eq $Matches.Value }).key
                    #TODO implement caching for attribute keys
                    if (-not ($Key -or $IKnowWhatImDoing))
                    {
                        throw ("Unable to find key for '{0}' in library '{1}'." -f $Matches.Value, $LibraryId)
                    }

                    $Matches.Value = @($Matches.Value, $Key)[[bool]$Key] # Null check for key
                }

                # Return API query
                $Attribute.Name, $Operator, $Matches.Value -join ''
            }

            # Return entire API query
            switch ($MatchType)
            {
                "MatchAll" { $Query -join '&' ; continue }
                "MatchAny" { "push=1&{0}&pop=1" -f ($Query -join '&or=1&') ; continue }
                default { throw "You should not see this." }
            }
        }
        catch
        {
            if ($_.Exception.Message -match "Index operation failed")
            {
                throw "Unable to parse filter clause '$Clause'."
            }
            else { $PSCmdlet.ThrowTerminatingError($_) }
        }
    }
}