Skip to main content
  1. Posts/

Organizing Local Lecture Videos in Plex with Proper Metadata

·773 words·4 mins
Author
Yang Hu

How to structure local lecture/course videos (without an official TMDB/TVDB entry) in Plex as a proper TV Show with seasons, episode titles, and custom descriptions set via the Plex API.

The example here is Jonathan Biss’s Exploring Beethoven’s Piano Sonatas — a 5-part Coursera course from the Curtis Institute of Music, stored on a Synology NAS.

The Problem
#

Plex’s default scrapers rely on TMDB or TVDB. Local lecture videos with no database entry either show up as a mess of unmatched files, or get incorrectly matched to something unrelated.

The solution: treat the course as a TV Show, use Plex’s Personal Media Shows agent, and push custom metadata via the Plex API.

Step 1: Reorganize Files into Season/Episode Structure
#

Plex’s TV Show scanner expects files named with SxxExx:

1
2
3
4
5
6
7
Exploring Beethoven's Piano Sonatas/
├── Season 01/
│   ├── Exploring Beethoven's Piano Sonatas - S01E01 - Lecture 1, Part 1.mp4
│   ├── Exploring Beethoven's Piano Sonatas - S01E01 - Lecture 1, Part 1.en.srt
│   └── ...
├── Season 02/
└── ...

Each “lecture” maps to a Season; each “part” maps to an Episode. Subtitle files go alongside the video with .en.srt suffix so Plex auto-detects them as English.

I wrote a small shell script to rename and move everything in one go. It supports --dry-run to preview changes first:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
BASE="/volume1/data/media/courses/Exploring Beethoven's Piano Sonatas"
SHOW="Exploring Beethoven's Piano Sonatas"
DRY_RUN=false
[[ "$1" == "--dry-run" ]] && DRY_RUN=true

for file in "$BASE"/*.mp4; do
    filename=$(basename "$file")
    S=$(echo "$filename" | sed 's/^\([0-9]*\) - .*/\1/')
    E=$(echo "$filename" | sed 's/^[0-9]* - \([0-9]*\) - .*/\1/')
    TITLE=$(echo "$filename" | sed 's/^[0-9]* - [0-9]* - //' | sed 's/ ([^)]*)\.[^.]*$//')
    SS=$(printf "%02d" "$S"); EE=$(printf "%02d" "$E")
    mkdir -p "$BASE/Season $SS"
    mv "$file" "$BASE/Season $SS/${SHOW} - S${SS}E${EE} - ${TITLE}.mp4"
done

# Move subtitles from srts/ subfolder
for file in "$BASE/srts"/*.srt; do
    filename=$(basename "$file")
    S=$(echo "$filename" | sed 's/^\([0-9]*\) - .*/\1/')
    E=$(echo "$filename" | sed 's/^[0-9]* - \([0-9]*\) - .*/\1/')
    TITLE=$(echo "$filename" | sed 's/^[0-9]* - [0-9]* - //' | sed 's/ ([^)]*)\.[^.]*$//')
    SS=$(printf "%02d" "$S"); EE=$(printf "%02d" "$E")
    mv "$file" "$BASE/Season $SS/${SHOW} - S${SS}E${EE} - ${TITLE}.en.srt"
done

Step 2: Configure the Plex Library
#

In Plex, add a new library:

  • Type: TV Shows
  • Folder: point to the show’s root folder (or a courses/ parent)
  • Advanced → Agent: Personal Media Shows
  • Advanced: enable “Prefer local metadata”

Scan the library. Plex will pick up all seasons and episodes automatically from the file names.

Step 3: Set Descriptions via the Plex API
#

Personal Media Shows doesn’t pull descriptions from anywhere. You can edit them manually in the Plex UI, or push them programmatically via the API — much better when you have multiple seasons.

Finding your Plex token
#

Open Plex Web → play any item → “…” → “Get Info” → “View XML”. The URL contains X-Plex-Token=XXXXX.

Key API calls
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import urllib.request, urllib.parse, json

BASE = "http://plex.nas:32400"
TOKEN = "your-token-here"

def plex_get(path, params=None):
    url = BASE + path
    if params:
        url += "?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url)
    req.add_header("X-Plex-Token", TOKEN)
    req.add_header("Accept", "application/json")
    with urllib.request.urlopen(req) as r:
        return json.loads(r.read())

# List libraries
sections = plex_get("/library/sections")

# List shows in a library (key=7 in my case)
shows = plex_get("/library/sections/7/all")

# Update show summary (type=2) or season summary (type=3)
def set_summary(section_key, rating_key, item_type, summary):
    params = {
        "type": item_type,
        "id": rating_key,
        "summary.value": summary,
        "summary.locked": 1,
    }
    url = f"{BASE}/library/sections/{section_key}/all?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, method="PUT")
    req.add_header("X-Plex-Token", TOKEN)
    urllib.request.urlopen(req)

summary.locked=1 prevents Plex from overwriting your text on the next metadata refresh.

Full script
#

The complete script discovers the show and all seasons automatically, then pushes pre-written descriptions for each:

1
2
3
4
5
6
7
section_key, show_key = find_show(base_url, token, SHOW_TITLE)
seasons = get_seasons(base_url, token, show_key)

set_summary(section_key, show_key, 2, SHOW_SUMMARY)          # show
for season in seasons:
    idx = season["index"]
    set_summary(section_key, season["ratingKey"], 3, SEASON_SUMMARIES[idx])

Notes
#

  • If Plex auto-matches to a wrong show, either use “Fix Incorrect Match → None” in the UI, or switch the library agent to Personal Media Shows and re-scan.
  • The show’s display title in Plex may differ from the folder name depending on what metadata was previously matched. Discover the actual title via the API (/library/sections/{key}/all) before searching.
  • Poster artwork: drop a poster.jpg in the show root folder and Plex will pick it up automatically.