Files
watchstate/README.md

13 KiB

WatchState

WatchState is primarily commandline tool that can sync your play state cross your different media backends, without relying on 3rd party services, this tool support Jellyfin, Plex Media Serverand Emby out of the box.

It's also come with some goodies, like finding mis-identified items or unmatched items. querying your backend for specific item.

Install

create your docker-compose.yaml file:

version: '3.3'
services:
    watchstate:
        image: ghcr.io/arabcoders/watchstate:latest
        container_name: watchstate
        restart: unless-stopped
        # For more environment variables please read at the bottom of this page.
        # works for both global and container specific environment variables. 
        environment:
            - WS_UID=${UID:-1000} # Set container user id.
            - WS_GID=${GID:-1000} # Set container group id.
        ports:
            - "8081:80" # webhook listener port
        volumes:
            - ${PWD}/:/config:rw # mount current directory to container /config directory.

After creating your docker compose file, start the container.

$ docker-compose pull && docker-compose up -d

First time

Run the following command to see all available commands you can also run help on each command to get more info.

# Show all commands.
$ docker exec -ti watchstate console list

# Show help document for each command.
$ docker exec -ti watchstate console help state:import

After starting the container, you have to add your media backends, to do so run the following command:

$ docker exec -ti watchstate console servers:manage --add -- [SERVER_NAME]

This command is interactive and will ask you for some questions to add your backend, you can run the command as many times as you want, if you want to edit the config again or if you made mistake just run the same command without --add flag. After adding your backends, You should import your current play state by running the following command.

$ docker exec -ti watchstate console state:import -vvf

Pulling play state.

Now that you have imported your current play state, you can stop manually running the command, and rely on the tasks scheduler and webhooks to keep update your play state. To start receiving webhook events from backends you need to do few more steps.

Enable webhooks events for specific backend.

To see the backend specific webhook api key run the following command:

$ docker exec -ti watchstate console servers:view --servers-filter [SERVER_NAME] -- webhook.token 

If you see 'Not configured, or invalid key.' or empty value. run the following command

$ docker exec -ti watchstate console servers:edit --regenerate-webhook-token -- [SERVER_NAME] 

Notice:

If you have multiple plex backends and use the same PlexPass account for all of them, you have to unify the API key, by running the following command:

$ docker exec -ti watchstate console servers:unify plex 
Plex global webhook API key is: [random_string]

The reason is due to the way plex handle webhooks, And to know which webhook request belong to which server we have to identify the backends, The unify command will do the necessary adjustments to handle multi plex server setup. for more information run.

$ docker exec -ti watchstate console help servers:unify 

If you don't want to/can't use webhooks and want to rely on task scheduler importing, then set the value of WS_CRON_IMPORT to 1. By default, we run the import command every hour. However, you can change the scheduled task timer by adding another variable WS_CRON_IMPORT_AT and set its value to valid cron expression. for example, 0 */2 * * * it will run every two hours instead of 1 hour. If your backends and this tool are not on same server it might consume a lot of bandwidth depending on how big is your library as it's pulls the entire server library listing.


Notice

You should still have WS_CRON_IMPORT enabled to keep healthy relation between storage and backend changes.


Exporting play state

To manually export your play state back to backends you can run the following command

$ docker exec -ti watchstate console state:export -vv

to sync specific server/s, use the [-s, --servers-filter] which accept comma seperated list of server names.

$ docker exec -ti watchstate console state:export -vv --servers-filter 'server1,server2' 

To enable export scheduled task set the value of WS_CRON_EXPORT to 1. By default, we run export every 90 minutes. However, you can change the timer by adding another variable called WS_CRON_EXPORT_AT and set its value to valid cron expression. for example, 0 */3 * * * it will run every three hours instead of 90 minutes.

Start receiving webhook events.

By default, the official container includes http server exposed at port 80, we officially don't support HTTPS inside the container for the HTTP server. However, for the adventurous people we expose port 443 as well, as such you can customize the docker/files/nginx.conf to support SSL. and do the necessary adjustments.

Example nginx reverse proxy.

server {
    server_name watchstate.domain.example;
    
    location / {
        proxy_http_version 1.1;
        proxy_set_header Host                   $host;
        proxy_set_header X-Real-IP              $remote_addr;
        proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto      $scheme;
        proxy_set_header X-Forwarded-Protocol   $scheme;
        proxy_set_header X-Forwarded-Host       $http_host;
        proxy_pass http://localhost:8081/;
    }
}

Adding webhook

To add webhook for your server the URL will be dependent on how you exposed webhook frontend, but typically it will be like this:

Webhook URL

Via reverse proxy : https://watchstate.domain.example/?apikey=[WEBHOOK_TOKEN].

