awsModule.psm1

function Export-Log {
    <#
 
    .SYNOPSIS
    Exports a message to a log file
 
    .DESCRIPTION
    Exports a message to a log file
 
    .PARAMETER Message
    The message to export
 
    .PARAMETER LogsPath
    The path to the logs folder
 
    .PARAMETER LogFile
    The name of the log file
 
    .EXAMPLE
    Export-Log "Ping failed, now attempting to minimize shells + run reboot script" -Logfile $LogFileName
 
    .EXAMPLE
    Export-Log "Ping failed, now attempting reboot" -Logfile $LogFileName -LogsPath "C:\Stuff\Logs"
 
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)][String]$Message,
        [Parameter(Mandatory = $false)][String]$LogFile = 'Unknown.log'

    )

    if ($LogFile -notlike '*.log') {
        $LogFile = $LogFile + '.log'
    }

    $LogFilePath = "$env:logs\$LogFile"
    if (!(Get-Item $LogFilePath -ErrorAction SilentlyContinue)) {
        try {
            New-Item -ItemType File -Path $LogFilePath | Out-Null
        } catch {
            New-Item -ItemType Directory -Path $env:logs
            New-Item -ItemType File -Path $LogFilePath
            Exit
        }
    }

    $Output = (Get-Date -Format "[dd/MM HH:mm:ss] [PID: $PID] ") + '[User: ' + [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + '] ' + $Message
    $Output | Out-File $LogFilePath -Append -Force
    Return $Error
}

function Restart-LLM {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)][Alias ('w')][Switch]$Window
    )
    Begin {
        New-awsEvent 100 "LLM: Restart-agent running. PID: $($PID)"
        if ($Window) {
            Start-Process pwsh -ArgumentList '-noexit -command & {restart-llm}'
            Exit
        } else {
            $OutputPath = 'S:\llama.cpp\output'
            $TimeStamp = Get-Date -Format '[dd/MM HH:mm:ss]'
            Write-Color "`n$TimeStamp ", "LLM loop-check module running...`n" -C Yellow, White
        }
    }
    Process {
        while ($true) {
            $j = 0
            [Int]$LineNo = ((Get-Content (Get-ChildItem $outputpath -Filter *.txt | Sort-Object lastwritetime)[-1]).IndexOf('### Response:') + 2)
            $CurrentFile = (Get-ChildItem $outputpath -Filter *.txt | Sort-Object lastwritetime)[-1]
            $Content = ((Get-Content $CurrentFile) | Select-Object -Skip $LineNo)
            $Content = $Content -replace 'Oliver|Amanda|Anton|Nikolaj|Valentin|Johannes|Frederik|Peter|Manon|Isabella|Sophie|Romy|Anna', '<NAME>'
            $Last3 = ($Content | Where-Object { $_ -notlike '' } | Select-Object -Last 3)
            $Hits = @(0, 0, 0)
            $i = 0
            foreach ($line in $Last3) {
                $Hits[$i] = ($Content | Select-String $line -SimpleMatch).Count
                $i++
            }
            if (($Hits[0] -gt 2 -and $Hits[1] -gt 2 -and $Hits[2] -gt 2) -or (($CurrentFile).Length) -gt 40000) {
                Write-Output "[$(Get-Date -Format 'HH:mm:ss')] Model is stuck, restarting"
                Get-Process | Where-Object { $_.ProcessName -like 'main' -or $_.ProcessName -like 'falcon_main' } | Select-Object -First 1 | Stop-Process
            }
            [GC]::Collect()
            while ((Get-Process | Where-Object { $_.ProcessName -like 'main' -or $_.ProcessName -like 'falcon_main' }).Count -eq 0) {
                $j++
                Start-Sleep -Seconds 30
                if ($j -gt 5) {
                    Exit
                }
            }
            Start-Sleep -Seconds 60
        }
    }
    End {
        Return
    }
}

function Set-LLM {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)][Alias ('m')][String]$Model
    )
    Begin {
        $ModelPath = 'S:\llama.cpp\models'
        $Models = Get-ChildItem $ModelPath -Filter *.bin -Recurse | Sort-Object Length
    }
    Process {
        if (!($Model)) {
            $Model = Menu $Models.name
        } else {
            $Model = ($Models | Where-Object { $_.Name -like "*$Model*" }).Name
        }
        New-awsEvent 400 "LLM: Change-model: $Model"
    }
    End {
        Return
    }
}

function Stop-LLM {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)][Alias ('i')][Switch]$Immediate
    )
    Begin {

    }
    Process {
        if (!($Immediate)) {
            New-awsEvent 402 "LLM: Setting 'stop'-flag."
        } else {
            Get-Process | Where-Object { $_.ProcessName -like 'main' -or $_.ProcessName -like 'falcon_main' } | Stop-Process
        }

    }

    End {

    }
}

function Out-LLM {
    [CmdletBinding()]
    param (

    )
    Begin {
        $ErrorActionPreference = 'SilentlyContinue'
        $OutputPath = 'S:\llama.cpp\output'
        $Models = @()
        $i = (Get-ChildItem "$OutputPath\Trim").Count
        $RunningLLMs = (Get-Process | Where-Object { $_.ProcessName -like 'main' -or $_.ProcessName -like 'falcon_main' }).Count
        $Texts = (Get-ChildItem $OutputPath -Filter *.txt | Sort-Object LastWriteTime -Descending | Select-Object -Skip $RunningLLMs)
        $TBD = $Texts | Where-Object { $_.Length -le 6000 }
        $Texts = $Texts | Where-Object { $_.Length -gt 6000 } | Sort-Object LastWriteTime
        $TBD | Remove-ItemSafely
        $OutputArray = @()
        $TitleArray = @()
    }
    Process {


        $i = 0

        foreach ($Text in $Texts) {
            $Content = Get-Content $Text.FullName
            $Content = $Content -join "`n"
            $Content = $Content -split '\[0m'
            $Content = $Content[-1].TrimStart()
            $Content = ($Content.Replace("`n`n`n`n", "`n"))
            $Content = ($Content.Replace("`n`n`n", "`n"))
            $Content = ($Content.Replace("`n`n", "`n"))
            $Content = ($Content.Replace("`n", "`n`n"))
            $Content = ($Content.Replace('ΓÇô', '-'))
            $Content = ($Content.Replace('ΓÇÖ', "'"))
            $Content = ($Content.Replace('Γǥ', "'"))
            $Model = "$(($Text.name -split '_' | Select-Object -SkipLast 1) -join '-')"

            $FileName = "$($($Text.Name).Replace('_','-').Replace("$Model","$($Model)_$(($i).ToString().PadLeft(3, '0'))"))"
            $Content | Out-File "$OutputPath\Trim\$FileName"
            $OutputArray += "$FileName"
            $Timestamp = (Get-Date -Format 'MM-dd_HH-mm-ss')
            Move-Item $($Text.FullName) "$OutputPath\archive\$($Timestamp)_$($Text.Name)"
            $i++
        }
        foreach ($Output in $OutputArray) {
            $Title = (($Output -split '_')[-1] -split '-', 2)[1].Split('(')[0]
            $TitleArray += $Title
        }

        $TitleArray = $TitleArray | Sort-Object -Unique
        $TitleArray = $TitleArray | Where-Object { $_ -notlike '' }

        $Trim = (Get-ChildItem "$OutputPath\Trim" -Filter *.txt* | Sort-Object LastWriteTime)


        foreach ($TT in $Trim) {
            $Model = "$(($TT.name -split '_')[0])"
            if ($Models.Name -notcontains $Model) {
                $ModelObject = [PSCustomObject]@{
                    Name   = $Model
                    Number = 1
                }
                $Models += $ModelObject
            }

            $TitleNo = (($Models | Where-Object { $_.Name -like $Model }).Number | Out-String).Trim().PadLeft(3, '0')
            $Title = ((($TT.name).Split('(')[0]).Split('_')[-1]).Split('-', 2)[1].Replace('.txt', '')
            $NewName = "$Model" + '_' + "$TitleNo" + '_' + "$Title" + '.txt'

            if ($Title -notin $TitleArray) {
                $MovePath = "$OutputPath\trim\Unread"
            } else {
                $MovePath = "$OutputPath\trim"
            }
            while (Test-Path "$MovePath\$NewName") {
                $Models | Where-Object { $_.Name -like $Model } | ForEach-Object { $_.Number++ }
                $TitleNo = (($Models | Where-Object { $_.Name -like $Model }).Number | Out-String).Trim().PadLeft(3, '0')
                $NewName = "$Model" + '_' + "$TitleNo" + '_' + "$Title" + '.txt'
            }
            if ($TT.Name -in $OutputArray) {
                $OutputArray[$($OutputArray.IndexOf($TT.Name))] = $NewName
            }
            Move-Item $TT.FullName -Destination "$MovePath\$NewName"
        }
    }

    End {
        Write-Color "`n$($Texts.count) files formatted and moved to $OutputPath\Trim:`n" -C Yellow
        Return $OutputArray
    }
}

