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

124 lines
9.1 KiB
Markdown

# 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:
```bash
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:
```bash
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:
```bash
./plex-recover.sh # core
cat /mnt/recovery/plex-state-core-recovery-summary.txt
```
Optional plugin and scanner pass:
```bash
./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:
```text
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.