Public/Add-PatPlaylistItem.ps1

function Add-PatPlaylistItem {
    <#
    .SYNOPSIS
        Adds items to an existing playlist on a Plex server.
 
    .DESCRIPTION
        Adds one or more media items to an existing playlist. Items are specified by
        their rating keys (unique identifiers in the Plex library). Items are added
        to the end of the playlist.
 
    .PARAMETER PlaylistId
        The unique identifier of the playlist to add items to.
 
    .PARAMETER PlaylistName
        The name of the playlist to add items to. Supports tab completion.
 
    .PARAMETER RatingKey
        One or more media item rating keys to add to the playlist.
        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 playlist object.
 
    .EXAMPLE
        Add-PatPlaylistItem -PlaylistId 12345 -RatingKey 67890
 
        Adds the media item with rating key 67890 to playlist 12345.
 
    .EXAMPLE
        Add-PatPlaylistItem -PlaylistName 'My Favorites' -RatingKey 111, 222, 333
 
        Adds three items to the playlist named 'My Favorites'.
 
    .EXAMPLE
        Get-PatLibraryItem -SectionId 1 -Filter 'year=2024' |
            ForEach-Object { $_.ratingKey } |
            Add-PatPlaylistItem -PlaylistName 'New Releases'
 
        Adds all 2024 items from library section 1 to the 'New Releases' playlist.
 
    .EXAMPLE
        Add-PatPlaylistItem -PlaylistId 12345 -RatingKey 67890 -PassThru
 
        Adds an item and returns the updated playlist object.
 
    .OUTPUTS
        PlexAutomationToolkit.Playlist (when -PassThru is specified)
 
        Returns the updated playlist 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]
        $PlaylistId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByName')]
        [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']
            }

            $playlists = Get-PatPlaylist @getParams

            foreach ($playlist in $playlists) {
                if ($playlist.Title -ilike "$strippedWord*") {
                    $title = $playlist.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]
        $PlaylistName,

        [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 {
        # Use default server if ServerUri not specified
        $server = $null
        $effectiveUri = $ServerUri
        $machineIdentifier = $null

        if (-not $ServerUri) {
            try {
                $server = Get-PatStoredServer -Default -ErrorAction 'Stop'
                if (-not $server) {
                    throw "No default server configured. Use Add-PatServer with -Default or specify -ServerUri."
                }
                $effectiveUri = $server.uri
                Write-Verbose "Using default server: $effectiveUri"
            }
            catch {
                throw "Failed to get default server: $($_.Exception.Message)"
            }
        }
        else {
            Write-Verbose "Using specified server: $effectiveUri"
        }

        # Build headers with authentication
        $headers = if ($server) {
            Get-PatAuthHeaders -Server $server
        }
        else {
            @{ Accept = 'application/json' }
        }

        # 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)"
        }

        # Resolve playlist ID if using name
        $resolvedId = $PlaylistId
        $playlistInfo = $null

        if ($PSCmdlet.ParameterSetName -eq 'ByName') {
            $playlist = Get-PatPlaylist -PlaylistName $PlaylistName -ServerUri $effectiveUri -ErrorAction 'Stop'
            if (-not $playlist) {
                throw "No playlist found with name '$PlaylistName'"
            }
            $resolvedId = $playlist.PlaylistId
            $playlistInfo = $playlist
        }
        else {
            try {
                $playlistInfo = Get-PatPlaylist -PlaylistId $PlaylistId -ServerUri $effectiveUri -ErrorAction 'Stop'
            }
            catch {
                Write-Verbose "Could not retrieve playlist info for ID $PlaylistId"
            }
        }

        # Collect all rating keys from pipeline
        $allRatingKeys = [System.Collections.ArrayList]::new()
    }

    process {
        # Collect rating keys from pipeline
        foreach ($key in $RatingKey) {
            $null = $allRatingKeys.Add($key)
        }
    }

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

        # Build descriptive target for confirmation
        $playlistDesc = if ($playlistInfo) {
            "'$($playlistInfo.Title)'"
        }
        else {
            "Playlist $resolvedId"
        }
        $target = "$($allRatingKeys.Count) item(s) to $playlistDesc"

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

        try {
            # Build the URI with items
            # Format: server://machineIdentifier/com.plexapp.plugins.library/library/metadata/ratingKey
            $itemUris = foreach ($key in $allRatingKeys) {
                "server://$machineIdentifier/com.plexapp.plugins.library/library/metadata/$key"
            }
            $uriParam = $itemUris -join ','

            $queryString = "uri=$([System.Uri]::EscapeDataString($uriParam))"
            $endpoint = "/playlists/$resolvedId/items"
            $uri = Join-PatUri -BaseUri $effectiveUri -Endpoint $endpoint -QueryString $queryString

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

            $null = Invoke-PatApi -Uri $uri -Method 'PUT' -Headers $headers -ErrorAction 'Stop'

            if ($PassThru) {
                Get-PatPlaylist -PlaylistId $resolvedId -ServerUri $effectiveUri -ErrorAction 'Stop'
            }
        }
        catch {
            throw "Failed to add items to playlist: $($_.Exception.Message)"
        }
    }
}