function Update-LLM {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)][Alias ('fa', 'fal')][Switch]$Falcon,
        [Parameter(Mandatory = $false)][Alias ('ll', 'lla')][Switch]$Llama,
        [Parameter(Mandatory = $false)][Alias ('f')][Switch]$Force
    )
    Begin {
        if ($Falcon) {
            $UpdateObj = [PSCustomObject]@{
                Falcon = $true
                Llama  = $false
            }
        } elseif ($Llama) {
            $UpdateObj = [PSCustomObject]@{
                Falcon = $false
                Llama  = $true
            }
        } else {
            $UpdateObj = [PSCustomObject]@{
                Falcon = $true
                Llama  = $true
            }
        }
    }
    Process {
        while ($UpdateObj.Llama -or $UpdateObj.Falcon) {

            if ($UpdateObj.Falcon) {
                $Foldername = 'ggllm.cpp'
                $GitClone = 'https://github.com/cmp-nct/ggllm.cpp'
                $cmake = { cmake -DLLAMA_CUBLAS=1 -DCUDAToolkit_ROOT="C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA" .. }
                $UpdateObj.Falcon = $false
            } elseif ($UpdateObj.Llama) {
                $Foldername = 'llama.cpp'
                $GitClone = 'https://github.com/ggerganov/llama.cpp'
                $cmake = { cmake .. -DLLAMA_CUBLAS=ON }
                $UpdateObj.Llama = $false
            }

            if ($Force) {
                if (Test-Path "$($Foldername)_old") {
                    Rename-Item "$($Foldername)_old" -NewName "$($Foldername)_old_old"
                }
                Rename-Item "P:\llama\$($Foldername)_backup" -NewName "$($Foldername)_old"
                Copy-Item "P:\llama\$Foldername" "P:\llama\$($Foldername)_backup"

                while (Test-Path "P:\llama\$Foldername") {
                    try {
                        Remove-Item "P:\llama\$Foldername" -Recurse -Force -ErrorAction Stop
                    } catch {
                        Write-Color "`n $($Foldername) is currently in use.", "`n`n Please close all instances of it and press Enter to try again.`n" -C Red, Yellow
                        Read-Host
                    }
                }

                Set-Location 'P:\llama'
                git clone $GitClone
                Set-Location "P:\llama\$Foldername"
            } else {
                Set-Location "P:\llama\$Foldername"
                git fetch
                $GitStatus = git status -uno
            }

            if (($GitStatus -like '*Your branch is behind*') -or ($Force)) {

                if (!($Force)) {
                    if (Test-Path "$($Foldername)_old") {
                        Rename-Item "$($Foldername)_old" -NewName "$($Foldername)_old_old"
                    }
                    Rename-Item "P:\llama\$($Foldername)_backup" -NewName "$($Foldername)_old"
                    Copy-Item "P:\llama\$Foldername" "P:\llama\$($Foldername)_backup"
                    git pull
                    Remove-Item 'Build' -Recurse -Force
                }

                New-Item -ItemType Directory -Path "P:\llama\$($Foldername)\build"
                Set-Location "P:\llama\$($Foldername)\build"
                Invoke-Command -ScriptBlock $cmake
                Invoke-Command -ScriptBlock { cmake --build . --config Release }

                $OldFolders = Get-ChildItem 'P:\llama' -Directory -Filter "$($Foldername)_old*" | Sort-Object CreationTime

                foreach ($OldFolder in $OldFolders) {
                    if ($OldFolder.CreationTime -lt (Get-Date).AddDays(-7)) {
                        $OldFolder | Remove-ItemSafely
                    } else {
                        $i = 0
                        while (Test-Path "P:\llama\$($Foldername)_old$i") {
                            $i++
                        }
                        Rename-Item $OldFolder -NewName "$($Foldername)_old$i"
                    }
                }
            } else {
                Write-Color "`nNo updates available for $Foldername`n" -C Yellow
            }
        }
        Set-Location 'P:\llama'
    }
    End {

    }
}

function Use-LLM {
    [CmdletBinding('Use-LLM')]
    param (
        [Parameter(Mandatory = $false, Position = 0)][Alias ('p')][String]$Prompt,
        [Parameter(Mandatory = $false, Position = 1)][Alias ('m')][String]$ModelName,
        [Parameter(Mandatory = $false)][Alias ('r')][Switch]$Repeat,
        [Parameter(Mandatory = $false)][Alias ('th')][Int]$Threads = 4,
        [Parameter(Mandatory = $false)][Alias ('b')][Int]$Batchsize = 512,
        [Parameter(Mandatory = $false)][Alias ('c', 'ctx')][Int]$Context = 2048,
        [Parameter(Mandatory = $false)][Alias ('t', 'temp')]$Temperature = 0.7,
        [Parameter(Mandatory = $false)][Alias ('n', 'ngl', 'l')][Int]$Layers
    )
    Begin {
        $AllModels = @()
        $Models = @()
        $LastUsed = Get-Content 'P:\llama\lastused.txt' -ErrorAction SilentlyContinue

        foreach ($ModelFile in (Get-ChildItem 'P:\llama\models' -Filter *.gguf -Recurse)) {
            $Obj = [PSCustomObject]@{
                WeightsB = [Double]($(((($ModelFile.Name) -Split '-') | Where-Object { $_ -match '[0-9]b' }) -Split 'b')[0])
                Weights  = ($((($ModelFile.Name) -Split '-') | Where-Object { $_ -match '[0-9]B' }) -split '(?<=b)')[0]
                Name     = ($ModelFile.Name -split $($((($ModelFile.Name) -Split '-') | Where-Object { $_ -match '[0-9]b' })))[0].TrimEnd('-')
                NameLong = "$(($ModelFile.Name -split $($((($ModelFile.Name) -Split '-') | Where-Object { $_ -match '[0-9]b' })))[0].TrimEnd('-')) [$($((($ModelFile.Name) -Split '-') | Where-Object { $_ -match '[0-9]b' }))]"
                Quant    = $((($ModelFile.Name).Split('.')[-2]))
                Size     = $('{0:N2} GB' -f (($ModelFile | Measure-Object -Property length -Sum).sum / 1GB))
                FullName = $($ModelFile.FullName)
                FileName = $($ModelFile.Name)
                PrevRun  = $false
            }
            $AllModels += $Obj
        }
        $AllModels | Where-Object { $_.FileName -eq $LastUsed } | ForEach-Object { $_.PrevRun = $true }
        $Models = ($AllModels | Sort-Object -Property @{e = { $_.PrevRun }; Ascending = $false }, Name, WeightsB, Quant, Size)

        if (!($ModelName)) {
            $Model = ($Models | Select-Object Name, Weights, Quant, Size ) | Out-ConsoleGridView -Title 'Select model to use' -OutputMode Single
            $Model = $Models | Where-Object { $_.Name -eq "$($Model.Name)" -and $_.Quant -eq "$($Model.Quant)" -and $_.Size -eq "$($Model.Size)" -and $_.Weights -eq "$($Model.Weights)" }
        }

        $Model.FileName | Out-File 'P:\llama\lastused.txt' -Force

        if ($Model.WeightsB -lt 10) {
            $DefaultNgl = 50
        } elseif ($Model.WeightsB -lt 41) {
            $DefaultNgl = 18
        } else {
            $DefaultNgl = 8
        }

        $NGLArray = Import-Csv 'P:\llama\models.csv'
        $IndexNo = ($NGLArray.model).IndexOf($Model.filename)
        if ($IndexNo -eq -1) {
            $modelngl = [PSCustomObject]@{
                Model = $Model.FileName
                NGL   = $DefaultNgl
            }
            $NGLArray += $modelngl
            $IndexNo = ($NGLArray.model).IndexOf($Model.filename)
        }
        [Int]$ngl = $NGLArray[$IndexNo].NGL

        if ($Layers) {
            $ngl = $Layers
        }

        $NGLArray[$IndexNo].NGL = $ngl

        $NGLArray | Export-Csv 'P:\llama\models.csv' -Force
    }

    Process {
        Do {
            if (!$Prompt) {
                Try {
                    $Prompt = Get-Content 'P:\llama\prompt.txt' -ErrorAction Stop
                } Catch {
                    Return "`nNo prompt file found"
                }
            }
            $PromptFormat = (Import-Csv 'P:\llama\promptformats.csv' | Where-Object { $_.Model -match $Model.Name }).Prompt

            if ($PromptFormat -and $Prompt -notmatch $PromptFormat) {
                $Prompt = "$($PromptFormat.Replace('<REPLACE>', $Prompt))"
            }

            [String]$TimeStamp = Get-Date -Format '[dd/MM HH:mm:ss]'
            Write-Color "`n$TimeStamp ", "Starting inference with the following parameters:`n" -C Yellow, White
            Write-Color 'Model: ', "$($Model.NameLong)", "`nThreads: ", "$Threads", "`nBatchsize: ", "$Batchsize", "`nContext: ", "$Context", "`nTemperature: ", "$Temperature", "`nNGL: ", "$ngl" -C White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
            Write-Output `n
            [String]$FileTimeStamp = Get-Date -Format 'yyyy-MM-dd_HH-mm-ss'

            P:\llama\llama.cpp\main.exe -t $Threads -p "$Prompt" --color -c $Context -b $Batchsize --temp $Temperature -ngl $ngl --repeat-last-n 128 --keep -1 --mlock --no-penalize-nl -m "$($Model.Fullname)" | Tee-Object -File "P:\llama\Output\$($Model.Name)_($FileTimeStamp).txt"
        } while ($Repeat)
    }
    End {
        Return
    }
}

function Test-LLM {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)][Alias ('a')][Switch]$Average,
        [Parameter(Mandatory = $false)][Alias ('r')][Switch]$Recent
    )
    Begin {
        $backups = Get-ChildItem -Path S:\llama.cpp\ -Filter 'backup_out*.txt'

        foreach ($backup in $backups) {
            if ($backup.CreationTime -lt (Get-Date).AddDays(-7)) {
                $backup | Remove-ItemSafely
            }
        }
        $files = Get-ChildItem -Path S:\llama.cpp\ -Filter out*.txt
        Copy-Item 'S:\llama.cpp\Results.csv' 'S:\llama.cpp\Results_backup.csv' -Force
        Copy-Item 'S:\llama.cpp\Averages.csv' 'S:\llama.cpp\Averages_backup.csv' -Force
        $i = 0
        $Results = @()
    }
    Process {
        if (!($Recent)) {
            foreach ($file in $files) {

                $ModelArray = @()
                $asd = (Get-Content $File) -join "`n"
                $asd = ($asd -split 'Starting inference of the following model:') | Where-Object { $_ -like '*_print_timings:*' }

                foreach ($as in $asd) {
                    $split = ($as -split "`n" | Where-Object { $_ -like '*_print_timings:*' } )
                    $StartTime = ((($as -split "`n" | Where-Object { $_ -like '*.bin' })[0]) -split '] ')[0].TrimStart('[')
                    $StartTime = [DateTime]::ParseExact($StartTime, 'dd/MM HH:mm:ss', ([System.Globalization.CultureInfo]::InvariantCulture))
                    $model = ((($as -split "`n" | Where-Object { $_ -like '*.bin' })[0]) -split '] ')[1]
                    if (($split) -and ($model -like '*.bin')) {
                        if ($model -like '*\*') {
                            $model = ($model -split '\\')[-1]
                        }
                        $Weights = ($Model -split '([0-9][0-9]B)')[1]
                        if (!($Weights)) {
                            $Weights = ('0' + ($Model -split '([0-9]B)')[1])
                        }

                        [Float]$Time = (($split[-2] -split '\(')[-1] -split ' ms')[0].Trim()

                        $Obj = [PSCustomObject]@{
                            Model = "$($Weights)_$((($Model).Trim('.bin')).Trim())"
                            Time  = $Time
                            Date  = $StartTime
                        }

                        $ModelArray += $Obj
                    }
                }

                $Models = $modelarray.model | Sort-Object | Get-Unique

                foreach ($Model in $Models) {
                    $Results += $ModelArray | Where-Object { $_.Model -like $Model }
                }

                while (Test-Path "S:\llama.cpp\backup_out$($i).txt") {
                    $i++
                }
                Copy-Item $File.FullName "S:\llama.cpp\backup_out$($i).txt" -Force
                Remove-ItemSafely -Path $File.FullName
            }
        }
    }
    End {
        if (!($Recent)) {
            $Results = $Results | Sort-Object Model, Time -Unique -Descending
            $Results | Export-Csv -Path S:\llama.cpp\Results.csv -NoTypeInformation -Append
        }

        $CSV = Import-Csv S:\llama.cpp\Results.csv | Sort-Object Model, Time -Unique -Descending

        if ($Recent) {
            $Avg = foreach ($Model in ($CSV.Model | Sort-Object -Unique)) {
                $CSV | Where-Object { $_.Model -like $Model } | Measure-Object -Property Time -Average | Select-Object @{Name = 'Model'; Expression = { $Model } }, @{Name = 'Average'; Expression = { $_.Average } }
            }
        } else {
            $Avg = foreach ($Model in ($CSV.Model | Sort-Object -Unique)) {
                $CSV | Where-Object { $_.Model -like $Model -and $_.Date -gt ((Get-Date).AddMinutes(-1)) } | Measure-Object -Property Time -Average | Select-Object @{Name = 'Model'; Expression = { $Model } }, @{Name = 'Average'; Expression = { $_.Average } }
            }
        }

        $Avg | ForEach-Object { $_.Average = [Math]::Round($_.Average, 2) }
        $Avg = $Avg | Sort-Object Average -Descending

        $CSV | Select-Object Model, Time, Date | Sort-Object Model, Time -Unique -Descending | Export-Csv -Path S:\llama.cpp\Results.csv -NoTypeInformation -Force
        $Avg | Sort-Object Average -Descending | Export-Csv -Path S:\llama.cpp\Averages.csv -NoTypeInformation -Force

        if ($Recent) {
            $i = 0
            $NameLengths = @()
            $RecentAverages = @()
            foreach ($Model in ($CSV.Model | Sort-Object -Unique -Descending)) {
                $NameLengths += $Model.Length
            }
            [Int]$Length = $NameLengths | Sort-Object -Descending | Select-Object -First 1

            foreach ($Model in ($CSV.Model | Sort-Object -Unique -Descending)) {
                $Spaces = (' ' * ($Length - ($Model.Length)))
                $Recent5 = $CSV | Where-Object { $_.Model -like $Model } | Sort-Object Date, Time -Descending | Select-Object -First 5
                $Recent5 | ForEach-Object { $_.Time = "$($_.Time) " ; $_.Model = "$($_.Model)$Spaces " ; $_.Date = "$((($_.Date) -split ' ')[0]) " }
                $5Avg = $Recent5 | Measure-Object -Property Time -Average | Select-Object @{Name = 'Model'; Expression = { $Model } }, @{Name = 'Average'; Expression = { $_.Average } }
                $5Avg | ForEach-Object { $_.Average = [Math]::Round($_.Average, 2) }
                $5Avg = $5Avg | Sort-Object Average -Descending
                $ModelAvg = $5Avg.Average
                $AvgObject = [PSCustomObject]@{
                    Model   = $Model
                    Average = $ModelAvg
                }
                $RecentAverages += $AvgObject
                if ($i -gt 0) {
                    Write-Output ($Recent5 | Select-Object Date, Model, Time | Format-Table -AutoSize -HideTableHeaders)
                } else {
                    Write-Output ($Recent5 | Select-Object Date, Model, Time | Format-Table -AutoSize)
                }
                Write-Color ' Average (last 5): ', "$($ModelAvg)", "`n`n" -C Gray, Yellow, Gray

                $i++
            }
            Write-Output ($RecentAverages | Sort-Object Model, Average | Format-Table -AutoSize)
            Return
        } elseif ($Average) {
            Return $Avg
        } else {
            Return $Results
        }
    }
}

