WatchState: Sync Watch History Between Jellyfin, Plex, Emby, and Trakt Simultaneously (2026 Guide)

WatchState: Sync Watch History Between Jellyfin, Plex, Emby, and Trakt Simultaneously (2026 Guide)

WatchState: Sync Watch History Between Jellyfin, Plex, Emby, and Trakt (2026)

You run Jellyfin for yourself and Plex for family members who refuse to switch. Both servers point at the same media files. You watch a movie on Jellyfin. Your partner watches it on Plex. Neither server knows about the other's watch state. Your "Continue Watching" is wrong on both.

WatchState fixes this. It is a self-hosted synchronization engine that keeps watch history consistent across multiple media servers and tracking services simultaneously.


What WatchState Does

WatchState is a bidirectional sync engine for play state (watched/unwatched) and play progress (resume position). It supports:

  • Jellyfin (full support)
  • Plex (full support)
  • Emby (full support)
  • Trakt (full support)

Any combination works. Jellyfin + Plex. Jellyfin + Trakt. Plex + Emby + Trakt. All four simultaneously.

Sync capabilities

DataSynced?
Watched/Unwatched statusYes
Play progress (resume point)Yes
Play date/timeYes
RatingsTrakt only
Collections/PlaylistsNo
MetadataNo

Use Cases

1. Running Jellyfin + Plex simultaneously

You migrated to Jellyfin but some family members still use Plex. Both servers read the same media files. WatchState keeps watched status synchronized so a movie marked as watched on Plex also shows as watched on Jellyfin, and vice versa.

2. Migrating from Plex to Jellyfin

Transfer your entire watch history from Plex to Jellyfin in one sync. No Trakt intermediary needed. Direct server-to-server transfer.

3. Trakt as a universal backup

Sync all your watch history to Trakt as a permanent backup. If you ever rebuild your server, import from Trakt to restore everything.

4. Multi-user sync

WatchState supports per-user configuration. Each user on your server can have their own sync profile with their own Trakt account.


Docker Setup

services:
  watchstate:
    image: ghcr.io/arabcoders/watchstate:latest
    container_name: watchstate
    ports:
      - 8080:8080
    volumes:
      - ./watchstate/config:/config
    environment:
      - WS_TZ=Europe/Paris
    restart: unless-stopped
docker compose up -d

Access the web UI at http://your-server:8080.


Step 1: Add Your Backends

WatchState calls each connected service a "backend." Add them via the web UI or CLI:

Add Jellyfin

docker exec watchstate console config:add
# Follow the interactive prompts:
# Type: jellyfin
# Name: my-jellyfin
# URL: http://jellyfin:8096
# Token: your_jellyfin_api_key
# User: your_username

Add Plex

docker exec watchstate console config:add
# Type: plex
# Name: my-plex
# URL: http://plex:32400
# Token: your_plex_token

Add Trakt

docker exec watchstate console config:add
# Type: trakt
# Name: my-trakt
# Follow OAuth flow to authenticate

Add Emby

docker exec watchstate console config:add
# Type: emby
# Name: my-emby
# URL: http://emby:8096
# Token: your_emby_api_key

Step 2: Import Current State

Before enabling sync, import the current watch state from all backends:

# Import from all backends
docker exec watchstate console state:import

# Or import from a specific backend
docker exec watchstate console state:import --select-backend my-jellyfin

This reads the current watched/unwatched status from each server and stores it in WatchState's local database.


Step 3: Enable Sync

Once imported, enable ongoing synchronization:

# Push local state to all backends
docker exec watchstate console state:push

# Or run a full sync (import + push)
docker exec watchstate console state:sync

Automated sync via cron

WatchState includes a built-in scheduler. Configure it to sync every 15 minutes:

docker exec watchstate console system:cron --run

Or use the web UI to configure sync schedules.

Webhook-based real-time sync

