Watchable

Watchable

Docker app from Uirel's Repository

Overview

Syncs watch state (played/unplayed, in-progress position) between Plex, Jellyfin, and Emby through a local SQLite database -- nothing syncs server-to-server directly. Configured entirely from one YAML file: servers, per-person account mapping, and sync pairs that can be one-way or bidirectional with configurable conflict resolution. Also takes scheduled database backups with automatic retention. Beta: actively developed, config format and behaviour may still change between releases. Full setup docs at https://github.com/Poag/watchable.

watchable icon

watchable

Sync watch state between Plex, Jellyfin, and Emby through a local database, configured entirely in YAML.


Watch something on one media server, and watchable can carry that watched / in-progress state to the others -- one-way (server A always overwrites server B) or bidirectional (either side can change, with a configurable rule for who wins when both did). Nothing syncs server-to-server directly: every run pulls current watch state from each configured server into a local SQLite database first, reconciles it there, then pushes out whatever changed. See docs/ARCHITECTURE.md for why.

Features

  • Plex, Jellyfin, and Emby, matched to each other by external ids (IMDb/TMDb/TVDb) so it doesn't matter that each server assigns its own unrelated internal IDs to the same movie or episode.
  • One-way or bidirectional sync, set per pair of servers.
  • Configurable conflict resolution for bidirectional pairs: most-progressed-wins (default), most-recently-played-wins, or a fixed side always wins.
  • Everything in one YAML file -- servers, per-person account mapping, and sync pairs. No database setup, no web UI to click through.
  • A local audit trail (watchable log) of every push it's made, and --dry-run to see what a sync would do before it does it.
  • Database backups (watchable backup create/list/restore/purge), on their own configurable schedule if you use watchable run, with old backups purged automatically.

Quickstart

pip install -e .
cp config.example.yaml config.yaml
$EDITOR config.yaml   # fill in your servers, users, and sync pairs

watchable validate config.yaml         # check the config parses and is internally consistent
watchable test-connections config.yaml # check every server is reachable
watchable sync config.yaml --dry-run   # see what would happen
watchable sync config.yaml             # do it
watchable log config.yaml              # see what it did

Full configuration reference: docs/CONFIGURATION.md. Per-server API details and caveats (Plex in particular works differently enough from Jellyfin/Emby to matter): docs/PROVIDERS.md.

Running continuously

watchable sync runs once and exits -- a cron job or systemd timer calling it on a schedule is the simplest way to run it continuously. If you'd rather run it as a long-lived process instead (e.g. the provided Docker image), set sync.schedule.interval_minutes in your config and use watchable run in place of watchable sync.

Backups

watchable backup create config.yaml    # snapshot the database now
watchable backup list config.yaml      # see what's available
watchable backup restore config.yaml <file>   # roll back to a snapshot
watchable backup purge config.yaml     # delete backups older than backup.keep_days

Set backup.interval_hours in your config to have watchable run also take backups on its own schedule (independent of the sync interval), purging anything older than backup.keep_days afterwards. See docs/CONFIGURATION.md.

Docker

docker build -t watchable -f docker/Dockerfile .
docker run -e PUID=1000 -e PGID=1000 \
  -v ./config.yaml:/config/config.yaml:ro -v ./data:/data watchable

The container starts as root and drops to the built-in watchable user remapped to PUID/PGID (default 1000:1000) before running anything -- set them to match whoever owns ./data on the host if that's not 1000:1000.

Or see docker/docker-compose.example.yml. Every push to main publishes a multi-arch (linux/amd64 + linux/arm64) image to ghcr.io/poag/watchable.

For Dockhand or any other environment-variable-driven deployment, see docker/docker-compose.dockhand.yml and docker/.env.dockhand.example -- it pulls the published GHCR image instead of building locally, and every deployment-specific setting (image tag, paths, restart policy, the secrets config.yaml references) comes from an environment variable.

Example configuration

A one-way mirror plus a bidirectional pair, from config.example.yaml:

sync:
  conflict_strategy: most_watched
  pairs:
    - name: plex-to-jellyfin
      source: plex_main
      target: jellyfin_main
      direction: one-way

    - name: jellyfin-emby-mirror
      source: jellyfin_main
      target: emby_main
      direction: bidirectional
      conflict_strategy: latest

How it works, briefly

  1. Pull: for every server and every configured person with an account on it, fetch their current watch state into the local database.
  2. Reconcile + push: for every sync pair, compare each item's locally stored state on both sides and push whichever side needs to change -- respecting that pair's direction and (for bidirectional pairs) conflict strategy.

Full details, including how items are matched across servers and the database schema: docs/ARCHITECTURE.md.

Development

pip install -e ".[dev]"
pytest
ruff check src tests
mypy src

See CONTRIBUTING.md.

License

MIT

Install Watchable on Unraid in a few clicks.

Find Watchable in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for Watchable Review the template variables and paths Click Install

Related apps

Details

Repository
ghcr.io/poag/watchable:latest
Last Updated2026-09-07
First Seen2026-09-06

Runtime arguments

Network
bridge
Shell
bash
Privileged
false

Template configuration

ConfigPathrw

Directory containing config.yaml -- mounted as a folder, not the file directly (Unraid's Docker UI doesn't handle single-file mappings well).

Target
/config
Default
/mnt/user/appdata/watchable/config
Value
/mnt/user/appdata/watchable/config
DataPathrw

SQLite database (watch state, sync log) and scheduled backups live here.

Target
/data
Default
/mnt/user/appdata/watchable/data
Value
/mnt/user/appdata/watchable/data
PLEX_TOKENVariable

Referenced by config.yaml as ${PLEX_TOKEN}. Plex scopes watch state to the token making the request, so add one extra variable per additional synced person (e.g. ALEX_PLEX_TOKEN) via this template's Add another Path, Port, Variable... button.

JELLYFIN_API_KEYVariable

Referenced by config.yaml as ${JELLYFIN_API_KEY}.

EMBY_API_KEYVariable

Referenced by config.yaml as ${EMBY_API_KEY}.

PUIDVariable

The container starts as root and drops to the built-in watchable user remapped to this id before running anything -- match whoever owns the paths above (Unraid's default user is 99).

Default
99
Value
99
PGIDVariable

See PUID (Unraid's default group is 100).

Default
100
Value
100
TZVariable

Timezone, e.g. Europe/London.

Default
UTC
Value
UTC