JTLib.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 _* /XF *-poster.jpg *.nfo /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
  }
}