For instant sync (no polling delay), configure webhooks:

  1. In Jellyfin: install the Webhook plugin, point it at http://watchstate:8080/v1/api/jellyfin/webhook
  2. In Plex: configure a webhook to http://watchstate:8080/v1/api/plex/webhook

Now when you finish a movie on Jellyfin, WatchState immediately marks it as watched on Plex and Trakt.

JellyWatchTry JellyWatch — Your Jellyfin companion, everywhere.

Conflict Resolution

What happens when the same item has different states on different backends?

Default behavior: newest wins

WatchState uses timestamps. If a movie was marked watched on Jellyfin at 8:00 PM and is still unwatched on Plex, WatchState marks it as watched on Plex (because the Jellyfin event is newer).

Manual override

The web UI shows conflicts and lets you resolve them manually:

  • Force a specific backend's state as the source of truth
  • Ignore specific items from sync
  • Set per-backend priority

Matching: How WatchState Identifies the Same Content

WatchState matches items across backends using:

  1. Provider IDs (TMDB, TVDB, IMDB) - most reliable
  2. Title + Year - fallback for items without provider IDs
  3. File path - if backends share the same media files

For best results, ensure your libraries have proper metadata with TMDB/TVDB IDs on all backends.


Multi-User Configuration

WatchState supports multiple users, each with their own backend connections:

# Add a second user
docker exec watchstate console config:add --user partner
# Configure their Jellyfin user, Plex user, and Trakt account separately

Each user's watch history syncs independently.


WatchState vs JellyPlex-Watched vs Trakt Plugin

FeatureWatchStateJellyPlex-WatchedTrakt Plugin
Jellyfin supportYesYesYes
Plex supportYesYesNo
Emby supportYesNoYes (Emby plugin)
Trakt supportYesNoYes
BidirectionalYesConfigurableYes
Real-time (webhooks)YesNo (scheduled)Yes
Multi-userYesLimitedPer-user
Resume position syncYesNo (watched only)No
Web UIYesNo (CLI only)No
Conflict resolutionYes (UI)Newest winsNewest wins

WatchState is the most comprehensive option. JellyPlex-Watched is simpler for one-time Plex-to-Jellyfin migration. Trakt plugins are best if you only need Trakt sync.


Performance and Resource Usage

  • RAM: ~100-200 MB
  • CPU: negligible (API calls only)
  • Storage: ~50-200 MB (local state database)
  • Network: minimal (API calls to each backend)

WatchState is extremely lightweight. It makes API calls to your media servers, it does not process media files.


Common Issues

ProblemCauseFix
Items not matchingMissing provider IDsEnsure TMDB/TVDB metadata is set on all backends
Sync not runningCron not configuredEnable the built-in scheduler or set up webhooks
Wrong user syncedBackend user mismatchVerify the correct user is selected for each backend
Duplicate entriesSame movie in multiple librariesConfigure library filtering in backend settings

FAQ

Does WatchState modify my media files? No. It only reads and writes play state via media server APIs. Your files are never touched.

Can I sync between two Jellyfin servers? Yes. Add both as separate backends. WatchState syncs between any combination of supported servers.

Does it sync ratings? Only to/from Trakt. Jellyfin, Plex, and Emby ratings are not synced between each other (different rating systems).

Is it safe to run on a production server? Yes. WatchState only marks items as watched/unwatched. It cannot delete content, modify metadata, or affect playback.

Can I use WatchState alongside the Trakt plugin? Yes, but disable Trakt sync in the Jellyfin/Emby plugin to avoid conflicts. Let WatchState handle all sync centrally.


Watch history synced across all your servers. Monitor active sessions from your phone. Download JellyWatch on Google Play - real-time session monitoring for Jellyfin.

On Emby? Download EmbyWatch on Google Play

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment

Never displayed publicly.
0 / 2000 · Supports limited Markdown: **bold**, *italic*, `code`, [link](url), lists, > quote.