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]);