Public/Timestamps/Add-SubtitleOffset.ps1

function Add-SubtitleOffset {
    <#
    .SYNOPSIS
        Shifts all subtitle timestamps by a given offset (positive or negative).
    .DESCRIPTION
        Adds the specified TimeSpan to both Start and End of every entry.
        Negative values shift subtitles earlier. By default, entries that would
        become negative are clamped to zero. Use -AllowNegative to disable this.
    .PARAMETER InputObject
        A SubtitleFile object.
    .PARAMETER Offset
        A TimeSpan representing the shift amount. Use negative values for earlier.
        Alternatively use -Milliseconds, -Seconds, or -Minutes for convenience.
    .PARAMETER Milliseconds
        Offset in milliseconds.
    .PARAMETER Seconds
        Offset in seconds.
    .PARAMETER Minutes
        Offset in minutes.
    .PARAMETER EntryRange
        Limit the offset to a range of entry indices, e.g. 10..50.
    .PARAMETER AllowNegative
        Do not clamp entries to zero when they would become negative.
    .EXAMPLE
        Import-SubtitleFile 'movie.srt' | Add-SubtitleOffset -Seconds 2.5
    .EXAMPLE
        Import-SubtitleFile 'movie.srt' | Add-SubtitleOffset -Milliseconds -500
    #>

    [CmdletBinding(DefaultParameterSetName = 'Offset')]
    [OutputType('SubtitleFile')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [SubtitleFile] $InputObject,

        [Parameter(Mandatory, ParameterSetName = 'Offset')]
        [TimeSpan] $Offset,

        [Parameter(Mandatory, ParameterSetName = 'Milliseconds')]
        [double] $Milliseconds,

        [Parameter(Mandatory, ParameterSetName = 'Seconds')]
        [double] $Seconds,

        [Parameter(Mandatory, ParameterSetName = 'Minutes')]
        [double] $Minutes,

        [int[]] $EntryRange,

        [switch] $AllowNegative
    )

    process {
        $shift = switch ($PSCmdlet.ParameterSetName) {
            'Offset'       { $Offset }
            'Milliseconds' { [TimeSpan]::FromMilliseconds($Milliseconds) }
            'Seconds'      { [TimeSpan]::FromSeconds($Seconds) }
            'Minutes'      { [TimeSpan]::FromMinutes($Minutes) }
        }

        $useRange = $EntryRange -and $EntryRange.Count -gt 0
        $rangeSet = if ($useRange) { [System.Collections.Generic.HashSet[int]]::new($EntryRange) } else { $null }

        foreach ($entry in $InputObject.Entries) {
            if ($useRange -and -not $rangeSet.Contains($entry.Index)) { continue }

            $newStart = $entry.Start + $shift
            $newEnd   = $entry.End   + $shift

            if (-not $AllowNegative) {
                if ($newStart -lt [TimeSpan]::Zero) { $newStart = [TimeSpan]::Zero }
                if ($newEnd   -lt [TimeSpan]::Zero) { $newEnd   = [TimeSpan]::Zero }
            }

            $entry.Start = $newStart
            $entry.End   = $newEnd
        }

        return $InputObject
    }
}