Directly to container: https://localhost:8081/?apikey=[WEBHOOK_TOKEN]

If your media backend support sending headers then remove query parameter ?apikey=[WEBHOOK_TOKEN], and add this header

X-apikey: [WEBHOOK_TOKEN]

where [WEBHOOK_TOKEN] Should match the backend specific webhook.token value. Refer to the steps described at Steps to enable webhook servers.

Configuring media backends to send webhook events.

Jellyfin (Free)

go to your jellyfin dashboard > plugins > Catalog > install: Notifications > Webhook, restart your jellyfin. After that go back again to dashboard > plugins > webhook. Add Add Generic Destination,

Webhook Name:

Choose whatever name you want. For example, Watchstate-Webhook

Webhook Url:

http://localhost:8081

Notification Type:

Select the following events

  • Item Added
  • User Data Saved
  • Playback Start
  • Playback Stop
User Filter:
  • Select your user.
Item Type:
  • Movies
  • Episodes

Send All Properties (ignores template)

Toggle this checkbox.

Add Request Header

Key: x-apikey

Value: [WEBHOOK_TOKEN]

Click save

Emby (you need "Emby Premiere" to use webhooks)

Go to your Manage Emby Server > Server > Webhooks > (Click Add Webhook)

Webhook Url:

http://localhost:8081/?apikey=[WEBHOOK_TOKEN]

Webhook Events

Select the following events

  • Playback events
  • User events

Click Add Webhook

Plex (you need "PlexPass" to use webhooks)

Go to your plex Web UI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK)

URL:

http://localhost:8081/?apikey=[WEBHOOK_TOKEN]

Click Save Changes


Known Webhook limitations

Plex

  • Plex does not send webhooks events for "marked as Played/Unplayed".
  • Sometimes does not send events if you add more than one item at time.
  • If you have multi-user setup, Plex will still report the admin account user id as 1.
  • When you mark items as unwatched, Plex reset the date on the object.

Emby

  • Emby does not send webhooks events for newly added items. See feature request
  • Emby webhook test event does not contain data. To test if your setup works, play something or do mark an item as played or unplayed you should see changes reflected in docker exec -ti watchstate console db:list.

Jellyfin

  • If you don't select a user id, the Plugin will send itemAdd event without user data, and will fail the check if you happen to enable strict user match for jellyfin.
  • Sometimes jellyfin will fire webhook itemAdd event without the item being matched.
  • Even if you select user id, sometimes itemAdd event will fire without user data.

Environment variables.

Key Type Description Default
WS_DATA_PATH string Where key data stored (config, db). ${BASE_PATH}/var
WS_TMP_DIR string Where temp data stored. (logs, cache). ${WS_DATA_PATH}
WS_TZ string Set timezone. UTC
WS_CRON_IMPORT bool Enable import scheduled task. Value casted to bool. false
WS_CRON_IMPORT_AT string When to run import scheduled task. Valid Cron Expression Expected. 0 */1 * * * (Every 1h)
WS_CRON_IMPORT_ARGS string Flags to pass to the import command. -v
WS_CRON_EXPORT bool Enable export scheduled task. Value casted to bool. false
WS_CRON_EXPORT_AT string When to run export scheduled task. Valid Cron Expression Expected. 30 */1 * * * (Every 1h 30m)
WS_CRON_EXPORT_ARGS string Flags to pass to the export command. -v
WS_CRON_PUSH bool Enable push scheduled task. Value casted to bool. false
WS_CRON_PUSH_AT string When to run push scheduled task. Valid Cron Expression Expected. */10 * * * * (Every 10m)
WS_CRON_PUSH_ARGS string Flags to pass to the push command. -v
WS_LOGS_PRUNE_AFTER string Delete logs older than specified time. Set to disable to disable the pruning. -3 DAYS
WS_LOGS_CONTEXT bool Add context to console output messages. false
WS_LOGGER_FILE_ENABLE bool Save logs to file. true
WS_LOGGER_FILE string Full path to log file. ${WS_TMP_DIR}/logs/app.(Ymd).log
WS_LOGGER_FILE_LEVEL string File Logger Level. ERROR
WS_WEBHOOK_DEBUG bool If enabled, allow dumping request/webhook using rdump & wdump parameters. false

Container specific environment variables.

Key Type Description Default
WS_DISABLE_CHOWN integer Do not change ownership for needed directories inside the container. 0
WS_DISABLE_HTTP integer Disable included HTTP Server. 0
WS_DISABLE_CRON integer Disable included Task Scheduler. 0
WS_DISABLE_CACHE integer Disable included Cache Server. 0
WS_UID integer Set container user id. 1000
WS_GID integer Set container group id. 1000

FAQ

For some common questions, Take look at this frequently asked questions page.