Skip to content

Instantly share code, notes, and snippets.

@Desani
Last active November 15, 2024 19:59
Show Gist options
  • Save Desani/129be27da7d735d7c75192ec1aa96c65 to your computer and use it in GitHub Desktop.
Save Desani/129be27da7d735d7c75192ec1aa96c65 to your computer and use it in GitHub Desktop.
3.6 - Corrected issues with determining terminal size in specific use cases. Added file encoding thanks to recommendation by LordKenmou. Fixed update script checker.

This script utilizes ffmpeg, the same tool Plex uses, to decode the video stream and captures the output for any errors during playback and sends the playback errors to a log file. So essentially it plays the video in the background faster than regular speed. It then checks the error output log file to see if there is anything inside. If ffmpeg was able to cleanly play the file, it counts as a passed file. If there is any error output, an error could be anything from a container issue, a missed frame issue, media corruption or more, it counts the file as failed. So if there would be an issue with playback and a video freezing, it would be caught by this method of checking for errors. Because of the nature of the error log, any errors that show up, even simple ones, will all count as a fail and the output is captured so you can view the error log. Some simple errors are easy to fix so I have included an auto-repair feature which attempts to re-encode the file which is able to correct some issues that would cause problems during playback. It can attempt a repair and delete the original if the new file scans successfully, as an option.

Script use: Download and save the script as ScanMedia.ps1 file on your windows computer. Download the ffmpeg windows binary and place the ffmpeg.exe in the same folder as the downloaded script or use the new built in FFMPEG downloader in the utilities menu. Open powershell navigate to the folder with the script file and type .\ScanMedia.ps1 and push enter to get started. You are also able to right click on the file once it is downloaded and select "Run with PowerShell".

Note: in order to run powershell scripts you download you might have to type the following in an elevated powershell window to run the script:
Set-ExecutionPolicy Unrestricted
Once you are done running the script you can set the Execution policy back with:
Set-ExecutionPolicy Restricted

EXAMPLES:

.\ScanMedia.ps1 -Path "C:\Media\Videos"
    Both recursively scans the folder C:\Media\Videos for errors

.\ScanMedia.ps1 -AutoRepair "C:\Media\Videos"
    To scan media and attempt to Auto Repair files use -AutoRepair

.\ScanMedia.ps1 -Rescan "C:\Media\Videos"
    To scan media and force a rescan of all files use -Rescan

.\ScanMedia.ps1 -CRF 18 -Path "C:\Media\Videos" -AutoRepair
    To Change the CRF Value used for encodes, change the following

.\ScanMedia.ps1 -Path "C:\Media\Videos" -LimitCPU -AutoRepair -RemoveOriginal -RemoveRepaired
    Scans the path C:\Media\Videos with lower CPU usage and attempts to repair the files that are found to have an issue. 
    The script will delete repaired files that don't repair properly and it will delete the original files of successfully repaired files.

Changelog:

1.O - Initial Script creation
1.1 - Separated the Error Log files as some could be very big
1.2 - Incorporated an Auto-Repair function
1.3 - Added additional logging to CSV file for easy sorting
1.4 - Corrected issue with detecting old error logs
1.5 - Added check to make sure ffmpeg exists and change calling behavior
1.6 - Implemented Join-Path to allow script to be run on non-Windows machines
1.7 - Enable file scanned history and added a -Rescan switch to force scanning
        all files.
1.8 - Found an Error with Get-ChildItem and -path where it wouldn't scan top
        level folders with [] in the name. Using -LiteralPath now.
1.9 - Fixed an issue with LiteralPath ignoring ignored extensions
1.9.1 Changed how the auto-repair function worked to duplicate plex's optimized
        version settings to create an x264 file to better address file errors. It
        can now correct more issues but not everything. Added a CRF argument so
        users can now select the quality of the repaired video files.
        Enabled AutoDelete on repaired files only that have failed checks
1.9.2 Corrected issue with not getting the file size on a video file for a PASSED
        video file. Modified the writing of the CSV into a function to remove commas
        from file names
2.0 - Changed ScanPath to Path. Corrected double logging of repaired files. Bugfixes.
        Moved log directories into a Log folder for better organization
        Condensed the method of writing output
2.1 - Correct issue with logging skipped files
2.2 - Added the ability to Limit CPU usage to half the cores detected on the machine.
        Added the ability to remove original files and all files that are found with errors.
2.3 - Corrected issue with testing a path of a file containing "[]" characters
3.0 - Major Update: Massive re-write and new feature introduction. New Menu for easier user interaction. New scanning user interface. Updated output and moved more information to the log file.
        Added the following new parameters:
            -ConfigFile
            -IdleScan
            -BypassMenu
            -GPUEncoding
            -Extension
            -ContinuousScan
        Moved the parameter overview into the user interface help menu. Created new methods and options for automation including -ConfigFile, -IdleScan, -ContinuousScan
        New built in version checker which will scan the GitHub repo on startup of the user interface and check for a new version.
        New FFMPEG utility menu which will help the user download a copy of FFMPEG and place it in the correct directory.
        New Help menu to help guide users on the functionality of the script.
        New CPU idle function that will only scan when the machine's CPU is idle and not working on other processes
        A bunch of bugfixes
3.1 - New Feature: -TrashEnabled This command will allow you to send files are found to have errors to a single directory for further analysis and some minor bugfixes
3.2 - Added a check to make sure write access was available to the ffmpeg binary
3.3 - Added arguments for FFMPEG to increase compatibility with larger file formats (Should fix Too many packets buffered for output stream error)
    - Changed file extension exclusion list to inclusion list, so now it will only target media files with the extensions provided in $include
    - Changed some wording surrounding the failed deletion prompt for better understanding
3.4 - Fixed output when scans were started on very long file paths
    - Added more feedback to let the user know a scan was in progress
3.5 - Correct issues with running inside of PowerShell ISE
3.6 - Corrected issues with determining terminal size in specific use cases. Added file encoding thanks to recommendation by LordKenmou. Fixed update script checker.

Notes:

Note: It continue to use the command line parameters for calling syntax through 3rd party applications, please include the new parameter -BypassMenu to avoid user interaction from halting the script.
Note: If resizing the PowerShell window during a scan, the text will adjust to the new screen size on an output change. So just wait and it will refresh and correct the output.
Note: The new scanning output will only show the results of the last 30 items scanned. The log file must be used to view previous scan results. A log viewing tool is recommended when viewing the log files.

GPU Encoding Note:

You are able to take advantage of encoding with GPU Acceleration on linux and windows if you compile the ffmpeg binaries to support nvec.
This has not been tested with this script and is not supported but it should still be possible.
Please refer to this link to see if your GPU is supported: https://developer.nvidia.com/ffmpeg
1: Compile FFMPEG using this cross compile script:
https://github.com/rdp/ffmpeg-windows-build-helpers
2: Verify that the new NVENC encoders are now included by using the following commands against the the compiled FFMPEG binaries:
ffmpeg -encoders
ffmpeg -decoders
3: Find and replace the following term "libx264" with "h264_nvenc" in this script
Encoding should now be utilizing the GPU instead of the CPU

Thank you to all the users for their feedback on issues and feature requests This is a new initial release of version 3.0. I would appreciate any feedback users have for issues or usability changes.

