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

223 lines
8.0 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.
Supports Windows wildcards (* and ?) to match multiple files. When wildcards are used,
metadata is extracted and displayed for each matching file sequentially.
.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
# Resolve video paths, supporting wildcards
$videoPaths = @()
if ($Video -match '[*?]') {
# Wildcard pattern - use Get-ChildItem to resolve
$resolvedFiles = Get-ChildItem -Path $Video -File -ErrorAction SilentlyContinue
if (-not $resolvedFiles) {
Write-Error ('No files matched wildcard pattern: {0}' -f $Video)
exit 1
}
$videoPaths = $resolvedFiles | Select-Object -ExpandProperty FullName
} else {
# Single file path (literal)
if (-not (Test-Path -LiteralPath $Video)) {
Write-Error ('Video file not found: {0}' -f $Video)
exit 1
}
$videoPaths = @($Video)
}
$PSNativeCommandUseErrorActionPreference = $false
$firstFile = $true
foreach ($VideoPath in $videoPaths) {
# Add separator between files (but not before the first one)
if (-not $firstFile) {
Write-Output ''
Write-Output '========================================'
Write-Output ''
}
$firstFile = $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]) {
if ($fieldValue.Count -gt 1) {
# 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 {
$jsonArray = $fieldValue | ConvertTo-Json -Depth 10 -Compress
$valueDisplay = $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 processing {0}: {1}' -f $VideoPath, $_.Exception.Message)
# Continue to next file instead of exiting
continue
}
}