Event-driven bidirectional sync of watched status and playback progress between Plex and Jellyfin. Runs anywhere Docker runs. No Plex Pass required.
JellyPlex-Watched syncs watched status between Plex and Jellyfin, but it does so by polling on a fixed interval (default 30 minutes). This wastes resources when nothing has changed and introduces unnecessary delay.
This repo provides an event-driven wrapper around JellyPlex-Watched:
- Plex side: Tautulli monitors Plex activity (no Plex Pass needed) and runs a trigger script on playback stop/watched events.
- Jellyfin side: The Jellyfin Webhook plugin sends a POST to a lightweight receiver on playback events.
- Both triggers start JellyPlex-Watched in one-shot mode (
RUN_ONLY_ONCE=true). It does a full state diff, syncs both directions, and exits in about 2 seconds.
Plex Docker Host Jellyfin
| | |
| Tautulli detects event | |
|------------------------------>| |
| trigger-sync.py |
| starts jellyplex-watched |
|<------------------------------|------------------------------->|
| full state diff, syncs both ways |
| | |
| | Jellyfin Webhook plugin |
| |<------------------------------|
| webhook-receiver |
| starts jellyplex-watched |
|<------------------------------|------------------------------->|
docker-compose.yml
.env.example
scripts/
trigger-sync.py # Called by Tautulli on Plex events
webhook_receiver.py # HTTP server that receives Jellyfin webhooks
Three containers:
| Container | Image | What it does |
|---|---|---|
| jellyplex-watched | luigi311/jellyplex-watched | Syncs watched state. Runs on demand, exits after each sync. |
| tautulli | tautulli/tautulli | Monitors Plex. Fires trigger script on playback stop and watched events. |
| webhook-receiver | python:3.11-slim | Listens for Jellyfin webhook POSTs. Triggers sync on each event. |
Both trigger scripts talk to the Docker daemon through the Unix socket (/var/run/docker.sock). They send POST /containers/jellyplex-watched/start using Python's standard library.
The webhook-receiver also handles:
- Boot sync: 30 seconds after startup, it triggers a sync to catch up on anything missed while the system was offline.
- Retry: If a sync fails (server down, container already running from a previous trigger), it retries every 60 seconds until it succeeds.
JellyPlex-Watched does a full state comparison on every run. It is not tracking incremental changes. This means a single successful run catches up on everything, no matter how long the system was offline.
See SETUP.md to get started. CONFIG.md and TROUBLESHOOTING.md for reference.
| Scenario | What happens | Recovery |
|---|---|---|
| Docker host is off | No triggers fire | Boot sync catches up on restart |
| Network is down | Sync fails | Retry loop retries every 60s |
| Plex is down | jellyplex-watched fails to connect | Retry loop retries every 60s |
| Jellyfin is down | jellyplex-watched fails to connect | Retry loop retries every 60s |
| Rapid back-to-back events | Second trigger gets 304 (container already running) | Retry loop picks it up in 60s |
| System offline for days | Events are not captured | Boot sync does full diff on restart |
- tautulli: ~150MB RAM, minimal CPU when idle
- webhook-receiver: ~30MB RAM, negligible CPU
- jellyplex-watched: ~50MB RAM, runs for ~2 seconds per sync then exits
Runs fine on low-end hardware.
MIT