Files
Video-Processing-Scripts/Get-VideoInfo.ps1
T

191 lines
6.8 KiB
PowerShell

<#
.SYNOPSIS
Extracts and displays video metadata from the comment tag in a video file's format metadata.
.PARAMETER Video
Path to the input video file. Can be absolute or relative to current directory.
.PARAMETER Full
When present, outputs the parsed comment metadata as JSON.
.DESCRIPTION
This script uses ffprobe to extract video format metadata, parses the comment tag as JSON,
and outputs the structured information with proper error handling.
#>
param(
[Parameter(Mandatory = $true)]
[string]$Video,
[Parameter(Mandatory = $false)]
[switch]$Full
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
$VideoPath = $Video
if (-not (Test-Path -LiteralPath $VideoPath)) {
Write-Error ('Video file not found: {0}' -f $VideoPath)
exit 1
}
$PSNativeCommandUseErrorActionPreference = $false
try {
$ffprobeOutput = & ffprobe -v quiet -print_format json -show_format -show_streams -- $VideoPath 2>&1
$ffprobeExit = $LASTEXITCODE
if ($ffprobeExit -ne 0) {
$ffprobeOutput | ForEach-Object { Write-Error $_ }
Write-Error ('ffprobe failed to read video file: {0}' -f $VideoPath)
exit 1
}
$raw = $ffprobeOutput | ConvertFrom-Json
if (-not $raw -or -not $raw.format) {
Write-Error ('ffprobe did not return valid format metadata for: {0}' -f $VideoPath)
exit 1
}
if (-not $raw.format.tags -or -not $raw.format.tags.comment) {
Write-Error ('Video file does not contain a comment tag in format metadata: {0}' -f $VideoPath)
exit 1
}
$commentJson = $raw.format.tags.comment
# Extract the actual (encoded) resolution and frame rate from the first video
# stream reported by ffprobe. These may differ from the values stored in the
# JSON comment metadata (e.g. WanGP/LTX records "720x1280" but the file is
# actually encoded at 704x1280 after the model's internal padding/cropping).
$actualResolution = $null
$actualFps = $null
$get = { param($obj, $name) if ($obj -and ($obj.PSObject.Properties.Name -contains $name)) { $obj.$name } else { $null } }
if ($raw.PSObject.Properties.Name -contains 'streams') {
$videoStream = $raw.streams | Where-Object { $_.codec_type -eq 'video' } | Select-Object -First 1
if ($videoStream) {
$w = & $get $videoStream 'width'
$h = & $get $videoStream 'height'
if ($w -and $h) {
$actualResolution = '{0}x{1}' -f [int]$w, [int]$h
}
$rfr = & $get $videoStream 'r_frame_rate'
if ($rfr -and $rfr -match '^(\d+)/(\d+)$') {
$num = [double]$matches[1]
$den = [double]$matches[2]
if ($den -ne 0) {
$actualFps = [Math]::Round($num / $den, 3)
}
}
}
}
try {
$parsedComment = $commentJson | ConvertFrom-Json
}
catch {
Write-Error ('Failed to parse comment tag as JSON: {0}' -f $_.Exception.Message)
exit 1
}
$output = $parsedComment | ConvertTo-Json -Depth 20
if ($Full) {
Write-Output $output
}
Write-Output '----------------------------------------'
Write-Output ('File Name: {0}' -f $Video)
$iniPath = Join-Path $PSScriptRoot 'Get-VideoInfo.ini'
if (-not (Test-Path -LiteralPath $iniPath)) {
Write-Error ('INI configuration file not found: {0}' -f $iniPath)
exit 1
}
$iniLines = Get-Content -LiteralPath $iniPath -Encoding UTF8
foreach ($line in $iniLines) {
$trimmedLine = $line.Trim()
if ([string]::IsNullOrWhiteSpace($trimmedLine) -or $trimmedLine.StartsWith('#')) {
continue
}
if ($trimmedLine -notmatch '^([^=]+)=([^=]+)$') {
continue
}
$displayName = $matches[1].Trim()
$jsonFieldName = $matches[2].Trim()
$displayNameFormatted = $displayName -replace '_', ' '
$fieldExists = $parsedComment.PSObject.Properties.Name -contains $jsonFieldName
if (-not $fieldExists) {
$valueDisplay = 'Not found'
}
else {
$fieldValue = $parsedComment.$jsonFieldName
if ($null -eq $fieldValue) {
$valueDisplay = 'Not found'
}
elseif ($jsonFieldName -eq 'generation_time') {
$totalSeconds = [int]$fieldValue
$minutes = [Math]::Floor($totalSeconds / 60)
$seconds = $totalSeconds % 60
$valueDisplay = ('{0}s ({1}m {2}s)' -f $totalSeconds, $minutes, $seconds)
}
elseif ($jsonFieldName -eq 'creation_date') {
try {
$dateTime = [DateTime]::Parse($fieldValue)
$valueDisplay = $dateTime.ToString('yyyy-MM-dd HH:mm:ss')
}
catch {
$valueDisplay = $fieldValue -replace 'T', ' '
}
}
elseif ($jsonFieldName -eq 'video_length') {
# Always use the real frame rate reported by ffprobe for the
# duration calculation. The JSON metadata's force_fps may be a
# non-numeric mode marker (e.g. "control") or simply not match
# the actual encoded frame rate.
$fps = if ($actualFps) { $actualFps } else { 24.0 }
$frames = [int]$fieldValue
$durationSeconds = [Math]::Round($frames / $fps, 1)
$valueDisplay = ('{0} frames ({1}s, {2} fps)' -f $frames, $durationSeconds, $fps)
}
elseif ($fieldValue -is [array]) {
# Pretty-print array values as JSON, prefixed with a newline so the
# multi-line output starts cleanly under the field label.
$jsonArray = $fieldValue | ConvertTo-Json -Depth 10
$valueDisplay = [Environment]::NewLine + $jsonArray
}
else {
$valueDisplay = $fieldValue
# If the value looks like a WxH resolution string and ffprobe
# reports a different actual encoded resolution, surface both.
if ($actualResolution `
-and ($fieldValue -is [string]) `
-and ($fieldValue -match '^\d+x\d+$') `
-and ($fieldValue -ne $actualResolution)) {
$valueDisplay = "$fieldValue (real: $actualResolution)"
}
}
}
Write-Output ('{0}: {1}' -f $displayNameFormatted, $valueDisplay)
}
Write-Output '----------------------------------------'
}
catch {
Write-Error ('Unexpected error: {0}' -f $_.Exception.Message)
exit 1
}