Public/Utilities/Invoke-SubtitleBatch.ps1

function Invoke-SubtitleBatch {
    <#
    .SYNOPSIS
        Applies a subtitle operation to multiple files in a directory.
    .DESCRIPTION
        Finds all subtitle files matching the specified format/pattern and runs
        a scriptblock against each one. Supports progress reporting, logging,
        and PS 7+ parallel execution.

        The scriptblock receives a single SubtitleFile object as $_ and should
        return a SubtitleFile (or $null to skip writing output).

    .PARAMETER Path
        Root directory to search. Default: current directory.
    .PARAMETER ScriptBlock
        Operation to apply to each SubtitleFile. Receives the file as $_.
        Return a SubtitleFile to write output, or $null to skip.
    .PARAMETER Format
        Subtitle format to process: SRT, ASS, SSA, or All. Default: All.
    .PARAMETER Pattern
        Filename wildcard pattern. Default: *.
    .PARAMETER Recurse
        Search subdirectories.
    .PARAMETER OutputSuffix
        Suffix to append before the extension for output files (e.g. '_fixed').
        If omitted, the original file is overwritten.
    .PARAMETER LogPath
        Path to a log file for operation results.
    .PARAMETER ThrottleLimit
        Maximum parallel threads (PowerShell 7+ only). Default: 4.
    .PARAMETER Parallel
        Use ForEach-Object -Parallel (requires PowerShell 7+).
    .EXAMPLE
        # Fix encoding on all SRT files recursively
        Invoke-SubtitleBatch -Path 'D:\Movies' -Recurse -Format SRT -ScriptBlock {
            $_ | Repair-SubtitleEncoding
        }
    .EXAMPLE
        # Shift all subtitles by 2 seconds, save to new file
        Invoke-SubtitleBatch -Path '.' -OutputSuffix '_shifted' -ScriptBlock {
            $_ | Add-SubtitleOffset -Seconds 2
        }
    #>

    [CmdletBinding()]
    param(
        [string] $Path = '.',

        [Parameter(Mandatory)]
        [scriptblock] $ScriptBlock,

        [ValidateSet('SRT', 'ASS', 'SSA', 'All')]
        [string] $Format = 'All',

        [string] $Pattern = '*',

        [switch] $Recurse,

        [string] $OutputSuffix,

        [string] $LogPath,

        [int] $ThrottleLimit = 4,

        [switch] $Parallel
    )

    $files = Find-SubtitleFile -Path $Path -Format $Format -Pattern $Pattern -Recurse:$Recurse

    if ($files.Count -eq 0) {
        Write-Warning "No subtitle files found in '$Path'."
        return
    }

    $total    = $files.Count
    $done     = 0
    $success  = 0
    $failed   = 0
    $results  = [System.Collections.Generic.List[PSCustomObject]]::new()

    if ($Parallel -and $PSVersionTable.PSVersion.Major -ge 7) {
        $fileList = $files.FullName
        $fileList | ForEach-Object -Parallel {
            $filePath = $_
            try {
                $sub    = Import-SubtitleFile -Path $filePath
                $output = $sub | & $using:ScriptBlock
                if ($output) {
                    $suffix   = $using:OutputSuffix
                    $destPath = if ($suffix) {
                        $base = [System.IO.Path]::GetFileNameWithoutExtension($filePath)
                        $ext  = [System.IO.Path]::GetExtension($filePath)
                        $dir  = [System.IO.Path]::GetDirectoryName($filePath)
                        Join-Path $dir ($base + $suffix + $ext)
                    } else { $filePath }

                    Export-SubtitleFile -InputObject $output -Path $destPath
                }
                [PSCustomObject]@{ File = $filePath; Status = 'OK'; Error = '' }
            } catch {
                [PSCustomObject]@{ File = $filePath; Status = 'FAILED'; Error = $_.Exception.Message }
            }
        } -ThrottleLimit $ThrottleLimit
        return
    }

    # Sequential with progress
    foreach ($file in $files) {
        $done++
        Write-Progress -Activity 'Processing subtitle files' `
            -Status "$done / $total : $($file.Name)" `
            -PercentComplete ([int](($done / $total) * 100))

        try {
            $sub    = Import-SubtitleFile -Path $file.FullName
            $output = & $ScriptBlock $sub

            if ($output) {
                $destPath = if ($OutputSuffix) {
                    $base = [System.IO.Path]::GetFileNameWithoutExtension($file.FullName)
                    $ext  = [System.IO.Path]::GetExtension($file.FullName)
                    Join-Path $file.DirectoryName ($base + $OutputSuffix + $ext)
                } else { $file.FullName }

                Export-SubtitleFile -InputObject $output -Path $destPath
            }

            $success++
            $record = [PSCustomObject]@{ File = $file.FullName; Status = 'OK'; Error = '' }
        } catch {
            $failed++
            Write-Warning "Failed: $($file.Name) — $($_.Exception.Message)"
            $record = [PSCustomObject]@{ File = $file.FullName; Status = 'FAILED'; Error = $_.Exception.Message }
        }

        $results.Add($record)

        if ($LogPath) {
            $line = '[{0}] [{1}] {2}{3}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $record.Status, $file.FullName,
                (if ($record.Error) { ' -- ' + $record.Error } else { '' })
            Add-Content -Path $LogPath -Value $line -Encoding UTF8
        }
    }

    Write-Progress -Activity 'Processing subtitle files' -Completed
    Write-Verbose "Batch complete: $success succeeded, $failed failed out of $total files."

    return $results.ToArray()
}