###########################################################################################
################################# ScanMedia.ps1 ###########################################
###########################################################################################
#Requires -Version 3
<#
.SYNOPSIS
A validation check on video files to make sure there are no errors with the file. The script is able to auto-repair some encoding issues with video files.
The script will keep track of all scanned files that are not deleted so that it can be setup to re-scan the same directory but only newly added files will be scanned.
Script use: Takes a PATH parameter to a folder for scanning media
.EXAMPLE
.\ScanMedia.ps1 -Path "C:\Media\Videos"
Both recursively scans the folder C:\Media\Videos for errors
.\ScanMedia.ps1 -AutoRepair "C:\Media\Videos"
To scan media and attempt to Auto Repair files use -AutoRepair
.\ScanMedia.ps1 -Rescan "C:\Media\Videos"
To scan media and force a re-scan of all files use -Rescan
.\ScanMedia.ps1 -CRF 18 -Path "C:\Media\Videos" -AutoRepair
To Change the CRF Value used for encodes, change the following
.\ScanMedia.ps1 -Path "C:\Media\Videos" -LimitCPU -AutoRepair -RemoveOriginal -RemoveRepaired
Scans the path C:\Media\Videos with lower CPU usage and attempts to repair the files that are found to have an issue.
The script will delete repaired files that don't repair properly and it will delete the original files of successfully repaired files.
.NOTES
Changelog:
1.O - Initial Script creation
1.1 - Separated the Error Log files as some could be very big
1.2 - Incorporated an Auto-Repair function
1.3 - Added additional logging to CSV file for easy sorting
1.4 - Corrected issue with detecting old error logs
1.5 - Added check to make sure ffmpeg exists and change calling behavior
1.6 - Implemented Join-Path to allow script to be run on non-Windows machines
1.7 - Enable file scanned history and added a -Rescan switch to force scanning
all files.
1.8 - Found an Error with Get-ChildItem and -path where it wouldn't scan top
level folders with [] in the name. Using -LiteralPath now.
1.9 - Fixed an issue with LiteralPath ignoring ignored extensions
1.9.1 Changed how the auto-repair function worked to duplicate plex's optimized
version settings to create an x264 file to better address file errors. It
can now correct more issues but not everything. Added a CRF argument so
users can now select the quality of the repaired video files.
Enabled AutoDelete on repaired files only that have failed checks
1.9.2 Corrected issue with not getting the file size on a video file for a PASSED
video file. Modified the writing of the CSV into a function to remove commas
from file names
2.0 - Changed ScanPath to Path. Corrected double logging of repaired files. Bugfixes.
Moved log directories into a Log folder for better organization
Condensed the method of writing output
2.1 - Correct issue with logging skipped files
2.2 - Added the ability to Limit CPU usage to half the cores detected on the machine.
Added the ability to remove original files and all files that are found with errors.
2.3 - Corrected issue with testing a path of a file containing "[]" characters
3.0 - Major Update: Massive re-write and new feature introduction. New Menu for easier user interaction. New scanning user interface. Updated output and moved more information to the log file.
Added the following new parameters:
-ConfigFile
-IdleScan
-BypassMenu
-GPUEncoding
-Extension
-ContinuousScan
Moved the parameter overview into the user interface help menu. Created new methods and options for automation including -ConfigFile, -IdleScan, -ContinuousScan
New built in version checker which will scan the GitHub repo on startup of the user interface and check for a new version.
New FFMPEG utility menu which will help the user download a copy of FFMPEG and place it in the correct directory.
New Help menu to help guide users on the functionality of the script.
New CPU idle function that will only scan when the machine's CPU is idle and not working on other processes
A bunch of bugfixes
3.1 - New Feature: -TrashEnabled This command will allow you to send files are found to have errors to a single directory for further analysis and some minor bugfixes
3.2 - Added a check to make sure write access was available to the ffmpeg binary
3.3 - Added arguments for FFMPEG to increase compatibility with larger file formats (Should fix Too many packets buffered for output stream error)
- Changed file extension exclusion list to inclusion list, so now it will only target media files with the extensions provided in $include
- Changed some wording surrounding the failed deletion prompt for better understanding
3.4 - Fixed output when scans were started on very long file paths
- Added more feedback to let the user know a scan was in progress
3.5 - Correct issues with running inside of PowerShell ISE
Note: It continue to use the command line parameters for calling syntax through 3rd party applications, please include the new parameter -BypassMenu to avoid user interaction from halting the script.
Note: If resizing the PowerShell window during a scan, the text will adjust to the new screen size on an output change. So just wait and it will refresh and correct the output.
Note: The new scanning output will only show the results of the last 30 items scanned. The log file must be used to view previous scan results. A log viewing tool is recommended when viewing the log files.
3.6 - Corrected issues with determining terminal size in specific use cases. Added file encoding thanks to recommendation by LordKenmou. Fixed update script checker.
You are able to take advantage of encoding with GPU Acceleration on linux and windows if you compile the ffmpeg binaries to support nvec.
This has not been tested with this script and is not supported but it should still be possible.
Please refer to this link to see if your GPU is supported: https://developer.nvidia.com/ffmpeg
1: Compile FFMPEG using this cross compile script:
https://github.com/rdp/ffmpeg-windows-build-helpers
2: Verify that the new NVENC encoders are now included by using the following commands against the the compiled FFMPEG binaries:
ffmpeg -encoders
ffmpeg -decoders
3: Find and replace the following term "libx264" with "h264_nvenc" in this script
Encoding should now be utilizing the GPU instead of the CPU
Thank you to all the users for their feedback on issues and feature requests
#>
[CmdletBinding()]
Param (
[String]$Path,
[String]$ConfigFile,
[switch]$AutoRepair,
[switch]$Rescan,
[Int]$CRF = 21,
[switch]$LimitCPU,
[switch]$RemoveAll,
[switch]$RemoveRepaired,
[switch]$RemoveOriginal,
[switch]$IAcceptResponsibility,
[switch]$IdleScan,
[switch]$BypassMenu,
[switch]$GPUEncoding,
[String]$Extension = "mp4",
[switch]$ContinuousScan,
[String]$TrashEnabled
)
# Included file types. Add more extensions that you want included with the media scan
$include = ".mp4", ".avi", ".mkv", ".m4v", ".ogg", ".mpeg", ".wmv", ".flv", ".mov", ".ogv", ".mpg", ".m4a", ".asf", ".ts", ".divx"
# Script Variables
[float]$ScriptVersion = 3.6
$fullName = ""
$fileName = ""
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$scanHistory = Join-Path -path $scriptPath -childpath "ScanHistory.txt"
$Date = "$((Get-Date).ToString('yyyy-MM-dd'))"
$LogDir = Join-Path -path $scriptPath -childpath "log"
$LogPath = Join-Path -path $LogDir -childpath "log_$Date"
$ffmpegLog = Join-Path -path $LogPath -childpath "ffmpegerror.log"
$Logfile = Join-Path -path $LogPath -childpath "results.log"
$CSVfile = Join-Path -path $LogPath -childpath "results.csv"
$env:PATH = $env:PATH + ";."
$global:OrigLength = $null
$global:RepairLength = $null
$global:RepairedFile = $null
$global:OriginalFile = $null
$global:OriginalRemoved = $false
$global:CPULimt = 1
$global:VideoLibrary = "libx264"
# Convert all Supplied Parameters to Global variables for use in all functions
$global:Path = $Path
$global:ConfigFile = $ConfigFile
$global:AutoRepair = $AutoRepair
$global:Rescan = $Rescan
$global:CRF = $CRF
$global:LimitCPU = $LimitCPU
$global:RemoveAll = $RemoveAll
$global:RemoveRepaired = $RemoveRepaired
$global:RemoveOriginal = $RemoveOriginal
$global:IAcceptResponsibility = $IAcceptResponsibility
$global:IdleScan = $IdleScan
$global:GPUEncoding = $GPUEncoding
$global:RepairVFileExtension = $Extension
$global:ContinuousScan = $ContinuousScan
$global:TrashEnabled = $TrashEnabled
# Function to write lines to a log file
Function LogWrite {
Param (
[String]$LogString,
[String]$Colour,
[switch]$Log)
If (!($log)) {
If ($Colour) {
Write-Host "$(Get-Date): $LogString" -ForegroundColor $Colour
}
Else {
Write-Host "$(Get-Date): $LogString"
}
}
Add-content $LogFile -value "$(Get-Date): $LogString" -Force
}
Function Write-PauseMessage {
param(
[String]$message)
# Check if running Powershell ISE
if ($psISE) {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.MessageBox]::Show("$message")
}
else {
Write-Host "$message" -ForegroundColor Yellow
$null = (Get-Host).UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
}
function Write-HostCenter {
param(
[String]$Message,
[String]$Colour)
try {
If ($Colour) {
Write-Host ("{0}{1}" -f (' ' * (([Math]::Max(0, (Get-Host).UI.RawUI.BufferSize.Width / 2) - [Math]::Floor($Message.Length / 2)))), $Message) -ForegroundColor $Colour
} Else {
Write-Host ("{0}{1}" -f (' ' * (([Math]::Max(0, (Get-Host).UI.RawUI.BufferSize.Width / 2) - [Math]::Floor($Message.Length / 2)))), $Message)
}
}
# Catch exception if unable to determine terminal size and just write messages normally
catch {
If ($Colour) {
Write-Host ($Message) -ForegroundColor $Colour
} Else {
Write-Host ($Message)
}
}
}
# Function to attempt to repair a video object with ffmpeg.exe if -AutoRepair is selected
Function RepairFile {
Param ([Object]$VideoFile)
# Get only the file name of the Video Object
$RepairFileName = $VideoFile.Name
# Get only the file name without the extension of the video object and remove the brackets
$RepairFileBaseName = $VideoFile.BaseName -replace '[][]', ''
# Get all the variables to create the another file in the same directory with _repaired in the filename
$dir = $VideoFile.DirectoryName
$RepairVFileName = "$RepairFileBaseName" + "_repaired.$global:RepairVFileExtension"
$RepairFullName = Join-Path -path $dir -ChildPath $RepairVFileName
If (!(Test-Path -LiteralPath $RepairFullName)) {
LogWrite "Creating Repair File: $RepairFullName" -Log
$RepairFileName = $RepairFileName -replace '[][]', ''
$RepairFileName = $RepairFileName + "_repair.log"
$errorLog = Join-Path -path $LogPath -childpath $RepairFileName
# Attempt to repair the video file with ffmpeg
If ($global:LimitCPU) {
ffmpeg.exe -y -i $VideoFile.FullName -max_muxing_queue_size 1024 -c:v $global:VideoLibrary -crf $global:CRF -c:a aac -q:a 100 -strict -2 -threads $global:CPULimt -movflags faststart -level 41 $RepairFullName 2> $errorLog
}
Else {
ffmpeg.exe -y -i $VideoFile.FullName -max_muxing_queue_size 1024 -c:v $global:VideoLibrary -crf $global:CRF -c:a aac -q:a 100 -strict -2 -movflags faststart -level 41 $RepairFullName 2> $errorLog
}
# Check to see if the repaired file still has errors
$RepairFile = Get-Item $RepairFullName
# Check the scan history file to see if the file has been scanned
$scanHistoryCheck = (Get-Content $scanHistory | Select-String -pattern $RepairFile.FullName -SimpleMatch)
If ($RepairFile.Name.Length -lt 1) {
LogWrite "There was an issue creating the repaired file with FFMPEG" -Log
}
ElseIf ($null -ne $scanHistoryCheck) {
CheckFile $RepairFile -AutoRepaired -RescanVideo
}
Else {
CheckFile $RepairFile -AutoRepaired
}
} Else {
LogWrite "Skipping repair: Repaired File already exists." -Log
LogWrite "Repair file found: $RepairFullName" -Log
$OriginalFile = Get-ChildItem -LiteralPath $global:OriginalFile.FullName
Update-VideoArray -VideoFile $OriginalFile -Status "Failed" -Update
Write-ScanOutput
}
}
# Function to check a supplied video object with ffmpeg.exe
Function CheckFile {
Param (
[Object]$VideoFile,
[switch]$AutoRepaired,
[switch]$RescanVideo
)
# If the file is a new file being check, clear out the variables
If (!($AutoRepaired)) {
$global:RepairLength = $null
$global:RepairedFile = $null
$global:OrigLength = $null
$global:OriginalFile = $null
}
# Reset the Deleted value
$Deleted = $false
# Get the full name of the Video Object with path included
$fullName = $VideoFile.FullName
# Get only the name of the video file
$fileName = $VideoFile.Name
# Counter for the number of files that have been scanned
$global:VideoCount = $global:VideoCount + 1
# Save the length and object of the video file to make sure the repaired video file length matches original
If ($AutoRepaired) {
$global:RepairLength = GetLength $VideoFile
$global:RepairedFile = $VideoFile
}
Else {
$global:OrigLength = GetLength $VideoFile
$global:OriginalFile = $VideoFile
}
If ($RescanVideo) {
Update-VideoArray -VideoFile $VideoFile -Status "Re-Scanning"
} Else {
Update-VideoArray -VideoFile $VideoFile -Status "Scanning"
}
LogWrite "Scanning File: $fullName" -Log
Write-ScanOutput -Scanning
If (Test-Path $ffmpegLog) {
Try {
Remove-Item -LiteralPath $ffmpegLog -Force
} Catch {
$ErrorMessage = $_.Exception.Message
LogWrite "Error while removing old FFMPEG Log: $ErrorMessage" -Colour "Red"
LogWrite "Scan cannot continue without the ability to modify: $ffmpegLog"
Exit 1
}
}
# Scan the file with FFMPEG
If ($global:LimitCPU) {
ffmpeg.exe -v error -i $fullName -max_muxing_queue_size 1024 -f null -threads $global:CPULimt - >$ffmpegLog 2>&1
}
Else {
ffmpeg.exe -v error -i $fullName -max_muxing_queue_size 1024 -f null - >$ffmpegLog 2>&1
}
# Check to see if the ffmpeg error log was empty
If ($Null -eq (Get-Content $ffmpegLog)) {
# Get information to log to the CSV file
$FileSize = "{0:N2}" -f (($VideoFile | Measure-Object -property length -sum ).sum / 1MB)
$Date = $((Get-Date).ToString('yyyy-MM-dd'))
# If the file is an Auto-Repaired File
If ($AutoRepaired) {
# File is only repaired successfully if the video file length matches original
If (($global:RepairLength -eq $global:OrigLength) -and ($Null -ne $global:RepairLength)) {
LogWrite "File Repaired Successfully: $fullName" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Passed" -Update
Write-ScanOutput
Write-CSV -VidfileName $fileName -VidfilePath $fullName -TestResults "Passed" -Date $Date -VidFileSize $FileSize -VidLength $RepairLength
$OriginalFile = Get-ChildItem -LiteralPath $global:OriginalFile.FullName
Update-VideoArray -VideoFile $OriginalFile -Status "Failed" -Update
Write-ScanOutput
If ($global:RemoveOriginal -or $global:RemoveAll -or $global:TrashEnabled) {
# Remove the original file and rename the repair file
Try {
If ($global:TrashEnabled) {
LogWrite "Moving the original file: $($global:OriginalFile.Name)" -Log
Move-Item -LiteralPath $global:OriginalFile.FullName -Destination $global:TrashEnabled -Force
Update-VideoArray -VideoFile $OriginalFile -Status "Moved" -Update
} Else {
LogWrite "Deleting the original file: $($global:OriginalFile.Name)" -Log
Get-ChildItem -LiteralPath $global:OriginalFile.FullName -File | Remove-Item -Force -ErrorAction Stop
Update-VideoArray -VideoFile $OriginalFile -Status "Deleted" -Update
}
Write-ScanOutput
LogWrite "Renaming Repaired file to Original File" -Log
$NewFileName = $global:OriginalFile.BaseName + $global:RepairedFile.Extension
Update-VideoArray -VideoFile $VideoFile -Status "Passed:Renamed" -Update
Write-ScanOutput
Rename-Item -Path $global:RepairedFile.FullName -NewName $NewFileName -ErrorAction Stop
$Deleted = $true
$global:OriginalRemoved = $true
}
Catch {
$ErrorMessage = $_.Exception.Message
LogWrite "Error: $ErrorMessage" -Log
}
}
}
Else {
LogWrite "ERROR: Error Found in Repaired File. Video Length does not match Original: $fullName" -Log
LogWrite "Estimate of error location in Original File: $global:RepairLength" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Failed" -Update
$OriginalFile = Get-ChildItem -LiteralPath $global:OriginalFile.FullName
Update-VideoArray -VideoFile $OriginalFile -Status "Failed" -Update
Write-ScanOutput
$global:RepairedErrorList.Add($fileName) | Out-Null
Write-CSV -VidfileName $fileName -VidfilePath $fullName -TestResults "Failed" -Date $Date -VidFileSize $FileSize -VidLength $RepairLength
If ($global:RemoveAll -or $global:RemoveRepaired) {
LogWrite "Deleting Repaired File: $fileName" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Deleted" -Update
Write-ScanOutput
Get-ChildItem -LiteralPath $VideoFile.FullName -File | Remove-Item -Force -ErrorAction Stop
$Deleted = $true
}
}
# If the file is not an Auto-Repaired file
}
Else {
LogWrite "Scanned Successfully: $fullName" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Passed" -Update
Write-ScanOutput
Write-CSV -VidfileName $fileName -VidfilePath $fullName -TestResults "Passed" -Date $Date -VidFileSize $FileSize -VidLength $OrigLength
}
# If the video file is not a re-scanned file, add it to the scan history
If (!$RescanVideo) {
#If the video has been deleted, do not log the file name in scanHistory file for scanning again at a later time.
If (!($Deleted)) {
Add-content $scanHistory $fullName -Encoding utf8
}
}
# Check to see if the ffmpeg error log was not empty
}
Elseif ($Null -ne (Get-Content $ffmpegLog)) {
# Get information to log to the CSV file
$FileSize = "{0:N2}" -f (($VideoFile | Measure-Object -property length -sum ).sum / 1MB)
$Date = $((Get-Date).ToString('yyyy-MM-dd'))
If ($AutoRepaired) {
LogWrite "ERROR: Error found in Repaired File: $fullName" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Failed" -Update
Write-ScanOutput
Write-CSV -VidfileName $fileName -VidfilePath $fullName -TestResults "Failed" -Date $Date -VidFileSize $FileSize -VidLength $RepairLength
$global:RepairedErrorList.Add($fileName) | Out-Null
If ($global:RemoveAll -or $global:RemoveRepaired) {
LogWrite "Deleting Repaired File: $fileName" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Deleted" -Update
Write-ScanOutput
Get-ChildItem -LiteralPath $VideoFile.FullName -File | Remove-Item -Force -ErrorAction Stop
$Deleted = $true
}
}
else {
LogWrite "ERROR: Error found: $fullName" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Failed" -Update
Write-ScanOutput
$global:ErrorList.Add($fileName) | Out-Null
Write-CSV -VidfileName $fileName -VidfilePath $fullName -TestResults "Failed" -Date $Date -VidFileSize $FileSize -VidLength $OrigLength
}
$fileName = $fileName -replace '[][]', ''
$fileName = $fileName + "_error.log"
$errorLog = Join-Path -path $LogPath -ChildPath $fileName
If (Test-Path $errorLog) {
LogWrite "Removing Error Log : $errorLog" -Log
Remove-Item -Path $errorLog
}
Try {
Rename-Item $ffmpegLog $errorLog
}
Catch {
$ErrorMessage = $_.Exception.Message
LogWrite "ERROR: Failed to rename the ffmpeg log: $ErrorMessage" -Log
LogWrite $ErrorMessage -Log
$errorLog = Join-Path -path $LogPath -childpath "GenericError.log"
Get-Content $ffmpegLog | Add-Content $errorLog -Encoding utf8
Remove-item $ffmpegLog
}
# Only continue if the file is not already flagged as auto-repaired
if (!$AutoRepaired) {
# Only continue if auto-repair is selected
if ($global:AutoRepair) {
Update-VideoArray -VideoFile $VideoFile -Status "Repairing" -Update
Write-ScanOutput -Repairing
LogWrite "Attempting to Repair : $VideoFile" -Log
# Supply the video object to the repair function
RepairFile $VideoFile
}
}
If ($global:RemoveAll -or $global:TrashEnabled) {
# Remove the original file even if the repair was not successful
If (!($AutoRepaired)) {
If ($global:OriginalRemoved -eq $false) {
If ($global:TrashEnabled) {
LogWrite "Moving the original file: $($VideoFile.Name)" -Log
Move-Item -LiteralPath $VideoFile.FullName -Destination $global:TrashEnabled -Force
Update-VideoArray -VideoFile $VideoFile -Status "Moved" -Update
Write-ScanOutput
$Deleted = $true
} Else {
LogWrite "Deleting the original file: $($VideoFile.Name)" -Log
Update-VideoArray -VideoFile $VideoFile -Status "Deleted" -Update
Write-ScanOutput
Get-ChildItem -LiteralPath $VideoFile.FullName -File | Remove-Item -Force -ErrorAction Stop
$Deleted = $true
}
}
$global:OriginalRemoved = $false
}
}
# If the video file is not a re-scanned file, add it to the scan history
If (!$RescanVideo) {
#If the video has been deleted, do not log the file name in scanHistory file for scanning again at a later time.
If (!($Deleted)) {
# Get the updated Original file name if the file was repaired
$NewFileName = Join-Path -Path $global:OriginalFile.DirectoryName -ChildPath $global:OriginalFile.BaseName
$NewFileName = $NewFileName + $global:RepairedFile.Extension
Try {
$NewFileObject = Get-ChildItem -LiteralPath $NewFileName -File -ErrorAction Stop
}
Catch {
# If no matches are found, do nothing
}
If ($NewFileObject) {
Add-Content $scanHistory $NewFileObject.FullName -Encoding utf8
}
Else {
Add-Content $scanHistory $fullName -Encoding utf8
}
}
}
}
}
Function GetLength {
Param ([Object]$VideoFile)
$LengthColumn = 27
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($VideoFile.DirectoryName)
$objFile = $objFolder.ParseName($VideoFile.Name)
$VideoLength = $objFolder.GetDetailsOf($objFile, $LengthColumn)
return $VideoLength
}
Function Write-CSV {
Param (
[String]$VidfileName,
[String]$VidfilePath,
[String]$TestResults,
[String]$Date,
[String]$VidFileSize,
[String]$VidLength
)
$VidfileName = $VidfileName -replace ',', ''
$VidfilePath = $VidfilePath -replace ',', ''
$VidFileSize = $VidFileSize -replace ',', ''
$CSVContent = "$VidfileName,$TestResults,$Date,$VidFileSize,$VidLength,$VidfilePath"
try {
Add-content $CSVfile -Value $CSVContent -Encoding utf8
}
catch {
$ErrorMessage = $_.Exception.Message
LogWrite "ERROR: Failed to log the result to the csv file $CSVfile" -Colour "Red"
LogWrite $ErrorMessage -Log
}
}
Function Get-YesNoResponse {
Param (
[String]$Message
)
$MenuChoice = "n"
Write-Host $Message -ForegroundColor Cyan
while ($MenuChoice -ne "y") {
Write-Host "Choice [Default: n]: " -ForegroundColor Cyan -NoNewline
$MenuChoice = Read-Host
If ([string]::IsNullOrEmpty($MenuChoice)) {
$MenuChoice = "n"
}
If ($MenuChoice -eq "n") {
return $false
}
ElseIf ($MenuChoice -eq "y") {
return $true
}
}
}
Function Get-MenuResponse {
Param (
[String]$Message,
[String]$Options
)
$MenuChoice = 0
Write-Host $Message -ForegroundColor Cyan
$OptionArray = $Options.Split(";")
$OptionNumber = 1
ForEach ($Option in $OptionArray) {
Write-Host " $($OptionNumber): $Option"
$OptionNumber++
}
while ($MenuChoice -lt 1 -or $MenuChoice -gt ($OptionNumber - 1)) {
Write-Host "Choice Number: " -ForegroundColor Cyan -NoNewline
$MenuChoice = Read-Host
}
return $MenuChoice
}
Function Show-Menu {
$MenuChoice = 0
Write-Host ""
Write-Host "-------------------------------------------------------" -ForegroundColor Yellow
Write-Host " Main Menu" -ForegroundColor Yellow
Write-Host "-------------------------------------------------------" -ForegroundColor Yellow
Write-Host " 1: Quick Scan"
Write-Host " 2: Default Scan"
Write-Host " 3: Background Scan"
Write-Host " 4: Create a Configuration file for automated scanning"
Write-Host " 5: FFMPEG Utilities"
Write-Host " 6: Help Menu"
Write-Host " 7: Quit Scanning"
Write-Host ""
Write-Host "Please make a selection from the Menu Choices." -ForegroundColor Cyan
while ($MenuChoice -lt 1 -or $MenuChoice -gt 7) {
Write-Host "Choice Number: " -ForegroundColor Cyan -NoNewline
$MenuChoice = Read-Host
}
switch ($MenuChoice) {
1 {
Get-MediaPath -ScanType "Quick"
Select-ScanOptions -ScanType "Quick"
}
2 {
Get-MediaPath -ScanType "Default"
Select-ScanOptions -ScanType "Default"
}
3 {
Get-MediaPath -ScanType "Background"
Select-ScanOptions -ScanType "Background"
}
4 {
New-Configuration
}
5 {
Show-FFMPEGUtilities
}
6 {
Show-Help
}
7 {
Exit
}
}
}
Function Get-OnlineVersion {
# Update Check function
Try {
$WebResponse = Invoke-WebRequest "https://gist.githubusercontent.com/Desani/129be27da7d735d7c75192ec1aa96c65/raw/ScanMedia.ps1" -UseBasicParsing
ForEach ($Line in $WebResponse.Content.Split([Environment]::NewLine)) {
$Line -match '(?:^|\W)ScriptVersion = [0-9].[0-9](?:$|\W)' | Out-Null
If (!([String]::IsNullOrEmpty($Matches.0)))
{
$VersionString = $Matches.0
}
}
$pos = $VersionString.IndexOf("=")
[float]$OnlineVersion = $VersionString.Substring($pos + 1)
If ($OnlineVersion -gt $ScriptVersion) {
Write-Host ""
Write-HostCenter "*****************************************************************************" -Colour Green
Write-HostCenter "A new version is available for download. New Version: $OnlineVersion" -Colour Green
Write-HostCenter "Please update your script to the latest version at your earliest convenience" -Colour Green
Write-HostCenter "*****************************************************************************" -Colour Green
Write-Host ""
}
}
Catch {
$ErrorMessage = $_.Exception.Message
Write-Verbose "There was an issue checking online for new version"
Write-Verbose $ErrorMessage
}
}
Function Get-MediaPath {
Param (
[String]$ScanType
)
Clear-Host
Write-Host ""
Write-Host "-------------------------------------------------------" -ForegroundColor Yellow
Write-Host " $ScanType Scan" -ForegroundColor Yellow
Write-Host "-------------------------------------------------------" -ForegroundColor Yellow
Write-Host ""
If ($global:Path) {
If (!(Test-Path $global:Path)) {
$PathDir = "InvalidPath:\"
Write-Host "The supplied Path folder for scanning files is invalid: $global:Path" -ForegroundColor Red
While (!(Test-Path -LiteralPath $PathDir)) {
Write-Host "Please enter the path that you would like to scan media files:" -ForegroundColor Cyan
Write-Host "Note: This path should be always accessible by the System so that files can be scanned" -ForegroundColor Yellow
Write-Host "Example: X:\Movies or Z:\Videos\Render.mp4"
while ($PathDir -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$PathDir = Read-Host
}
If (!(Test-Path -LiteralPath $PathDir)) {
Write-Host "Error: The path supplied is invalid."
$PathDir = "InvalidPath:\"
}
}
$global:Path = $PathDir
}
Write-Host "Using supplied path: $global:Path" -ForegroundColor Cyan
}
Else {
$global:Path = "InvalidPath:\"
While (!(Test-Path -LiteralPath $global:Path)) {
Write-Host "Please enter the path that you would like to use to scan:" -ForegroundColor Cyan
Write-Host "Example: X:\Movies or Z:\Videos\Render.mp4"
while ($global:Path -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$global:Path = Read-Host
}
If (!(Test-Path -LiteralPath $global:Path)) {
Write-Host "Error: The path supplied is invalid."
$global:Path = "InvalidPath:\"
}
}
}
If ($global:TrashEnabled) {
If (!(Test-Path $global:TrashEnabled)) {
$TrashDir = "InvalidPath:\"
Write-Host "The supplied Trash folder for corrupt video files is invalid: $global:TrashEnabled" -ForegroundColor Red
While (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Please enter the path that you would like to store videos with errors:" -ForegroundColor Cyan
Write-Host "Note: This path should be always accessible by the System so that files can be moved" -ForegroundColor Yellow
Write-Host "Example: C:\BadVideos or D:\Media\Temp"
while ($TrashDir -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$TrashDir = Read-Host
}
If (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Error: The path supplied is invalid."
$TrashDir = "InvalidPath:\"
}
If ($TrashDir.Contains("[") -or $TrashDir.Contains("]")) {
Write-Host "Error: The path supplied is contains the characters [ or ]. Please supply a path that does not contain square brackets."
$TrashDir = "InvalidPath:\"
}
}
$global:TrashEnabled = $TrashDir
}
Write-Host "Using the Trash Directory: $global:TrashEnabled"
}
}
Function Select-ScanOptions {
Param (
[String]$ScanType
)
switch ($ScanType) {
{ $_ -match "Quick" } {
$global:Rescan = $true
}
{ $_ -match "Default" } {
If (!($global:Rescan)) {
$global:Rescan = Get-YesNoResponse "Would you like to re-scan files that have already been scanned? (y/n)"
}
If (!($global:LimitCPU)) {
$global:LimitCPU = Get-YesNoResponse "Would you like to Limit FFMPEG CPU's core usage? (y/n)"
}
If (!($global:AutoRepair)) {
$global:AutoRepair = Get-YesNoResponse "Would you like to attempt to auto-repair damaged files? (y/n)"
}
If ((!($global:RemoveAll)) -and (!($global:RemoveRepaired)) -and (!($global:RemoveOriginal))) {
$MenuChoice = Get-YesNoResponse "Would you like to delete failed files? (y/n)"
If ($MenuChoice -eq "y") {
$Choice = Get-MenuResponse -Message "What failed files would you like to remove?" -Options "Failed Repaired Files;Failed Original Files;All Failed Files;No Deletions"
switch ($Choice) {
1 { $global:RemoveRepaired = $true }
2 { $global:RemoveOriginal = $true }
3 { $global:RemoveAll = $true }
4 { }
}
}
}
If (!($global:TrashEnabled)) {
$TrashEnabled = Get-YesNoResponse "Would you like to enabled the trash folder and send files with errors to this folder? (y/n)"
If ($TrashEnabled) {
$TrashDir = "InvalidPath:\"
While (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Please enter the path that you would like to store videos with errors:" -ForegroundColor Cyan
Write-Host "Note: This path should be always accessible by the System so that files can be moved" -ForegroundColor Yellow
Write-Host "Example: C:\BadVideos or D:\Media\Temp"
while ($TrashDir -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$TrashDir = Read-Host
}
If (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Error: The path supplied is invalid."
$TrashDir = "InvalidPath:\"
}
If ($TrashDir.Contains("[") -or $TrashDir.Contains("]")) {
Write-Host "Error: The path supplied is contains the characters [ or ]. Please supply a path that does not contain square brackets."
$TrashDir = "InvalidPath:\"
}
}
$global:TrashEnabled = $TrashDir
}
}
}
{ $_ -match "Background" } {
# Auto Limit CPU usage for a background scan
$global:LimitCPU = $true
If (!($global:IdleScan)) {
$global:IdleScan = Get-YesNoResponse "Would you like to limit scanning to only when the CPU is not being used by other processes? (y/n)"
}
If (!($global:Rescan)) {
$global:Rescan = Get-YesNoResponse "Would you like to re-scan files that have already been scanned? (y/n)"
}
If (!($global:AutoRepair)) {
$global:AutoRepair = Get-YesNoResponse "Would you like to attempt to auto-repair damaged files? (y/n)"
}
If ((!($global:RemoveAll)) -and (!($global:RemoveRepaired)) -and (!($global:RemoveOriginal))) {
$MenuChoice = Get-YesNoResponse "Would you like to delete failed files? (y/n)"
If ($MenuChoice -eq "y") {
$Choice = Get-MenuResponse -Message "What failed files would you like to remove?" -Options "Failed Repaired Files;Failed Original Files;All Failed Files;No Deletions"
switch ($Choice) {
1 { $global:RemoveRepaired = $true }
2 { $global:RemoveOriginal = $true }
3 { $global:RemoveAll = $true }
4 { }
}
}
}
If (!($global:ContinuousScan)) {
$global:ContinuousScan = Get-YesNoResponse "Would you like to setup the scan to run continuously in the background? (y/n)"
}
If (!($global:TrashEnabled)) {
$TrashEnabled = Get-YesNoResponse "Would you like to enabled the trash folder and send files with errors to this folder? (y/n)"
If ($TrashEnabled) {
$TrashDir = "InvalidPath:\"
While (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Please enter the path that you would like to store videos with errors:" -ForegroundColor Cyan
Write-Host "Note: This path should be always accessible by the System so that files can be moved" -ForegroundColor Yellow
Write-Host "Example: C:\BadVideos or D:\Media\Temp"
while ($TrashDir -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$TrashDir = Read-Host
}
If (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Error: The path supplied is invalid."
$TrashDir = "InvalidPath:\"
}
If ($TrashDir.Contains("[") -or $TrashDir.Contains("]")) {
Write-Host "Error: The path supplied is contains the characters [ or ]. Please supply a path that does not contain square brackets."
$TrashDir = "InvalidPath:\"
}
}
$global:TrashEnabled = $TrashDir
}
}
}
}
New-MediaScan
}
Function New-Configuration {
$ConfigList = New-Object System.Collections.ArrayList
Clear-Host
Write-Host ""
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-HostCenter "Write a new Configuration" -Colour Yellow
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-Host ""
Write-Host (Write-WrapString "In this section you will be given a series of questions that will be used to generate a configuration JSON file. This file can be used with the -ConfigFile parameter to setup automated scanning.")
$Path = "InvalidPath:\"
While (!(Test-Path -LiteralPath $Path)) {
Write-Host "Please enter the path that you would like to use to scan:" -ForegroundColor Cyan
Write-Host "Example: X:\Movies or Z:\Videos\Render.mp4"
while ($Path -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$Path = Read-Host
}
If (!(Test-Path -LiteralPath $Path)) {
Write-Host "Error: The path supplied is invalid."
$Path = "InvalidPath:\"
}
}
$ConfigItem = New-Object PSObject -Property @{Name="Path";Value=$Path}
$ConfigList.Add($ConfigItem) | Out-Null
$LogDir = "InvalidPath:\"
While (!(Test-Path -LiteralPath $LogDir)) {
Write-Host "Please enter the path that you would like to store the log files:" -ForegroundColor Cyan
Write-Host "Note: This path should be always accessible by the System so that errors can be logged" -ForegroundColor Yellow
Write-Host "Example: C:\ScanLogs or D:\Media\ScanLogs"
while ($LogDir -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$LogDir = Read-Host
}
If (!(Test-Path -LiteralPath $LogDir)) {
Write-Host "Error: The path supplied is invalid."
$LogDir = "InvalidPath:\"
}
If ($LogDir.Contains("[") -or $LogDir.Contains("]")) {
Write-Host "Error: The path supplied is contains the characters [ or ]. Please supply a path that does not contain square brackets."
$LogDir = "InvalidPath:\"
}
}
$ConfigItem = New-Object PSObject -Property @{Name="LogDir";Value=$LogDir}
$ConfigList.Add($ConfigItem) | Out-Null
$IdleScan = Get-YesNoResponse "Would you like to limit scanning to only when the CPU is not being used by other processes? (y/n)"
$ConfigItem = New-Object PSObject -Property @{Name="IdleScan";Value=$IdleScan}
$ConfigList.Add($ConfigItem) | Out-Null
$LimitCPU = Get-YesNoResponse "Would you like to Limit FFMPEG CPU's core usage? (y/n)"
$ConfigItem = New-Object PSObject -Property @{Name="LimitCPU";Value=$LimitCPU}
$ConfigList.Add($ConfigItem) | Out-Null
$Rescan = Get-YesNoResponse "Would you like to re-scan files that have already been scanned? (y/n)"
$ConfigItem = New-Object PSObject -Property @{Name="Rescan";Value=$Rescan}
$ConfigList.Add($ConfigItem) | Out-Null
$AutoRepair = Get-YesNoResponse "Would you like to attempt to auto-repair damaged files? (y/n)"
$ConfigItem = New-Object PSObject -Property @{Name="AutoRepair";Value=$AutoRepair}
$ConfigList.Add($ConfigItem) | Out-Null
If ($AutoRepair) {
$CRF = $null
Write-Host "What CRF value would you like to use for Repaired Video Encodes?" -ForegroundColor Cyan
Write-Host "Must be a value between 0 and 51"
While (($CRF -lt 0) -or ($CRF -gt 51)) {
Write-Host "Choice Number [Default: 21]: " -ForegroundColor Cyan -NoNewline
$CRF = Read-Host
If ([string]::IsNullOrEmpty($CRF)) {
$CRF = 21
}
}
} Else {
$CRF = 21
}
$ConfigItem = New-Object PSObject -Property @{Name="CRF";Value=$CRF}
$ConfigList.Add($ConfigItem) | Out-Null
$Delete = Get-YesNoResponse "Would you like to delete failed files? (y/n)"
If ($Delete -eq "y") {
$Choice = Get-MenuResponse -Message "What failed files would you like to remove?" -Options "Failed Repaired Files;Failed Original Files;All Failed Files;No Deletions"
switch ($Choice) {
1 { $RemoveRepaired = $true }
2 { $RemoveOriginal = $true }
3 { $RemoveAll = $true }
4 { }
}
} Else {
$RemoveRepaired = $false
$RemoveOriginal = $false
$RemoveAll = $false
}
If ($RemoveRepaired -or $RemoveAll -or $RemoveOriginal) {
$Acceptance = ""
Write-Host "************************* WARNING *******************************" -ForegroundColor Red
Write-Host "The commands supplied have the ability to delete original files" -ForegroundColor Yellow
Write-Host "A backup is highly recommended when supplying these commands" -ForegroundColor Yellow
Write-Host "Please type the following confirm you understand: IACCEPT" -ForegroundColor Yellow
Write-Host "************************* WARNING *******************************" -ForegroundColor Red
while ($Acceptance -ne "IACCEPT") {
Write-Host "Please Type the above command: " -ForegroundColor Magenta -NoNewline
$Acceptance = Read-Host
}
}
$ConfigItem = New-Object PSObject -Property @{Name="RemoveRepaired";Value=$RemoveRepaired}
$ConfigList.Add($ConfigItem) | Out-Null
$ConfigItem = New-Object PSObject -Property @{Name="RemoveOriginal";Value=$RemoveOriginal}
$ConfigList.Add($ConfigItem) | Out-Null
$ConfigItem = New-Object PSObject -Property @{Name="RemoveAll";Value=$RemoveAll}
$ConfigList.Add($ConfigItem) | Out-Null
$ContinuousScan = Get-YesNoResponse "Would you like to setup the scan to run continuously in the background? (y/n)"
$ConfigItem = New-Object PSObject -Property @{Name="ContinuousScan";Value=$ContinuousScan}
$ConfigList.Add($ConfigItem) | Out-Null
$TrashEnabled = Get-YesNoResponse "Would you like to enabled the trash folder and send files with errors to this folder? (y/n)"
If ($TrashEnabled) {
$TrashDir = "InvalidPath:\"
While (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Please enter the path that you would like to store videos with errors:" -ForegroundColor Cyan
Write-Host "Note: This path should be always accessible by the System so that files can be moved" -ForegroundColor Yellow
Write-Host "Example: C:\BadVideos or D:\Media\Temp"
while ($TrashDir -eq "InvalidPath:\") {
Write-Host "Path: " -ForegroundColor Cyan -NoNewline
$TrashDir = Read-Host
}
If (!(Test-Path -LiteralPath $TrashDir)) {
Write-Host "Error: The path supplied is invalid."
$TrashDir = "InvalidPath:\"
}
If ($TrashDir.Contains("[") -or $TrashDir.Contains("]")) {
Write-Host "Error: The path supplied is contains the characters [ or ]. Please supply a path that does not contain square brackets."
$TrashDir = "InvalidPath:\"
}
}
$ConfigItem = New-Object PSObject -Property @{Name="TrashEnabled";Value=$TrashDir}
$ConfigList.Add($ConfigItem) | Out-Null
}
Write-Host "Generating Configuration INI file"
Try {
$ConfigList | ConvertTo-Json | Set-Content $LogDir\ScanConfig-$Date.json -Force -ErrorAction Stop
$JSONPath = Join-Path -Path $LogDir -ChildPath "ScanConfig-$Date.json"
Write-Host "Configuration Successfully Saved in: $JSONPath"
Write-Host "Please test the new configuration out by using the command: .\ScanMedia.ps1 -ConfigFile $JSONPath"
} Catch {
$ErrorMessage = $_.Exception.Message
Write-Verbose "Error. There was an issue generating the JSON file."
Write-Verbose $ErrorMessage
}
}
Function Show-FFMPEGUtilities {
Clear-Host
Write-Host ""
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-HostCenter "FFMPEG Utilities" -Colour Yellow
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-Host ""
Write-Host "ScanMedia Script utilizes FFMPEG to scan each video. It will need to be accessible by the script by adding the location to the Environmental Variable PATH or by placing ffmpeg.exe in the same directory as the script"
Write-Host "This menu can be used to attempt to download the newest version essentials edition and unpack it for use with this script"
Write-Host ""
Write-Host " 1: Check ffmpeg.exe access from script"
Write-Host " 2: Download latest version of ffmpeg-release-essentials.zip"
Write-Host " 3: Main Menu"
Write-Host ""
Write-Host "Please make a selection from the Menu Choices." -ForegroundColor Cyan
$MenuChoice = 0
while ($MenuChoice -lt 1 -or $MenuChoice -gt 3) {
Write-Host "Choice Number: " -ForegroundColor Cyan -NoNewline
$MenuChoice = Read-Host
}
switch ($MenuChoice) {
1 {
If ($null -eq (Get-Command "ffmpeg.exe" -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: Unable to find ffmpeg.exe on the computer or in the local script directory" -ForegroundColor "Red"
Write-Host "Please download ffmpeg and place ffmpeg.exe in: $scriptPath or use the downloader in the FFMPEG Utilities Menu" -ForegroundColor "Cyan"
Write-PauseMessage "Press a key to continue..."
Show-FFMPEGUtilities
} Else {
Write-PauseMessage "Success! ffmpeg.exe is usable by this script. Press a key to continue..."
Show-FFMPEGUtilities
}
}
2 {
$Url = 'https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip'
Write-Host "Attempting to download ffmpeg-release-essentials.zip from $Url"
Try {
$ZipFile = Join-Path -Path $scriptPath -ChildPath "ffmpeg-release-essentials.zip"
If (Test-Path $ZipFile) {
Write-Host "Removing previous ffmpeg-release-essentials.zip download"
Remove-Item -Path $ZipFile -Force -ErrorAction Stop
}
Invoke-WebRequest -Uri $Url -OutFile $ZipFile -UseBasicParsing -ErrorAction Stop
$Destination = Join-Path -Path $scriptPath -ChildPath "FFMPEG"
If (Test-Path $Destination) {
Write-Host "Removing previous un-packed zip folder"
Remove-Item -Path $Destination -Force -Recurse -ErrorAction Stop
}
Write-Host "Unpacking Zip File..."
Expand-Archive -LiteralPath $ZipFile -DestinationPath $Destination -ErrorAction Stop
$ExeFile = Join-Path -Path $scriptPath -ChildPath "ffmpeg.exe"
If (Test-Path $ExeFile) {
Write-Host "Removing previous ffmpeg.exe executable"
Remove-Item -Path $ExeFile -Force -ErrorAction Stop
}
Write-Host "Copying ffmpeg.exe to $scriptPath"
$ffmpegFolder = Join-Path -Path $Destination -ChildPath "ffmpeg*essentials_build"
$GetFolder = Get-Item -Path $ffmpegFolder
$FFMPEGLocation = Join-Path -Path $GetFolder.FullName -ChildPath "\bin\ffmpeg.exe"
Copy-Item -Path $FFMPEGLocation -Destination $ExeFile
Write-Host "Latest version of FFMPEG executable successfully downloaded." -ForegroundColor "Green"
} Catch {
$ErrorMessage = $_.Exception.Message
Write-Verbose $ErrorMessage
Write-Host "There was an error while attempting to download and unzip the zip file. Please manually download the file and place ffmpeg in $scriptPath"
}
Write-PauseMessage "Press a key to continue..."
Show-FFMPEGUtilities
}
3 { }
}
Clear-Host
Get-OnlineVersion
Show-Menu
}
Function Show-Help {
Clear-Host
Write-Host ""
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-HostCenter "Help Menu" -Colour Yellow
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-Host ""
Write-HostCenter "Welcome to the Scan Media help screen."
Write-Host ""
Write-Host (Write-WrapString "This script utilizes ffmpeg, the same tool Plex uses, to decode the video stream and captures the output for any errors during playback and sends the playback errors to a log file. So essentially it plays the video in the background faster than regular speed. It then checks the error output log file to see if there is anything inside. If ffmpeg was able to cleanly play the file, it counts as a passed file. If there is any error output, an error could be anything from a container issue, a missed frame issue, media corruption or more, it counts the file as failed. So if there would be an issue with playback and a video freezing, it would be caught by this method of checking for errors. Because of the nature of the error log, any errors that show up, even simple ones, will all count as a fail and the output is captured so you can view the error log. Some simple errors are easy to fix so I have included an auto-repair feature which attempts to re-encode the file which is able to correct some issues that would cause problems during playback. It can attempt a repair and delete the original if the new file scans successfully, as an option. " )
Write-Host ""
Write-Host (Write-WrapString "This script can be used to scan on or more video files by targeting a single file or a directory that contains multiple files or folders. While running any powershell script, you can stop it at any time by pushing Ctrl + C.")
Write-Host ""
Write-Host "You can get further explanations with the following help menus:" -ForegroundColor Yellow
Write-Host " 1: Main Menu Help"
Write-Host " 2: Command line Parameter and Script Options Help"
Write-Host " 3: Main Menu"
Write-Host ""
Write-Host "Please make a selection from the Menu Choices." -ForegroundColor Cyan
$MenuChoice = 0
while ($MenuChoice -lt 1 -or $MenuChoice -gt 3) {
Write-Host "Choice Number: " -ForegroundColor Cyan -NoNewline
$MenuChoice = Read-Host
}
switch ($MenuChoice) {
1 {
Clear-Host
Write-Host ""
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-HostCenter "Main Menu Help" -Colour Yellow
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-Host ""
Write-Host (Write-WrapString "The main menu consists of three scan options, the ability to generate a configuration file and FFMPEG utilities. The three scan options are designed to allow you to quickly get started scanning without the use of command line or configurations. Command line arguments or a configuration file will allow you have more control over scanning and bypass the menu completely." )
Write-Host ""
Write-Host "Here are the menu options and a description of their use:"
$HelpItems = New-Object System.Collections.ArrayList
$HelpItemBlank = New-Object PSObject -Property @{MenuItem="";HelpText=""}
$HelpItem = New-Object PSObject -Property @{MenuItem="Quick Scan";HelpText="This scan option provides a quick ability to initiate a scan on a directory or file without getting some more advanced options provided with this script. Auto-Rescan is automatically enabled to provided users a fast and easy experience with scanning all of files targeted."}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="Default Scan";HelpText="This scan option provides the regular scan ability like Quick Scan but also exposes more options to change the scanner abilities. Unlike Quick Scan where re-scan enabled, you are asked if you would like to enable it. The following options can be configured for this scan: Re-Scan of items already scanned, LimitCPU if enabled limits the amount of CPU usage, Auto-Repair that when enabled attempts to repair files by re-encoding them to correct some errors, deletion options that allow for the deletion of bad repaired files, bad original files, or both, and the TrashEnabled which will allow you to move files found to have an error into a single directory."}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="Background Scan";HelpText="This scan option is designed to allow for a long scan duration or a continuous scan without massively interrupting other resources on the same machine. A CPU limit is automatically enabled for this scan. The following options can be configured for this scan: Idle Scan prevents the scanner from running if CPU usage on the machine is above 25%, Re-Scan of items already scanned, deletion options that allow for the deletion of bad repaired files, bad original files, or both, Continuous scan which will automatically start another scan in 15 minutes once a scan has completed, and the TrashEnabled which will allow you to move files found to have an error into a single directory."}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="Create a Configuration file";HelpText="This allows for the creation of a JSON settings file that can be used in conjunction with the command line parameter -ConfigFile. A series of questions will be asked about the type of scan you would like to run and then a file will be saved. If a configuration file is supplied at command line, no menu will be shown. This can be used to setup an automated scanning process with a program like Task Scheduler."}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="FFMPEG Utilities";HelpText="The option allows you to verify that the ffmpeg.exe file required by the script is configured correctly. If ffmpeg.exe is not found there is an option to download the latest version and place it into the proper location. Once downloaded you can validate that the script can access the ffmpeg.exe before proceeding to scan."}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItems | Select-Object -Property MenuItem,HelpText | Format-Table -AutoSize -Wrap -HideTableHeaders
Write-PauseMessage "Push any key to continue..."
Show-Help
}
2 {
Clear-Host
Write-Host ""
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-HostCenter "Command line Parameter and Script Options Help" -Colour Yellow
Write-HostCenter "-------------------------------------------------------" -Colour Yellow
Write-Host ""
Write-Host (Write-WrapString "This help section will go over all of the available command line arguments with a description of their functionality and examples. All script functionality can be accessed through the command line and the script menu system can be bypassed allowing for another method for automation." )
Write-Host "Command line parameters can be used at the same time as calling the script as seen in the example below example:"
Write-Host ""
Write-Host '.\ScanMedia.ps1 -Path "C:\Media\Videos" -LimitCPU -AutoRepair -RemoveOriginal -RemoveRepaired -BypassMenu'
Write-Host ""
Write-Host (Write-WrapString "This example bypasses the menu and scans the path C:\Media\Videos with lower CPU usage and attempts to repair the files that are found to have an issue. The script will delete repaired files that don't repair properly and it will delete the original files of successfully repaired files.")
Write-Host ""
Write-Host "Here is a list of parameters/options with an overview of their use:"
$HelpItems = New-Object System.Collections.ArrayList
$HelpItemBlank = New-Object PSObject -Property @{MenuItem="";HelpText=""}
$HelpItem = New-Object PSObject -Property @{MenuItem="Path";HelpText='This is the folder or file that the script will begin recursively scanning with FFMPEG. If running this script as administrator, you typically want to provide the full network path instead of a drive letter. Calling Syntax: -Path "C:\Media\Videos" or -Path "\\SEVERVERSHARE\Media\Videos"'}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="AutoRepair";HelpText="When supplied, the script will automatically attempt to repair any file that is found to have issues. Only a certain number of issues will be able to be corrected with this method and it will not be possible to repair all files. After a video is repaired, it is then scanned to check to make sure the repair was successful and verifies that the runtime matches that of the original file. Bad auto-repaired files can be automatically removed with the use of -RemovedRepaired. Calling Syntax: -AutoRepair"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="Rescan";HelpText="When supplied, forces the script to ignore the Scan History File and will re-scan of all files in the path supplied. Calling Syntax: -Rescan"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="ContinuousScan";HelpText="When supplied, once a scan completes a new scan will be kicked off in 15 minutes with the same parameters as the last scan. This can be used to setup automatic scanning on a directory for any new files, if previous files are ignored. Calling Syntax: -ContinuousScan"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="CRF";HelpText="Stands for Constant Rate Factor, can be within the range of 0 - 51, where 0 is lossless and 51 is the worst quality possible. 21 is the default value for this script. A lower value generally leads to higher quality, and a subjectively same range is 17 - 28. Consider 17 or 18 to be visually lossless or nearly so; it should look the same or nearly the same as the input but it isn't technically lossless. The range is exponential, so increasing the CRF value +6 results in roughly half the bitrate / file size, while -6 leads to roughly twice the bitrate. Calling Syntax: -CRF 25"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="LimitCPU";HelpText="When supplied, the script will attempt to run FFMPEG with half of the available cores assigned to the current system. If 4 cores are available, FFMPEG will utilize 2. If 1 core is available to the system, this parameter will have no affect. Scan and repair time will increase with this parameter. Calling Syntax: -LimitCPU"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="IdleScan";HelpText="When supplied, stops a scan from being initiated unless CPU usage is below 25%. This will stop the script from competing for resources if the machine is in use with other tasks. Once the CPU is idle a scan will continue. Calling Syntax: -IdleScan"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="RemoveRepaired";HelpText="When supplied, the script will DELETE all of the repaired files that did not scan as successfully repaired. Calling Syntax: -RemoveRepaired"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="RemoveOriginal";HelpText="When supplied, the script will DELETE the original video file if it was repaired successfully. It will then overwrite the original file name with the name of the successfully repaired file. It will not remove original files unless a repair was attempted and it was successful. All other original files will be un-touched. It is recommended to only run this command if a backup is in place for the media being scanned. If -IAcceptResponsibility is not supplied at run time, the script will prompt the user to type IACCEPT during runtime. Calling Syntax: -RemoveOriginal"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="RemoveAll";HelpText="When supplied, will DELETE all of the files that are found to have errors and is not dependant of if they are able to be successfully repaired. It is recommended to only run this command if a backup is in place for the media being scanned. If -IAcceptResponsibility is not supplied at run time, the script will prompt the user to type IACCEPT during runtime. Calling Syntax: -RemoveAll"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="IAcceptResponsibility";HelpText="When supplied, the user accepts responsibility when supplying parameters that will potentially delete original files. This should only be used when you understand the risks involved. Calling Syntax: -IAcceptResponsibility"}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="ConfigFile";HelpText='When supplied, the script will load the configuration saved in the supplied JSON file and initiate a scan with those values. This will allow you to save a popular scan with the settings you require and then call the script with the configuration file for automatic scanning. This could be used in conjunction with scheduling software to setup an automated method to monitor directories. Calling Syntax: -ConfigFile "C:\ScanFiles\ScanConfig-2020-11-30.json" '}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="BypassMenu";HelpText="When supplied, the starting interactive menu will be bypassed so that no user interaction is required to initiate a scan. If calling the script using command line arguments where no user interaction is wanted, supply this parameter to ensure there is no interruptions when starting a scan. Not required when using the parameter -ConfigFile. The -path parameter is required for no user interaction. Calling Syntax: -BypassMenu "}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="GPUEncoding";HelpText="When supplied, ffmpeg with utilize the GPU to encode files, lowering CPU usage, by changing the encoding library being used to h264_nvenc. Note: This requires both your GPU and the ffmpeg binary to support GPU encoding. This will not work with the default ffmpeg binary file. To utilize this feature you need to find out what is required for GPU encoding through ffmpeg. Calling Syntax: -GPUEncoding "}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="Extension";HelpText='When supplied, will change the extension of files repaired with ffmpeg. To be used in conjunction with -GPUEncoding or else the default of .mp4 will be compatible with the default encoding library. Calling Syntax: -Extension ".mkv" '}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItem = New-Object PSObject -Property @{MenuItem="TrashEnabled";HelpText='When supplied, will move all files found with issues will be moved to the folder specified for remediation. This option will take priority over -RemoveAll or -RemoveOriginal if both are called. Calling Syntax: -TrashEnabled "X:\Media\BadFiles"'}
$HelpItems.Add($HelpItem) | Out-Null
$HelpItems.Add($HelpItemBlank) | Out-Null
$HelpItems | Select-Object -Property MenuItem,HelpText | Format-Table -AutoSize -Wrap -HideTableHeaders
Write-PauseMessage "Push any key to continue..."
Show-Help
}
3 {
}
}
Clear-Host
Get-OnlineVersion
Show-Menu
}
Function Update-VideoArray {
Param (
[String]$Status,
[Object]$VideoFile,
[Switch]$Update)
If ($global:VideoList.Length -eq 500) {
$global:VideoList.RemoveAt(0)
}
If ($Update) {
$MediaFile = New-Object PSObject -Property @{Name=$VideoFile.Name;Path=$VideoFile.DirectoryName;Status=$Status;FullName=$VideoFile.FullName}
$ScanList = $global:VideoList | Where-Object { ($_.FullName -eq $VideoFile.FullName) }
If ($ScanList) {
Foreach ($file in $ScanList) {
$global:VideoList.Remove($file)
}
}
$global:VideoList.Add($MediaFile) | Out-Null
} Else {
$MediaFile = New-Object PSObject -Property @{Name=$VideoFile.Name;Path=$VideoFile.DirectoryName;Status=$Status;FullName=$VideoFile.FullName}
$global:VideoList.Add($MediaFile) | Out-Null
}
}
Function Write-ScanOutput {
Param (
[switch]$Scanning,
[switch]$Repairing
)
Clear-Host
# Check if script is running in PowerShell ISE which does not expose console size
If ($Host.UI.SupportsVirtualTerminal) {
$Width = (Get-Host).UI.RawUI.WindowSize.Width
$Height = (Get-Host).UI.RawUI.WindowSize.Height
} Else {
$Width = (Get-Host).UI.RawUI.BufferSize.Width
$Height = 50
}
# Set minimum values on terminal size
If ($Width -lt 50)
{
$Width = 120;
}
If ($Height -lt 25)
{
$Height = 30;
}
$OutputPath = $global:Path.subString(0, [System.Math]::Min(($Width - 20), $global:Path.Length))
If ($global:Path.Length -gt ($Width - 20)) {
$OutputPath = $OutputPath + "..."
}
Write-Host ""
Write-HostCenter "##################################################################################################################" -Colour Cyan
Write-HostCenter "Scanning $OutputPath" -Colour Cyan
Write-HostCenter "##################################################################################################################" -Colour Cyan
Write-Host ""
If ($global:AutoRepair) {
Write-HostCenter "Files Scanned: $($global:VideoCount) | Errors Found: $($global:ErrorList.Count) | Repair Errors: $($global:RepairedErrorList.Count)"
} Else {
Write-HostCenter "Files Scanned: $($global:VideoCount) | Errors Found: $($global:ErrorList.Count)"
}
$global:VideoList | Select-Object -Property Name,Status,Path | Select-Object -Last ($Height - 13) | Format-Table -AutoSize | Out-String -Stream | ForEach-Object {
If ($_.Contains(" Skipped ")) {
Write-Host $_ -ForegroundColor Gray
} ElseIf ($_.Contains(" Passed ")) {
Write-Host $_ -ForegroundColor Green
} ElseIf ($_.Contains(" Failed ")) {
Write-Host $_ -ForegroundColor Red
} ElseIf ($_.Contains(" Deleted ")) {
Write-Host $_ -ForegroundColor Yellow
} ElseIf ($_.Contains(" Passed:Renamed ")) {
Write-Host $_ -ForegroundColor Green
} ElseIf ($_.Contains(" Moved ")) {
Write-Host $_ -ForegroundColor Yellow
} Else {
Write-Host $_
}
}
If ($Scanning) {
Write-HostCenter "Please wait, Scan in progress..."
}
If ($Repairing) {
Write-HostCenter "Please wait, Repair in progress..."
}
}
Function Write-WrapString {
Param (
[String]$str
)
# Check if script is running in PowerShell ISE which does not expose console size
If ($Host.UI.SupportsVirtualTerminal) {
$Width = (Get-Host).UI.RawUI.WindowSize.Width
} Else {
$Width = (Get-Host).UI.RawUI.BufferSize.Width
}
# Holds the final version of $str with newlines
$strWithNewLines = ""
# current line, never contains more than screen width
$curLine = ""
# Loop over the words and write a line out just short of window size
foreach ($word in $str.Split(" "))
{
# Lets see if adding a word makes our string longer then window width
$checkLinePlusWord = $curLine + " " + $word
if ($checkLinePlusWord.length -gt $Width)
{
# With the new word we've gone over width
# append newline before we append new word
$strWithNewLines += [Environment]::Newline
# Reset current line
$curLine = ""
}
# Append word to current line and final str
$curLine += $word + " "
$strWithNewLines += $word + " "
}
# return our word wrapped string
return $strWithNewLines
}
Function New-MediaScan {
# Test to see if the log directory exists and create it if not
If (!(test-Path $LogDir)) {
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
}
If (!(test-Path $LogPath)) {
New-Item -ItemType Directory -Force -Path $LogPath | Out-Null
}
If ($global:RemoveAll -or $global:RemoveOriginal) {
If (!($global:IAcceptResponsibility)) {
$Acceptance = ""
LogWrite "************************* WARNING *******************************" -Colour "Red"
LogWrite "The commands supplied have the ability to delete original files" -Colour "Yellow"
LogWrite "A backup is highly recommended when supplying these commands" -Colour "Yellow"
LogWrite "Please type the following confirm you understand: IACCEPT" -Colour "Yellow"
LogWrite "************************* WARNING *******************************" -Colour "Red"
while ($Acceptance -ne "IACCEPT") {
Write-Host "Please Type the above command: " -ForegroundColor Magenta -NoNewline
$Acceptance = Read-Host
}
}
}
LogWrite "Starting script $($MyInvocation.MyCommand.Name)" -Log
# Test the CSV file path and create the CSV with the header line if it does not exist
If (!(test-Path $CSVfile)) {
Set-Content $CSVfile -Value "File Name,FFMPEG Test,Check Date,File Size (MB),Video Length,Location"
}
If (!(test-Path $scanHistory)) {
Set-Content $scanHistory -Value "This is a history of all scanned items. Please do not delete or modify this file."
LogWrite "Creating a history file: $scanHistory "
LogWrite "Please do not remove this file if you would like to keep a history of scanned items."
}
If ($global:AutoRepair) {
LogWrite "Auto-Repair has been enabled" -Colour "Cyan"
}
If ($global:Rescan) {
LogWrite "Media Rescan has been enabled. All files will be scanned." -Colour "Cyan"
}
If ($global:RemoveRepaired) {
LogWrite "WARNING: Auto-Delete has been enabled for repaired files. All repaired files that have errors will be automatically removed" -Colour "Yellow"
}
If ($global:RemoveAll) {
LogWrite "WARNING: Auto-Delete has been enabled for all files. All files that have errors will be automatically removed" -Colour "Yellow"
}
If ($global:RemoveOriginal) {
LogWrite "WARNING: Auto-Delete has been enabled for original files. All original files that have errors will be automatically removed after a repair has completed successfully" -Colour "Yellow"
}
If ($global:IAcceptResponsibility) {
LogWrite "You have acknowledged that original files will potentially be removed when using RemoveAll or RemoveOriginal and supplying -IAcceptResponsibility" -Colour "Magenta"
}
If ($global:LimitCPU) {
LogWrite "Determining the number of CPUs to utilize for lower CPU usage." -Colour "Cyan"
$global:CPULimt = (Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors / 2
If ($global:CPULimt -lt 1) {
$global:CPULimt = 1
}
LogWrite "Limiting the CPU usage for FFMPEG to $global:CPULimt core(s)" -Colour "Cyan"
}
If ($global:IdleScan) {
LogWrite "IdleScan has been enabled. A file will only be scanned if CPU usage is below 25 percent" -Colour "Cyan"
}
If ($global:GPUEncoding) {
LogWrite "GPU Encoding has been enabled. This requires that FFMPEG and your GPU both support GPU encoding. This should only be enabled if you have verified functionality." -Colour "Cyan"
$global:VideoLibrary = "h264_nvenc"
}
If ($global:ContinuousScan) {
LogWrite "Continuous Scanning has been enabled. 15 minutes after a scan is completed a new scan will be started using the same values as the pervious scan" -Colour "Cyan"
}
If ($global:TrashEnabled) {
LogWrite "Trash has been enabled. Video files with errors found will be moved to the folder: $global:TrashEnabled" -Colour "Cyan"
}
# Check to see if ffmpeg exists and if it is installed on the local machine and added to path
If ($null -eq (Get-Command "ffmpeg.exe" -ErrorAction SilentlyContinue)) {
LogWrite "ERROR: Unable to find ffmpeg.exe on the computer or in the local script directory" -Colour "Red"
LogWrite "Please download ffmpeg and place ffmpeg.exe in: $scriptPath or use the downloader in the FFMPEG Utilities Menu" -Colour "Cyan"
LogWrite "Exiting Script" -Colour "Red"
Exit
}
# Check to make sure the path supplied to scan exists for the current user
If (!(Test-Path -LiteralPath $global:Path)) {
LogWrite "ERROR: Unable to access the directory: $global:Path" -Colour "Red"
If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
LogWrite "Running as administrator is not required and can cause issues accessing network folders that have been mapped. Please supply the direct share path I.E. \\SERVER\SHARE\PATH"
}
LogWrite "Exiting Script" -Colour "Red"
Exit
}
Write-Verbose "Path: $global:Path"
Write-Verbose "LogDir: $global:LogDir"
Write-Verbose "ConfigFile: $global:ConfigFile"
Write-Verbose "AutoRepair: $global:AutoRepair"
Write-Verbose "Rescan: $global:Rescan"
Write-Verbose "CRF: $global:CRF"
Write-Verbose "LimitCPU: $global:LimitCPU"
Write-Verbose "CPULimt: $global:CPULimt"
Write-Verbose "RemoveAll: $global:RemoveAll"
Write-Verbose "RemoveRepaired: $global:RemoveRepaired"
Write-Verbose "RemoveOriginal: $global:RemoveOriginal"
Write-Verbose "IAcceptResponsibility: $global:IAcceptResponsibility"
Write-Verbose "IdleScan: $global:IdleScan"
Write-Verbose "GPUEncoding: $global:GPUEncoding"
Write-Verbose "VideoLibrary: $global:VideoLibrary"
Write-Verbose "RepairVFileExtension: $global:RepairVFileExtension"
Write-Verbose "ContinuousScan: $global:ContinuousScan"
Write-Verbose "TrashEnabled: $global:TrashEnabled"
If ((!($global:ConfigFile)) -and (!($BypassMenu))) {
Write-PauseMessage "Ready to begin scan on $global:Path Press any key to continue..."
}
Clear-Host
$ContinuousScan = $true
While ($ContinuousScan) {
$global:VideoList = New-Object System.Collections.ArrayList
$global:VideoList.Capacity = 500
$global:ErrorList = New-Object System.Collections.ArrayList
$global:RepairedErrorList = New-Object System.Collections.ArrayList
$global:VideoCount = 0
$global:SkipCount = 0
switch ($true) {
$IsWindows {
<# PowerShell 6+ #>
LogWrite "Running on a Windows Machine with PowerShell version 6+" -Log
}
$IsMacOS {
LogWrite "Running on a Mac OS with PowerShell CORE" -Log
}
$IsLinux {
LogWrite "Running on a Linux OS with PowerShell CORE" -Log
}
Default {
<# Windows PowerShell #>
LogWrite "Running on a Windows Machine with PowerShell version below 6" -Log
}
}
$Timer = [system.diagnostics.stopwatch]::StartNew()
LogWrite "" -Log
LogWrite "##################################################################################################################" -Log
LogWrite "Beginning Scan on $global:Path" -Log
LogWrite "##################################################################################################################" -Log
LogWrite "" -Log
Write-ScanOutput
# Get all the child items of the supplied directory and attempt to scan the files with the function CheckFile
Get-ChildItem -LiteralPath $global:Path -File -Recurse | Where-Object { $include -contains $_.Extension } | ForEach-Object {
If ($global:IdleScan) {
$CPUAverage = Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object Average
While ($CPUAverage.Average -gt 25) {
Write-Verbose "CPU usage above 25 percent. Pausing for 5 minutes"
Write-Verbose "Current CPU Usage: $($CPUAverage.Average)"
Start-Sleep -s 300
$CPUAverage = Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object Average
}
}
# Check the scan history file to see if the file has been scanned
$scanHistoryCheck = (Get-Content $scanHistory | Select-String -pattern $_.FullName -SimpleMatch)
# If the file exists in the scan history and Rescan has not been enabled, skip
If (($null -ne $scanHistoryCheck) -and ($global:Rescan -eq $false)) {
Update-VideoArray -VideoFile $_ -Status "Skipped"
LogWrite "Skipping scanned file: $($_.Name)" -Log
$global:SkipCount = $global:SkipCount + 1
Write-ScanOutput
}
# If the file exists in the scan history and Rescan has been enabled, scan the file
Elseif (($null -ne $scanHistoryCheck) -and ($global:Rescan -eq $true)) {
CheckFile $_ -RescanVideo
}
Else {
CheckFile $_
}
}
Write-ScanOutput
$TimeSpan = New-TimeSpan -Seconds $Timer.Elapsed.TotalSeconds
$Elapsed = '{0:000}h:{1:00}m:{2:00}s' -f $TimeSpan.Hours,$TimeSpan.Minutes,$TimeSpan.Seconds
$Timer.Stop()
LogWrite "Number of Files Scanned: $global:VideoCount"
LogWrite "Elapsed Time: $($Elapsed)"
LogWrite "Scan Log file: $Logfile"
If ($global:ErrorList.Count -gt 0) {
LogWrite "Number of files that were found with an error: $($global:ErrorList.Count)" -Colour Yellow
LogWrite "Printing out filename(s) with errors into the log file"
foreach ($file in $global:ErrorList) {
LogWrite "$file" -Log
}
}
If ($global:RepairedErrorList.Count -gt 0) {
LogWrite "Number of repaired files that were found with an error: $($global:RepairedErrorList.Count)" -Colour Yellow
}
If ($global:SkipCount -gt 0) {
LogWrite "Number of files skipped: $($global:SkipCount)"
}
If (!($global:ContinuousScan)) {
$ContinuousScan = $false
LogWrite "Scan Complete."
If ((!($BypassMenu)) -or (!($global:ConfigFile))) {
Write-PauseMessage "Push any key to quit the script..."
}
} Else {
LogWrite "Starting new scan in 15 minutes"
Start-Sleep -Seconds 900
}
}
}
Clear-Host
If ($scriptPath.Contains("[") -or $scriptPath.Contains("]")) {
Write-Host "ERROR: Invalid Script Path. Due to how Powershell handles [] characters. Please run the script from a path that does not contain [ or ]." -ForegroundColor Yellow
Write-Error "ERROR: Invalid Script Path. Due to how Powershell handles [] characters. Please run the script from a path that does not contain [ or ]."
Exit 1
}
# Change the location of the working directory to where the script is launched from
Set-Location -Path $scriptPath
If ((!$global:ConfigFile) -and (!$BypassMenu)) {
Write-Host ""
Write-Host ""
Write-HostCenter "##################################################################################################################" -Colour Cyan
Write-HostCenter "Scan Media PowerShell Script" -Colour Cyan
Write-HostCenter "##################################################################################################################" -Colour Cyan
Write-Host ""
Write-Host ""
Write-Host "$(Get-Date) Initializing ScanMedia Script"
Write-Host "$(Get-Date) Script Version: $ScriptVersion"
Write-Host "$(Get-Date) Program's GitHub: https://gist.github.com/Desani/129be27da7d735d7c75192ec1aa96c65"
Get-OnlineVersion
Show-Menu
} Else {
If ($global:ConfigFile) {
# Convert JSON Values to the Global Variables
Try {
$JSON = Get-Content $ConfigFile | Out-String | ConvertFrom-Json
} Catch {
$ErrorMessage = $_.Exception.Message
Write-Error "Error. There was an issue reading from the JSON file."
Write-Host $ErrorMessage
Exit 1
}
$PathHash = $JSON | Where-Object { ($_.Name -eq "Path") }
$global:Path = $PathHash.Value
$LogDirHash = $JSON | Where-Object { ($_.Name -eq "LogDir") }
$global:LogDir = $LogDirHash.Value
$LogPath = Join-Path -path $global:LogDir -childpath "Log_$Date"
$ffmpegLog = Join-Path -path $LogPath -childpath "ffmpegerror.log"
$Logfile = Join-Path -path $LogPath -childpath "results.log"
$CSVfile = Join-Path -path $LogPath -childpath "results.csv"
$IdleScanHash = $JSON | Where-Object { ($_.Name -eq "IdleScan") }
$global:IdleScan = $IdleScanHash.Value
$LimitCPUHash = $JSON | Where-Object { ($_.Name -eq "LimitCPU") }
$global:LimitCPU = $LimitCPUHash.Value
$RescanHash = $JSON | Where-Object { ($_.Name -eq "Rescan") }
$global:Rescan = $RescanHash.Value
$AutoRepairHash = $JSON | Where-Object { ($_.Name -eq "AutoRepair") }
$global:AutoRepair = $AutoRepairHash.Value
$CRFHash = $JSON | Where-Object { ($_.Name -eq "CRF") }
$global:CRF = $CRFHash.Value
$RemoveRepairedHash = $JSON | Where-Object { ($_.Name -eq "RemoveRepaired") }
$global:RemoveRepaired = $RemoveRepairedHash.Value
$RemoveOriginalHash = $JSON | Where-Object { ($_.Name -eq "RemoveOriginal") }
$global:RemoveOriginal = $RemoveOriginalHash.Value
$RemoveAllHash = $JSON | Where-Object { ($_.Name -eq "RemoveAll") }
$global:RemoveAll = $RemoveAllHash.Value
$ContinuousScanHash = $JSON | Where-Object { ($_.Name -eq "ContinuousScan") }
$global:ContinuousScan = $ContinuousScanHash.Value
$TrashEnabledHash = $JSON | Where-Object { ($_.Name -eq "TrashEnabled") }
If ($TrashEnabledHash) {
$global:TrashEnabled = $TrashEnabledHash.Value
}
$global:IAcceptResponsibility = $true
New-MediaScan
} ElseIf ($BypassMenu) {
Get-MediaPath -ScanType "Custom"
New-MediaScan
}
}
@VintageEngineer
Copy link

