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

mediabox-bootstrap

Everything the media box (10.0.1.20, hostname mediabox) needs after the Ubuntu 24.04 autoinstall has laid down the base system.

The autoinstall USB is deliberately thin: sshd, a key, DHCP, Docker CE, /srv, and a first-boot unit that clones this repo and runs bootstrap.sh. That split exists so the interesting parts stay in git, reviewable and re-runnable, instead of frozen onto a USB stick that nobody can diff.

Full context, decisions and the phase-by-phase runbook live in the workspace repo at projects/mediabox-linux/.


Layout

bootstrap.sh              orchestrator — runs the stages, fails soft
scripts/
  00-preflight.sh         asserts hardware/BIOS state. Changes nothing.
  10-secrets.sh           idempotent ADD-ONLY seeder for /srv/secrets/stacks.env
  20-cifs.sh              the 8 NAS mounts (--verify mode included)
  30-nvidia.sh            NVIDIA 580 + container toolkit
  40-shell-mcp.sh         builds and starts the native shell-mcp
  50-plex.sh              Plex. Run BY HAND — needs a live claim token.
shell-mcp/                amd64 build of the MCP server (server.py, Dockerfile,
                          docker-compose.yml, pinned requirements.txt)
plex/docker-compose.yml   Plex service definition
secrets/stacks.env.example  placeholder master env

Running it

sudo /srv/mediabox-bootstrap/bootstrap.sh          # all stages
sudo /srv/mediabox-bootstrap/scripts/20-cifs.sh    # one stage
sudo /srv/mediabox-bootstrap/scripts/20-cifs.sh --verify

Every stage is idempotent. Re-running is the normal way to use this, not an emergency measure.


Design rules

Nothing may leave the box unreachable. sshd is up before any of this runs and no stage may compromise that. bootstrap.sh catches stage failures and continues; the systemd unit declares SuccessExitStatus=0 1 so a bad stage can never wedge boot. There is no keyboard attached to this machine.

Secrets never ride on removable media. The CIFS credentials file is created empty by the autoinstall and filled in post-boot over the MCP. PLEX_CLAIM tokens expire in four minutes and are passed as a one-shot environment variable, never written to disk or committed.

One secrets file per host. This box has its own /srv/secrets/stacks.env, using the same [OPERATOR] / [VALUES] / [MANIFEST] tiering as arrsstack, but it is not shared or mounted from anywhere. Canonical copies live in Vaultwarden.

The seeder only ever adds. 10-secrets.sh will never overwrite an existing key, change its value, or move it. If KEY= is present it is skipped entirely. Verified against a pre-populated file: existing values, unrelated keys, the operator tier and existing manifest lines all survive byte-for-byte, and a second run is a no-op.


Notes that will save you an evening

shell-mcp was originally built on arm64. It builds clean on amd64 — the whole Python dependency tree resolves to prebuilt manylinux x86_64 wheels, so no compiler is needed. Two real changes were required:

  • Dependencies are now pinned. The original installed mcp starlette uvicorn unpinned. starlette has since gone 1.x. Unpinned installs are tolerable in a hand-run build and dangerous inside a first-boot script.
  • Port and description. It listens on 8103 (not 8085) so NPM proxy host 42 can simply be repointed, and the tool description says media box, not arrsstack — otherwise every session starts with the wrong idea of which host it is touching.

The GTX 1070 is Pascal, and NVIDIA branch 580 is the last one that supports it. There will be no 590 for this card. 30-nvidia.sh installs 580 explicitly and pins against newer branches. Never use ubuntu-drivers autoinstall here — it will cheerfully install a branch that drops the card. And it must be the proprietary module, not -open: the open kernel modules need Turing or newer.

Secure Boot must stay off. It is off today. A BIOS update resets ASUS defaults and turns it back on, at which point the DKMS module needs interactive MOK enrollment at a console this box does not have. 00-preflight.sh checks for this.

nofail on every CIFS mount is not optional. Without it, an unreachable NAS drops a headless box to an emergency shell waiting for a root password on a keyboard that is not plugged in.

autoEmptyTrash is the most dangerous default here. The NAS account is read-write. If a mount is missing when Plex scans, Plex sees an empty library and will act on that. 50-plex.sh disables it before any library is added.

Plex's PUID/PGID must equal the CIFS uid=/gid=. Both are 3000 (media). If they ever drift, every file is permission-denied.

S
Description
First-boot bootstrap for the media box (10.0.1.20) — CIFS mounts, NVIDIA 580, native shell-mcp, Plex. Paired with projects/mediabox-linux in claude-workspace.
Readme
142 KiB
Languages
Shell 79.1%
Python 12.2%
PowerShell 7.2%
Dockerfile 1.5%