Files
2026-09-29 06:46:40 -05:00

9.1 KiB

Selective Plex state salvage — Banshee

Purpose: Recover Plex identity, settings, library databases, scheduled database backups and small customizations without crawling Plex's Metadata, Media, Cache, Logs, transcoding, .nv or xcode trees.

This tool is specific to the Banshee recovery incident. It is not a general-purpose ext4 recovery program: both the damaged LV and known-dead block range are hard-coded in plex-state.py.

What we know

  • Damaged source LV: /dev/mapper/plex--new-plex--new, ext4, 4,096-byte filesystem blocks.
  • Confirmed blank replacement-PV extent in filesystem block numbers: 3,418,636,288 through 4,395,389,951, inclusive.
  • Recovery drive: /mnt/recovery (currently exFAT; physically separate from damaged LV).
  • Original Plex state directory: /plexmediaserver/Library/Application Support/Plex Media Server.
  • The important Plex directories and their contents were located using debugfs -c because the damaged ext4 filesystem cannot be mounted.
  • Preferences.xml (inode 249299786, 1,416 bytes, physical block 2,379,907,133) and .LocalAdminToken (inode 249299774, 42 bytes, block 33,822,208) both lie outside the dead block range. They still need extracting; knowing their extents does not itself create a backup.
  • The inspected Plug-in Support/Databases directory contained two live databases and four dated pairs from 2026-08-20, -23, -26 and -29: roughly 2.94 GiB for all 10 database files combined. The directory listing alone does not establish which of these files survived; scan them before promising a restore.

Package

  • plex-state.py: shared scanner and extractor.
  • plex-scan.sh: select files by explicit allowlist and inspect their full ext4 block mapping. No file data is copied.
  • plex-recover.sh: extract only files classified SAFE in that profile's scan manifest.

These names won't collide with the generic scan.sh / recover.sh already in your tools directory.

The two profiles

core (default) includes:

  • Preferences.xml and .LocalAdminToken at the Plex Media Server root;
  • live library and blobs databases, matching scheduled dated backups, and any present -wal / -shm companions;
  • XML files directly beneath Plug-in Support/Preferences (including the observed Hama/Audnexus preferences).

extras (optional) recursively includes only:

  • Plug-ins/Hama.bundle;
  • Plug-ins/Audnexus.bundle;
  • Scanners/Series (your custom scanner location).

Neither profile includes Plex's giant Metadata, Media, Cache, Plug-in Support/Caches, diagnostics, crash reports or .nv / xcode directories. Plugin-specific Plug-in Support/Data is deliberately excluded: top-level inspection found many agent directories, including Hama and Audnexus, but we haven't inspected their children to distinguish durable custom settings from caches. If you need those directories too, inspect them first rather than blindly copying everything.

Before scanning

Plex must remain stopped; don't start it against the incomplete/empty media tree. Confirm the recovery drive is mounted and that the source LV is still kernel read-only:

findmnt /mnt/recovery
blockdev --getro /dev/mapper/plex--new-plex--new   # must print 1

The scripts independently enforce these checks and refuse to run otherwise. No software installation is required beyond existing Python 3 and debugfs/blockdev/findmnt.

Scan, inspect, recover

Unzip this package into your existing plex-tools directory. Then:

chmod +x plex-*.sh plex-state.py
./plex-scan.sh                  # defaults to 'core'
cat /mnt/recovery/plex-state-core-scan-summary.txt

Review the summary and optionally upload it for assessment before extracting anything. It lists the individual database files and their extent classifications. In particular, determine whether a complete matching pair of the library DB and library blobs DB survives, ideally using the same scheduled-backup date if the live files aren't both usable.

If the scan results justify extraction:

./plex-recover.sh               # core
cat /mnt/recovery/plex-state-core-recovery-summary.txt

Optional plugin and scanner pass:

./plex-scan.sh extras
cat /mnt/recovery/plex-state-extras-scan-summary.txt
./plex-recover.sh extras         # only after reviewing the extras scan

