Bulk Renaming TV & Movie Files

Bulk Renaming TV & Movie Files

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

Output formats

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.


Usage

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


What it recognises

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:

  1. A year in parentheses or brackets wins — (2026) is unambiguous
  2. Otherwise the last bare year followed by release junk (1080p, 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.


What it refuses to do

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 are refused, not resolved

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


Worked example

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.

Design notes

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.


Adding a pattern

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.