Files
command-center/agent/managed-skills/plex-title-authority-for-relayout/SKILL.md
T

3.7 KiB
Raw Blame History

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 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).