--- name: plex-title-authority-for-relayout description: "Read Plex's metadata cache as the clean-title authority (ziggurat schema, immutable read, no sqlite3 CLI) when naming or regrouping a media library from scripts instead of filename parsing." --- Plex has already matched most of a library to real titles. When a script must name folders/files (Jellyfin relayout, dedup reports, library census), **read Plex's DB and let it override filename parsing** — `Alien.Directors.Cut.1979.1080p.BRrip.x265-RARBG` → `Alien`, `1917.2019.2160p…` → `1917`. Measured on dankserver's Movies section: 3,773 parts, **0** filename-shaped titles, 80% of media files matched (unmatched = extras, BDMV disc trees, TV). ## Locate and open the DB (no sqlite3 CLI needed) ``` DB="/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db" # data dir: /var/lib/plexmediaserver (host install) or the container's volume ``` ```python con = sqlite3.connect(f"file:{DB}?mode=ro&immutable=1", uri=True) # no copy, no wal, no lock ``` `immutable=1` is the whole trick: the DB was 1.5 GiB and Plex was live — no copy, no `sqlite3` binary, no risk of touching the service. Set `busy_timeout`; expect occasional inconsistency, acceptable for a read-only census. ## Schema trap: Plex renamed the tables ("ziggurat") The old `sections` table **does not exist** — querying it throws, which reads like "wrong DB". Real names: | table | join | key columns | |---|---|---| | `library_sections` | — | `id`, `name`, `section_type` (`movie`/`show`), `agent`, `scanner` | | `section_locations` | `library_section_id` | `root_path`, `available` | | `metadata_items` | `parent_id` → own `id` | `metadata_type`, `title`, `original_title`, `edition_title`, `year`, `indexed`, `deleted_at` | | `media_items` | `metadata_item_id` | `container`, `video_codec`, `audio_codec`, `width`, `height` | | `media_parts` | `media_item_id` | **`file`** (full path), `size`, `duration`, `deleted_at` | Census section coverage first (`count(*) where file like '/%'`), then dump: ```sql select p.file, mi.title, mi.original_title, mi.edition_title, mi.year, m.video_codec, m.width, m.height from media_parts p join media_items m on m.id = p.media_item_id join metadata_items mi on mi.id = m.metadata_item_id where p.file like '/%' and p.deleted_at is null and mi.deleted_at is null ``` For a movie library the leaf *is* the film; `parent_id` is NULL, so a `left join` on parent returns the same title. `metadata_type`: 1 = movie, 2 = show (a show section riding the same root means episodes live in the MOVIES folder — worth reporting). ## Using it in a renamer - **One film, one name:** match any copy → apply that title to the whole group. Keyed by `file` path, so the map is exact. - **Override, don't replace:** the parser still names the unmatched (extras, disc trees, TV, unscanned). Keep it as a fallback tier. - **Bucket on the old key, name with Plex:** group/score sidecars with the parser's words (a `.srt` matches on the year token), format the folder from Plex: `f"{title} ({year})"`. - **Sanitize for the FS:** Plex titles carry `:` and `&` (`2001: A Space Odyssey`, `Rock & Rule`) — those must go through the same sanitizer as filename-derived names. ## Cross-check before trusting a parser Score the parser against the cache: count titles matching `2160p|1080p|x26[45]|bluray|hevc|10bit` — Plex returned **0**. Every parser mismatch is a real bug class: year-echo pseudo-titles (`1917.2019.2160p…`), two-year titles (`2001…1968`), numeric titles eaten as track indexes (`12 Monkeys`, `007 Skyfall`, `000 Miles to Graceland`, `0 - First Dates` — keep the number unless it's 1–2 digits glued to the words with a year present, i.e. `01. Bye Bye Baby`).