If, like me, you have a collection of video backups on your computer that spans a long time, you’ve probably got a mixture of file type, file size, resolution, aspect ratios and more, some of which are not the most ideal to have within your current folder system, but too sentimental to completely get rid of. Like a home video collection that was converted from betamax/VHS, and is irreplaceable, but you don’t want these videos showing up within your library on your streaming device.

You’re probably thinking there’s got to be an easy way to sift through the entire collection and single out any videos that don’t match the resolution requirements you’d like. For me, it was anything under 400 pixels tall.

Using Windows File Explorer was no-use, as it doesn’t display the file resolutions for many of the mixed codec types that have been used for file conversions or storage over the years!

I’d done some googling to try and find the answer, but came up short. A few stackoverflow.com articles came close and suggested the right path, but ultimately didn’t do what I wanted.

The first thing we need is ffmpeg, for it’s ffprobe.exe functionality. I’m a big fan of chocolatey so I installed it using choco install ffmpeg -y

By default, this installs ffmpeg to the directory:
C:\ProgramData\chocolatey\lib\ffmpeg\
Specifically for ffprobe:
C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin\ffprobe.exe

This is important, if you install manually-elsewhere, you’ll need to know where ffprobe.exe has been installed to update the code later on.

Now we’ve got ffprobe, which can analyse video to determine the video resolution using command line, we can prepare a script to loop through our directories and do what we’d like.

Initially, I made a list of all videos showing their resolution that I intended to go through manually, however, as I’m automating this with a script, it felt churlish to have to go through anything manually at this point, and decided to show a list of ONLY videos that did not meet my criteria of 400px tall.

Once I’d made the list however, I decided that actually, putting them into a separate directory, but keeping the file structure intact would suit me better. I’d have an archive of older footage that I could go through at my own pace without having to refer to a separate list to find out if it was a low resolution or a higher resolution video. So that’s what came next.

Here’s the code that does exactly that. Save as “MoveFilesLessThan400p.ps1”

# Set the path to the directory you want to list files from
$directoryPath = "E:\Footage"

# Set the path to the output file
$outputFilePath = "$directoryPath\VideoFileListWithDurations.txt"

# Set the base path for the archive directory
$archiveBasePath = "E:\FootageArchive"

try {
    # Get all video files in the specified directory and its subdirectories
    $videoFiles = Get-ChildItem -Path $directoryPath -Recurse | Where-Object {
        !$_.PSIsContainer -and $_.Extension -match '\.(mp4|avi|mkv|wmv)$'
    }

    # Initialize an array to store file information strings
    $fileInfoStrings = @()

    # Add headers to the array
    $fileInfoStrings += "FilePath`tDuration`tFileSize (MB)`tResolution"

    # Loop through each video file and retrieve its information
    foreach ($file in $videoFiles) {
        $fileInfo = @{
            FilePath = $file.FullName  # Use the full file path here
            FileSize = "{0:N3}" -f ($file.Length / 1MB) # Format in megabytes with 3 decimal places
            Duration = "N/A"
            Resolution = "N/A"
        }

        try {
            Write-Host "Getting duration and resolution for $($file.FullName)"
            
            # Get video duration
            $ffprobeOutputDuration = & "C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin\ffprobe.exe" -i $($file.FullName) -show_entries format=duration -v quiet -of csv="p=0"
            $duration = [double]$ffprobeOutputDuration.Trim()
            $timeSpan = [TimeSpan]::FromSeconds($duration)
            $fileInfo.Duration = $timeSpan.ToString("h\:mm\:ss")

            # Get video resolution
            $ffprobeOutputResolution = & "C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin\ffprobe.exe" -i $($file.FullName) -select_streams v:0 -show_entries stream=width,height -v quiet -of csv="p=0"
            $resolutionParts = $ffprobeOutputResolution.Trim().Split(',')
            
            # Check if we got valid resolution values
            if ($resolutionParts.Count -eq 2 -and $resolutionParts[0] -match '^\d+$' -and $resolutionParts[1] -match '^\d+$') {
                $width = $resolutionParts[0]
                $height = $resolutionParts[1]
                $fileInfo.Resolution = "${width}x${height}"

                # Only include files with height less than 400
                if ([int]$height -lt 400) {
                    # Create the target subdirectory structure
                    $relativePath = $file.FullName.Substring($directoryPath.Length + 1)  # Get the relative path
                    $targetSubDirectory = Split-Path -Path $relativePath -Parent  # Get the parent directory
                    $archiveTargetDirectory = Join-Path -Path $archiveBasePath -ChildPath $targetSubDirectory  # Combine with archive base path

                    # Ensure the target directory exists
                    if (-not (Test-Path -Path $archiveTargetDirectory)) {
                        New-Item -Path $archiveTargetDirectory -ItemType Directory -Force | Out-Null  # Create the directory if it doesn't exist
                    }

                    # Move the file to the target directory
                    $targetFilePath = Join-Path -Path $archiveTargetDirectory -ChildPath $file.Name
                    Move-Item -Path $file.FullName -Destination $targetFilePath -Force

                    # Log the moved file to the output array
                    $fileInfoStrings += "$($fileInfo.FilePath)`t$($fileInfo.Duration)`t$($fileInfo.FileSize)`t$($fileInfo.Resolution)"
                }
            } else {
                Write-Host "No valid video stream for $($file.FullName)"
            }
        } catch {
            Write-Host "Error getting duration or resolution for $($file.FullName): $_"
        }
    }

    # Export the file information strings to the output file if there are any entries
    if ($fileInfoStrings.Count -gt 1) {
        $fileInfoStrings | Out-File -FilePath $outputFilePath -Append
        Write-Host "File information exported to $outputFilePath"
    } else {
        Write-Host "No files with height less than 400 pixels found."
    }
} catch {
    Write-Host "An error occurred: $_"
    Read-Host "Press Enter to exit"
}

In order to use this code, we need to make sure that the link to ffprobe.exe is correct for both the code checking the duration and also the code checking the resolution. We also need to set the Directory to search within the top lines and also where files matching the criteria are going to be moved to – in the code above those locations are “E:\Footage\” and “E:\FootageArchive\” respectively.

To run this code we are going to use POWERSHELL whilst elevated, I saved this file within the same directory as we’re running in; E:\Footage. So open Windows Powershell (Run as Administrator) and enter:

cd E:\Footage\

then type MoveFilesLessThan400p.ps1 and execute.

This script also outputs the list of footage with it’s full path, resolution and duration in the root of the directory that you’re searching, so you can check what’s been moved without having to go through the Archive folder individually to see what items have moved.

I hope this helps some of you to archive your video library and identify footage/movies/films that need to be re-converted from their source in higher resolution, or removed completely if the footage is no longer viable.

Please Note: Whenever you are running code found on the internet, especially with an elevated client, there is a potential for your machine to be compromised – at the time of posting the script above is completely clear of any wrongdoing – that said, my blog has been compromised previously and whilst I do my best to keep it and it’s content protected, I accept no liability for any code run as a result of reading this blog. I’m just out here trying to help my fellow man, man!