Files
mediabox-bootstrap/migration/gen-preferences.ps1
T
ClaudeandClaude Opus 5 351eab41ac Full Plex migration: preserve everything, no rebuild
Reverses the forward-only decision. That call predated measuring the library;
at 27 TB / 25,148 video items / 337 GB of generated preview cache, rebuilding
costs 1-3 weeks of saturated gigabit and permanently loses every manual match
fix across 23,493 episodes. Migration preserves watch history, resume points,
collections, playlists, manual matches, artwork choices, added-at dates, the
337 GB cache, and server identity - so shared users stay invited, clients do
not re-add, and there is no claim step at all.

migration/remap.sql
  Four path columns remapped: media_parts.file (80,126), section_locations
  .root_path (11 -> 9), media_streams.url (14,837) and metadata_items.guid (9).
  Two formats, not one: backslash/UNC for the first two, file:// with %20
  encoding for the last two - decoding those %20s would break every subtitle
  reference containing a space, which given share names like Radio Shows is
  most of them.

  A scan of all 80 text columns across 82 tables found SIX columns matching
  korval. Only four are paths. taggings.text is one row reading Dr. Korval,
  and metadata_items.summary is four Liaden Universe blurbs about Clan Korval.
  A bare REPLACE on the word would have corrupted a cast credit and four book
  summaries, so every statement is anchored to a path prefix. Proven against a
  synthetic database built from the real path shapes: 12/12, including both
  false positives surviving byte-identical.

  Also drops the Audio Books and Music Organized roots - configured as library
  roots but holding 0 files / 0 bytes. The consolidation had already happened;
  only the dead roots remained.

migration/migrate-db.sh
  Runs the remap through Plex own SQLite build borrowed from the container
  image, keeps a .pre-remap rollback, asserts the counts moved 1:1 and the
  false positives did not, then stats 200 random remapped paths against the
  real filesystem. That last check is the one that matters - SQL running
  without error proves nothing.

migration/gen-preferences.ps1
  Plex live settings store on Windows is the REGISTRY, not Preferences.xml,
  and the two disagree here. Folds ~50 values into one Linux file, dropping
  Windows-only keys including the per-GPU limit keyed by 10de:1b81, the GTX
  1070 PCI ID. Preserves MachineIdentifier and the online token, which is what
  keeps the server identity. The existing Preferences.xml is malformed anyway
  (duplicate allowedNetworks) and will not parse strictly.

scripts/20-cifs.sh
  Eight shares down to six. Mount points now mirror the share names verbatim -
  /mnt/nas/Home Movies, space and all - because that reduces the remap to one
  uniform prefix substitution instead of eleven special cases. New requirement
  this creates: \040 escaping in BOTH fstab fields, not just the share name.
  Verified, plus a round-trip check that a remapped DB path lands under the
  generated mount point.

plex/docker-compose.yml
  Six read-only NAS binds whose source path equals target path, so database,
  host and container agree with no translation. No PLEX_CLAIM. Phase 2 Media
  relocation present but commented.

scripts/60-media-relocate.sh
  Post-soak, optional: moves the 337 GB cache to the 3TB ext4 disk so future
  growth (~28 MB per content-hour) stops eating the SSD. Plex does not support
  this, so the script forces generation on one title afterwards to prove writes
  survive the EXDEV boundary, and rollback is deleting a compose override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 21:41:26 -04:00

111 lines
4.6 KiB
PowerShell

<#
================================================================================
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('<?xml version="1.0" encoding="utf-8"?>')
[void]$sb.Append('<Preferences')
foreach ($k in $pairs.Keys) {
$esc = [System.Security.SecurityElement]::Escape($pairs[$k])
[void]$sb.Append(" $k=`"$esc`"")
}
[void]$sb.AppendLine('/>')
# 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 " $_ <redacted>" }
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 " <per-GPU transcode limit keys>"
Write-Host ""
Write-Host "Verify it parses before you rely on it:"
Write-Host " [xml](Get-Content -Raw '$Out') | Out-Null; 'XML OK'"