function Get-Logs {
    <#
 
    .SYNOPSIS
    Gets the last X lines from a log file
 
    .DESCRIPTION
    Gets the last X lines from a log file
 
    .PARAMETER Log
    The log file to get the lines from
 
    .PARAMETER Amount
    The amount of lines to get
 
    .PARAMETER LogsPath
    The path to the logs folder
 
    .EXAMPLE
    Get-Logs -Log Ping -Amount 3
 
    .EXAMPLE
    Get-Logs -Log Plex -Amount 3
 
    .EXAMPLE
    Get-Logs -Log Plex -Amount 3 -LogsPath "C:\Stuff\Logs"
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]$Log,
        [Parameter(Mandatory = $false)][switch]$All = $false,
        [Parameter(Mandatory = $false)]$Amount = 3
    )

    $Log = $Log.ToLower()
    $LogPath = "$env:logs\$Log.log"

    if ($All) {
        $Lines = (Get-Content "$LogPath") | Where-Object { $_ -notlike '' }
    } else {
        $Lines = (Get-Content "$LogPath") | Where-Object { $_ -notlike '' } | Select-Object -Last $Amount
    }
    $Output = @()

    foreach ($Line in $Lines) {
        $Time = ((($Line -split '\]').TrimStart('\['))[0]).Replace('-', '/').Replace('.', ':')

        $Instance = [PSCustomObject]@{
            Context = (($Line -split '\[User: ')[-1] -split '\]')[0]
            PID     = (($Line -split '\[PID: ')[1] -split '\]')[0]
            Time    = [DateTime]::ParseExact($Time, 'dd/MM HH:mm:ss', ([System.Globalization.CultureInfo]::InvariantCulture))
            Running = $false
            Message = (($Line -split '\[User: ')[-1] -split '\]')[1].TrimStart(' ')
        }
        if ($Log -like 'deluged') {
            switch (($Line -split ('Deluged switch: '))[-1]) {
                'on' {
                    $Instance.Running = $true 
                }
                'off' {
                    $Instance.Running = $false 
                }
            }
        } elseif ((Get-Process -Id $Instance.PID -ErrorAction SilentlyContinue).Count -gt 0) {
            $Instance.Running = $true
        }
        $Output += $Instance
    }
    Return $Output
}

