Files
mediabox-bootstrap/migration
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
..

Plex Migration — Windows → Linux, nothing lost

Supersedes the earlier "forward-only, fresh Plex" decision. That call was made before the library was measured. At 27 TB, 25,148 video items and 337 GB of already-generated preview cache, rebuilding fresh costs one to three weeks of saturated NAS link and permanently loses every manual match fix. Migration is the cheaper and lower-risk option.

What survives

Watch history and resume points across all items, play counts, ratings, collections, playlists, every manual match correction, poster and artwork choices, added-at dates (so Recently Added stays meaningful), the 337 GB preview cache, and the server identity — shared users stay invited, clients do not need to re-add the server, and there is no claim step at all.

Plex's own article calls cross-OS moves "possible but not officially supported and not covered here." That is a documentation gap, not a technical barrier. The database is platform-independent; what Plex declines to help with is mapping the settings, which gen-preferences.ps1 does.

Facts everything below depends on

Source PMS 1.43.3.10828, Windows 10, data dir D:\Plex\Local Data
Server identity machineIdentifier eb69ec8a… — preserved, not regenerated
Data directory ~374 GB total → ~364 GB after pruning
Library 1,360 movies · 23,493 episodes · 295 other video · 20,798 audio
Path rows 80,126 media_parts · 14,837 media_streams · 11 → 9 roots
Target layout Whole data dir on the 500 GB SSD (~75 GB free)
NAS mounts 6 shares at /mnt/nas/<exact share name>

M0 — Prepare, while Windows is still running

Disable automatic trash emptying first. This is step 1 of Plex's own procedure and it matters more here than usual: the registry confirms autoEmptyTrash = 1, and the new NAS account is read-write. A missing mount at scan time would mean Plex deletes library records it has permission to delete. Settings → Library → untick Empty trash automatically after every scan.

Generate the Linux settings file from the registry:

powershell -ExecutionPolicy Bypass -File .\gen-preferences.ps1 -Out C:\mbx\Preferences.xml
[xml](Get-Content -Raw C:\mbx\Preferences.xml) | Out-Null; 'XML OK'

Stop Plex Media Server completely — tray icon → Exit, then confirm no Plex Media Server.exe remains. The database must not be open when copied, or the WAL will be mid-flight and the copy inconsistent.

Copy the data directory to the NAS. Skip what must not travel:

robocopy "D:\Plex\Local Data\Plex Media Server" "\\korval\Share\backups\mediabox\PMS" `
  /E /COPY:DAT /R:2 /W:5 /MT:16 `
  /XD "Cache" "Codecs" "Drivers" "Crash Reports" "Logs" "Updates" `
  /LOG:C:\mbx\pms-copy.log /TEE

Codecs and Drivers are Windows binaries — copying them to Linux breaks transcoding. Cache is regenerable. Excluding them also trims the copy.

GATE M0 — robocopy reports 0 failures. Plug-in Support\Databases\, Metadata\ and Media\ are all present on the NAS. Do not proceed on a copy you have not listed.


M1 — Dry run, before anything is destroyed

The whole migration can be rehearsed while Windows still works. This is the step that makes the rest safe.

On arrsstack (or any Linux host with the NAS mounted), take only the database and Preferences — a couple of GB, not 364 — and run the remap:

sudo ./migrate-db.sh /path/to/copy/com.plexapp.plugins.library.db

Then start a throwaway Plex against it with no internet access, so it cannot contact plex.tv and collide with the live server that shares its identity:

docker network create --internal plex-dryrun
docker run --rm -d --name plex-dryrun --network plex-dryrun \
  -v /path/to/dryrun-config:/config \
  -v "/mnt/nas/media:/mnt/nas/media:ro" \
  lscr.io/linuxserver/plex:latest

GATE M1 — browse the unclaimed server directly and confirm: Movies shows 1,360, TV Shows shows 23,493 episodes, watch state and resume points are present, and a title's file path resolves. If this passes, the migration is proven. If it fails you have lost an afternoon and nothing else.


M2 — Install Linux

Follow the main runbook.md phases 2–5 (BIOS, USB, install, first boot). The autoinstall targets Disk 0 by serial 1808AE802176 and leaves the 3 TB disk untouched.

Write the NAS credentials and bring up the six mounts:

printf 'username=mediabox\npassword=<pass>\ndomain=WORKGROUP\n' \
  | sudo tee /etc/cifs/korval.cred >/dev/null
sudo chmod 600 /etc/cifs/korval.cred
sudo systemctl daemon-reload
sudo /srv/mediabox-bootstrap/scripts/20-cifs.sh --verify

GATE M2 — all six mounts report [ok], and stat -c '%u:%g' /mnt/nas/media returns 3000:3000. Wrong ownership means Plex hits permission errors on every file.


M3 — Restore and remap

sudo install -d -o 3000 -g 3000 /srv/plex/config
sudo rsync -a --info=progress2 \
  "/mnt/nas/Share/backups/mediabox/PMS/" \
  "/srv/plex/config/Library/Application Support/Plex Media Server/"

sudo install -o 3000 -g 3000 -m 0644 /path/to/Preferences.xml \
  "/srv/plex/config/Library/Application Support/Plex Media Server/Preferences.xml"

sudo /srv/mediabox-bootstrap/migration/migrate-db.sh \
  "/srv/plex/config/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db"

sudo chown -R 3000:3000 /srv/plex/config

migrate-db.sh keeps a .pre-remap rollback copy, asserts that no Windows paths remain, that the counts moved 1:1, that the "Dr. Korval" tag and the four Liaden book summaries are untouched, and finally stats 200 random remapped paths against the real filesystem.

GATE M3 — migrate-db.sh exits 0 with the sample check reporting 200/200 sampled files resolve on disk. That last line is the one that matters; SQL running without error proves nothing.


M4 — First start

sudo RENDER_GID=$(stat -c '%g' /dev/dri/renderD128) \
  /srv/mediabox-bootstrap/scripts/50-plex.sh

No claim token. The server already has its identity.

GATE M4 — plex.thewichersfamily.com loads and shows Magi Plex with your libraries. Watch history and Continue Watching are intact. A remote client that already had this server still sees it without re-adding. Play something and confirm intel_gpu_top shows the video engine working.


M5 — Soak

One to two weeks on the supported layout. Confirm mounts survive a NAS reboot, the GPU survives a kernel update, and the box comes back unattended from a real power cut. That last one is the acceptance test for "headless".


M6 — Relocate the preview cache (optional, post-soak)

Only after M5. Moves the 337 GB Media/ directory to the 3 TB ext4 disk so future thumbnail growth (~28 MB per content-hour) stops consuming the SSD.

sudo /srv/mediabox-bootstrap/scripts/60-media-relocate.sh

Plex does not support relocating this directory. The script performs the move, enables the bind mount, and then forces thumbnail generation on a single title to prove writes succeed across the filesystem boundary — the one failure mode that cannot be reasoned about, only tested. Rollback is moving it back.


If something goes wrong

Before M2, everything is reversible — Windows still boots. After M2 the recovery path is reinstall, not roll back: the USB rebuilds the box in about twenty minutes and the data directory is still on the NAS. The database copy on the NAS outlives every step here, and migrate-db.sh leaves a .pre-remap copy beside every database it touches.