Each profile maintains an independent manifest, scan summary, extraction log and recovery summary under /mnt/recovery. The extracted files keep their source directory structure under /mnt/recovery/plexmediaserver/... so they're easy to identify for later restoration. A small temporary directory lives under /mnt/recovery/.plex-state-tmp.

Recoveries may be restarted. Existing destination files of the same size are skipped, but this is not an integrity check of an existing copy; check your earlier logs/hashes or delete the untrusted destination copy before rerunning it.

How the strict block checks work

The original generic media salvage script classified a file SAFE if its observed data extents missed the dead disk. That's useful for media triage, but insufficiently conservative for Plex databases: missing extent-tree blocks could hide additional portions of the file.

This scanner also:

  1. Rejects files whose extent-tree metadata (ETB) is on the dead disk even if some data extents are visible.
  2. Requires the data extents to map every logical file block, in order, without overlaps or holes, with matching logical/physical lengths.
  3. Rejects unrecognized extent formats instead of assuming they're safe.
  4. Rechecks each SAFE inode and its mapping immediately before recovery.
  5. Requires the extracted byte count to match the inode size and computes a SHA-256 digest of each newly extracted file. Those digests go in the profile's *-recovery.jsonl log.

A matching size, SHA-256 hash of the recovered copy, and surviving extents do not prove that a SQLite database is internally consistent. Perform offline Plex-compatible database integrity checks before restoring the databases. Legitimately sparse files may be classified UNKNOWN by these conservative checks.

A scan may also warn about inaccessible/missing directories. Review the warnings: an unreadable directory cannot be traversed, and even a readable directory listing may be incomplete if filesystem metadata was badly damaged.

Restoring Plex after rebuilding

  1. Finish media recovery and rebuild the array. Confirm the new /storage is mounted and all required bind mounts/shares are configured.
  2. Do not start Plex yet. Copy the salvaged state from the recovery drive to its original location on the new filesystem, retaining complete paths. Consider restoring the August 29 matched database pair if the live pair is incomplete or fails validation. Never mix the two dates arbitrarily.
  3. The exFAT Passport does not preserve original POSIX file ownership and permissions. The inspected Plex tree was largely UID:GID 998:998, but verify the UID/GID used by your rebuilt Plex service before setting ownership. Restrict Preferences.xml, .LocalAdminToken and database file access to the Plex service account.
  4. Validate the database before launching Plex using a supported Plex SQLite workflow. If a -wal file is present, keep it with its matching live database during validation; -shm is transient and shouldn't be blindly restored. Dated database backups should be validated as corresponding pairs.
  5. Keep automatic trash emptying disabled and avoid scanning libraries until the actual media directories and their expected paths are present, or Plex may forget missing library items.
  6. Keep the extracted state and all manifests/logs untouched until Plex starts successfully, recognizes its libraries and preserves expected accounts/settings/watch history.

Token security

Both Preferences.xml and .LocalAdminToken may contain credentials or authentication material. This recovery target currently uses exFAT, so chmod/chown protections on the Passport don't behave like ext4. Keep the recovery drive physically secure, avoid uploading these two files or pasting their contents into chat, and move them to appropriately permissioned storage as soon as practical. The scan logs only metadata and paths, never file contents.

If the original geometry is different next time

Do not reuse these numeric block boundaries on another LV. Re-derive them from the actual dmsetup table mapping for the missing PV and convert 512-byte sectors to ext4 filesystem blocks. In this incident, the dead segment began at LV sector 27,349,090,304 and occupied 7,814,029,312 sectors. With 4 KiB filesystem blocks, 8 sectors/block:

first missing filesystem block = 27,349,090,304 / 8 = 3,418,636,288
missing filesystem block count =  7,814,029,312 / 8 =   976,753,664
last missing filesystem block  =  3,418,636,288 + 976,753,664 - 1
                               =  4,395,389,951

Your broader LVM recovery README documents how to identify the missing segment and create a replacement PV without writing over surviving data. Never run mkfs, e2fsck -y or a destructive vgcfgrestore on the old array before completing desired salvage.