function New-awsEvent {
    <#
    .SYNOPSIS
    Creates a new event in the aws log
 
    .DESCRIPTION
    Creates a new event in the aws log
 
    .PARAMETER evtID
    The event ID. Can be shortened to "id".
 
    .PARAMETER message
    The message to log
 
    .PARAMETER var1
    The first variable to log
 
    .PARAMETER var2
    The second variable to log
 
    .PARAMETER source
    The source of the event (can be either "Script" (default), "Plex" or "Torrent"). Can be shortened to "s".
 
    .PARAMETER type
    The type of event to create (can be either "Information" (default), "Warning" or "Error"). Can be shortened to "t".
 
    .EXAMPLE
    New-awsEvent 100 "This is a test"
 
    .EXAMPLE
    New-awsEvent 102 "This is a test" "This fills out a variable" "This fills out the second variable" -s "Script" -t "Information"
 
    .NOTES
 
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, Position = 0)][Alias('id')][int]$evtID = 100,
        [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline)][Alias('m')][string]$Message,
        [Parameter(Mandatory = $false, ValueFromPipeline)][Alias('v1')][string]$var1,
        [Parameter(Mandatory = $false, ValueFromPipeline)][Alias('v2')][string]$var2,
        [Parameter(Mandatory = $false, Position = 2)][Alias('s')][string]$Source,
        [Parameter(Mandatory = $false, Position = 3)][Alias('t')][ValidateSet('Information', 'Info', 'i', 'Warning', 'w', 'Error', 'e')][string]$Type
    )
    Begin {

        if (!$Source) {
            switch ($evtID) {
                100 {
                    $source = 'Script' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                101 {
                    $source = 'Script' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                102 {
                    $source = 'Script' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1, 1) ; $EventType = 'Error' 
                }
                200 {
                    $source = 'Plex' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                201 {
                    $source = 'Plex' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                202 {
                    $source = 'Plex' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1, 1) ; $EventType = 'Error' 
                }
                300 {
                    $source = 'Torrent' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                400 {
                    $source = 'LLM' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                401 {
                    $source = 'LLM' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                402 {
                    $source = 'LLM' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                default {
                    $source = 'Script' ; $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
            }
        }
        if ($Type) {
            switch ($type.ToLower()) {
                'information' {
                    $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                'info' {
                    $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                'i' {
                    $id = New-Object System.Diagnostics.EventInstance($evtID, 1) ; $EventType = 'Information' 
                }
                'warning' {
                    $id = New-Object System.Diagnostics.EventInstance($evtID, 1, 2) ; $EventType = 'Warning' 
                }
                'w' {
                    $id = New-Object System.Diagnostics.EventInstance($evtID, 1, 2) ; $EventType = 'Warning' 
                }
                'error' {
                    $id = New-Object System.Diagnostics.EventInstance($evtID, 1, 1) ; $EventType = 'Error' 
                }
                'e' {
                    $id = New-Object System.Diagnostics.EventInstance($evtID, 1, 1) ; $EventType = 'Error' 
                }
            }
        }
    }
    Process {
        $evtObject = New-Object System.Diagnostics.EventLog
        $evtObject.Log = 'aws'
        $evtObject.Source = $source
        try {
            $evtObject.WriteEvent($id, @($message, $var1, $var2))
        } catch {
            throw $_.Exception
        } finally {
            $evtObject.Dispose()
        }
    }
    End {
        $Return = [PSCustomObject]@{
            Log     = $evtObject.Log
            Type    = $EventType
            Source  = $evtObject.Source
            EventID = $evtID
            Message = "$($message)$($var1)$($var2)"
        }
        Return ($Return | Format-Table -AutoSize)
    }
}

function Test-Ping {
    <#
 
    .SYNOPSIS
    Tests if the internet is reachable
 
    .DESCRIPTION
    Tests if the internet is reachable
 
    .EXAMPLE
    Test-Ping
 
    .PARAMETER Limit
    The amount of times to test the internet before returning false
 
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    Param(
        [Parameter(Mandatory = $false)]
        [int] $Limit = 3
    )
    $Repeat = $true
    $i = 0

    while ($Repeat) {
        $Ping = Test-Connection 8.8.8.8

        if (($Ping | Where-Object { $_.Status -eq 'Success' }).count -eq 0) {

            if ($i -ge $Limit) {
                Return $false
            } else {
                $i++
                Start-Sleep -Seconds 30
            }
        } else {
            $Repeat = $false
        }
    }
    Return $true
}

function Start-SystemPS {
    <#
 
    .SYNOPSIS
    Starts a new PowerShell session as SYSTEM
 
    .DESCRIPTION
    Starts a new PowerShell session as SYSTEM
 
    .EXAMPLE
    Start-SystemPS
 
    #>

    [CmdletBinding()]
    param ()

    C:\Stuff\PSTools\PsExec64.exe -s -i 1 pwsh.exe
}

function Install-CustomModule {
    <#
 
    .SYNOPSIS
    Installs a module from the PowerShell Gallery, or imports it if it is already installed.
 
    .DESCRIPTION
    Installs a module from the PowerShell Gallery, or imports it if it is already installed.
 
    .EXAMPLE
    Install-CustomModule -Module 'Pester'
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $Module
    )
    process {
        # Check if the module is already installed on the machine
        if ( -not ( Get-Module $Module -ListAvailable ) ) {
            # Install the module as user scripts
            Install-Module $Module -Scope CurrentUser -Force
            Import-Module $Module
        }
        # The module was already installed
        if ( ( Get-Module $Module -ListAvailable ) ) {
            # Check if the module is already imported
            if ( -not ( Get-Module $Module ) ) {
                Import-Module $Module
            }
        }
    }
}

function Restart-AsAdmin {
    <#
 
    .SYNOPSIS
    Restarts the script as an administrator
 
    .DESCRIPTION
    Restarts the script as an administrator
 
    .EXAMPLE
    Restart-AsAdmin
 
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
    )
    process {
        $Path = '"' + $PSCommandPath + '"'
        $Script:User = [Security.Principal.WindowsIdentity]::GetCurrent()
        $Script:UserObject = (New-Object Security.Principal.WindowsPrincipal $User)
        if (($UserObject.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) -eq $false) -or ($host.Name -notlike 'ConsoleHost')) {
            $ArgList = @(
                "-file $Path",
                '-NoExit'
            )
            Start-Process pwsh.exe -Verb runas -ArgumentList $ArgList
            Exit
        }
    }
}

function Send-Reannounce {
    [CmdletBinding()]
    [OutputType([String])]
    param (
        [String]$Port = '8087'
    )
    begin {
    }
    process {
        $URI = 'http://192.168.0.127:' + $Port + '/'
        $data = 'username=aws&password=asdf1234'

        try {
            Invoke-RestMethod -Uri ($URI + 'api/v2/auth/login') -Headers @{'Referer' = $URI } -Method POST -Body $data -SessionVariable QBTSession -TimeoutSec 60
        } catch {
            Return 'Failed to login to qBittorrent'
        }

        try {
            Invoke-RestMethod -Uri ($URI + 'api/v2/torrents/reannounce') -WebSession $QBTSession -Method POST -Body 'hashes=all&value=true' -TimeoutSec 60
        } catch {
            Return 'Failed to send reannounce'
        }

    }
    end {
    }
}

function Limit-LogSize {
    <#
 
    .SYNOPSIS
    Deletes the oldest 75% of a log if it exceeds a specified size.
    .DESCRIPTION
    Deletes the oldest 75% of a log if it exceeds a specified size.
    .PARAMETER Size
    The maximum acceptable size of the log file in KB.
    .EXAMPLE
    Limit-LogSize -Size 1024
    .OUTPUTS
    The output of the function is a string containing the name of the log file and the size before and after the function was run.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]$Size = 2048
    )

    process {
        $Logs = Get-ChildItem $env:logs
        $Output = ''

        foreach ($Log in $Logs) {
            $LogSize = [math]::Round(((Get-ChildItem $Log.fullname | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum / 1KB), 2)
            $LogSizeMB = [math]::Round(((Get-ChildItem $Log.fullname | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum / 1MB), 2)

            if ($LogSize -gt $Size) {
                $Content = $Log | Get-Content
                $LinesToRemove = ($Content.count * 0.75)
                $Content = $Content | Select-Object -Skip $LinesToRemove
                $Content | Out-File $Log.fullname -Force
                if ($LogSizeMB -gt 1) {
                    $Output += "`n Size of $($Log.name) = $LogSizeMB MB, oldest 75% of log deleted. `n New size: $([math]::Round(((Get-ChildItem $Log.fullname | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum / 1KB), 2)) KB`n"
                } else {
                    $Output += "`n Size of $($Log.name) = $LogSize KB, oldest 75% of log deleted. `n New size:$([math]::Round(((Get-ChildItem $Log.fullname | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum / 1KB), 2)) KB`n"
                }
            } else {
                $Output += "`n Size of $($Log.name) = $LogSize KB, no changes made.`n"
            }
        }
        Return $Output
    }
}

function Convert-Currency {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)][string]$targetCurrency,
        [Parameter(Mandatory = $true, Position = 1)][string]$amount,
        [Parameter(Mandatory = $true, Position = 2)][string]$currency
    )

    $resultAs = $targetCurrency.ToUpper()
    $currency = $currency.ToUpper()

    $headers = @{
        'apikey' = 'b4xTe0eLZKy8lSqkgjczdWboc4d2eIqo'
    }

    [string]$apiCall = "https://api.apilayer.com/exchangerates_data/convert?to=$resultAs&from=$currency&amount=$amount"

    $exhangeRates = Invoke-RestMethod -Uri $apiCall -Headers $headers -Method Get

    Return "$([math]::Round($exhangeRates.result, 2)) $resultAs"

}

function Use-ChatGPT {
    <#
 
    .SYNOPSIS
    Sends a message to the OpenAI API and returns the chatbot's response.
    .DESCRIPTION
    Sends a message to the OpenAI API and returns the chatbot's response.
    .PARAMETER Message
    The message to send to the chatbot. The message must be a string. The message can be piped to the function, or used as an unnamed parameter.
    .PARAMETER Model
    The model to use for the chatbot. The default value is 3, which uses the "gpt-3.5-turbo" model. The value 4 uses the "gpt-4" model, which is $0.06/1k tokens (30 times the price of the "gpt-3.5-turbo" model).
    .EXAMPLE
    Use-ChatGPT -Message "Hello, how are you today?"
    .OUTPUTS
    The output of the function is the chatbot's response to the message sent to the API.
 
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline )][String]$Message,
        [Parameter(Mandatory = $false, Position = 2)][int]$Tokens = 0,
        [Parameter(Mandatory = $false, Position = 3)][int]$Model = 3
    )

    Begin {
    }

    Process {
        # The function uses a switch statement to assign a value to the $model variable based on the value of $Model. If $Model is 3, then $model is assigned the value "gpt-3.5-turbo". If $Model is 4, then $model is assigned the value "gpt-4".
        switch ($Model) {
            3 {
                [string]$model = 'gpt-3.5-turbo' 
            }
            4 {
                [string]$model = 'gpt-4' 
            }
        }


        # Next, the function sets up an API request to the OpenAI API endpoint for chat completions. It creates a hash table named $messages with two key-value pairs: role="user" and content=$Message. It then creates another hash table named $json with five key-value pairs: model=$model, messages=@($messages), max_tokens=50, temperature=0.5, and n=1. This hash table will be used as the request body for the API call.

        $url = 'https://api.openai.com/v1/chat/completions'
        $messages = @{
            role    = 'user'
            content = $Message
        }
        $json = @{
            model       = $model
            messages    = @($messages)
            temperature = 1.7
            n           = 1
        }

        switch ($Tokens) {
            0 {
                Break 
            }
            default {
                $json.max_tokens = $Tokens 
            }
        }

        # The function then sets up the headers for the API request, including the Content-Type and Authorization headers.
        $headers = @{
            'Content-Type'        = 'application/json'
            'Authorization'       = 'Bearer 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'
            'OpenAI-Organization' = 'org-rFpwivMhpH8OMr4XcSj4mZmW'
        }

        # It sends the API request using Invoke-RestMethod cmdlet from PowerShell and gets the response.
        $response = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body ($json | ConvertTo-Json -Depth 10)
    }

    End {
        switch ($Model) {
            'gpt-3.5-turbo' {
                [double]$CostUSD = (($($response.usage.total_tokens) / 1000) * 0.002) 
            }
            'gpt-4' {
                [double]$CostUSD = (($($response.usage.prompt_tokens) / 1000) * 0.03) + (($($response.usage.completion_tokens) / 1000) * 0.06) 
            }
        }

        [Double]$CostDKK = [math]::Round(($CostUSD * 6.75), 4)

        # Finally, the function returns the generated text from the response by accessing the content property of the message property of the first choice of the response.choices array.
        Write-Output "`nTokens used: $($response.usage.total_tokens) (P: $($response.usage.prompt_tokens) | C: $($response.usage.completion_tokens))`nPrice: $CostDKK DKK`n"
        Write-Output ($response.choices.message.content)
        Return "`n"
    }
}

function Use-gpt4free {
    [Alias('ugf')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline )][String]$Message
    )

    Begin {

    }

    Process {
        $py = Get-Content 'C:\Users\aws\Repos\gpt4free\test.py'
        $newpy = $py.Replace('<CONTENT>', $Message)
        $newpy | Out-File 'C:\Users\aws\Repos\gpt4free\test2.py' -Force
        python 'C:\Users\aws\Repos\gpt4free\test2.py'
    }

    End {

    }
}

function Set-WindowStyle {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $false, Position = 1)]
        [ValidateSet('FORCEMINIMIZE', 'HIDE', 'MAXIMIZE', 'MINIMIZE', 'RESTORE',
            'SHOW', 'SHOWDEFAULT', 'SHOWMAXIMIZED', 'SHOWMINIMIZED',
            'SHOWMINNOACTIVE', 'SHOWNA', 'SHOWNOACTIVATE', 'SHOWNORMAL')]$Style,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 2)]$MainWindowHandle
    )
    $WindowStates = @{
        FORCEMINIMIZE = 11; HIDE = 0
        MAXIMIZE = 3; MINIMIZE = 6
        RESTORE = 9; SHOW = 5
        SHOWDEFAULT = 10; SHOWMAXIMIZED = 3
        SHOWMINIMIZED = 2; SHOWMINNOACTIVE = 7
        SHOWNA = 8; SHOWNOACTIVATE = 4
        SHOWNORMAL = 1
    }
    Write-Verbose ('Set Window Style {1} on handle {0}' -f $MainWindowHandle, $($WindowStates[$style]))

    $Win32ShowWindowAsync = Add-Type -MemberDefinition @'
