Public/Add-PatCollectionItem.ps1

function Add-PatCollectionItem {
    <#
    .SYNOPSIS
        Adds items to an existing collection on a Plex server.
 
    .DESCRIPTION
        Adds one or more media items to an existing collection. Items are specified by
        their rating keys (unique identifiers in the Plex library).
 
    .PARAMETER CollectionId
        The unique identifier of the collection to add items to.
 
    .PARAMETER CollectionName
        The name of the collection to add items to. Supports tab completion.
        Requires LibraryName or LibraryId to be specified.
 
    .PARAMETER LibraryName
        The name of the library containing the collection. Supports tab completion.
        Required when using -CollectionName. This is the preferred way to specify a library.
 
    .PARAMETER LibraryId
        The library section ID containing the collection. Required when using -CollectionName.
        Use Get-PatLibrary to find library IDs.
 
    .PARAMETER RatingKey
        One or more media item rating keys to add to the collection.
        Rating keys can be obtained from library browsing commands like Get-PatLibraryItem.
 
    .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 PassThru
        If specified, returns the updated collection object.
 
    .EXAMPLE
        Add-PatCollectionItem -CollectionId 12345 -RatingKey 67890
 
        Adds the media item with rating key 67890 to collection 12345.
 
    .EXAMPLE
        Add-PatCollectionItem -CollectionName 'Marvel Movies' -LibraryName 'Movies' -RatingKey 111, 222, 333
 
        Adds three items to the collection named 'Marvel Movies' in the Movies library.
 
    .EXAMPLE
        Get-PatLibraryItem -LibraryName 'Movies' -Title '*Avengers*' |
            ForEach-Object { $_.ratingKey } |
            Add-PatCollectionItem -CollectionName 'Marvel Movies' -LibraryName 'Movies'
 
        Adds all items matching 'Avengers' from the Movies library to the 'Marvel Movies' collection.
 
    .EXAMPLE
        Add-PatCollectionItem -CollectionId 12345 -RatingKey 67890 -PassThru
 
        Adds an item and returns the updated collection object.
 
    .OUTPUTS
        PlexAutomationToolkit.Collection (when -PassThru is specified)
 
        Returns the updated collection object showing the new item count.
    #>

    [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(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ById')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'ById')]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $CollectionId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByNameWithLibraryName')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ByNameWithLibraryId')]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

            $quoteChar = ''
            $strippedWord = $wordToComplete
            if ($wordToComplete -match "^([`"'])(.*)$") {
                $quoteChar = $Matches[1]
                $strippedWord = $Matches[2]
            }

            $getParams = @{ ErrorAction = 'SilentlyContinue' }
            if ($fakeBoundParameters.ContainsKey('ServerUri')) {
                $getParams['ServerUri'] = $fakeBoundParameters['ServerUri']
            }
            if ($fakeBoundParameters.ContainsKey('LibraryName')) {
                $getParams['LibraryName'] = $fakeBoundParameters['LibraryName']
            }
            elseif ($fakeBoundParameters.ContainsKey('LibraryId')) {
                $getParams['LibraryId'] = $fakeBoundParameters['LibraryId']
            }
            else {
                return
            }

            $collections = Get-PatCollection @getParams

            foreach ($collection in $collections) {
                if ($collection.Title -ilike "$strippedWord*") {
                    $title = $collection.Title
                    if ($quoteChar) {
                        $text = "$quoteChar$title$quoteChar"
                    }
                    elseif ($title -match '\s') {
                        $text = "'$title'"
                    }
                    else {
                        $text = $title
                    }

                    [System.Management.Automation.CompletionResult]::new(
                        $text,
                        $title,
                        'ParameterValue',
                        $title
                    )
                }
            }
        })]
        [string]
        $CollectionName,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByNameWithLibraryName')]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

            $quoteChar = ''
            $strippedWord = $wordToComplete
            if ($wordToComplete -match "^([`"'])(.*)$") {
                $quoteChar = $Matches[1]
                $strippedWord = $Matches[2]
            }

            $getParams = @{ ErrorAction = 'SilentlyContinue' }
            if ($fakeBoundParameters.ContainsKey('ServerUri')) {
                $getParams['ServerUri'] = $fakeBoundParameters['ServerUri']
            }

            $libraries = Get-PatLibrary @getParams

            foreach ($lib in $libraries.Directory) {
                if ($lib.title -ilike "$strippedWord*") {
                    $title = $lib.title
                    if ($quoteChar) {
                        $text = "$quoteChar$title$quoteChar"
                    }
                    elseif ($title -match '\s') {
                        $text = "'$title'"
                    }
                    else {
                        $text = $title
                    }

                    [System.Management.Automation.CompletionResult]::new(
                        $text,
                        $title,
                        'ParameterValue',
                        $title
                    )
                }
            }
        })]
        [string]
        $LibraryName,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByNameWithLibraryId')]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $LibraryId,

        [Parameter(Mandatory = $true, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateRange(1, [int]::MaxValue)]
        [int[]]
        $RatingKey,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-PatServerUri -Uri $_ })]
        [string]
        $ServerUri,

        [Parameter(Mandatory = $false)]
        [switch]
        $PassThru
    )

    begin {
        try {
            $script:serverContext = Resolve-PatServerContext -ServerUri $ServerUri
        }
        catch {
            throw "Failed to resolve server: $($_.Exception.Message)"
        }

        $effectiveUri = $script:serverContext.Uri
        $headers = $script:serverContext.Headers

        # Get machine identifier for URI construction
        try {
            $serverInfoUri = Join-PatUri -BaseUri $effectiveUri -Endpoint '/'
            $serverInfo = Invoke-PatApi -Uri $serverInfoUri -Headers $headers -ErrorAction 'Stop'
            $machineIdentifier = $serverInfo.machineIdentifier
            Write-Verbose "Server machine identifier: $machineIdentifier"
        }
        catch {
            throw "Failed to retrieve server machine identifier: $($_.Exception.Message)"
        }

        $resolvedId = $CollectionId
        $collectionInfo = $null

        if ($PSCmdlet.ParameterSetName -like 'ByName*') {
            # Only pass ServerUri if explicitly specified, otherwise let Get-PatCollection use default server with auth
            $getParams = @{
                CollectionName = $CollectionName
                ErrorAction    = 'Stop'
            }
            if ($script:serverContext.WasExplicitUri) { $getParams['ServerUri'] = $effectiveUri }
            if ($LibraryName) {
                $getParams['LibraryName'] = $LibraryName
            }
            else {
                $getParams['LibraryId'] = $LibraryId
            }

            $collection = Get-PatCollection @getParams
            if (-not $collection) {
                $libDesc = if ($LibraryName) { "library '$LibraryName'" } else { "library $LibraryId" }
                throw "No collection found with name '$CollectionName' in $libDesc"
            }
            $resolvedId = $collection.CollectionId
            $collectionInfo = $collection
        }
        else {
            try {
                # Only pass ServerUri if explicitly specified, otherwise let Get-PatCollection use default server with auth
                $getParams = @{ CollectionId = $CollectionId; ErrorAction = 'Stop' }
                if ($script:serverContext.WasExplicitUri) { $getParams['ServerUri'] = $effectiveUri }
                $collectionInfo = Get-PatCollection @getParams
            }
            catch {
                Write-Verbose "Could not retrieve collection info for ID $CollectionId"
            }
        }

        $allRatingKeys = [System.Collections.ArrayList]::new()
    }

    process {
        foreach ($key in $RatingKey) {
            $null = $allRatingKeys.Add($key)
        }
    }

    end {
        if ($allRatingKeys.Count -eq 0) {
            Write-Verbose "No rating keys provided, nothing to add"
            return
        }

        $collectionDesc = if ($collectionInfo) {
            "'$($collectionInfo.Title)'"
        }
        else {
            "Collection $resolvedId"
        }
        $target = "$($allRatingKeys.Count) item(s) to $collectionDesc"

        if (-not $PSCmdlet.ShouldProcess($target, 'Add to collection')) {
            return
        }

        try {
            # Add each item individually (collections require separate API calls per item)
            # Format: server://machineIdentifier/com.plexapp.plugins.library/library/metadata/ratingKey
            $endpoint = "/library/collections/$resolvedId/items"

            Write-Verbose "Adding $($allRatingKeys.Count) item(s) to collection $resolvedId"

            foreach ($key in $allRatingKeys) {
                $itemUri = "server://$machineIdentifier/com.plexapp.plugins.library/library/metadata/$key"
                $queryString = "uri=$([System.Uri]::EscapeDataString($itemUri))"
                $uri = Join-PatUri -BaseUri $effectiveUri -Endpoint $endpoint -QueryString $queryString

                Write-Verbose "Adding item $key to collection $resolvedId"
                $null = Invoke-PatApi -Uri $uri -Method 'PUT' -Headers $headers -ErrorAction 'Stop'
            }

            if ($PassThru) {
                # Only pass ServerUri if explicitly specified
                $getParams = @{ CollectionId = $resolvedId; ErrorAction = 'Stop' }
                if ($script:serverContext.WasExplicitUri) { $getParams['ServerUri'] = $effectiveUri }
                Get-PatCollection @getParams
            }
        }
        catch {
            throw "Failed to add items to collection: $($_.Exception.Message)"
        }
    }
}