Enhanced Populating HDHomeRun Metadata - PowerShell Script

Want to write your own code to work with a HDHomeRun or work with the HDHomeRun DVR? We are happy to help with concepts, APIs, best practices.
Post Reply
msuckow
Posts: 61
Joined: Fri Mar 10, 2017 4:20 pm
x 2

Enhanced Populating HDHomeRun Metadata - PowerShell Script

Post by msuckow »

This post is for folks who would like to bring in external recordings into the HDHomeRun environment.

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:

Image

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
}

Post Reply