Automatic video encoding and renaming service for your media library. Continuously watches directories for video files, encodes them to your preferred quality, and renames them with a clean, consistent format.
Built for Plex, Jellyfin, Emby, or any media server that benefits from standardized file naming.
- Continuous directory watching — runs as a background service, scanning for new/unprocessed files
- Smart encoding — uses HandBrake with hardware acceleration (VideoToolbox on macOS) and x265 software fallback
- Intelligent renaming — clean
Title (Year).Resolution.Bitrate.mkvformat for movies,Show S01E01 Title.Resolution.Bitrate.mkvfor series - Auto-detection — automatically detects whether a directory contains movies or TV series
- Duplicate resolution — when the same title exists twice, the best copy keeps the tagged name and the other one is cleaned up
- Filename repair with TMDb verification —
fix-namesrepairs damaged names (double spaces,R I P D→R.I.P.D., duplicate resolution tags) and verifies movie titles against TMDb so Plex always recognizes them - Container repair (lossless) — files whose extension lies about the container (an MP4 renamed to
.mkvwithout remuxing breaks Direct Play) are automatically rebuilt as real MKVs with a bit-for-bit stream copy: zero quality loss, no re-encode - Subtitles follow the video — external
.srt/.sub/.idxsidecars are renamed along with the file, so Plex never loses them - No-bloat protection — skips encoding if the output would be larger than the source
- Bitrate tolerance — skips encoding if the file is already within ±5% of the target bitrate
- Multiple watch directories — monitor as many directories as you need
- 7 quality presets + custom — from UltraSaver to MaxPunch
- Cross-platform — macOS (with VideoToolbox HW acceleration) and Linux
- Service integration — LaunchAgent (macOS) or systemd (Linux) for auto-start at boot
- Interactive installer — guided setup for dependencies, quality, directories, and service
| Type | Format | Example |
|---|---|---|
| Movies | Title (Year).Resolution.Bitrate.mkv |
The Matrix (1999).1080p.6mb.mkv |
| Series | Show S01E01 Title.Resolution.Bitrate.mkv |
Breaking Bad S01E01 Pilot.1080p.6mb.mkv |
Resolution is always visible in the filename, allowing you to store multiple resolutions in the same directory.
git clone https://github.com/ronaldjonkers/PlexScripts.git && cd PlexScripts && bash install.sh installcd PlexScripts && bash install.sh upgradecd PlexScripts && bash install.sh uninstall| # | Preset | 2160p (4K) | 1080p | 720p |
|---|---|---|---|---|
| 1 | UltraSaver | 7 Mbps | 3 Mbps | 1 Mbps |
| 2 | DataDiet | 8 Mbps | 4 Mbps | 1.5 Mbps |
| 3 | StreamSaver | 10 Mbps | 5 Mbps | 2.5 Mbps |
| 4 | Netflix-ish | 12 Mbps | 6 Mbps | 3 Mbps |
| 5 | CrispCable | 16 Mbps | 8 Mbps | 4 Mbps |
| 6 | ArchivalLite | 20 Mbps | 10 Mbps | 5 Mbps |
| 7 | MaxPunch | 24 Mbps | 12 Mbps | 6 Mbps |
| 8 | Custom | User-defined | User-defined | User-defined |
| Dependency | Purpose |
|---|---|
| HandBrakeCLI | Video encoding (H.265/HEVC) |
| ffmpeg/ffprobe | Media analysis (resolution, bitrate, duration) |
| python3 | Numeric calculations |
All dependencies are automatically installed by install.sh.
PlexScripts/
├── install.sh # Interactive installer/updater/uninstaller
├── README.md # This file
├── CHANGELOG.md # Version history
├── .env.example # Environment template (TMDb token lives in .env)
├── .gitignore # Git ignore rules
├── bin/
│ └── media-manager # Main service executable
├── lib/
│ ├── utils.sh # Shared utilities (OS detection, ffprobe helpers)
│ ├── encoding.sh # Encoding logic (VideoToolbox + x265)
│ ├── dedupe.sh # Duplicate resolution & subtitle sidecars
│ ├── naming.sh # File naming & media type detection
│ ├── fixnames.sh # fix-names command (library filename repair)
│ ├── remux.sh # Container detection & lossless repair
│ └── fix_filename.py # Name repair logic + TMDb title verification
├── config/
│ └── media-manager.conf.example # Example configuration
├── service/
│ ├── com.media-manager.plist # macOS LaunchAgent template
│ └── media-manager.service # Linux systemd unit template
└── tests/
├── test_naming.sh # Unit tests for naming logic
├── test_dedupe.sh # Unit tests for duplicate handling
├── test_fixnames.sh # Unit tests for filename repair
└── test_remux.sh # Unit tests for container repair
Configuration is stored in config/media-manager.conf (created by the installer). Key settings:
# Quality profile
PROFILE_NAME="Netflix-ish"
VB2160=12000 # 4K target bitrate (kbps)
VB1080=6000 # 1080p target bitrate (kbps)
VB720=3000 # 720p target bitrate (kbps)
# Encoding
VT_PRESET="quality" # VideoToolbox preset (macOS): fast|balanced|quality
X265_PRESET="slow" # x265 software preset: fast|medium|slow|veryslow
TOL_PCT=5 # Skip encoding if bitrate is within ±5%
# File management
DELETE_ORIGINALS="no" # Delete source files after encoding
DUPLICATE_ACTION="keep_best" # Same title twice: keep_best | trash | skip
SIDECAR_EXTS="srt sub idx ass ssa vtt smi sup" # Sidecars that follow a rename
REMUX_MISLABELED="yes" # Losslessly rebuild files whose extension lies
# Service
SCAN_INTERVAL=300 # Seconds between scans
# Watch directories (path|type)
# Type: movies, series, or auto (auto-detect)
WATCH_DIRS=(
"/Volumes/Media/Movies|movies"
"/Volumes/Media/TV Shows|series"
"/Volumes/Media/Downloads|auto"
)# Start the service (runs continuously)
./bin/media-manager start
# Run a single scan and exit
./bin/media-manager scan
# Start with verbose output (shows full HandBrakeCLI output)
./bin/media-manager start --verbose
# Single scan with verbose output (great for debugging)
./bin/media-manager scan -V
# Preview filename repairs (nothing is renamed)
./bin/media-manager fix-names --dry-run
# Repair filenames in all watch directories (TMDb-verified)
./bin/media-manager fix-names
# Repair one specific directory
./bin/media-manager fix-names /Volumes/Plex/Films
# Repair without TMDb (mechanical fixes only)
./bin/media-manager fix-names --no-tmdb
# Check if the service is running
./bin/media-manager status
# Stop the service
./bin/media-manager stop
# Use a custom config file
./bin/media-manager start -c /path/to/config.conf
# Show help
./bin/media-manager --helpTip: Use
--verbose/-Vwhen running manually to see the full HandBrakeCLI encoder output in real-time. Without it, encoder output is only shown (last 20 lines) when an encode fails.
macOS (LaunchAgent):
# Start
launchctl load ~/Library/LaunchAgents/com.media-manager.plist
# Stop
launchctl unload ~/Library/LaunchAgents/com.media-manager.plistLinux (systemd user service):
# Start
systemctl --user start media-manager
# Stop
systemctl --user stop media-manager
# View logs
journalctl --user -u media-manager -f- Scan — The service recursively scans each configured watch directory for video files (
.mp4,.mkv,.mov,.avi,.m4v,.wmv) - Skip tagged — Files already in our naming format (
Name.Resolution.Bitrate.mkv) are skipped - Skip active — Files modified within the last 30 seconds are skipped (still being written/downloaded)
- Dedupe — If the title already has a tagged copy, only the best copy survives (see below)
- Analyze — Resolution, bitrate, duration, and audio streams are read via
ffprobe - Decide — If the current bitrate is within ±5% of the target: rename only. If the estimated output would be ≥98% of the source: rename only. Otherwise: encode.
- Encode — Uses HandBrake with VideoToolbox (macOS HW) or x265 (software fallback). All audio tracks and subtitles are preserved.
- Rename — Output is named with the clean format including resolution and bitrate, and external subtitles are renamed along with it
- Repeat — Waits for the configured interval, then scans again
Plex matches movies on the exact Title (Year) stem, so a damaged filename means an
unrecognized movie. Earlier renaming passes (and sloppy download sources) left three
kinds of damage behind, all repaired by fix-names:
| Damage | Example | Repaired to |
|---|---|---|
" - " collapsed into a double space |
Movie Subtitle (2020).mkv |
Movie - Subtitle (2020).mkv |
| Abbreviations lost their dots | R I P D - 2 - Rise of the Damned (2022).1080p.6mb.mkv |
R.I.P.D. 2 - Rise of the Damned (2022).1080p.6mb.mkv |
| Resolution tag duplicated | Movie (2022).2160p.2160p.mkv |
Movie (2022).mkv (re-tagged on next scan) |
With a TMDb API token configured, movie titles are verified against
The Movie Database: the title and year are parsed from the
filename, looked up, and replaced by the official movie title — so
Hansel Gretel Witch Hunters (2013) becomes Hansel & Gretel - Witch Hunters (2013).
Characters that are illegal in filenames are converted safely (: becomes -).
To keep API usage low, only names that show damage are looked up — a name the
mechanical pass leaves untouched is assumed correct and costs zero requests.
Pass --tmdb-all to verify every single movie instead (useful for a one-time
full audit; expect one or two requests per unique title). Series never use TMDb.
A rename is only accepted when TMDb confidently matches the title (year-anchored search with a similarity check) — unrecognized titles just get the mechanical fixes. Renames go through the same collision-safe path as the scanner, so duplicates are resolved and external subtitles travel along.
cp .env.example .env
# Edit .env and set your TMDb v4 Read Access Token
# (themoviedb.org → Settings → API → API Read Access Token):
# TMDB_API_TOKEN=eyJhbGci....env is gitignored — the token never ends up in the repository. Without a token,
fix-names still runs all mechanical repairs and simply skips TMDb verification.
# 1. Always preview first
./bin/media-manager fix-names --dry-run
# 2. Happy with the plan? Run it for real
./bin/media-manager fix-names
# 3. In Plex: Library → Scan Library Files, then refresh metadata if neededThe regular scan loop also applies the mechanical repairs to every new file it
processes, so freshly downloaded files get a clean name from the start. TMDb
verification only runs in fix-names (deliberately — the scan loop stays offline).
Older versions of this tool renamed .mp4 files to .mkv without remuxing. The result is
a file whose extension lies about its contents: Plex is told "Matroska", finds MP4 data
(with mov_text subtitles and the audio track in front of the video track) and refuses
Direct Play, even though the video and audio streams are perfectly compatible.
Every scan now checks the real container (a 12-byte magic-number read, so it costs almost nothing) and repairs mismatches automatically:
- Lossless remux — video and audio streams are bit-for-bit copied into a real MKV. No re-encode, zero quality loss, and it runs at network speed (~3% CPU).
- Subtitles preserved —
mov_textsubtitle streams are converted to SRT tracks embedded in the MKV (plain-text conversion, nothing is lost). External.srtsidecars are untouched and keep working, since the filename does not change. - Verified before replacing — the copy must match the source duration (±2s) before it atomically replaces the original; a failed remux leaves the original untouched.
- A real MKV wearing a
.mp4name is repaired the other way around (remuxed to.mkv); an existing sibling file is never overwritten.
Handled cases: .mkv that is really MP4, .mp4/.m4v that is really Matroska.
Disable with REMUX_MISLABELED="no" in the config.
A rename can collide with a file that is already there — for example Movie (2020).mkv and
Movie (2020).1080p.6mb.mkv both sitting in the same folder. Instead of leaving both behind,
the service picks a winner:
- Byte-identical copies → the copy that already carries the tag is kept
- Higher resolution wins
- A copy within the bitrate policy beats an oversized copy
- Otherwise the higher bitrate wins (better quality, or the better source for re-encoding)
What happens to the losing copy is set by DUPLICATE_ACTION:
| Value | Behaviour |
|---|---|
keep_best (default) |
The lesser copy is deleted |
trash |
The lesser copy is moved to a .duplicates folder next to it |
skip |
Nothing is touched, a warning is logged |
Two tagged copies of the same title (e.g. .1080p.6mb.mkv next to .720p.3mb.mkv) are resolved
the same way. When DELETE_ORIGINALS="no", an encoded source is renamed to Name.original.ext
so it is never picked up and re-encoded again.
Plex matches external subtitles on the filename stem, so a rename would orphan them:
Fantasia (1940).nl.srt no longer belongs to Fantasia (1940).1080p.6mb.mkv. Sidecars therefore
travel with the video everywhere it goes:
| Situation | What happens to the subtitles |
|---|---|
| Rename | Movie (2020).nl.srt → Movie (2020).1080p.6mb.nl.srt |
| Encode | Re-attached to the encoded file (not to a kept original) |
| Duplicate cleaned up | Moved to the copy that survives, so they are never lost with it |
| Both copies have the same subtitle | The winner keeps its own; the redundant one follows DUPLICATE_ACTION |
Which files count as sidecars is set by SIDECAR_EXTS (default srt sub idx ass ssa vtt smi sup).
Add nfo there if you want metadata files to move along too.
When a directory is set to auto, the service detects the media type by:
- Looking for
Season XXsubdirectories → series - Looking for
SxxExxpatterns in filenames → series - Default → movies
bash tests/test_naming.sh
bash tests/test_dedupe.sh
bash tests/test_fixnames.sh
bash tests/test_remux.shMIT
1.3.0 — See CHANGELOG.md for history.