diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e80f35e1..52302484 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -46,7 +46,7 @@ on:
env:
DOCKERHUB_SLUG: arabcoders/watchstate
GHCR_SLUG: ghcr.io/arabcoders/watchstate
- PLATFORMS: linux/amd64,linux/arm64,linux/arm
+ PLATFORMS: linux/amd64,linux/arm64
jobs:
unit-tests:
@@ -90,6 +90,49 @@ jobs:
- run: composer install --prefer-dist --no-interaction --no-progress
- run: composer run test
+ pr_build_test:
+ needs: unit-tests
+ if: github.event_name == 'pull_request'
+ runs-on: "ubuntu-latest"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: bahmutov/npm-install@v1
+ with:
+ working-directory: frontend
+ install-command: yarn --production --prefer-offline --frozen-lockfile
+
+ - uses: bahmutov/npm-install@v1
+ with:
+ working-directory: frontend
+ install-command: yarn run generate
+
+ - name: Update Version File
+ uses: arabcoders/write-version-to-file@master
+ with:
+ filename: "/config/config.php"
+ placeholder: "$(version_via_ci)"
+ with_date: "true"
+ with_branch: "true"
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ platforms: ${{ env.PLATFORMS }}
+ context: .
+ push: false
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha, scope=pr_${{ github.workflow }}
+ cache-to: type=gha, scope=pr_${{ github.workflow }}
+
publish_docker_images:
needs: unit-tests
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build == 'true')
diff --git a/src/Commands/State/SyncCommand.php b/src/Commands/State/SyncCommand.php
index c0fd5b5c..d3a0870a 100644
--- a/src/Commands/State/SyncCommand.php
+++ b/src/Commands/State/SyncCommand.php
@@ -74,6 +74,7 @@ protected function configure(): void
->addOption('force-full', 'f', InputOption::VALUE_NONE, 'Force full export. Ignore last export date.')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not commit changes to backends.')
->addOption('timeout', null, InputOption::VALUE_REQUIRED, 'Set request timeout in seconds.')
+ ->addOption('test', null, InputOption::VALUE_NONE, 'Run on one user only.')
->addOption(
'select-backend',
's',
@@ -102,10 +103,9 @@ protected function configure(): void
Known limitations
- We have some known limitations:
- * Cannot be used with plex users that have PIN enabled.
- * Can Only sync played status.
- * Cannot sync play progress.
+ Known limitations:
+ * Cannot be used with plex users that have PIN enabled.
+ * Cannot sync play progress.
Some or all of these limitations will be fixed in future releases.
@@ -193,6 +193,13 @@ protected function process(iInput $input, iOutput $output): int
continue;
}
+ if (true !== (bool)ag($backend, 'import.enabled')) {
+ $this->logger->info("SYSTEM: Ignoring '{backend}' as the backend has import disabled.", [
+ 'backend' => $backendName
+ ]);
+ continue;
+ }
+
if (true !== (bool)ag($backend, 'export.enabled')) {
$this->logger->info("SYSTEM: Ignoring '{backend}' as the backend has export disabled.", [
'backend' => $backendName
@@ -272,6 +279,7 @@ protected function process(iInput $input, iOutput $output): int
$this->logger->info("SYSTEM: Getting users from '{backend}'.", [
'backend' => $client->getContext()->backendName
]);
+
try {
foreach ($client->getUsersList(['tokens' => true]) as $user) {
$info = $backend;
@@ -325,27 +333,54 @@ protected function process(iInput $input, iOutput $output): int
'results' => arrayToString($this->usersList($users)),
]);
- foreach (array_reverse($users) as $user) {
+ foreach (($input->getOption('test') ? array_reverse($users) : $users) as $user) {
+ $this->queue->reset();
+
$userName = ag($user, 'name', 'Unknown');
$perUserCache = perUserCacheAdapter($userName);
+ $this->logger->info("SYSTEM: Loading user mapper data.");
$perUserMapper = perUserMapper($this->mapper, $userName)
->withCache($perUserCache)
->withLogger($this->logger)->loadData();
+ $this->logger->info("SYSTEM: Load user mapper data complete.");
- $this->queue->reset();
$list = [];
$displayName = null;
+ $configFile = ConfigFile::open(r(fixPath(Config::get('path') . '/users/{user}/servers.yaml'), [
+ 'user' => $userName
+ ]), 'yaml', autoSave: true, autoCreate: true);
+ $configFile->setLogger($this->logger);
+
foreach (ag($user, 'backends', []) as $backend) {
$name = ag($backend, 'client_data.backendName');
$clientData = ag($backend, 'client_data');
$clientData['name'] = $name;
+
+ if (false === $configFile->has($name)) {
+ $data = $clientData;
+ $data = ag_set($data, 'import.lastSync', null);
+ $data = ag_set($data, 'export.lastSync', null);
+ $data = ag_delete($data, ['webhook', 'name', 'backendName', 'displayName']);
+ $configFile->set($name, $data);
+ } else {
+ $clientData = ag_delete($clientData, 'import.lastSync');
+ $clientData = ag_delete($clientData, 'export.lastSync');
+ $clientData = array_replace_recursive($configFile->get($name), $clientData);
+ }
+
$clientData['class'] = makeBackend($clientData, $name, [
BackendCache::class => Container::get(BackendCache::class)->with(adapter: $perUserCache)
])->setLogger($this->logger);
+
$list[$name] = $clientData;
$displayName = ag($backend, 'client_data.displayName', '??');
+
+ if (false === $input->getOption('dry-run')) {
+ $configFile->set("{$name}.import.lastSync", time());
+ $configFile->set("{$name}.export.lastSync", time());
+ }
}
$start = makeDate();
@@ -356,7 +391,7 @@ protected function process(iInput $input, iOutput $output): int
]);
assert($perUserMapper instanceof iEImport);
- $this->handleImport($perUserMapper, $displayName, $list);
+ $this->handleImport($perUserMapper, $displayName, $list, $input->getOption('force-full'), $configFile);
assert($perUserMapper instanceof MemoryMapper);
/** @var MemoryMapper $changes */
@@ -401,21 +436,37 @@ protected function process(iInput $input, iOutput $output): int
'peak' => getPeakMemoryUsage(),
],
]);
- exit(1);
+
+
+ if ($input->getOption('test')) {
+ break;
+ }
}
return self::SUCCESS;
}
- protected function handleImport(iEImport $mapper, string $name, array $backends): void
- {
+ protected function handleImport(
+ iEImport $mapper,
+ string $name,
+ array $backends,
+ bool $isFull,
+ ConfigFile $config
+ ): void {
/** @var array $queue */
$queue = [];
foreach ($backends as $backend) {
/** @var iClient $client */
$client = ag($backend, 'class');
- array_push($queue, ...$client->pull($mapper));
+ $context = $client->getContext();
+ $after = ag($context->options, Options::FORCE_FULL) || $isFull ? null : $config->get(
+ $context->backendName . '.import.lastSync'
+ );
+ if (null !== $after) {
+ $after = makeDate($after);
+ }
+ array_push($queue, ...$client->pull(mapper: $mapper, after: $after));
}
$start = makeDate();
@@ -580,7 +631,7 @@ private function generate_users_list(array $users, array $map = []): array
* 'backends' => [
* 'backend1' => userObj,
* 'backend2' => userObj,
- * ...
+ * ...,
* ]
* ]
*
diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php
index a94871e3..e31a1dbf 100644
--- a/src/Libs/helpers.php
+++ b/src/Libs/helpers.php
@@ -254,13 +254,20 @@ function ag_exists(array $array, string|int $path, string $separator = '.'): boo
* Delete given key path.
*
* @param array $array The array to search in.
- * @param int|string $path The key path to delete.
+ * @param int|string|array $path The key path to delete.
* @param string $separator The separator used in the key path (default is '.').
*
* @return array The modified array.
*/
- function ag_delete(array $array, string|int $path, string $separator = '.'): array
+ function ag_delete(array $array, string|int|array $path, string $separator = '.'): array
{
+ if (is_array($path)) {
+ foreach ($path as $key) {
+ $array = ag_delete($array, $key, $separator);
+ }
+ return $array;
+ }
+
if (array_key_exists($path, $array)) {
unset($array[$path]);