diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6792b496..7905e660 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,11 +4,9 @@ on:
push:
branches:
- 'master'
+ - 'dev'
tags:
- 'v*'
- pull_request:
- branches:
- - 'master'
jobs:
publish_docker_images:
@@ -23,12 +21,12 @@ jobs:
images: arabcoders/watchstate
tags: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
+ type=raw,value=dev-latest,enable=${{ endsWith(github.ref, '/dev') }}
type=ref,event=tag
- type=ref,event=pr
+ type=sha
flavor: |
latest=false
- name: Login to DockerHub
- if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -37,6 +35,6 @@ jobs:
uses: docker/build-push-action@v2
with:
context: .
- push: ${{ github.event_name != 'pull_request' }}
+ push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
diff --git a/Dockerfile b/Dockerfile
index 469d7269..6d29850e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,13 +10,12 @@ ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/do
RUN mv "${PHP_INI_DIR}/php.ini-production" "${PHP_INI_DIR}/php.ini" && chmod +x /usr/bin/install-php-extensions && \
sync && install-php-extensions pdo mbstring ctype sqlite3 json opcache xhprof && \
apk add --no-cache caddy nano curl procps net-tools iproute2 shadow runuser && \
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
-
-RUN mkdir -p /app /config
+ curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer && \
+ mkdir -p /app /config
COPY . /app
-RUN echo '* * * * * /usr/bin/run-app-cron'>>/etc/crontabs/root && \
+RUN echo '* * * * * /usr/bin/run-app-cron'>>/etc/crontabs/www-data && \
cp /app/docker/files/Caddyfile /etc/caddy/Caddyfile && \
cp /app/docker/files/entrypoint.sh /usr/bin/entrypoint-docker && \
cp /app/docker/files/app_console.sh /usr/bin/console && \
diff --git a/README.md b/README.md
index cf5de158..b65311c0 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ before running this tool. while its works for me, it might not work for your set
# Watch State Sync (Early Preview)
-A CLI based app to sync watch state between different media servers.
+A commandline based tool to sync watch state between different media servers.
# Introduction
@@ -19,30 +19,42 @@ that the plugin no longer supported. And I like to keep my own data locally if p
* Emby
* Jellyfin
-## Install (Early Preview)
+## Install
-Clone this repo by
+create your `docker-compose.yaml` file
-```bash
-git clone https://github.com/ArabCoders/watchstate.git
+```yaml
+version: '3.3'
+services:
+ watchstate:
+ image: arabcoders/watchstate:latest
+ container_name: watchstate
+ restart: unless-stopped
+ environment:
+ WS_UID: ${UID:-1000} # Set container operation user id.
+ WS_GID: ${GID:-1000} # Set container operation group id.
+ WS_WEBHOOK_ENABLE: 0 # Enable webhook listening server. Disabled by default.
+ WS_CRON_IMPORT: 0 # Enable manual pulling of watch state from servers. Disabled by default.
+ WS_CRON_EXPORT: 1 # Enable manual push of watch state back to servers. Disabled by default.
+ ports:
+ - "8081:80" # webhook listener port
+ volumes:
+ - ./:/config:rw # mount current directory to container /config directory.
```
-after cloning the app, start the docker container
+After creating your docker-compose file, start the container.
```bash
-cd watchstate/docker/
docker-compose up -d
```
-This docker container will expose port 80 by default to listen for webhooks calls. mapped to port 8081 on host.
-
# First time
-You have to set up your servers in ``docker/config/config/servers.yaml`` you will have examples inside the file, after
-editing the file remove the unused servers examples.
+You have to set up your servers in `config/servers.yaml` you will have examples inside the file, after editing the file
+remove the unused servers examples.
-after configuring your servers at ``docker/config/config/servers.yaml`` you should import your current watch state by
-running the following command.
+after configuring your servers at `config/servers.yaml` you should import your current watch state by running the
+following command.
```bash
docker exec -ti watchstate console state:import -vvrm
@@ -50,47 +62,65 @@ docker exec -ti watchstate console state:import -vvrm
#### TIP
-to watch lovely debug information you could run the command with -vvvrm it will show excessive information, be careful
-it might crash your terminal depending on how many servers and media you have. the output is excessive.
+To see the whole sync operation information you could run the command with `-vvvr` it will show all debug information,
+be careful it might crash your terminal depending on how many servers and media you have. the output is excessive.
---
-now that you have imported your watch state, you can stop manually running the command again. and rely solely on the
-webhooks to update the import state.
+# Pull watch state.
-If however you don't want to run a webhook server, then you have to make a few adjustments edit ``docker-compose.yaml``
-and enable the environment variable ``WS_CRON`` by changing its value to ``1``, you can also control whether you want to
-run both import and export by using the other ``WS_CRON_*`` variables. All those variables can be edited
-using ``config/config/config.yaml`` file, the options modified get to override the ``config/config.php``. After editing
-the variables restart the docker container for the changes to take effect.
+now that you have imported your watch state, you can stop manually running the command again. and rely on the webhooks
+to update the watch state.
-### You can manually export your watch state back to servers using the following command
+To enable webhook listening server, Edit the `WS_WEBHOOK_ENABLE` variable by setting its value to `1`. If the variable
+does not exist or is set to value other than `1` it will not start the included caddy server.
+
+If you don't want to use webhook events to update the watch state, and want to rely on manual polling the servers for
+state change, then set the value of `WS_CRON_IMPORT` to `1`. By default, we run import every hour. However, you can
+change the schedule by adding another variable `WS_CRON_IMPORT_AT` and set it value to valid cron timing. for
+example, `0 */2 * * *` it will run every two hours instead of 1 hour.
+
+# Push watch state
+
+To manually push your watch state back to servers you can run the following command
```bash
-docker exec -ti watchstate console state:export -vvrm
+docker exec -ti watchstate console state:export -vvr
```
+to sync specific server/s, use the `--servers-filter` which accept comma seperated list of server names.
+
+```bash
+docker exec -ti watchstate console state:export -vvr --servers-filter 'server1,server2'
+```
+
+If you want to automate the pushing of your watch state back to servers, set the value of `WS_CRON_EXPORT` to `1`. By
+default, we run export every 90 minutes. However, you can change the schedule by adding another variable
+called `WS_CRON_EXPORT_AT` and set it value to valid cron timing. for example, `0 */3 * * *` it will run every three
+hours instead of 90 minutes.
+
# Memory usage (Import)
-By default, We use something called ``MemoryMapper`` this mapper store the state in memory during import/export to
-massively speed up the comparison. However, this approach has drawbacks which is large memory usage. Depending on your
-media count, it might use 1GB or more of memory per sync operation.(tests done on 2k movies and 70k episodes and 4
-servers). We recommend this mapper and use it as default.
+The default mapper we use is called `MemoryMapper` this mapper store the entire state in memory during import operation,
+by doing so we gain massive speed up. However, this approach has drawbacks mainly **large memory usage**. Depending on
+your media count, it might use 1GB+ or more of memory. Tests done on 2k movies and 70k episodes and 4 servers. Ofc the
+memory will be freed as as as the comparison is done.
-However, If you are on a memory constraint system, there is an alternative mapper implementation called ``DirectMapper``
-, this one work directly on db the only thing stored in memory is the api call body. which get removed as soon as it's
-parsed. The drawback for this mapper is it's like 10x slower than the memory mapper. for large media servers.
+However, If you are on a memory constraint system, there is an alternative mapper implementation called `DirectMapper`
+, this implementation works directly via db the only thing stored in memory is the api call body. which get removed as
+soon as it's parsed. The drawback for this mapper is it's like really slow for large libraries. It has to do 3x call to
+db for each time.
-To see memory usage during the operation run the import command with following flags. ``-vvvrm`` these flags will
-redirect logger output, log memory usage and print them to the screen.
+To see memory usage during the operation run the import command with following flags. `-vvvrm` these flags will redirect
+logger output, log memory usage and print them to the screen.
-```bash
-docker exec -ti watchstate console state:import -vvvrm
-```
+``bash docker exec -ti watchstate console state:import -vvvrm
+``
-### How to change the Mapper
+## How to change Import mapper.
-Edit ``config/config/config.yaml``
+Set the environment variable `WS_IMPORT_MAPPER` to `DirectMapper` or edit the ``config/config.yaml`` and add the
+following lines
```yaml
mapper:
@@ -122,40 +152,42 @@ my_home_server:
enabled: true # Enable import.
options:
http2: false # Enable HTTP/2 support for faster http requests (server must support http 2.0).
- importUnwatched: false # By default, We do not import unwatched state to enable support, Set this to true. Webhooks can set unwatched state as they are explicit user action.
+ importUnwatched: false # By default, We do not import unwatched state to enable it set to true.
exportIgnoreDate: false # By default, we respect the server watch date. To override the check, set this to true.
```
-# Running Webhook server.
+# Start Webhook Server.
-You should have a working webhook server already. view the contents of ``config/config/config.yaml`` and take note of
-the ``webhook.apikey``. if the apikey does not exist run the following command.
+The default container includes webserver to listen for webhook events, to enable the server. edit
+your `docker-compose.yaml` file, and set the environment variable `WS_WEBHOOK_ENABLE` to `1`.
+
+View the contents of your `config/config.yaml` and take note of the `webhook.apikey`. if the apikey does not exist run
+the following command.
```bash
docker exec -ti watchstate console config:generate
```
-it should update your ``config.yaml`` and add randomly generated key, and it will be printed to your screen.
+it should populate your `config.yaml` with randomly generated key, and it will be printed to your screen. Adding webhook
+to your server the url will be dependent on how you expose the server, but typically it will be like this:
-Adding webhook to your server the url will be dependent on how you expose the server, but typically it will be like this
-``http://localhost:8081/?type=[SERVER_TYPE]&apikey=[YOUR_API_KEY]``
+`http://localhost:8081/?type=[SERVER_TYPE]&apikey=[YOUR_API_KEY]`
### [SERVER_TYPE]
-Change the parameter to one of those ``emby, plex or jellyfin``. it should the server that you are adding to, as each
-server has different webhook payload.
+Change the parameter to one of supported backends. e.g. `emby, plex or jellyfin`. it should match the server type that
+you are adding the link to, Each server type has different webhook payload.
### [YOUR_API_KEY]
-Change this parameter to your api key you can find it by viewing ``config/config/config.yaml`` under the key
-of ``webhook.apikey``.
+Change this parameter to your api key you can find it by viewing `config/config.yaml` under the key of `webhook.apikey`.
# Configuring Media servers 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 A ``Add Generic Destination``,
+go back again to dashboard > plugins > webhook. Add A `Add Generic Destination`,
##### Webhook Name:
@@ -163,7 +195,7 @@ Choose whatever name you want.
##### Webhook Url:
-``http://localhost:8081/?type=jellyfin&apikey=[YOUR_API_KEY]``
+`http://localhost:8081/?type=jellyfin&apikey=[YOUR_API_KEY]`
##### Notification Type:
@@ -172,7 +204,7 @@ Select the following events
* Item Added
* User Data Saved
-Click ``save``
+Click `save`
#### Emby (you need emby premiere to use webhooks)
@@ -180,7 +212,7 @@ Go to your Manage Emby Server > Server > Webhooks > (Click Add Webhook)
##### Webhook Url:
-``http://localhost:8081/?type=emby&apikey=[YOUR_API_KEY]``
+`http://localhost:8081/?type=emby&apikey=[YOUR_API_KEY]`
##### Webhook Events
@@ -189,7 +221,7 @@ Select the following events
* Playback events
* User events
-Click ``Add Webhook``
+Click `Add Webhook`
#### Plex (you need PlexPass to use webhooks)
@@ -197,20 +229,14 @@ Go to your plex WebUI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK)
##### URL:
-``http://localhost:8081/?type=plex&apikey=[YOUR_API_KEY]``
+`http://localhost:8081/?type=plex&apikey=[YOUR_API_KEY]`
-Click ``Save Changes``
+Click `Save Changes`
# Finally
-after making sure everything is running smoothly, edit your ``docker-compose.yaml`` file and enable the exporting of
-your watch state back to servers. by enabling the following options
-
-```yaml
-environment:
- WS_CRON: 1
- WS_CRON_EXPORT: 1
-```
+after making sure everything is running smoothly, edit your `docker-compose.yaml` file and enable the exporting of your
+watch state back to servers. by setting the value of `WS_CRON_EXPORT` to `1`.
restart your docker container for changes to take effect.
@@ -218,12 +244,12 @@ restart your docker container for changes to take effect.
---
-### (Q01): How to update new server watched state without overwriting the existing watch state?
+### Q1: How to update new server watched state without overwriting the existing watch state?
Add the server, disable the import operation, and enable export. Then run the following commands.
```bash
-docker exec -ti watchstate console state:export --vvrm --ignore-date --force-full --servers-filter [SERVER_NAME]
+docker exec -ti watchstate console state:export -vvrm --ignore-date --force-full --servers-filter [SERVER_NAME]
```
### [SERVER_NAME]
@@ -233,7 +259,7 @@ Replace `[SERVER_NAME]` with what you have chosen to name your server in config
this command will force export your current database state back to the selected server. If the operation is successful
you can then enable the import feature if you want.
-### (Q02): Is there support for Multi-user setup?
+### Q2: Is there support for Multi-user setup?
No, Not at this time. The database design centered on single user. However, It's possible to run container for each
user.
diff --git a/config/config.php b/config/config.php
index f63f79c3..cfbf1601 100644
--- a/config/config.php
+++ b/config/config.php
@@ -44,13 +44,13 @@ return (function () {
$config['mapper'] = [
'import' => [
- 'type' => MemoryMapper::class,
+ 'type' => env('WS_IMPORT_MAPPER', MemoryMapper::class),
'opts' => [
'lazyload' => true
],
],
'export' => [
- 'type' => ExportMapper::class,
+ 'type' => env('WS_EXPORT_MAPPER', ExportMapper::class),
'opts' => [
'lazyload' => true
],
@@ -156,10 +156,10 @@ return (function () {
'import' => [
Task::NAME => 'import',
Task::ENABLED => (bool)env('WS_CRON_IMPORT', true),
- Task::RUN_AT => (string)env('WS_CRON_EXPORT_AT', '1 */1 * * *'),
+ Task::RUN_AT => (string)env('WS_CRON_IMPORT_AT', '0 */1 * * *'),
Task::COMMAND => '@state:import',
Task::ARGS => [
- '-vvrm' => null,
+ '-vvr' => null,
]
],
'export' => [
@@ -168,7 +168,7 @@ return (function () {
Task::RUN_AT => (string)env('WS_CRON_EXPORT_AT', '30 */1 * * *'),
Task::COMMAND => '@state:export',
Task::ARGS => [
- '-vvrm' => null,
+ '-vvr' => null,
]
],
];
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 9bc4d991..6c718ff8 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -10,29 +10,16 @@ ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/do
RUN mv "${PHP_INI_DIR}/php.ini-production" "${PHP_INI_DIR}/php.ini" && chmod +x /usr/bin/install-php-extensions && \
sync && install-php-extensions pdo mbstring ctype sqlite3 json opcache xhprof && \
apk add --no-cache caddy nano curl procps net-tools iproute2 shadow runuser && \
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
+ curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer && \
+ mkdir -p /app /config
-# Create needed directories.
-RUN mkdir -p /app /config
-
-# App Verison
-#
-ARG APP_VERSION=latest
-
-# Add Latest Watchstate release.
-#
-COPY ./files/download.sh /usr/bin/downloadapp
+COPY ./files/entrypoint.sh /usr/bin/entrypoint-docker
COPY ./files/Caddyfile /etc/caddy/Caddyfile
-COPY ./files/entrypoint.sh /entrypoint-docker
-COPY ./files/app_console.sh /usr/bin/console
-COPY ./files/cron.sh /usr/bin/run-app-cron
-RUN chmod +x /entrypoint-docker /usr/bin/console /usr/bin/run-app-cron /usr/bin/downloadapp && \
- echo '* * * * * /usr/bin/run-app-cron'>>/etc/crontabs/root
+RUN echo '* * * * * /usr/bin/run-app-cron'>>/etc/crontabs/www-data && \
+ chmod +x /usr/bin/entrypoint-docker
-RUN /usr/bin/downloadapp ${APP_VERSION}
-
-ENTRYPOINT ["/entrypoint-docker"]
+ENTRYPOINT ["/usr/bin/entrypoint-docker"]
WORKDIR /config
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 40d341ad..b1f56f4a 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -3,17 +3,16 @@ services:
watchstate:
container_name: watchstate
restart: unless-stopped
- build:
- context: .
- args:
- APP_VERSION: latest
+ build: .
environment:
- WS_UID: 1000
- WS_GID: 1000
- WS_CRON: 0
+ WS_UID: ${UID:-1000}
+ WS_GID: ${GID:-1000}
+ WS_WEBHOOK_ENABLE: 1
WS_CRON_IMPORT: 1
WS_CRON_EXPORT: 1
+ WS_CRON_EXPORT_AT: '*/2 * * * *'
ports:
- "8081:80"
volumes:
+ - ../:/app
- ./config:/config:rw
diff --git a/docker/files/cron.sh b/docker/files/cron.sh
index 44707a8d..e9ea1037 100755
--- a/docker/files/cron.sh
+++ b/docker/files/cron.sh
@@ -9,7 +9,7 @@ if [ -z "${WS_DATA_PATH}" ]; then
WS_DATA_PATH="/config"
fi
-if [ 1 == "${WS_CRON}" ]; then
+if [ "1" == "${WS_CRON_IMPORT}" ] || [ "1" == "${WS_CRON_EXPORT}" ]; then
LOGFILE="${WS_DATA_PATH}/logs/cron/${NOW}.log"
if [ ! -d "${WS_DATA_PATH}/logs/cron/" ]; then
diff --git a/docker/files/download.sh b/docker/files/download.sh
deleted file mode 100755
index cef2ca3d..00000000
--- a/docker/files/download.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env sh
-set -e
-
-if [ -z "${1}" ]; then
- echo "No Version was given"
- exit 1
-fi
-
-APP_VERSION=${1}
-
-if [ ! -f "/app/public/index.php" ]; then
-
- if [ "latest" == "${APP_VERSION}" ]; then
- URL=https://github.com/ArabCoders/watchstate/archive/refs/heads/master.tar.gz
- else
- URL=https://github.com/ArabCoders/watchstate/archive/refs/tags/${APP_VERSION}.tar.gz
- fi
-
- echo "Downloading Version ${APP_VERSION} via ${URL}"
-
- /usr/bin/wget ${URL} -O /tmp/master.tar.gz
-
- if [ ! -f "/tmp/master.tar.gz" ]; then
- echo "No source code was downloaded."
- exit 1
- fi
-
- if [ ! -d "/app" ]; then
- mkdir -p /app
- fi
-
- tar -xz --strip-components=1 --directory=/app -f /tmp/master.tar.gz
- /usr/bin/composer --working-dir=/app/ -o --no-dev --no-progress --no-cache install
-
- chown -R www-data:www-data /app
-
- # cp /app/docker/files/entrypoint.sh /entrypoint-docker
- # cp /app/docker/files/Caddyfile /etc/caddy/Caddyfile
- # cp /app/docker/files/app_console.sh /usr/bin/console
- # cp /app/docker/files/cron.sh /usr/bin/run-app-cron
- #
- # chmod +x /usr/bin/console /usr/bin/downloadapp /usr/bin/run-app-cron
- # echo '* * * * * /usr/bin/run-app-cron' >/etc/crontabs/root
- # chown root:root /entrypoint-docker
-fi
diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh
index 2429dbbd..02ff3d35 100755
--- a/docker/files/entrypoint.sh
+++ b/docker/files/entrypoint.sh
@@ -14,7 +14,19 @@ if [ ! -f "/app/vendor/autoload.php" ]; then
runuser -u www-data -- composer --ansi --working-dir=/app/ -o --no-dev --no-progress --no-cache install
fi
-chown www-data:www-data /config
+if [ ! -f "/usr/bin/console" ]; then
+ cp /app/docker/files/app_console.sh /usr/bin/console
+ chmod +x /usr/bin/console
+fi
+
+if [ ! -f "/usr/bin/run-app-cron" ]; then
+ cp /app/docker/files/cron.sh /usr/bin/run-app-cron
+ chmod +x /usr/bin/run-app-cron
+fi
+
+if [ -z "${WS_NO_CHOWN}" ]; then
+ chown -R www-data:www-data /config
+fi
/usr/bin/console config:php >"${PHP_INI_DIR}/conf.d/zz-app-custom-ini-settings.ini"
/usr/bin/console config:php --fpm >"${PHP_INI_DIR}/../php-fpm.d/zzz-app-pool-settings.conf"
@@ -30,11 +42,13 @@ if [ ! -f "/config/config/servers.yaml" ]; then
/usr/bin/console config:dump servers
fi
-if [ -f "/etc/caddy/Caddyfile" ]; then
+if [ "1" == "${WS_WEBHOOK_ENABLE}" ] && [ -f "/etc/caddy/Caddyfile" ]; then
caddy start -config /etc/caddy/Caddyfile
fi
-/usr/sbin/crond -b -l 2
+if [ "1" == "${WS_CRON_IMPORT}" ] || [ "1" == "${WS_CRON_EXPORT}" ]; then
+ /usr/sbin/crond -b -l 2
+fi
# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then
diff --git a/src/Commands/Scheduler/Run.php b/src/Commands/Scheduler/Run.php
index ff1c10ad..9aeffa07 100644
--- a/src/Commands/Scheduler/Run.php
+++ b/src/Commands/Scheduler/Run.php
@@ -60,7 +60,7 @@ final class Run extends Command
foreach ($tasks as $task) {
$newTask = $this->makeTask($task);
- if (true !== $newTask[Task::ENABLED] && $newTask[Task::NAME] !== $runSpecificTask) {
+ if (true !== $task[Task::ENABLED] && $task[Task::NAME] !== $runSpecificTask) {
continue;
}
@@ -75,7 +75,7 @@ final class Run extends Command
if (0 === $count) {
$output->writeln(
- '#@date No Tasks Scheduled to run at this time.',
+ '!{date} No Tasks Scheduled to run at this time.',
OutputInterface::VERBOSITY_VERY_VERBOSE
);
}