#
================================================================================
gen-preferences.ps1 — build a Linux Preferences.xml from the Windows registry
Run ON THE WINDOWS BOX, before it is wiped. Read-only: it does not modify the
registry or the running server.
powershell -ExecutionPolicy Bypass -File .\gen-preferences.ps1 -Out C:\mbx\Preferences.xml
WHY THIS EXISTS
On Windows, Plex's live settings store is the REGISTRY
(HKCU\SOFTWARE\Plex, Inc.\Plex Media Server), not Preferences.xml. The two
disagree on this install — the on-disk Preferences.xml was last written in
2021 and is missing settings the registry has. Linux has no registry, so
everything must be folded into a single Preferences.xml. This is the
"mapping the additional server settings" that Plex's own migration article
calls complicated and declines to cover.
It also rebuilds the file cleanly, which is required anyway: the existing
Preferences.xml is malformed (duplicate allowedNetworks attribute) and will
not parse with a strict XML parser.
WHAT IS PRESERVED
MachineIdentifier, ProcessedMachineIdentifier, CertificateUUID and the
online token carry across. Those are what keep your server IDENTITY — shared
users stay invited, clients do not need to re-add the server.
Secrets are written to the output file but never printed to the console.
================================================================================
#>
[CmdletBinding()]
param(
[string]$Out = "$PSScriptRoot\Preferences.xml",
[string]$TranscodeDir = "/transcode"
)
$ErrorActionPreference = 'Stop'
$key = 'HKCU:\SOFTWARE\Plex, Inc.\Plex Media Server'
if (-not (Test-Path $key)) { throw "registry key not found: $key" }
# --- keys that are meaningless or harmful on Linux ---------------------------
$drop = @(
'InstallFolder' # C:\Program Files\... — Windows path
'LocalAppDataPath' # D:\Plex\Local Data — Windows path
'PreferredNetworkInterface' # a Windows adapter GUID
'LastAutomaticMappedPort' # regenerated on first run
'PubSubServer' # regenerated; the Ping value is a byte array
'PubSubServerPing'
'PubSubServerRegion'
'CloudSyncNeedsUpdate'
)
# Per-GPU transcode limits are keyed by PCI vendor:device. 10de:1b81 is the
# GTX 1070. Meaningless once the GPU is addressed through /dev/dri or the
# NVIDIA container runtime.
$dropPattern = '^_[0-9a-f]{16}-'
# --- keys we deliberately override -------------------------------------------
$override = @{
# Step 1 of Plex's own migration procedure, and doubly important here: the
# NAS account is read-write, so a missing mount at scan time plus this
# setting means Plex deletes library records it has permission to delete.
'autoEmptyTrash' = '0'
# Windows path -> the tmpfs mount defined in the compose file.
'TranscoderTempDirectory' = $TranscodeDir
}
$props = Get-ItemProperty $key
$pairs = [ordered]@{}
foreach ($p in ($props.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | Sort-Object Name)) {
$n = $p.Name
if ($drop -contains $n) { continue }
if ($n -match $dropPattern) { continue }
$v = $p.Value
if ($v -is [byte[]]) { continue } # not representable as an attribute
$pairs[$n] = [string]$v
}
foreach ($k in $override.Keys) { $pairs[$k] = $override[$k] }
# --- emit ---------------------------------------------------------------------
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('')
[void]$sb.Append('')
# UTF-8 WITHOUT BOM. A BOM here is the same class of bug that silently breaks
# administrators_authorized_keys on Windows OpenSSH.
[System.IO.File]::WriteAllText($Out, $sb.ToString(), (New-Object System.Text.UTF8Encoding($false)))
# --- report: NAMES ONLY, never values ----------------------------------------
$secretish = '(Token|Identifier|UUID|GracenoteUser|Mail)'
Write-Host "wrote $Out ($($pairs.Count) settings)"
Write-Host ""
Write-Host "carried across:"
$pairs.Keys | Where-Object { $_ -match $secretish } | ForEach-Object { Write-Host " $_ " }
Write-Host ""
Write-Host "overridden:"
$override.Keys | ForEach-Object { Write-Host (" {0} = {1}" -f $_, $override[$_]) }
Write-Host ""
Write-Host "dropped (Windows-only):"
$drop | ForEach-Object { Write-Host " $_" }
Write-Host " "
Write-Host ""
Write-Host "Verify it parses before you rely on it:"
Write-Host " [xml](Get-Content -Raw '$Out') | Out-Null; 'XML OK'"