Public/Sync-PatWatchStatus.ps1

function Sync-PatWatchStatus {
    <#
    .SYNOPSIS
        Syncs watch status from one Plex server to another.
 
    .DESCRIPTION
        Compares watch status between source and target Plex servers and marks items
        as watched on the target server that are watched on the source. Uses the Plex
        scrobble endpoint to mark items as watched.
 
        Supports bidirectional sync for scenarios like syncing watch status after
        watching media on a travel server.
 
    .PARAMETER SourceServerName
        The name of the source server (as stored with Add-PatServer).
 
    .PARAMETER TargetServerName
        The name of the target server (as stored with Add-PatServer).
 
    .PARAMETER Direction
        The direction of the sync:
        - SourceToTarget (default): Sync watched items from source to target
        - TargetToSource: Sync watched items from target to source
        - Bidirectional: Sync watched items in both directions
 
    .PARAMETER SectionId
        Optional array of library section IDs to sync. If not specified, syncs all sections.
 
    .PARAMETER PassThru
        Returns the sync results after completion.
 
    .EXAMPLE
        Sync-PatWatchStatus -SourceServerName 'Travel' -TargetServerName 'Home'
 
        Syncs all watched status from Travel server to Home server.
 
    .EXAMPLE
        Sync-PatWatchStatus -SourceServerName 'Travel' -TargetServerName 'Home' -Direction TargetToSource
 
        Syncs watched status from Home server back to Travel server.
 
    .EXAMPLE
        Sync-PatWatchStatus -SourceServerName 'Travel' -TargetServerName 'Home' -Direction Bidirectional
 
        Syncs watched status in both directions between Travel and Home servers.
 
    .EXAMPLE
        Sync-PatWatchStatus -SourceServerName 'Travel' -TargetServerName 'Home' -SectionId 1, 2
 
        Syncs watched status only for library sections 1 and 2.
 
    .EXAMPLE
        Sync-PatWatchStatus -SourceServerName 'Travel' -TargetServerName 'Home' -WhatIf
 
        Shows what would be synced without making changes.
 
    .OUTPUTS
        PlexAutomationToolkit.WatchStatusSyncResult (with -PassThru)
 
        Objects with properties:
        - Title: Item title
        - Type: 'movie' or 'episode'
        - ShowName: Series name (episodes only)
        - Season: Season number (episodes only)
        - Episode: Episode number (episodes only)
        - RatingKey: Target server rating key
        - Status: 'Success' or 'Failed'
        - Error: Error message if failed
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SourceServerName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $TargetServerName,

        [Parameter(Mandatory = $false)]
        [ValidateSet('SourceToTarget', 'TargetToSource', 'Bidirectional')]
        [string]
        $Direction = 'SourceToTarget',

        [Parameter(Mandatory = $false)]
        [int[]]
        $SectionId,

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

    begin {
        # Get both server configurations
        try {
            $sourceServer = Get-PatStoredServer -Name $SourceServerName -ErrorAction Stop
            if (-not $sourceServer) {
                throw "Source server '$SourceServerName' not found. Use Add-PatServer to configure it."
            }

            $targetServer = Get-PatStoredServer -Name $TargetServerName -ErrorAction Stop
            if (-not $targetServer) {
                throw "Target server '$TargetServerName' not found. Use Add-PatServer to configure it."
            }
        }
        catch {
            throw "Failed to get server configuration: $($_.Exception.Message)"
        }

        Write-Verbose "Syncing watch status between '$SourceServerName' and '$TargetServerName' (Direction: $Direction)"
    }

    process {
        try {
            $allResults = @()

            # Determine which directions to sync
            $syncOperations = switch ($Direction) {
                'SourceToTarget' {
                    @(@{
                        FromName   = $SourceServerName
                        ToName     = $TargetServerName
                        FromServer = $sourceServer
                        ToServer   = $targetServer
                    })
                }
                'TargetToSource' {
                    @(@{
                        FromName   = $TargetServerName
                        ToName     = $SourceServerName
                        FromServer = $targetServer
                        ToServer   = $sourceServer
                    })
                }
                'Bidirectional' {
                    @(
                        @{
                            FromName   = $SourceServerName
                            ToName     = $TargetServerName
                            FromServer = $sourceServer
                            ToServer   = $targetServer
                        },
                        @{
                            FromName   = $TargetServerName
                            ToName     = $SourceServerName
                            FromServer = $targetServer
                            ToServer   = $sourceServer
                        }
                    )
                }
            }

            foreach ($syncOp in $syncOperations) {
                Write-Verbose "Syncing from '$($syncOp.FromName)' to '$($syncOp.ToName)'"

                # Get differences (items watched on source but not target)
                $compareParams = @{
                    SourceServerName    = $syncOp.FromName
                    TargetServerName    = $syncOp.ToName
                    WatchedOnSourceOnly = $true
                    ErrorAction         = 'Stop'
                }

                if ($SectionId) {
                    $compareParams['SectionId'] = $SectionId
                }

                $differences = @(Compare-PatWatchStatus @compareParams)

                if ($differences.Count -eq 0) {
                    Write-Verbose "No differences found for $($syncOp.FromName) -> $($syncOp.ToName)"
                    Write-Information "Watch status already in sync: $($syncOp.FromName) -> $($syncOp.ToName)" -InformationAction Continue
                    continue
                }

                Write-Verbose "Found $($differences.Count) items to mark as watched on '$($syncOp.ToName)'"

                $successCount = 0
                $failCount = 0

                foreach ($item in $differences) {
                    $itemDisplay = if ($item.Type -eq 'episode') {
                        "$($item.ShowName) - S$($item.Season.ToString('D2'))E$($item.Episode.ToString('D2')) - $($item.Title)"
                    }
                    else {
                        "$($item.Title) ($($item.Year))"
                    }

                    $percentComplete = [int]((($successCount + $failCount) / $differences.Count) * 100)
                    Write-Progress -Activity "Syncing watch status to $($syncOp.ToName)" `
                        -Status "Processing $($successCount + $failCount + 1) of $($differences.Count)" `
                        -PercentComplete $percentComplete `
                        -CurrentOperation $itemDisplay `
                        -Id 1

                    if ($PSCmdlet.ShouldProcess($itemDisplay, "Mark as watched on $($syncOp.ToName)")) {
                        try {
                            # Use scrobble endpoint to mark as watched
                            $scrobbleEndpoint = "/:/scrobble?key=$($item.TargetRatingKey)&identifier=com.plexapp.plugins.library"
                            $scrobbleUri = Join-PatUri -BaseUri $syncOp.ToServer.uri -Endpoint $scrobbleEndpoint
                            $headers = Get-PatAuthenticationHeader -Server $syncOp.ToServer

                            Invoke-PatApi -Uri $scrobbleUri -Headers $headers -ErrorAction Stop | Out-Null

                            $successCount++
                            Write-Verbose "Marked as watched: $itemDisplay"

                            $allResults += [PSCustomObject]@{
                                PSTypeName  = 'PlexAutomationToolkit.WatchStatusSyncResult'
                                Title       = $item.Title
                                Type        = $item.Type
                                ShowName    = $item.ShowName
                                Season      = $item.Season
                                Episode     = $item.Episode
                                RatingKey   = $item.TargetRatingKey
                                SyncedTo    = $syncOp.ToName
                                Status      = 'Success'
                                Error       = $null
                            }
                        }
                        catch {
                            $failCount++
                            Write-Warning "Failed to mark as watched: $itemDisplay - $($_.Exception.Message)"

                            $allResults += [PSCustomObject]@{
                                PSTypeName  = 'PlexAutomationToolkit.WatchStatusSyncResult'
                                Title       = $item.Title
                                Type        = $item.Type
                                ShowName    = $item.ShowName
                                Season      = $item.Season
                                Episode     = $item.Episode
                                RatingKey   = $item.TargetRatingKey
                                SyncedTo    = $syncOp.ToName
                                Status      = 'Failed'
                                Error       = $_.Exception.Message
                            }
                        }
                    }
                }

                Write-Progress -Activity "Syncing watch status to $($syncOp.ToName)" -Completed -Id 1
                Write-Verbose "Sync to '$($syncOp.ToName)' completed: $successCount succeeded, $failCount failed"
                Write-Information "Synced $successCount items to '$($syncOp.ToName)'$(if ($failCount -gt 0) { " ($failCount failed)" })" -InformationAction Continue
            }

            if ($PassThru) {
                $allResults
            }
        }
        catch {
            throw "Failed to sync watch status: $($_.Exception.Message)"
        }
    }
}