3.7 KiB
name, description
| name | description |
|---|---|
| plex-title-authority-for-relayout | 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="<data>/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
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 '<root>/%'), then dump:
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 '<root>/%' 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
filepath, 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
.srtmatches 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).