[DllImport("user32.dll")]
public static extern bool ShowWindowAsync(int hWnd, int nCmdShow);
'@
 -Name 'Win32ShowWindowAsync' -Namespace Win32Functions -PassThru

    $Win32ShowWindowAsync::ShowWindowAsync($MainWindowHandle, $WindowStates[$Style]) | Out-Null
}

function Invoke-FFmpeg {
    <#
    .SYNOPSIS
    Uses FFmpeg to convert a video file to an MP4 file.
 
    .DESCRIPTION
    Uses FFmpeg to convert a video file to an MP4 file.
 
    .PARAMETER Infile
    The file to convert. This must be a string or FileInfo object.
 
    .PARAMETER Replace
    If this switch is used, the original file will be replaced with the converted file.
 
    .EXAMPLE
    Invoke-FFmpeg -Infile "C:\Users\Public\Videos\Sample Videos\Wildlife.wmv"
 
    .EXAMPLE
    Invoke-FFmpeg -Infile "C:\Users\Public\Videos\Sample Videos\Wildlife.wmv" -Replace
 
    .EXAMPLE
    Get-ChildItem -Path "C:\Users\Public\Videos\Sample Videos" -Filter "*.wmv" | Invoke-FFmpeg
 
    .OUTPUTS
    The output of the function is the file that was converted.
    #>


    [Alias('ffmp')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)][Alias('in', 'i')]$Infile,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 1)][Alias('out', 'o')]$Outfile,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 2)][Alias('format')][string]$Container = 'mkv',
        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 3)][ValidateRange(1, 7)][Alias('p')][int]$Preset = 1,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false)][Alias('ow')][switch]$Overwrite,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false)][Alias('rip')][switch]$AudioRip,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false)][Alias('audioreplace')][switch]$ReplaceAudio,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false)][switch]$Copy
    )
    Begin {
        if ($Format -notmatch '^\.') {
            $Format = ".$Format"
        }

        if ($ReplaceAudio) {
            if ($Infile.Count -lt 2) {
                Throw "`nTo replace audio in file, pass two input files (video first, then audio).`n"
            } else {
                $VidIn = Get-Item -Path $Infile[0]
                $AudIn = Get-Item -Path $Infile[1]

                if (!$Outfile) {
                    $Out = ((($AudIn.PSParentPath).Replace('Microsoft.PowerShell.Core\FileSystem::', '')) + '\' + "$($AudIn.BaseName)" + "$Format")
                
                    if ($Out -eq $AudIn.FullName) {
                        $Out = ((($AudIn.PSParentPath).Replace('Microsoft.PowerShell.Core\FileSystem::', '')) + '\' + "$($AudIn.BaseName)" + "_new$Format")
                    }
                } else {
                    $Out = $Outfile
                }

                $cmd = "ffmpeg -i ""$($VidIn.FullName)"" -i ""$($AudIn.FullName)"" -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 ""$Out"""
            }
        }
        $i = 0
        $PresetInt = $Preset + 11
    }
    Process {
        if (!$cmd) {
            Try {
                if (($Infile.GetType().Name) -like 'String') {
                    $Infile = Get-Item -Path $Infile
                }
                $In = $Infile.FullName
            } Catch {
                Throw "`nInput must be a string or FileInfo object.`n"
            }

            if (!$Outfile -and ($Format -eq $Infile.Extension)) {
                $Out = ((($Infile.PSParentPath).Replace('Microsoft.PowerShell.Core\FileSystem::', '')) + '\' + "$($Infile.BaseName)" + "$Format").Replace("$($InFile.BaseName)", "$($InFile.BaseName)_2")
            } elseif (!$Outfile) {
                $Out = ((($Infile.PSParentPath).Replace('Microsoft.PowerShell.Core\FileSystem::', '')) + '\' + "$($Infile.BaseName)" + "$Format")
            } else {
                $Out = $Outfile
            }

            if ($Copy) {
                $cmd = "ffmpeg -i ""$In"" -c copy ""$Out"""
            } elseif ($AudioRip) {
                $cmd = "ffmpeg -y -v error -i ""$In"" -vn -acodec copy ""$Out"""
            } else {
                $cmd = "ffmpeg -i ""$In"" -c:v hevc_nvenc -preset $PresetInt -rc vbr -cq 0 -qmin:v 28 -qmax:v 32 ""$Out"""
            }
            $OverwriteCMD = "Remove-Item ""$In"""

        }

        # Write-Host "`n`n Command: " -NoNewline
        # Write-Host "$cmd`n" -ForegroundColor Yellow
        Invoke-Expression $cmd -ErrorAction Stop

        if ($Overwrite -and !$AudioRip -and !$ReplaceAudio) {
            Invoke-Expression $OverwriteCMD
        }
        $i++
        Write-Host "`n`n Output: " -NoNewline
        Write-Host "$Out`n" -ForegroundColor Yellow
    }
    End {
        Return " Total files processed: $i`n`n"
    }
}

function Connect-AI {
    <#
 
    .SYNOPSIS
    Connects to the AI.
 
    .DESCRIPTION
    Connects to the AI.
 
    .PARAMETER AI
    The AI to connect to. This must be a string.
 
    .EXAMPLE
    Connect-AI -AI "bing"
 
    .EXAMPLE
    Connect-AI -AI "chatgpt"
 
    .OUTPUTS
    The output of the function is the chat session.
 
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $false, Position = 1)]$AI = 'bing'
    )

    switch ($AI.ToLower()) {
        bing {
            $Settings = 'settingsbing.js' 
        }
        chatgpt {
            $Settings = 'settingschatgpt.js' 
        }
        default {
            Return "`nAI not found.`n" 
        }
    }

    Copy-Item -Path "$env:userprofile\Repos\aws\node-chatgpt-api\$settings" -Destination "$env:userprofile\Repos\aws\node-chatgpt-api\settings.js" -Force

    Set-Location "$env:userprofile\Repos\aws\node-chatgpt-api\"

    npm run cli

    Return
}

function Set-WindowStyle {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $false, Position = 1)]
        [ValidateSet('FORCEMINIMIZE', 'HIDE', 'MAXIMIZE', 'MINIMIZE', 'RESTORE',
            'SHOW', 'SHOWDEFAULT', 'SHOWMAXIMIZED', 'SHOWMINIMIZED',
            'SHOWMINNOACTIVE', 'SHOWNA', 'SHOWNOACTIVATE', 'SHOWNORMAL')]$Style,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 2)]$MainWindowHandle
    )
    $WindowStates = @{
        FORCEMINIMIZE = 11; HIDE = 0
        MAXIMIZE = 3; MINIMIZE = 6
        RESTORE = 9; SHOW = 5
        SHOWDEFAULT = 10; SHOWMAXIMIZED = 3
        SHOWMINIMIZED = 2; SHOWMINNOACTIVE = 7
        SHOWNA = 8; SHOWNOACTIVATE = 4
        SHOWNORMAL = 1
    }
    Write-Verbose ('Set Window Style {1} on handle {0}' -f $MainWindowHandle, $($WindowStates[$style]))

    $Win32ShowWindowAsync = Add-Type -MemberDefinition @'
[DllImport("user32.dll")]
public static extern bool ShowWindowAsync(int hWnd, int nCmdShow);
'@
 -Name 'Win32ShowWindowAsync' -Namespace Win32Functions -PassThru

    $Win32ShowWindowAsync::ShowWindowAsync($MainWindowHandle, $WindowStates[$Style]) | Out-Null
}


function Start-awsTray {
    [CmdletBinding()]
    param (
    )
    Begin {
    }
    Process {
        Get-ScheduledTask -TaskName 'PingCheckTray' -ErrorAction SilentlyContinue | Start-ScheduledTask
    }
    End {
    }
}

function Get-WinGetUpgrades {
    [Alias('gwgu')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)][Alias('u')][Switch]$Upgrade
    )
    Begin {
        class Software {
            [string]$Name
            [string]$Id
            [string]$Version
            [string]$AvailableVersion
        }

        [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
        $Exclude = @(
            'WinDirStat 1.1.2',
            'Transmission Remote GUI 5.18',
            'VMware Workstation'
        )
        $AddToList = $true
    }
    Process {

        $upgradeResult = winget upgrade --include-unknown | Out-String

        if ($upgradeResult -match 'No installed package found matching input criteria\.') {
            Return 'No upgrades available'
        }

        $lines = $upgradeResult.Split([Environment]::NewLine)


        # Find the line that starts with Name, it contains the header
        $fl = 0
        while (-not $lines[$fl].StartsWith('Name')) {
            $fl++
        }

        # Find the line that begins with "X upgrades available", it marks the end of the list
        $ll = 0
        while (-not ($lines[$ll] -match '^[0-9]+ upgrades available\.')) {
            $ll++
        }

        # Line $i has the header, we can find char where we find ID and Version
        $idStart = $lines[$fl].IndexOf('Id')
        $versionStart = $lines[$fl].IndexOf('Version')
        $availableStart = $lines[$fl].IndexOf('Available')
        $sourceStart = $lines[$fl].IndexOf('Source')

        # Now cycle in real package and split accordingly
        $upgradeList = @()
        For ($i = $fl + 1; $i -le $ll; $i++) {
            $line = $lines[$i]
            if ($line.Length -gt ($availableStart + 1) -and -not $line.StartsWith('-')) {
                $name = $line.Substring(0, $idStart).TrimEnd()
                $id = $line.Substring($idStart, $versionStart - $idStart).TrimEnd()
                $version = $line.Substring($versionStart, $availableStart - $versionStart).TrimEnd()
                $available = $line.Substring($availableStart, $sourceStart - $availableStart).TrimEnd()
                $software = [Software]::new()
                $software.Name = $name
                $software.Id = $id
                $software.Version = $version
                $software.AvailableVersion = $available

                foreach ($Exclusion in $Exclude) {
                    if ("*$Exclusion*" -like "*$name*") {
                        $AddToList = $false
                    }
                }
                if ($AddToList) {
                    $upgradeList += $software
                }
                $AddToList = $true
            }
        }


    }
    End {
        if ($Upgrade) {
            $PacksToUpg = $upgradeList | Out-ConsoleGridView -Title 'Choose packages to upgrade'
            $PacksToUpg | ForEach-Object {
                Write-Color "`nUpgrading package: ", "$($_.Name)", " [$($_.Version) -> ", "$($_.AvailableVersion)", "]`n" -Color White, Yellow, White, Green, White
                winget upgrade --id $_.Id --accept-package-agreements --include-unknown --silent
            }
            Return
        } else {
            Return $upgradeList
        }
    }
}


