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.