I'm also unclear on how to get the utility to ask me what CRF I want... I see the query text in the code, but cannot prod it into asking me. I'm not a coder but... Maybe I need to set line 107 like this?

[Int]$CRF = $null

That did not work... any ideas?

@JasonXG
Copy link

JasonXG commented Oct 9, 2023

Hi, many thanks for making this script!

I've noticed that when the rescan is set to 'no', file paths that contains an emoji are not skipped. The filename exists in the ScanHistory.txt file, but the script doesn't seem to recognise it on subsequent runs and ends up rescanning the video and adding a duplicate entry to the text file.

@HumanIMDB
Copy link

Any updates on getting the script to flag the download as failed in Sonarr/Radarr? This would be a great feature to have!

@Naugrimohtar
Copy link

Hello, I see that CRF is an argument however it is hardcoded in two places with '$CRF = 21' within the If ($AutoRepair) block

@pho2000s
Copy link

Hi, thank you very much for your useful script. Unfortunately I found another error. The script has a problem with German umlauts in the file name. The file is scanned and saved in the ScanHistory, but the next time the program is started, all files with German umlauts will be scanned again. It would be great if you could correct that. Thanks!

@bluttmer
Copy link

I am not sure if this is still active but I get errors with 4K mkv files that have DTS-HD MA TrueHD.7.1 Atmos audio, I am sure thats an FFMpeg issue though. and I got this to work with gpu hardware acceleration partially from what I can see.

