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 -LiteralPath $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 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 = 'Information'
    )
    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 ()

    . "$Env:utilities\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 -LiteralPath $env:logs
        $Output = ''

        foreach ($Log in $Logs) {
            $LogSize = [math]::Round(((Get-ChildItem -LiteralPath $Log.fullname | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum / 1KB), 2)
            $LogSizeMB = [math]::Round(((Get-ChildItem -LiteralPath $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 -LiteralPath $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 -LiteralPath $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 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 -LiteralPath "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 $Infile[0]
                $AudIn = Get-Item $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 -LiteralPath $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 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 New-Remux {
    [Alias('remux')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)][String]$Path,
        [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]$Movies,
        [Parameter(Mandatory = $false)][Switch]$TV,
        [Parameter(Mandatory = $false)][Switch]$All
    )
    
    Begin {
        $i = 0
        $TBD = @()
        $Remuxed = @()
        $ext = "$OutputFormat"
        if ($TV) {
            $OldFiles = Get-ChildItem -LiteralPath 'D:\Plex\TV\' -Recurse -Filter "*.$InputFormat"
        } elseif ($Movies) {
            $OldFiles = Get-ChildItem -LiteralPath 'D:\Plex\Movies\' -Recurse -Filter "*.$InputFormat"
        } elseif ($All) {
            $OldFiles = Get-ChildItem -LiteralPath 'D:\Plex\TV\' -Recurse -Filter "*.$InputFormat"
            $OldFiles += Get-ChildItem -LiteralPath 'D:\Plex\Movies\' -Recurse -Filter "*.$InputFormat"
        } else {
            if (!$Path) {
                $Path = Get-FileName
            }
            if ((Get-Item -LiteralPath $Path).Attributes -notmatch 'Directory') {
                $OldFiles = Get-Item -LiteralPath $Path
            } else {
                $OldFiles = Get-ChildItem -LiteralPath "$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 -LiteralPath "$($_.directory.fullname)\$($_.basename).$ext" -ErrorAction SilentlyContinue).Length -gt ($_.Length * 0.9)) {
                    Write-Host "Finished processing $($_.Name)"
                    $TBD += $_
                    $Remuxed += $_
                } else {
                    Write-Host "Error processing $($_.Name)" -ForegroundColor Red
                    $TBD += (Get-Item -LiteralPath "$($_.directory.fullname)\$($_.basename).$ext" -ErrorAction SilentlyContinue)
                }
                Write-Host "`n"
            }
        }
    }
        
    End {
        if ($OldFiles) {
            $i = 0
            Write-Host "Finished remuxing the following files into .$($OutputFormat):`n" -ForegroundColor Green
            $Remuxed | ForEach-Object {
                $i++
                Write-Host " [$i/$($Remuxed.Count)] " -ForegroundColor Yellow -NoNewline
                Write-Host "$($_.Name)"
            }

            $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 -LiteralPath $ResolvedFile[0]).BaseName).ico"
            } else {
                $OutPath = "$((Get-Item -LiteralPath $ResolvedFile[0]).DirectoryName)\$((Get-Item $ResolvedFile[0]).BaseName).ico"
            }
            #endregion

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

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"
                }
            }
            Invoke-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
    )
    $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    # 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 -LiteralPath '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 -LiteralPath $File
                } Catch {
                    Throw "File not found: $File"
                }
            }
            if ($Destination.GetType().Name -eq 'String') {
                Try {
                    $Destination = Get-Item -LiteralPath $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 -LiteralPath $FolderObj -Directory -Recurse -Filter '.reencode'
        }

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

                    if ($ReEncFiles.Count -gt 0) {
                        $ParentFolderContents = Get-ChildItem -LiteralPath $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 -LiteralPath $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 ($Env:COMPUTERNAME -eq 'AWS-DESKTOP') {
        $InitialDirectory = '\\aws-server\D\Plex'
    }
    else {
        $InitialDirectory = 'C:\'
    }

    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 $InitialDirectory)

            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 -InitialDirectory $InitialDirectory
            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 {
    }
}

function New-Compose {
    [Alias('doco')]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline)][Alias('p')]$Path = '.',
        [Parameter()][Alias('u')][Switch]$Update
    )
    
    Begin {
        Switch ($Path.GetType().Name) {
            DirectoryInfo {
                $Compose = Get-ChildItem $Path -Filter 'docker-compose.yml' 
            }
            FileInfo {
                $Compose = $Path 
            }
            String {
                if (Get-ChildItem $Path -Filter 'docker-compose.yml' -ErrorAction SilentlyContinue) {
                    $Compose = Get-ChildItem $Path -Filter 'docker-compose.yml'
                } else {
                    $Dir = Get-ChildItem "$Env:USERPROFILE\Docker\" -Filter "*$Path*" | Select-Object -First 1
                    $Compose = Get-ChildItem $Dir -Filter 'docker-compose.yml'
                }
            }
            Default {
                Throw 'Unknown object type passed for parameter "Path". Please pass either a String, Directory or File object (or leave blank to use current location).'
            }
        }
        
        if ($Compose.Name -ne 'docker-compose.yml') {
            Throw 'No "docker-compose.yml"-file found in input. Please pass either a String, Directory or File object pointing to a Docker compose file or a folder containing one (or leave blank to use current location).'
        }
    }
    
    Process {
        if ($Update) {
            docker compose --file $($Compose.FullName) up -d --force-recreate --pull always
        } else {
            docker compose --file $($Compose.FullName) up -d --force-recreate
        }
    }
    
    End {
        Return
    }
}