function Remux-File {
    [Alias('remux')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)][String]$Path = 'D:\Plex\Movies\',
        [Parameter(Mandatory = $false, Position = 1)][Alias('Input', 'i')][String]$InputFormat = 'mp?',
        [Parameter(Mandatory = $false, Position = 2)][Alias('Output', 'o')][String]$OutputFormat = 'mkv',
        [Parameter(Mandatory = $false)][Alias('Keep')][Switch]$NoOverwrite,
        [Parameter(Mandatory = $false)][Switch]$TV,
        [Parameter(Mandatory = $false)][Switch]$All
    )
    
    Begin {
        $i = 0
        $TBD = @()
        $Remuxed = @()
        $ext = "$OutputFormat"
        if ($TV) {
            $OldFiles = Get-ChildItem -Path 'D:\Plex\TV\' -Recurse -Filter "*.$InputFormat"
        } elseif ($All) {
            $OldFiles = Get-ChildItem -Path 'D:\Plex\TV\' -Recurse -Filter "*.$InputFormat"
            $OldFiles += Get-ChildItem -Path 'D:\Plex\Movies\' -Recurse -Filter "*.$InputFormat"
        } else {
            $OldFiles = Get-ChildItem -Path "$Path" -Recurse -Filter "*.$InputFormat"
        }
    }
    
    Process {
        if ($OldFiles) {
            $OldFiles | ForEach-Object {
                ffmpeg -y -fflags +genpts -i "$($_.fullname)" -c:a copy -c:v copy "$($_.directory.fullname)\$($_.basename).$ext"
                $i++
                Write-Host "`n`n [$i/$($OldFiles.Count)] " -ForegroundColor Yellow -NoNewline
                if ((Get-Item "$($_.directory.fullname)\$($_.basename).$ext" -ErrorAction SilentlyContinue).Length -gt ($_.Length * 0.9)) {
                    Write-Host "Finished processing $($_.BaseName)"
                    $TBD += $_
                    $Remuxed += $_
                } else {
                    Write-Host "Error processing $($_.BaseName)" -ForegroundColor Red
                    $TBD += (Get-Item "$($_.directory.fullname)\$($_.basename).$ext" -ErrorAction SilentlyContinue)
                }
                Write-Host "`n"
            }
        }
    }
        
    End {
        if ($OldFiles) {
            $i = 0
            Write-Host "Finished remuxing the following files:`n" -ForegroundColor Green
            $Remuxed | ForEach-Object {
                $i++
                Write-Host " [$i/$($Remuxed.Count)] " -ForegroundColor Yellow -NoNewline
                Write-Host "$($_.BaseName)"
            }

            $Errored = $(($TBD | Where-Object { $_.extension -match $ext }).Name)
            if ($Errored.count -gt 0) {
                Write-Host " Errors processing the following files:`n" -ForegroundColor Red
                $Errored
            }
            
            if (!$NoOverwrite) {
                $TBD | Remove-Item
            }
        } else {
            Write-Host "`n No files matching input parameters found.`n" -ForegroundColor Yellow
        }
    }
}

function ConvertTo-SRT {
    <#
        .SYNOPSIS
        Fast conversion of Microsoft Stream VTT subtitle file to SRT format.
        .DESCRIPTION
        Uses select-string instead of get-content to improve speed 2 magnitudes.
        .PARAMETER Path
        Specifies the path to the VTT text file (mandatory).
        .PARAMETER OutFile
        Specifies the path to the output SRT text file (defaults to input file with .srt).
        .EXAMPLE
        ConvertTo-SRT -Path .\caption.vtt
        .EXAMPLE
        ConvertTo-SRT -Path .\caption.vtt -OutFile .\SRT\caption.srt
        .EXAMPLE
        Get-Item caption*.vtt | ConvertTo-SRT
        .EXAMPLE
        ConvertTo-SRT -Path ('.\caption.vtt','.\caption.vtt','.\caption3.vtt')
        .EXAMPLE
        ('.\caption.vtt','.\caption2.vtt','.\caption3.vtt') | ConvertTo-SRT
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true,
            Position = 0,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Path to VTT file.')]
        [Alias('PSPath')]
        [ValidateNotNullOrEmpty()]
        [Object[]]$Path,

        [Parameter(Mandatory = $false,
            Position = 1,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Path to output SRT file.')]
        [string]$OutFile
    )

    process {
        foreach ($File in $Path) {
            $Lines = @()
            if ( $File.FullName ) {
                $VTTFile = $File.FullName
            } else {
                $VTTFile = $File
            }

            if ( -not($PSBoundParameters.ContainsKey('OutFile')) ) {
                $OutFile = $VTTFile -replace '(\.vtt$|\.txt$)', '.srt'
                if ( $OutFile.split('.')[-1] -ne 'srt' ) {
                    $OutFile = $OutFile + '.srt'
                }
            }

            New-Item -Path $OutFile -ItemType File -Force | Out-Null
            $Subtitles = Select-String -Path $VTTFile -Pattern '(^|\s)(\d\d):(\d\d):(\d\d)\.(\d{1,3})' -Context 0, 2

            for ($i = 0; $i -lt $Subtitles.count; $i++) {
                $Lines += $i + 1
                $Lines += $Subtitles[$i].line -replace '\.', ','
                $Lines += $Subtitles[$i].Context.DisplayPostContext
                $Lines += ''
            }

            $Lines | Out-File -FilePath $OutFile -Append -Force
        }
    }
}

Function ConvertTo-Icon {
    <#
.Synopsis
    Converts .PNG images to icons
.Description
    Converts a .PNG image to an icon
.Example
    ConvertTo-Icon -Path .\Logo.png -Destination .\Favicon.ico
#>

    [CmdletBinding()]
    param(
        # The file
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias('Fullname', 'File', 'F', 'P')]
        [string]$Path,

        # If provided, will output the icon to a location
        [Parameter(Position = 1, ValueFromPipelineByPropertyName = $true)]
        [Alias('OutputFile', 'O', 'D')]
        [string]$Destination
    )

    Begin {

        $TypeDefinition = @'
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Collections.Generic;
using System.Drawing.Drawing2D;
 
/// <summary>
/// Adapted from this gist: https://gist.github.com/darkfall/1656050
/// Provides helper methods for imaging
/// </summary>
public static class ImagingHelper
{
    /// <summary>
    /// Converts a PNG image to a icon (ico) with all the sizes windows likes
    /// </summary>
    /// <param name="inputBitmap">The input bitmap</param>
    /// <param name="output">The output stream</param>
    /// <returns>Wether or not the icon was succesfully generated</returns>
    public static bool ConvertToIcon(Bitmap inputBitmap, Stream output)
    {
        if (inputBitmap == null)
            return false;
 
        int[] sizes = new int[] { 256, 48, 32, 16 };
 
        // Generate bitmaps for all the sizes and toss them in streams
        List<MemoryStream> imageStreams = new List<MemoryStream>();
        foreach (int size in sizes)
        {
            Bitmap newBitmap = ResizeImage(inputBitmap, size, size);
            if (newBitmap == null)
                return false;
            MemoryStream memoryStream = new MemoryStream();
            newBitmap.Save(memoryStream, ImageFormat.Png);
            imageStreams.Add(memoryStream);
        }
 
        BinaryWriter iconWriter = new BinaryWriter(output);
        if (output == null || iconWriter == null)
            return false;
 
        int offset = 0;
 
        // 0-1 reserved, 0
        iconWriter.Write((byte)0);
        iconWriter.Write((byte)0);
 
        // 2-3 image type, 1 = icon, 2 = cursor
        iconWriter.Write((short)1);
 
        // 4-5 number of images
        iconWriter.Write((short)sizes.Length);
 
        offset += 6 + (16 * sizes.Length);
 
        for (int i = 0; i < sizes.Length; i++)
        {
            // image entry 1
            // 0 image width
            iconWriter.Write((byte)sizes[i]);
            // 1 image height
            iconWriter.Write((byte)sizes[i]);
 
            // 2 number of colors
            iconWriter.Write((byte)0);
 
            // 3 reserved
            iconWriter.Write((byte)0);
 
            // 4-5 color planes
            iconWriter.Write((short)0);
 
            // 6-7 bits per pixel
            iconWriter.Write((short)32);
 
            // 8-11 size of image data
            iconWriter.Write((int)imageStreams[i].Length);
 
            // 12-15 offset of image data
            iconWriter.Write((int)offset);
 
            offset += (int)imageStreams[i].Length;
        }
 
        for (int i = 0; i < sizes.Length; i++)
        {
            // write image data
            // png data must contain the whole png data file
            iconWriter.Write(imageStreams[i].ToArray());
            imageStreams[i].Close();
        }
 
        iconWriter.Flush();
 
        return true;
    }
 
    /// <summary>
    /// Converts a PNG image to a icon (ico)
    /// </summary>
    /// <param name="input">The input stream</param>
    /// <param name="output">The output stream</param
    /// <returns>Wether or not the icon was succesfully generated</returns>
    public static bool ConvertToIcon(Stream input, Stream output)
    {
        Bitmap inputBitmap = (Bitmap)Bitmap.FromStream(input);
        return ConvertToIcon(inputBitmap, output);
    }
 
    /// <summary>
    /// Converts a PNG image to a icon (ico)
    /// </summary>
    /// <param name="inputPath">The input path</param>
    /// <param name="outputPath">The output path</param>
    /// <returns>Wether or not the icon was succesfully generated</returns>
    public static bool ConvertToIcon(string inputPath, string outputPath)
    {
        using (FileStream inputStream = new FileStream(inputPath, FileMode.Open))
        using (FileStream outputStream = new FileStream(outputPath, FileMode.OpenOrCreate))
        {
            return ConvertToIcon(inputStream, outputStream);
        }
    }
 
 
 
    /// <summary>
    /// Converts an image to a icon (ico)
    /// </summary>
    /// <param name="inputImage">The input image</param>
    /// <param name="outputPath">The output path</param>
    /// <returns>Wether or not the icon was succesfully generated</returns>
    public static bool ConvertToIcon(Image inputImage, string outputPath)
    {
        using (FileStream outputStream = new FileStream(outputPath, FileMode.OpenOrCreate))
        {
            return ConvertToIcon(new Bitmap(inputImage), outputStream);
        }
    }
 
 
    /// <summary>
    /// Resize the image to the specified width and height.
    /// Found on stackoverflow: https://stackoverflow.com/questions/1922040/resize-an-image-c-sharp
    /// </summary>
    /// <param name="image">The image to resize.</param>
    /// <param name="width">The width to resize to.</param>
    /// <param name="height">The height to resize to.</param>
    /// <returns>The resized image.</returns>
    public static Bitmap ResizeImage(Image image, int width, int height)
    {
        var destRect = new Rectangle(0, 0, width, height);
        var destImage = new Bitmap(width, height);
 
        destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);
 
        using (var graphics = Graphics.FromImage(destImage))
        {
            graphics.CompositingMode = CompositingMode.SourceCopy;
            graphics.CompositingQuality = CompositingQuality.HighQuality;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.SmoothingMode = SmoothingMode.HighQuality;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
 
            using (var wrapMode = new ImageAttributes())
            {
                wrapMode.SetWrapMode(WrapMode.TileFlipXY);
                graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
            }
        }
 
        return destImage;
    }
}
'@


        Add-Type -TypeDefinition $TypeDefinition -ReferencedAssemblies 'System.Drawing', 'System.IO', 'System.Collections', 'System.Drawing.Common', 'System.Drawing.Primitives'

        If (-Not 'ImagingHelper' -as [Type]) {
            Throw 'The custom "ImagingHelper" type is not loaded'
        }
    }

    Process {
        foreach ($Item in $Path) {
            #region Resolve Path
            $ResolvedFile = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Item)
            If (-not $ResolvedFile) {
                return
            }
            if ($Destination) {
                $OutPath = "$Destination\$((Get-Item $ResolvedFile[0]).BaseName).ico"
            } else {
                $OutPath = "$((Get-Item $ResolvedFile[0]).DirectoryName)\$((Get-Item $ResolvedFile[0]).BaseName).ico"
            }
            #endregion

            [ImagingHelper]::ConvertToIcon($ResolvedFile[0].Path, $OutPath)
        }
    }
    End {
    }
}

$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()

function Invoke-Async {
    <#
            .SYNOPSIS
            Runs code, with variables, asynchronously
 
            .DESCRIPTION
            This function runs the given code in an asynchronous runspace.
            This lets you process data in the background while leaving the UI responsive to input
 
            .PARAMETER Code
            The code to run in the runspace
 
            .PARAMETER Variables
            A hashtable containing variable names and values to pass into the runspace
 
            .EXAMPLE
 
            $AsyncParameters = @{
                Variables = @{
                    Key1 = 'Value1'
                    Key2 = $SomeOtherVariable
                }
                Code = {
                    Write-Host "Key1: $Key1`nKey2: $Key2"
                }
            }
            Run-Async @AsyncParameters
 
            .NOTES
            It's more reliable to pass single values than complex objects duje to the way PowerShell handles value/reference passing with objects
 
            .INPUTS
            Variables, Code
 
            .OUTPUTS
            None
        #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ScriptBlock]
        $Code,
        [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true)]
        [hashtable]
        $Variables
    )
    # Add the above code to a runspace and execute it.
    $PSinstance = [powershell]::Create() #| Out-File -Append -FilePath $LogFile
    $PSinstance.Runspace = [runspacefactory]::CreateRunspace($InitialSessionState)
    $PSinstance.Runspace.ApartmentState = 'STA'
    $PSinstance.Runspace.ThreadOptions = 'UseNewThread'
    $PSinstance.Runspace.Open()
    if ($Variables) {
        # Pass in the specified variables from $VariableList
        $Variables.keys.ForEach({
                $PSInstance.Runspace.SessionStateProxy.SetVariable($_, $Variables.$_)
            })
    }
    $PSInstance.AddScript($Code)
    $PSinstance.BeginInvoke()
}

function Add-FMSignature {
    [Alias('signfm')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)][Alias('f')]$File
    )
    Begin {
        $CodeSignCert = Get-Item -Path 'Microsoft.PowerShell.Security\Certificate::CurrentUser\My\E992867E7D48FBFE439C9909B35E12244133A72D'
    }
    Process {
        $FilePath = Convert-Path -Path $File
        Try {
            Set-AuthenticodeSignature -FilePath $FilePath -Certificate $CodeSignCert -TimestampServer 'http://timestamp.digicert.com' -ErrorAction Stop
        } Catch {
            Write-Error $_
        }
    }
    End {
        Return
    }
}

function Get-FileName {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [string]$WindowTitle = '',

        [Parameter(Mandatory = $false)]
        [string]$InitialDirectory = '',

        [Parameter(Mandatory = $false)]
        [string]$Filter = 'All files (*.*)|*.*',

        [Alias('f')][switch]$Folder
    )
    Add-Type -AssemblyName System.Windows.Forms

    if (!$WindowTitle) {
        if ($Folder) {
            $WindowTitle = 'Select Folder(s)'
        } else {
            $WindowTitle = 'Select File(s)'
        }
    }

    if (!$InitialDirectory) {
        $InitialDirectory = [Environment]::GetFolderPath('UserProfile')
    }

    if ($Folder) {
        $openFileDialog = [System.Windows.Forms.FolderBrowserDialog]@{
            InitialDirectory       = $InitialDirectory
            Description            = $WindowTitle
            UseDescriptionForTitle = $true
            Multiselect            = $true
        }
    } else {
        $openFileDialog = [System.Windows.Forms.OpenFileDialog]@{
            InitialDirectory = $InitialDirectory
            Title            = $WindowTitle
            Filter           = $Filter
            CheckFileExists  = $true
            Multiselect      = $true
        }
    }

    if ($openFileDialog.ShowDialog().ToString() -eq 'OK') {
        if ($Folder) {
            $Selected = @($openFileDialog.SelectedPaths)
        } else {
            $Selected = @($openFileDialog.Filenames)
        }
    }

    $openFileDialog.Dispose()

    return $Selected
}

function Move-Enc {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline)][Alias('p')]$Path
    )
    Begin {
        function Move-ItemRetry {
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)][Alias('f')]$File,
                [Parameter(Mandatory = $true, Position = 1)][Alias('d')]$Destination
            )
            if ($File.GetType().Name -eq 'String') {
                Try {
                    $File = Get-Item -Path $File
                } Catch {
                    Throw "File not found: $File"
                }
            }
            if ($Destination.GetType().Name -eq 'String') {
                Try {
                    $Destination = Get-Item -Path $Destination
                } Catch {
                    Throw "Destination not found: $Destination"
                }
            }
    
            $i = 0
            $break = $false
            do {
                Try {
                    $NewFile = $File | Move-Item -Destination $Destination.FullName -Force -ErrorAction Stop -PassThru
                    $break = $true
                } Catch {
                    $ErrorMessage = $_.Exception.Message
                    Start-Sleep -Seconds 5
                    $i++
                }
            }
            until ($i -ge 6 -or $break)
    
            if ($i -ge 6 -and !$break) {
                Throw "Failed to move file: $($File.FullName)`n`n$ErrorMessage"
            } else {
                Return $NewFile
            }
        }

        $ErrorActionPreference = 'Continue'
        Install-CustomModule PSWriteColor
        [Int64]$OldTotalSpace = 0
        [Int64]$NewTotalSpace = 0
        $ToBeDeleted = @()
        $LargerThanOriginals = @()
        $ReEncFolders = @()

        if ($Path) {
            $PathsArray = @($Path)
        } else {
            $PathsArray = @(Get-FileName -Folder)
        }
    }

    Process {

        foreach ($FolderObj in $PathsArray) {
            $ReEncFolders += Get-ChildItem -Path $FolderObj -Directory -Recurse -Filter '.reencode'
        }

        if ($ReEncFolders.Count -gt 0) {
            foreach ($FolderObj in $PathsArray) {
                foreach ($Folder in $ReEncFolders) {
                    $ReEncFiles = Get-ChildItem -Path $Folder.FullName -File -Recurse
                    [Int64]$OldFolderSize = 0
                    [Int64]$NewFolderSize = 0

                    if ($ReEncFiles.Count -gt 0) {
                        $ParentFolderContents = Get-ChildItem -Path $Folder.Parent.FullName -File | Where-Object { $_.extension -notin @('.srt', '.nfo', '.jpg', '.jpeg', '.png', '.txt', '.bmp') }
                        $OldFilesFolderPath = $Folder.Parent.FullName + '\.old'
                        Try {
                            $OldFilesFolder = New-Item -Path $Folder.Parent.FullName -Name '.old' -ItemType Directory -ErrorAction Stop
                        } Catch {
                            $OldFilesFolder = Get-Item -Path $OldFilesFolderPath
                        }

                        foreach ($File in $ReEncFiles) {
                            $OldFile = $ParentFolderContents | Where-Object { $_.BaseName -match [RegEx]::Escape("$(($File.BaseName -split ('\([0-9]\)$'))[0])") } | Sort-Object Length -Descending | Select-Object -First 1
                            [Double]$FileSize = $([Math]::Round(($File.Length / 1MB), 2))
                            [Double]$OldFileSize = $([Math]::Round(($OldFile.Length / 1MB), 2))

                            if ($FileSize -lt $OldFileSize) {

                                $OldFolderSize += $OldFile.Length
                                $NewFolderSize += $File.Length
                                $NewTotalSpace += $File.Length
                                $OldTotalSpace += $OldFile.Length

                                Try {
                                    $OldFile = $OldFile | Move-ItemRetry -Destination $OldFilesFolder.FullName -ErrorAction Stop
                                    $i = 0
                                    Try {
                                        $File | Move-ItemRetry -Destination $Folder.Parent.FullName -ErrorAction Stop > $null
                                        Write-Color "`n Replaced", " $(($OldFile.Name).TrimStart("$SplitString"))", " with x265 re-encoded .mkv `n [size: ", "$OldFileSize MB", ' -> ', "$FileSize MB", "]`n" -Color Gray, Yellow, Gray, Red, Gray, Green, Gray
                                    } Catch {
                                        Write-Color ' Handbrake is still processing ', "$(($File.Name).TrimStart("$SplitString"))", " - reencode and original file both skipped.`n" -Color White, Yellow, White
            
                                        Try {
                                            $OldFile | Move-ItemRetry -Destination $Folder.Parent.FullName -ErrorAction Stop > $null
                                        } Catch {
                                            Write-Error $_
                                        }
                                    }
                                } Catch {
                                    Write-Error $_
                                }
                            } else {
                                Write-Color "`n Skipping", " $(($File.Name).TrimStart("$SplitString"))", " - x265 re-encoded file is larger than original file `n [size: ", "$OldFileSize MB", ' -> ', "$FileSize MB", "]`n" -Color Gray, Yellow, Gray, Green, Gray, Red, Gray
                                $File | Add-Member -MemberType NoteProperty -Name 'OldSize' -Value $OldFileSize
                                $File | Add-Member -MemberType NoteProperty -Name 'NewSize' -Value $FileSize
                                $LargerThanOriginals += $File
                            }
                        }
                        $ToBeDeleted += $OldFilesFolder
                        Write-Color "`n Space saved in \$($Folder.Parent.Parent.Name)\$($Folder.Parent.Name): ", "$([Math]::Round(($OldFolderSize - $NewFolderSize)/1GB, 2)) GB`n" -Color Gray, Yellow
                    }
                }
            }
        }
    }
    End {
        if ($ReEncFolders.Count -gt 0) {
            Write-Color "`n Total file size (before/after): ", "$([Math]::Round(($OldTotalSpace)/1GB, 2)) GB", ' -> ', "$([Math]::Round(($NewTotalSpace)/1GB, 2)) GB" -Color White, Red, White, Green -ShowTime
            Write-Color "`n Total space saved: ", "$([Math]::Round(($OldTotalSpace - $NewTotalSpace)/1GB, 2)) GB`n" -Color White, Yellow
            
            if ($LargerThanOriginals.Count -gt 0) {
                Write-Color "`n The following re-encodes have been skipped due to a larger file size than the originals:`n" -Color White
                $LargerThanOriginals | ForEach-Object { Write-Color " $($_.Name) `n ", '[', "$([Math]::Round(($_.NewSize - $_.OldSize), 2)) MB", " larger than original]`n" -Color Yellow, White, Red, White }
            }
            
            Write-Color "`n Delete replaced encodes (as well as re-encodes with a larger file size than the originals)? [", 'Y', '/', 'N', "]`n" -Color White, Green, White, Red, White
    
            $DeleteFilesInput = Read-Host 'Input'
    
            switch ($DeleteFilesInput.ToUpper()) {
                'Y' {
                    $DeleteFiles = $true
                }
                Default {
                    $DeleteFiles = $false
                }
            }
    
            if ($DeleteFiles) {
                $LargerThanOriginals | ForEach-Object { Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue }
                $ToBeDeleted | ForEach-Object { Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }
                foreach ($Folder in $ReEncFolders) {
                    if ($((Get-ChildItem $Folder).Count) -eq 0) {
                        Try {
                            $Folder | Remove-Item -Force -ErrorAction Stop
                        } Catch {
                            Write-Color ' Handbrake is still processing ', "$(($Folder.FullName).TrimStart('D:\Plex'))", " - '.reencode'-folder will be left intact.`n" -Color White, Yellow, White
                        }
                    } else {
                        Write-Color ' Handbrake is still processing ', "$(($Folder.FullName).TrimStart('D:\Plex'))", " - '.reencode'-folder will be left intact.`n" -Color White, Yellow, White
                    }
                }
            }
            Return
        } else {
            Write-Warning "No '.reencode'-folders found in the specified path(s)."
            Write-Output "`n Press any key to exit..."
            [Console]::ReadKey('NoEcho,IncludeKeyDown') > $null
            Return
        }
    }
}