@Wolfenk
Copy link

Wolfenk commented May 26, 2024

Thank you for this awesome script. I have some suggestions to make it better.
I think it will be nice to have option for ffmpeg -hwaccel to use GPU while scanning (maybe "-hwaccel auto" can be default?). Also it will be better if we can specify encoder in config file, without needing to edit script as replacing "libx264".

Lastly, I think of a more tidy and practical way to save scan results. "results.csv" file in logs folder should replace ScanHistory.txt in main folder, so we can see all scans and results in single file as csv.

@bluttmer
Copy link

Hi Desani,

Are we allowed to modify the script we downloaded? I was able to get gpu hardware acceleration working but not the way you suggested.

Again, thank you for a great script.

@parkatoast
Copy link

Hi, @pho2000s, did you happen to find a fix for it? It's rescanning all my files with umlauts in the filename as well.

@parkatoast
Copy link

Hi Desani,

Are we allowed to modify the script we downloaded? I was able to get gpu hardware acceleration working but not the way you suggested.

Again, thank you for a great script.

I also got GPU hardware acceleration working, but I couldn't use "h264_nvenc" as Desantis says, but I realised that that's because "nvenc" is only for NVIDIA GPUs. I have Intel Processors, so I used "h264_qsv", but every time I scan VC-1 file, it fails the file. If I go back to "libx264" it takes very long time, but everything passes.

