JiffyTekUtils.psm1

function IsNull($obj) {
    if ($null -eq $obj) { return $true }
    if ($obj -is [String] -and $obj -eq [String]::Empty) { return $true }
    if ($obj -is [DBNull] -or $obj -is [System.Management.Automation.Language.NullString]) { return $true }
    return $false
}

function IsNotNull($obj) { return (-Not (IsNull $obj)) }

function Convert-TimeSpan([timespan]$ts) {
    if ($ts.Ticks -eq 0 ) { return "0 seconds" }

    $pts = @()
    $secs = $ts.Seconds + ($ts.Milliseconds / 1000)

    if ($ts.Days -ne 0) { $pts += ("{0} days" -f $ts.Days) }
    if ($ts.Hours -ne 0) { $pts += ("{0} hours" -f $ts.Hours) }
    if ($ts.Minutes -ne 0) { $pts += ("{0} minutes" -f $ts.Minutes) }
    if ($secs -ne 0) { $pts += ("{0:f1} seconds" -f $secs) }

    return ($pts -join " ")
}

class JTResult
{
    hidden [bool]$Success
    hidden [string]$Message

    JTResult() {
        $this.Success = $False
        $this.Message = "unknown!"
    }

    [bool] GetSuccess() { return $this.Success }
    [String] GetMessage() { return $this.Message }

    SetMessage([string]$Message, [bool]$Success) {
        $this.Message = $Message
        $this.Success = $Success
    }

    [String] ToString() {
        if ($this.Success) { return "SUCCESS : " + $this.Message }
        else { return "FAILED : " + $this.Message }
    }

    WriteMessage() {
        $this.ToString() | Out-Default
    }
}

#### Join-Files ##################################################################################################
#
# .SYNOPSIS
# Concatenates two or more files into a single file
#
# .PARAMETER $Path
# Array of strings : Full path names of the files to be joined together in sequence
#
# .PARAMETER $Destination
# string : Full path name of the single file
#
# .EXAMPLE
# Join-Files "c:\temp\file1.vob","c:\temp\file2.vob","c:\temp\file3.vob" "c:\temp\joinedfile.vob"
##>
function Join-Files (
    [parameter(Position=0,Mandatory=$True,ValueFromPipeline=$True)]
    [string[]] $Path,
    [parameter(Position=1,Mandatory=$True)]
    [string] $Destination) {
    write-verbose "Join-Files: Create $Destination"
    $OutFile = [System.IO.File]::Create($Destination)
    foreach ( $File in $Path ) {
        write-verbose "Join-Files: OpenRead $File"
        $InFile = [System.IO.File]::OpenRead($File)
        write-verbose "Join-Files: CopyTo Destination"
        $InFile.CopyTo($OutFile)
        $InFile.Dispose()
    }
    $OutFile.Dispose()
    write-verbose "Join-Files: finished"
}

#### Get-Telnet ##################################################################################################
#
# .SYNOPSIS
# Execute a sequence of commands to a telnet server and save response to a file
#
# .PARAMETER $Commands
# Array of string : The commands to execute as string array
#
# .PARAMETER $RemoteHost
# string : The telnet server ip address or hostname
#
# .PARAMETER $Port
# string : The port to use
#
# .PARAMETER $WaitTime
# Int : Wait time in milliseconds between each command
#
# .PARAMETER $OutputPath
# string : the file name of the saved response
#
# .EXAMPLE
# Get-Telnet -RemoteHost "10.17.68.7" -Commands "admin","admin1","PRINTCONNECTEDDEVICES" -OutputPath "c:\temp\10.17.68.7.txt"
##>
Function Get-Telnet (
    [string[]]$Commands = @("username","password"),
    [string]$RemoteHost = "HostnameOrIPAddress",
    [string]$Port = "23",
    [int]$WaitTime = 1000,
    [string]$OutputPath = "c:\temp\telnet.txt") {
    # Attach to the remote device, setup streaming requirements
    $Socket = New-Object System.Net.Sockets.TcpClient($RemoteHost, $Port)
    If ($Socket) {
        $Stream = $Socket.GetStream()
        $Writer = New-Object System.IO.StreamWriter($Stream)
        $Buffer = New-Object System.Byte[] 1024
        $Encoding = New-Object System.Text.AsciiEncoding

        # Now start issuing the commands
        ForEach ($Command in $Commands) {
            $Writer.WriteLine($Command)
            $Writer.Flush()
            Start-Sleep -Milliseconds $WaitTime
        }

        # All commands issued, but since the last command is usually going to be
        # the longest let's wait a little longer for it to finish
        Start-Sleep -Milliseconds ($WaitTime * 4)

        # Save all the results
        $Result = ""
        While($Stream.DataAvailable) {
            $Read = $Stream.Read($Buffer, 0, 1024)
            $Result += ($Encoding.GetString($Buffer, 0, $Read))
        }
    }
    Else {
        $Result = "Unable to connect to host: $($RemoteHost):$Port"
    }
    # Done, now save the results to a file
    $Result | Out-File $OutputPath
}

Function Move-Video (
  $InFileItem,
  [string]$MoveTo) {
    $MoveToFolder = Join-Path -Path $InFileItem.DirectoryName -ChildPath $MoveTo
    New-Item -Path $MoveToFolder -ItemType "Directory" -Force
    Move-Item -LiteralPath $InFileItem.FullName -Destination $MoveToFolder
}

#### Convert-Video ##################################################################################################
#
# .SYNOPSIS
# Converts a single video file to HEVC / AAC format using FFMPEG
#
# .PARAMETER $InFile
# string : The video file to convert
#
# .PARAMETER $DropAudio
# bool : Allows you to remove the audio stream from the OutFile
#
# .EXAMPLE
# Convert-Video -InFile "C:\Users\Lea\Downloads\NHD\Porn\WMV\Bareback Bath House.wmv" -DropAudio
####
Function Convert-Video (
    [string]$InFile,
    [switch]$DropAudio) {
    [JTResult]$Result = [JTResult]::new()

    $InFileItem = Get-Item -LiteralPath $InFile
    if (IsNotNull $InFileItem) {
        $VideoExts = @(".3g2",".3gp",".amv",".asf",".avi",".drc",".f4a",".f4b",".f4p",".f4v",".flv",".m2ts",".m2v",".m4p",".m4v",".mkv",".mng",".mov",".mp2",".mp4",".mpe",".mpeg",".mpg",".mpv",".mts",".mxf",".nsv",".ogg",".ogv",".qt",".rm",".rmvb",".roq",".svi",".ts",".vob",".webm",".wmv",".yuv")
        if ($VideoExts -contains $InFileItem.Extension) {
            $codecs = ffprobe -show_entries stream=codec_name -print_format default=nw=1:nk=1 -v quiet "$InFile"
            if (IsNotNull $codecs) {
                [string]$Audio = if ($DropAudio) { "-an" } else { if ($codecs -contains "aac") { "-c:a copy" } else { "-c:a aac" } }
                [string]$Video = if ($codecs -contains "av1" -or $codecs -contains "hevc") { "-c:v copy" } else { '-c:v libx265 -crf 28 -preset medium -tag:v hvc1 -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -pix_fmt yuv420p' }
                [string]$OutFile = [io.path]::ChangeExtension($InFile, ".hevc.mp4")
                [string]$ArgumentList = '-nostdin -y -i "{0}" -map_metadata -1 {1} {2} -movflags faststart -max_muxing_queue_size 1024 "{3}"' -f $InFile, $Audio, $Video, $OutFile

                "ffmpeg.exe {0}" -f $ArgumentList | Out-Default
                $proc = Start-Process -FilePath "ffmpeg.exe" -ArgumentList $ArgumentList -WindowStyle Hidden -PassThru -Wait
                $OutFileItem = Get-Item -LiteralPath $OutFile

                $procSuccess = ($proc.ExitCode -eq 0)
                $fileCreated = (IsNotNull $OutFileItem)

                if ($procSuccess -And $fileCreated) {
                    $sizeDiff = $OutFileItem.Length - $InFileItem.Length
                    if ($sizeDiff -le 1024) {
                        Move-Video -InFileItem $InFileItem -MoveTo "_original"
                        $Result.SetMessage(("{0:p1} of original size!" -f ($OutFileItem.Length / $InFileItem.Length)), $True)
                    }
                    else {
                        Move-Video -InFileItem $InFileItem -MoveTo "_failed"
                        Move-Video -InFileItem $OutFileItem -MoveTo "_failed"
                        $Result.SetMessage(("{0:p1} of original size!" -f ($OutFileItem.Length / $InFileItem.Length)), $False)
                    }
                }
                else {
                  Move-Video -InFileItem $InFileItem -MoveTo "_failed"
                  Remove-Item -LiteralPath $OutFile -ErrorAction Ignore
                  $Result.SetMessage("ffmpeg failed!", $False)
                }
            }
            else {
                Move-Video -InFileItem $InFileItem -MoveTo "_failed"
                $Result.SetMessage("ffprobe failed!", $False)
             }
        }
        else {
            Move-Video -InFileItem $InFileItem -MoveTo "_failed"
            $Result.SetMessage("file ext is not a known video file ext!", $False)
          }
    }
    else { $Result.SetMessage("invalid InFile!", $False) }

    $Result.WriteMessage()
    return $Result.GetSuccess()
}

