All apps · 0 apps
Watchable
Docker app from Uirel's Repository
Overview
Readme
View on GitHub
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-runto 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 usewatchable 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
- Pull: for every server and every configured person with an account on it, fetch their current watch state into the local database.
- 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
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.
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/poag/watchable:latestRuntime arguments
- Network
bridge- Shell
bash- Privileged
- false
Template configuration
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
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
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.
Referenced by config.yaml as ${JELLYFIN_API_KEY}.
Referenced by config.yaml as ${EMBY_API_KEY}.
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
See PUID (Unraid's default group is 100).
- Default
- 100
- Value
- 100
Timezone, e.g. Europe/London.
- Default
- UTC
- Value
- UTC