How did you get hardware acceleration working, @bluttmer? What method did you use?

@bluttmer
Copy link

Hi Desani,
Are we allowed to modify the script we downloaded? I was able to get gpu hardware acceleration working but not the way you suggested.
Again, thank you for a great script.

I also got GPU hardware acceleration working, but I couldn't use "h264_nvenc" as Desantis says, but I realised that that's because "nvenc" is only for NVIDIA GPUs. I have Intel Processors, so I used "h264_qsv", but every time I scan VC-1 file, it fails the file. If I go back to "libx264" it takes very long time, but everything passes.

How did you get hardware acceleration working, @bluttmer? What method did you use?

Hello,

I have an Nvidia gpu P2200 and I think I modified the script too.

@pho2000s
Copy link

Hi, @pho2000s, did you happen to find a fix for it? It's rescanning all my files with umlauts in the filename as well.

Hi @parkatoast, I decided to use a custom script to convert all files in a single folder in a loop, regardless of whether the file has already been tested for errors or not. This works for me.

@parkatoast
Copy link

parkatoast commented Oct 19, 2024

@pho2000s, I found out what causes the problem. "-Encoding utf8" is missing from two of the "$scanHistoryCheck" functions. Adding them back fixes this and I can run the script over files with umlauts in the name without it re-scanning them over and over again. Here is how to fix it:

Find:
$scanHistoryCheck = (Get-Content $scanHistory | Select-String -pattern $RepairFile.FullName -SimpleMatch)

And replace it with:
$scanHistoryCheck = (Get-Content $scanHistory -Encoding utf8 | Select-String -pattern $RepairFile.FullName -SimpleMatch)

And then find:
$scanHistoryCheck = (Get-Content $scanHistory | Select-String -pattern $_.FullName -SimpleMatch)

And replace it with:
$scanHistoryCheck = (Get-Content $scanHistory -Encoding utf8 | Select-String -pattern $_.FullName -SimpleMatch)

@JasonXG, this also fixes the issue with folders containing emojis being re-scanned and duplicates of them being added to the ScanHistory file.
@Desani, please consider implementing this change into your project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment