Search terms: rename episodes · bulk rename · SxxExx · scene naming · EZTVx · release group ·
Plex naming · Jellyfin naming · tv-rename.py · dry run
Tool: ~/.local/bin/tv-rename.py. Written 2026-08-08. Pure stdlib Python, no dependencies,
no network — it only reads filenames, never file contents.
Turns this:
Last.Week.Tonight.with.John.Oliver.S13E18.1080p.WEB.h264-GRACE[EZTVx.to].mkv
Lucky 2026 S01E05 Are We Bad People 1080p ATVP WEB-DL DDP5 1 Atmos H 264-FLUX[EZTVx.to].mkv
The Furious (2026) Eng 1080p WEBRip x265 DDP 5.1 ESub.mkv
into this:
Last Week Tonight with John Oliver - S13E18.mkv
Lucky - S01E05 2026.mkv
The Furious (2026).mkv
| Type | Format | Example |
|---|---|---|
| TV | <Show> - SxxExx[ YEAR].<ext> |
Lucky - S01E05 2026.mkv |
| Movie | <Title> (YEAR).<ext> |
The Furious (2026).mkv |
Parentheses on movies, bare year on TV — the Plex/Jellyfin convention, and it makes films distinguishable from episodes at a glance in a directory listing.
Dry run is the default. Nothing is ever renamed without --apply.
cd /path/to/media
tv-rename.py
Read the output. If it’s right:
tv-rename.py --apply
| Flag | Effect |
|---|---|
| (none) | Preview the current directory. Changes nothing. |
--apply |
Actually rename |
-r |
Recurse into subfolders |
<folder> |
Work somewhere other than the current directory |
Extensions handled: .mkv .mp4 .avi .m4v .mov .wmv .ts .mpg .mpeg
TV — first match wins, most specific first:
| Pattern | Example | Becomes |
|---|---|---|
| Multi-episode | S01E01E02, S02E10-E11 |
S01E01E02 |
| Standard | S01E02, S01.E02 |
S01E02 |
| Unpadded | S1E2 |
S01E02 |
| Alternate | 1x05 |
S01E05 |
| Verbose | Season 3 Episode 7 |
S03E07 |
Movies — only when a year is present. Two rules, in order:
(2026) is unambiguous1080p, WEBRip, x265,
BluRay, DDP5, HEVC, …)Rule 2 is why titles containing years survive:
Blade Runner 2049 (2017) 2160p UHD BluRay x265.mkv
-> Blade Runner 2049 (2017).mkv
A naive “first year wins” would produce Blade Runner (2049). Same trap catches 2012,
1917, and Apollo 13.
Betrayed Black Dragon King Reincarnates FULL DRAMA ENGSUB Bibi.F.mp4
reason: no season/episode marker and no year — cannot find where the title ends
No year, no episode tag, nothing separating title from junk. Is the title “Betrayed Black Dragon King Reincarnates”, or does it include “FULL DRAMA”? There is no signal in the filename that answers that, so it skips and says so.
A rename you can’t undo is worse than a rename that didn’t happen. Once the original
scene name is gone, the information needed to work out the correct title is gone with it.
Rename those by hand — and if a pattern shows up across several (a consistent marker like
ENGSUB or FULL DRAMA), that’s worth adding as a rule.
!! COLLISIONS (1) — skipped, resolve by hand:
target: Dupe Show S01E01.mkv
from: Dupe.Show.S01E01.1080p.WEB.mkv
from: Dupe.Show.S01E01.720p.HDTV.mkv
Two quality versions of the same episode both want the same name. A naive mv loop would
silently overwrite one with the other and you’d never know which survived. This stops, names
both sources, and renames nothing.
It also refuses to touch a file that already matches the target name, and any target that already exists on disk.
tv-rename.py
DRY RUN — nothing will be changed (.)
WILL RENAME (3):
Last.Week.Tonight.with.John.Oliver.S13E18.1080p.WEB.h264-GRACE[EZTVx.to].mkv
-> Last Week Tonight with John Oliver - S13E18.mkv
Lucky 2026 S01E05 Are We Bad People 1080p ATVP WEB-DL DDP5 1 Atmos H 264-FLUX[EZTVx.to].mkv
-> Lucky - S01E05 2026.mkv
The Furious (2026) Eng 1080p WEBRip x265 DDP 5.1 ESub.mkv
-> The Furious (2026).mkv
SKIPPED (1):
Betrayed Black Dragon King Reincarnates FULL DRAMA ENGSUB Bibi.F.mp4
reason: no season/episode marker and no year — cannot find where the title ends
Nothing changed. Re-run with --apply to perform the rename.
Dry run by default, not opt-in. A bulk rename over a media library is exactly the kind of operation where a wrong regex costs you hours of manual repair. Making the safe path the default one means the destructive path requires a deliberate extra word.
Everything after the marker is discarded. Resolution, codec, audio format, release group and tracker tag all live after the episode tag or the year, so taking the text before it and dropping the rest handles every scene-naming variant without needing to enumerate them.
Separators are normalised. Dots and underscores become spaces, runs of whitespace collapse,
trailing dashes go. That’s what turns Last.Week.Tonight.with.John.Oliver into readable text.
No metadata lookup. Tools like FileBot query TVDB and can supply episode titles and fix misspellings. This does none of that — it’s pure filename parsing, which means it’s offline, instant, has no API key, and can’t be wrong about which show you have. It also means it can’t rescue a file whose name lacks the information.
Patterns live in the PATTERNS list near the top, ordered most-specific-first, each with named
groups s (season), e (episode), and optionally e2 for multi-episode. Add a new one and it
slots into the existing logic.
Test against throwaway files before pointing it at a real library:
mkdir /tmp/rn && cd /tmp/rn
touch "Weird.Format.S1.E2.junk.mkv" "Another 1x05 720p.mkv"
tv-rename.py
Dry run costs nothing and the files are empty, so iterate freely.