#### Convert-AllVideos ##################################################################################################
#
# .SYNOPSIS
# Converts video files to HEVC / AAC format using FFMPEG
#
# .PARAMETER $LiteralPath
# string : The directory where to start scanning for matching videos
#
# .PARAMETER $Include
# string : The video file extention to match
#
# .PARAMETER $DropAudio
# bool : Allows you to remove the audio stream from the OutFile
#
# .EXAMPLE
# Convert-AllVideos -LiteralPath "C:\Users\Lea\Downloads\NHD\Porn\WMV\" -Include "*.wmv" -DropAudio
####
Function Convert-AllVideos (
  [string]$LiteralPath,
  [string]$Include = "*",
  [switch]$DropAudio) {
  [JTResult]$Result = [JTResult]::new()

  ">>---> STARTING CONVERSION`r`n"
  $TotalTime = Measure-Command {
    if (Test-Path -LiteralPath $LiteralPath -PathType Container) {
      [int]$i = 0
      [int]$s = 0
      $Files = Get-ChildItem -LiteralPath $LiteralPath -Include $Include -File
      foreach($f in $Files) {
        $i++
        ">>---> STARTING FILE {0} of {1} : {2}" -f $i, $Files.Length, $f.Name | Out-Default
        $FileTime = Measure-Command {
          if (Convert-Video -InFile $f.FullName -DropAudio:$DropAudio) { $s++ }
        }
        ">>---> FINISHED FILE : took {0}`r`n" -f (Convert-TimeSpan $FileTime) | Out-Default
      }

      if ($s -eq $i) {
        $Result.SetMessage("(⌐O_O) converted all videos!", $True)
      }
      else {
        $Result.SetMessage(("t(*_*t) only converted {0} out of {1} videos!" -f $s, $i), $False)
      }
    }
    else {
      $Result.SetMessage("invalid LiteralPath!", $False)
      }
  }
  $Result.WriteMessage()
  ">>---> FINISHED CONVERSION : took {0}" -f (Convert-TimeSpan $TotalTime)
}

Function Write-Utf8([string]$LiteralPath, [string]$Include = "*") {
  $Exts = @(".config", ".cs", ".csproj", ".css", ".editorconfig", ".htaccess", ".html", ".js", ".json", ".ps1", ".psd1", ".psm1", ".txt", ".xaml", ".xml")
  $Files = Get-ChildItem -LiteralPath $LiteralPath -Include $Include -File -Recurse
  foreach($f in $Files) {
    if ($Exts -contains $f.Extension) {
      "WRITING FILE : {0}`r`n" -f $f.Name | Out-Default
      [String]$s = [IO.File]::ReadAllText($f.FullName)
      [IO.File]::WriteAllText($f.FullName, $s, [Text.Encoding]::UTF8)
    }
  }
}

Function Redo-FileNames([string]$Prefix, [string]$LiteralPath, [string]$Include = "*") {
  if (Test-Path -LiteralPath $LiteralPath -PathType Container) {
    $Files = Get-ChildItem -LiteralPath $LiteralPath -Include $Include -File
    foreach($f in $Files) {
      [int]$ms = -1
      [string]$newName = ""
      [bool]$newNameOk = $False
      while ($ms -ne $f.LastWriteTime.Millisecond -and -not $newNameOk) {
        if ($ms -lt 0) { $ms = $f.LastWriteTime.Millisecond }
        elseif ($ms -eq 1000) { $ms = 0 }
        $newName = Join-Path -Path $f.DirectoryName -ChildPath ("{0}.{1:yyyyMMdd}.{1:HHmmss}.{2:d3}{3}" -f $Prefix, $f.LastWriteTime, $ms, $f.Extension)
        $newNameOk = -not (Test-Path -LiteralPath $newName)
        $ms++
      }
      if ($newNameOk) {
        "Move-Item -LiteralPath {0} -Destination {1}" -f $f.FullName, $newName | Out-Default
        Move-Item -LiteralPath $f.FullName -Destination $newName
      } else {
        "FAILED: Can not find unused millisecond for {0}" -f $f.Name | Out-Default
      }
    }
  } else {
    "FAILED: Invalid literal path {0}" -f $LiteralPath | Out-Default
  }
}

function Copy-AllFiles(
[string]$Source,
[string]$Dest,
[string]$Include = "*.*") {
  RoboCopy $Source $Dest $Include /S /DCOPY:DT /COPY:DT /Z /NP /V /TEE /XA:SH /XD _* /R:1 /W:1
}

function Move-AllFiles(
[string]$Source,
[string]$Dest,
[string]$Include = "*.*") {
  RoboCopy $Source $Dest $Include /S /MOV /DCOPY:DT /COPY:DT /Z /NP /V /TEE /XA:SH /XD _* /R:1 /W:1
}

function Sync-AllFiles(
[string]$Source,
[string]$Dest,
[string]$Include = "*.*") {
  RoboCopy $Source $Dest $Include /S /PURGE /DCOPY:DT /COPY:DT /Z /NP /V /TEE /XA:SH /XD _* /R:1 /W:1
}

function Sync-MyFiles(
  [string]$DriveLetter,
  [switch]$SkipEmulators) {
  $Dest = $DriveLetter + ":\Music"
  Sync-AllFiles M:\ $Dest
  $Dest = $DriveLetter + ":\Porn"
  Sync-AllFiles P:\ $Dest
  if (-not $SkipEmulators) {
    $Dest = $DriveLetter + ":\Emulators"
    Sync-AllFiles X:\ $Dest
  }
}