Keep Your Watch History in Sync Across Jellyfin, Plex, and Emby
If you're running multiple media servers—maybe you started with Plex, migrated to Jellyfin for the open-source ethos, and still have an Emby instance for a friend or family member—you've probably hit the same wall: your watched history lives in silos. Finishing a season on one server means manually marking episodes as watched on the others, which is tedious and error-prone. That's where JellyPlex-Watched comes in. It's a local tool that syncs watched history across all three servers automatically, so you don't have to think about it.
What It Does
JellyPlex-Watched is a Python script that connects to your Jellyfin, Plex, and Emby servers and keeps your users' watch states in sync. It works by matching items between servers using either file names or provider IDs (like IMDb or TVDB IDs) to figure out which episode or movie on one server corresponds to the same thing on another. This isn't a cloud service or a plugin—it's a local utility you run yourself, either on bare metal with uv or as a Docker container.
The sync is bidirectional by default, but you can configure it to be one-way if you prefer. It handles username mapping (so "bob" on Plex can match "bob_jellyfin" on Jellyfin), supports multiple servers of the same type by comma-separating their URLs and tokens in the .env file, and can sync both fully watched items and in-progress playback states. Jellyfin and Emby also get view date syncing, though Plex doesn't support that yet.
Configuration happens entirely through environment variables or a .env file. You set your server base URLs, API tokens, and any optional mappings for users, libraries, or blacklists/whitelists. The project provides a .env.sample file that documents every available option.
Why It's Cool
The most obvious win here is the time you get back. If you've ever manually marked a 200-episode anime as watched across two servers, you know exactly how painful that is. This tool just does it for you.
But there are a few design decisions worth calling out:
-
It's local-first. Your watch history is your data, and this tool keeps it on your own hardware. No third-party cloud service is scanning your media server activity. That's a privacy-friendly approach that fits well with the self-hosted crowd.
-
Matching is dual-strategy. Using both file names and provider IDs means it's flexible enough to handle most libraries. File name matching works when your files are consistently named, while provider IDs are more reliable when the same movie exists on both servers with different file naming conventions. The README is honest that it's "not perfect but works for most cases," which is refreshingly realistic.
-
Multi-server and multi-user support. You can sync as many servers as you want, and you can map usernames across them. So if your household has different accounts on different servers, you can still keep everyone's history in sync without flattening everyone into one shared account.
-
Flexible deployment. You can run it with
uvdirectly on a machine, or pull a pre-built Docker image. The Docker approach is particularly nice for a cron-style setup—just run it periodically and your sync happens in the background. -
The single-login feature for each server type is a nice touch. You don't need to create separate API tokens for every user; one login can handle the whole sync.
How to Try It
Getting started is straightforward. First, head over to the repository and grab the .env.sample file. You'll need to fill in your server base URLs and API tokens, and remember to uncomment any options you want to use—user mapping, library mapping, blacklists, and so on.
If you prefer bare metal, install uv first, then run:
uv run main.py
If you want to use a differently named or located env file, you can specify it:
ENV_FILE="Test.env" uv run main.py
For Docker, you can either build the image yourself or pull the pre-built one:
docker pull luigi311/jellyplex-watched:latest
Then run it with environment variables directly:
docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest
Or mount your .env file:
docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest
One thing to watch out for from the troubleshooting section: don't use quotes around variables in Docker Compose, and if you're not running all three server types, comment out the URL and token for the ones you don't use. Also, for Jellyfin specifically, make sure remote access is enabled or your Docker subnet is in the LAN networks setting, otherwise you'll hit JSON decode errors.
Final Thoughts
JellyPlex-Watched is a practical tool for a very specific problem, and it solves it well. It's not trying to be a full media manager or a replacement for your server—it just does one thing and does it competently. The honest limitations (Plex view dates aren't synced, matching isn't perfect) show that the author knows the tool's boundaries. If you're running multiple media servers and you're tired of manual watch-state upkeep, this is worth 15 minutes of setup. And since the project is open to pull requests, there's room for it to get even better over time.
Follow @githubprojects for more developer tools and open source projects.