A year ago, I posted a PowerShell script to populate the HDHomeRun Metadata. The script was limited in that it generated a metadata block that had to be inserted into the recording file.
This version has a UI to enter the metadata and code to overlay the new metadata in the recording file.
The UI looks like:

The code will also compute and populate the RecordStartTime and RecordEndTime fields in order to set the recording's length.
The script will then open a File Save dialog to select the recording to overlay.
Keep in mind that you will still need to use MCEBuddy with the HDHomeRun profile to prepare the file and make room for the metadata block
Please note that I had to sacrifice portability in order to add the UI, so this version will only work on Windows. You can always tweak the UI code for other platforms.
Here is the (AI-generated) code:
Code: Select all
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# --- JSON Field Defaults ---
$jsonDefaults = [ordered]@{
"Category" = "series"
"ChannelAffiliate" = "PBS"
"ChannelImageURL" = "https://img.hdhomerun.com/channels/US44690.png"
"ChannelName" = "KQEHDT"
"ChannelNumber" = "710"
"EndTime" = 1714942800
"EpisodeNumber" = "S18E03"
"EpisodeTitle" = "Florencia en el Amazonas"
"FirstAiring" = 1
"ImageURL" = "https://img.hdhomerun.com/titles/C241428ENQ05X.jpg"
"OriginalAirdate" = 1712448000
"ProgramID" = "EP008811450179"
"RecordEndTime" = ""
"RecordStartTime" = ""
"RecordSuccess" = 1
"Resume" = 1425
"SeriesID" = "C241428ENQ05X"
"StartTime" = 1714935600
"Synopsis" = "Mexican composer Daniel Catán's opera tells the story of an opera diva who returns to her native South America to perform at the Manaus opera house and to search for her lost lover."
"Title" = "Great Performances at the Met"
}
# Fields to be unquoted in JSON (must be numeric values)
$unquotedFields = @("EndTime", "OriginalAirdate", "RecordEndTime", "RecordStartTime", "RecordSuccess", "Resume", "StartTime", "FirstAiring")
# --- Build the form ---
$form = New-Object System.Windows.Forms.Form
$form.Text = "Enter JSON Fields"
$form.Size = New-Object System.Drawing.Size(900, 1350)
$form.StartPosition = "CenterScreen"
$form.AutoScroll = $true
$y = 20
$textboxes = @{}
foreach ($key in $jsonDefaults.Keys) {
$label = New-Object System.Windows.Forms.Label
$label.Text = "${key}:"
$label.Location = New-Object System.Drawing.Point(20, $y)
$label.AutoSize = $true
$form.Controls.Add($label)
if ($key -eq "Synopsis") {
$tb = New-Object System.Windows.Forms.TextBox
$tb.Multiline = $true
$tb.Size = New-Object System.Drawing.Size(820, 75)
$tb.Text = [string]$jsonDefaults[$key]
$tb.Location = New-Object System.Drawing.Point(20, ($y + 22))
$form.Controls.Add($tb)
$textboxes[$key] = $tb
$y += 100
} elseif ($key -eq "RecordStartTime") {
$dt = New-Object System.Windows.Forms.DateTimePicker
$dt.Format = [System.Windows.Forms.DateTimePickerFormat]::Custom
$dt.CustomFormat = "yyyy-MM-dd HH:mm:ss"
$dt.Value = Get-Date
$dt.Width = 250
$dt.Location = New-Object System.Drawing.Point(20, ($y + 22))
$form.Controls.Add($dt)
$textboxes[$key] = $dt
$y += 60
} elseif ($key -eq "RecordEndTime") {
$tb = New-Object System.Windows.Forms.TextBox
$tb.Size = New-Object System.Drawing.Size(150, 25)
$tb.Text = "01:00:00" # default 1 hour duration
$tb.Location = New-Object System.Drawing.Point(20, ($y + 22))
$hint = New-Object System.Windows.Forms.Label
$hint.Text = "(Duration: HH:MM:SS)"
$hint.Location = New-Object System.Drawing.Point(180, ($y + 25))
$hint.AutoSize = $true
$form.Controls.Add($hint)
$form.Controls.Add($tb)
$textboxes[$key] = $tb
$y += 60
} else {
$tb = New-Object System.Windows.Forms.TextBox
$tb.Size = New-Object System.Drawing.Size(820, 25)
$tb.Text = [string]$jsonDefaults[$key]
$tb.Location = New-Object System.Drawing.Point(20, ($y + 22))
$form.Controls.Add($tb)
$textboxes[$key] = $tb
$y += 60
}
}
# --- Buttons ---
$okButton = New-Object System.Windows.Forms.Button
$okButton.Text = "OK"
$okButton.Location = New-Object System.Drawing.Point(620, ($y + 20))
$okButton.Add_Click({ $form.Tag = "OK"; $form.Close() })
$form.Controls.Add($okButton)
$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.Text = "Cancel"
$cancelButton.Location = New-Object System.Drawing.Point(720, ($y + 20))
$cancelButton.Add_Click({ $form.Tag = "Cancel"; $form.Close() })
$form.Controls.Add($cancelButton)
# --- Show form ---
$form.ShowDialog() | Out-Null
if ($form.Tag -eq "OK") {
$jsonFields = @{}
# 1. Initialize $jsonFields with all values from the form inputs
foreach ($key in $textboxes.Keys) {
$inputValue = [string]$textboxes[$key].Text
if ($unquotedFields -contains $key) {
# Attempt to convert to a long integer for numeric fields
try {
$jsonFields[$key] = [long]::Parse($inputValue)
} catch {
# Store as string if conversion fails (e.g., if blank or non-numeric)
$jsonFields[$key] = $inputValue
}
} else {
# Standard string fields
$jsonFields[$key] = $inputValue
}
}
# 2. Recalculate RecordStartTime (Epoch time from DateTimePicker)
if ($textboxes.ContainsKey("RecordStartTime")) {
$key = "RecordStartTime"
$dt = $textboxes[$key].Value
$jsonFields[$key] = [long]([datetimeoffset]$dt).ToUnixTimeSeconds()
}
# 3. Recalculate RecordEndTime (Epoch time + Duration)
if ($textboxes.ContainsKey("RecordEndTime")) {
$key = "RecordEndTime"
$parts = $textboxes[$key].Text -split ":"
$durationSec = 0
# Ensure $parts are cast to [int] before multiplication/addition
if ($parts.Count -eq 3) {
$durationSec = ([int]$parts[0] * 3600) + ([int]$parts[1] * 60) + [int]$parts[2]
}
# Calculate RecordEndTime by adding duration to RecordStartTime
if ($jsonFields.ContainsKey("RecordStartTime")) {
$jsonFields[$key] = [long]($jsonFields["RecordStartTime"] + $durationSec)
} else {
# Fallback in case RecordStartTime failed to set
$jsonFields[$key] = [long]$durationSec
}
}
# --- File Picker ---
$fileDialog = New-Object System.Windows.Forms.OpenFileDialog
$fileDialog.Title = "Select file to update (will overwrite beginning)"
$fileDialog.Filter = "All Files (*.*)|*.*"
if ($fileDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$filePath = $fileDialog.FileName
Write-Host "`nSelected file:`n$filePath" -ForegroundColor Yellow
# --- Generate JSON-like byte array with separators ---
$blockSize = 184
# Separator bytes based on your input: "G_ú."
$separatorBytes = @(0x47, 0x5F, 0xFA, 0x10)
$byteArray = @()
$byteArray += $separatorBytes
$charCount = 1 # Start charCount at 1 for the initial '{'
$byteArray += [System.Text.Encoding]::UTF8.GetBytes("{")
# FIX: Create a guaranteed clean, non-null array of keys for iteration
$keys = @($jsonFields.Keys)
for ($i = 0; $i -lt $keys.Count; $i++) {
$key = $keys[$i]
# This check is now mostly redundant due to the fix, but harmless for safety
if (-not $jsonFields.ContainsKey($key)) { continue }
$value = $jsonFields[$key]
# Determine value format (quoted string or unquoted number)
if ($unquotedFields -contains $key -and $value -is [long]) {
# Unquoted value (number/long)
$textBlock = "`"$key`":$value"
} else {
# Quoted value (string)
$textBlock = "`"$key`":`"$value`""
}
# Add comma, unless it's the last element
if ($i -lt $keys.Count - 1) {
$textBlock += ","
}
$textBlockBytes = [System.Text.Encoding]::UTF8.GetBytes($textBlock)
foreach ($byte in $textBlockBytes) {
if ($charCount -eq $blockSize) {
# Insert separator and reset counter
$byteArray += $separatorBytes
$charCount = 0
}
$byteArray += $byte
$charCount++
}
}
# Add the closing brace '}'
$closeBraceBytes = [System.Text.Encoding]::UTF8.GetBytes("}")
foreach ($byte in $closeBraceBytes) {
if ($charCount -eq $blockSize) {
$byteArray += $separatorBytes
$charCount = 0
}
$byteArray += $byte
$charCount++
}
# --- Overwrite beginning of file ---
$fs = [System.IO.File]::Open($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Write)
try {
$fs.Write($byteArray, 0, $byteArray.Length)
} finally {
$fs.Close()
}
Write-Host "`nFile updated (beginning overwritten)." -ForegroundColor Green
Write-Host ("RecordStartTime = " + $jsonFields["RecordStartTime"])
Write-Host ("RecordEndTime = " + $jsonFields["RecordEndTime"])
} else {
Write-Host "`nNo file selected." -ForegroundColor Red
}
} else {
Write-Host "Operation cancelled by user." -ForegroundColor Red
}