function Get-YesNo {
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)][Alias('M')][String]$Message,
        [Parameter(Mandatory = $false, Position = 1)][Alias('C')][String]$Color = 'Yellow'
    )
    if ($Message) {
        Write-Host "`n$Message`n" -ForegroundColor $Color
    }
    Write-Host '[' -NoNewline
    Write-Host 'Y' -ForegroundColor DarkGreen -NoNewline
    Write-Host '/' -NoNewline
    Write-Host 'N' -ForegroundColor DarkRed -NoNewline
    Write-Host ']' -NoNewline
  
    $Loop = $True
  
    while ($Loop) {
        if ([Console]::KeyAvailable -eq $True) {
  
            $Key = [Console]::ReadKey('NoEcho,IncludeKeyDown')
  
            switch ($Key.Key) {
                'Y' {
                    $Answer = $True
                    $Loop = $False
                }
                'N' {
                    $Answer = $False
                    $Loop = $False
                }
                default {
                    Write-Host "`r[" -NoNewline
                    Write-Host 'Y' -ForegroundColor DarkGreen -NoNewline
                    Write-Host '/' -NoNewline
                    Write-Host 'N' -ForegroundColor DarkRed -NoNewline
                    Write-Host ']' -NoNewline
                    Write-Host ' - ' -NoNewline
                    Write-Host 'Invalid input. Please try again.' -ForegroundColor Yellow -BackgroundColor DarkRed -NoNewline
                }
            }
        } else {
            Start-Sleep -Milliseconds 50
        }
    }
    if ($Answer) {
        Write-Host "`r[" -NoNewline -ForegroundColor DarkGray
        Write-Host 'Yes' -ForegroundColor DarkGreen -NoNewline
        Write-Host '/' -NoNewline -ForegroundColor DarkGray
        Write-Host "N]$(' ' * 40)" -ForegroundColor DarkGray
    } else {
        Write-Host "`r[" -NoNewline -ForegroundColor DarkGray
        Write-Host 'Y/' -NoNewline -ForegroundColor DarkGray
        Write-Host 'No' -ForegroundColor DarkRed -NoNewline
        Write-Host "]$(' ' * 40)" -ForegroundColor DarkGray
    }
    Return $Answer
} 
function Invoke-AutoHotkey {
    [CmdletBinding()]
    [Alias('ahk')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)][String]$Script
    )
    $pipeName = 'AHK_' + [System.Environment]::TickCount
    $pipeDir = [System.IO.Pipes.PipeDirection]::Out
    $maxNum = [Int]254
    $pipeTMode = [System.IO.Pipes.PipeTransmissionMode]::Message
    $pipeOptions = [System.IO.Pipes.PipeOptions]::None
    
    $ahkPath = [Environment]::GetFolderPath('ProgramFiles') + '\AutoHotkey.AutoHotkey\v2\AutoHotkey64_UIA.exe'
    
    $pipe_ga = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName, $pipeDir, $maxNum, $pipeTMode, $pipeOptions)
        
    $pipe = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName, $pipeDir, $maxNum, $pipeTMode, $pipeOptions)
    
    if ($pipe_ga -and $pipe) {
        Start-Process $ahkPath "\\.\pipe\$pipeName"
        $pipe_ga.WaitForConnection()
        $pipe_ga.Dispose()
        $pipe.WaitForConnection()
        $script = [char]65279 + $script
        $sw = New-Object System.IO.StreamWriter($pipe)
        $sw.Write($script)
            
        $sw.Dispose()
        $pipe.Dispose()
    } else {
        Write-Host 'Operation cancelled: Failed to create named pipe' 
    }
}

function Start-OBS {
    [CmdletBinding()]
    param (
    )
    
    if (!(Get-Process 'obs-browser-page' -ErrorAction SilentlyContinue)) {
        Get-Process 'obs64' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
        Start-Sleep -Seconds 2
        Start-Process 'C:\Program Files\obs-studio\bin\64bit\obs64.exe' -ArgumentList '--disable-shutdown-check --startrecording' -WorkingDirectory 'C:\Program Files\obs-studio\bin\64bit\'
    }

    if (!(Get-Process 'obs64' -ErrorAction SilentlyContinue)) {
        Start-Process 'C:\Program Files\obs-studio\bin\64bit\obs64.exe' -ArgumentList '--disable-shutdown-check --startrecording' -WorkingDirectory 'C:\Program Files\obs-studio\bin\64bit\'
    } else {
        Connect-OBS
        Start-Sleep -Seconds 2
        Start-OBSRecord
    }
}

function New-Subs {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false, Position = 0)][Alias('l')][String]$Language = 'English',
        [Parameter(Mandatory = $false, Position = 1)][Alias('m')][String]$Model = 'large-v2',
        [Parameter(Mandatory = $false, Position = 2)][Alias('b')][int]$Batch = 1,
        [Alias('f')][switch]$Folder,
        [Alias('t')][switch]$Translate,
        [Alias('d')][switch]$Distil,
        [Alias()][switch]$NoFilter
    )

    $CodeArray = @()

    if ($Distil) {
        $Model = "distil-$Model"
    }

    if (!$NoFilter) {
        $FilterString = ' --ff_mdx_kim2'
    }
    else {
        $FilterString = ''
    }

    if ($Translate) {
        $Task = 'translate'
        $Model = 'large-v2'
    } else {
        $Task = 'transcribe'
    }

    if ($Folder) {
        for ($i = 0; $i -lt $Batch; $i++) {
            $FolderArray = @(Get-FileName -Folder -InitialDirectory '\\aws-server\D\Plex')

            foreach ($FolderObj in $FolderArray) {
            $CodeString = "S:\Utilities\Whisper-Faster\faster-whisper-xxl.exe ""$FolderObj"" --language=$Language --model=""$Model""$FilterString --beep_off -br --skip --standard --task=$Task"
            $CodeBlock = [ScriptBlock]::Create($CodeString)
            $CodeArray += $CodeBlock
            }
        }
    } else {
        for ($i = 0; $i -lt $Batch; $i++) {
            $Files = Get-FileName
            foreach ($File in $Files) {
                $CodeString = "S:\Utilities\Whisper-Faster\faster-whisper-xxl.exe ""$File"" --language=$Language --model=""$Model""$FilterString --beep_off -br --skip --standard --task=$Task"
                $CodeBlock = [ScriptBlock]::Create($CodeString)
                $CodeArray += $CodeBlock
            }
        }
    }

    foreach ($CodeObj in $CodeArray) {
        Invoke-Command -ScriptBlock $CodeObj
    }
    Return
}

function Connect-awsServer {
    [Alias('connect-server', 'awss')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)][Alias('c')][pscredential]$Credential
    )
    Begin {
        if (!$Credential) {
            $Credential = Get-StoredCredential -Target 'TERMSRV/192.168.0.200'
        } elseif ($Credential -isnot [pscredential]) {
            Try {
                $Credential = Get-Credential -UserName $Credential
            } Catch {
                Write-Error $_
            }
        }
    }
    Process {
        $Session = New-PSSession -ComputerName 'aws-server' -Credential $Credential
        Enter-PSSession -Session $Session
    }
    End {
    }
}