From d869111614057940eb01776c7ee36ebaa4cba97b Mon Sep 17 00:00:00 2001 From: "Abdulmhsen B. A. A" Date: Mon, 20 Jun 2022 18:57:43 +0300 Subject: [PATCH 1/3] Cleaned up shared import/export code. --- src/Backends/Jellyfin/Action/Export.php | 105 +++++++----------------- src/Backends/Jellyfin/Action/Import.php | 28 ++----- src/Backends/Plex/Action/Export.php | 91 +++++--------------- src/Backends/Plex/Action/Import.php | 52 +++++------- 4 files changed, 80 insertions(+), 196 deletions(-) diff --git a/src/Backends/Jellyfin/Action/Export.php b/src/Backends/Jellyfin/Action/Export.php index 7072a7e5..0fd6ae6b 100644 --- a/src/Backends/Jellyfin/Action/Export.php +++ b/src/Backends/Jellyfin/Action/Export.php @@ -5,97 +5,46 @@ declare(strict_types=1); namespace App\Backends\Jellyfin\Action; use App\Backends\Common\Context; -use App\Backends\Common\GuidInterface as iGuid; -use App\Backends\Common\Response; -use App\Backends\Jellyfin\JellyfinClient as JFC; +use App\Backends\Common\GuidInterface; +use App\Backends\Jellyfin\JellyfinClient; +use App\Libs\Container; use App\Libs\Data; -use App\Libs\Entity\StateInterface as iFace; use App\Libs\Mappers\ImportInterface; use App\Libs\Options; use App\Libs\QueueRequests; use DateTimeInterface; -use Symfony\Contracts\HttpClient\ResponseInterface as iResponse; use Throwable; class Export extends Import { - /** - * @param Context $context - * @param iGuid $guid - * @param ImportInterface $mapper - * @param DateTimeInterface|null $after - * @param array $opts - * - * @return Response - */ - public function __invoke( + protected function process( Context $context, - iGuid $guid, - ImportInterface $mapper, - DateTimeInterface|null $after = null, - array $opts = [] - ): Response { - return $this->tryResponse($context, fn() => $this->getLibraries( - context: $context, - handle: fn(array $logContext = []) => fn(iResponse $response) => $this->handle( - context: $context, - response: $response, - callback: fn(array $item, array $logContext = []) => $this->export( - context: $context, - guid: $guid, - queue: $opts['queue'], - mapper: $mapper, - item: $item, - logContext: $logContext, - opts: ['after' => $after], - ), - logContext: $logContext - ), - error: fn(array $logContext = []) => fn(Throwable $e) => $this->logger->error( - 'Unhandled Exception was thrown during [%(backend)] library [%(library.title)] request.', - [ - 'backend' => $context->backendName, - ...$logContext, - 'exception' => [ - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'kind' => get_class($e), - 'message' => $e->getMessage(), - ], - ] - ), - )); - } - - private function export( - Context $context, - iGuid $guid, - QueueRequests $queue, + GuidInterface $guid, ImportInterface $mapper, array $item, array $logContext = [], array $opts = [], ): void { - if (JFC::TYPE_SHOW === ($type = ag($item, 'Type'))) { + if (JellyfinClient::TYPE_SHOW === ($type = ag($item, 'Type'))) { $this->processShow(context: $context, guid: $guid, item: $item, logContext: $logContext); return; } try { - $after = ag($opts, 'after'); - $type = JFC::TYPE_MAPPER[$type]; + $queue = ag($opts, 'queue', fn() => Container::get(QueueRequests::class)); + $after = ag($opts, 'after', null); Data::increment($context->backendName, $type . '_total'); $logContext['item'] = [ 'id' => ag($item, 'Id'), 'title' => match ($type) { - iFace::TYPE_MOVIE => sprintf( + JellyfinClient::TYPE_MOVIE => sprintf( '%s (%d)', ag($item, ['Name', 'OriginalTitle'], '??'), - ag($item, 'ProductionYear', 0000) + ag($item, 'ProductionYear', '0000') ), - iFace::TYPE_EPISODE => trim( + JellyfinClient::TYPE_EPISODE => trim( sprintf( '%s - (%sx%s)', ag($item, 'SeriesName', '??'), @@ -241,22 +190,24 @@ class Export extends Import ] ); - if (false === (bool)ag($context->options, Options::DRY_RUN, false)) { - $queue->add( - $this->http->request( - $entity->isWatched() ? 'POST' : 'DELETE', - (string)$url, - $context->backendHeaders + [ - 'user_data' => [ - 'context' => $logContext + [ - 'backend' => $context->backendName, - 'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed', - ], - ], - ] - ) - ); + if (true === (bool)ag($context->options, Options::DRY_RUN, false)) { + return; } + + $queue->add( + $this->http->request( + $entity->isWatched() ? 'POST' : 'DELETE', + (string)$url, + $context->backendHeaders + [ + 'user_data' => [ + 'context' => $logContext + [ + 'backend' => $context->backendName, + 'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed', + ], + ], + ] + ) + ); } catch (Throwable $e) { $this->logger->error( 'Unhandled exception was thrown during handling of [%(backend)] [%(library.title)] [%(item.title)] export.', diff --git a/src/Backends/Jellyfin/Action/Import.php b/src/Backends/Jellyfin/Action/Import.php index 89fa8ee7..8485eb9e 100644 --- a/src/Backends/Jellyfin/Action/Import.php +++ b/src/Backends/Jellyfin/Action/Import.php @@ -59,13 +59,13 @@ class Import handle: fn(array $logContext = []) => fn(iResponse $response) => $this->handle( context: $context, response: $response, - callback: fn(array $item, array $logContext = []) => $this->import( + callback: fn(array $item, array $logContext = []) => $this->process( context: $context, guid: $guid, mapper: $mapper, item: $item, logContext: $logContext, - opts: ['after' => $after], + opts: $opts + ['after' => $after], ), logContext: $logContext ), @@ -120,9 +120,7 @@ class Import if (empty($listDirs)) { $this->logger->warning('Request for [%(backend)] libraries returned with empty list.', [ 'backend' => $context->backendName, - 'context' => [ - 'body' => $json, - ] + 'body' => $json, ]); Data::add($context->backendName, 'no_import_update', true); return []; @@ -421,7 +419,7 @@ class Import $this->logger->debug('Processing [%(backend)] %(item.type) [%(item.title) (%(item.year))] payload.', [ 'backend' => $context->backendName, ...$logContext, - 'trace' => $item, + 'body' => $item, ]); } @@ -437,9 +435,7 @@ class Import $this->logger->info($message, [ 'backend' => $context->backendName, ...$logContext, - 'data' => [ - 'guids' => !empty($providersId) ? $providersId : 'None' - ], + 'guids' => !empty($providersId) ? $providersId : 'None' ]); return; @@ -454,7 +450,7 @@ class Import ); } - private function import( + protected function process( Context $context, iGuid $guid, ImportInterface $mapper, @@ -495,9 +491,7 @@ class Import $this->logger->debug('Processing [%(backend)] %(item.type) [%(item.title)] payload.', [ 'backend' => $context->backendName, ...$logContext, - 'response' => [ - 'body' => $item - ], + 'body' => $item ]); } @@ -509,9 +503,7 @@ class Import 'backend' => $context->backendName, 'date_key' => $dateKey, ...$logContext, - 'response' => [ - 'body' => $item, - ], + 'body' => $item, ]); Data::increment($context->backendName, $type . '_ignored_no_date_is_set'); @@ -565,9 +557,7 @@ class Import $this->logger->info($message, [ 'backend' => $context->backendName, ...$logContext, - 'context' => [ - 'guids' => !empty($providerIds) ? $providerIds : 'None' - ], + 'guids' => !empty($providerIds) ? $providerIds : 'None' ]); Data::increment($context->backendName, $type . '_ignored_no_supported_guid'); diff --git a/src/Backends/Plex/Action/Export.php b/src/Backends/Plex/Action/Export.php index 451b9312..f0d6892f 100644 --- a/src/Backends/Plex/Action/Export.php +++ b/src/Backends/Plex/Action/Export.php @@ -5,76 +5,27 @@ declare(strict_types=1); namespace App\Backends\Plex\Action; use App\Backends\Common\Context; -use App\Backends\Common\GuidInterface as iGuid; -use App\Backends\Common\Response; +use App\Backends\Common\GuidInterface; use App\Backends\Plex\PlexClient; +use App\Libs\Container; use App\Libs\Data; use App\Libs\Mappers\ImportInterface; use App\Libs\Options; use App\Libs\QueueRequests; use DateTimeInterface; -use Symfony\Contracts\HttpClient\ResponseInterface as iResponse; use Throwable; final class Export extends Import { - /** - * @param Context $context - * @param iGuid $guid - * @param ImportInterface $mapper - * @param DateTimeInterface|null $after - * @param array $opts - * - * @return Response - */ - public function __invoke( + protected function process( Context $context, - iGuid $guid, - ImportInterface $mapper, - DateTimeInterface|null $after = null, - array $opts = [] - ): Response { - return $this->tryResponse($context, fn() => $this->getLibraries( - context: $context, - handle: fn(array $logContext = []) => fn(iResponse $response) => $this->handle( - context: $context, - response: $response, - callback: fn(array $item, array $logContext = []) => $this->export( - context: $context, - guid: $guid, - queue: $opts['queue'], - mapper: $mapper, - item: $item, - logContext: $logContext, - opts: ['after' => $after], - ), - logContext: $logContext, - ), - error: fn(array $logContext = []) => fn(Throwable $e) => $this->logger->error( - 'Unhandled Exception was thrown during [%(backend)] library [%(library.title)] request.', - [ - 'backend' => $context->backendName, - ...$logContext, - 'exception' => [ - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'kind' => get_class($e), - 'message' => $e->getMessage(), - ], - ] - ), - )); - } - - private function export( - Context $context, - iGuid $guid, - QueueRequests $queue, + GuidInterface $guid, ImportInterface $mapper, array $item, array $logContext = [], array $opts = [], ): void { + $queue = ag($opts, 'queue', fn() => Container::get(QueueRequests::class)); $after = ag($opts, 'after', null); $library = ag($logContext, 'library.id'); $type = ag($item, 'type'); @@ -253,22 +204,24 @@ final class Export extends Import ] ); - if (false === (bool)ag($context->options, Options::DRY_RUN, false)) { - $queue->add( - $this->http->request( - 'GET', - (string)$url, - array_replace_recursive($context->backendHeaders, [ - 'user_data' => [ - 'context' => $logContext + [ - 'backend' => $context->backendName, - 'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed', - ], - ] - ]) - ) - ); + if (true === (bool)ag($context->options, Options::DRY_RUN, false)) { + return; } + + $queue->add( + $this->http->request( + 'GET', + (string)$url, + array_replace_recursive($context->backendHeaders, [ + 'user_data' => [ + 'context' => $logContext + [ + 'backend' => $context->backendName, + 'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed', + ], + ] + ]) + ) + ); } catch (Throwable $e) { $this->logger->error( 'Unhandled exception was thrown during handling of [%(backend)] [%(library.title)] [%(item.title)] export.', diff --git a/src/Backends/Plex/Action/Import.php b/src/Backends/Plex/Action/Import.php index cb5ccfdb..8b788cbf 100644 --- a/src/Backends/Plex/Action/Import.php +++ b/src/Backends/Plex/Action/Import.php @@ -59,13 +59,13 @@ class Import handle: fn(array $logContext = []) => fn(iResponse $response) => $this->handle( context: $context, response: $response, - callback: fn(array $item, array $logContext = []) => $this->import( + callback: fn(array $item, array $logContext = []) => $this->process( context: $context, guid: $guid, mapper: $mapper, item: $item, logContext: $logContext, - opts: ['after' => $after], + opts: $opts + ['after' => $after], ), logContext: $logContext ), @@ -121,9 +121,7 @@ class Import if (empty($listDirs)) { $this->logger->warning('Request for [%(backend)] libraries returned with empty list.', [ 'backend' => $context->backendName, - 'context' => [ - 'body' => $json, - ] + 'body' => $json, ]); Data::add($context->backendName, 'no_import_update', true); return []; @@ -428,10 +426,14 @@ class Import protected function processShow(Context $context, iGuid $guid, array $item, array $logContext = []): void { - if (null === ($item['Guid'] ?? null)) { - $item['Guid'] = [['id' => $item['guid']]]; - } else { - $item['Guid'][] = ['id' => $item['guid']]; + $guids = []; + + if (null !== ($item['Guid'] ?? null)) { + $guids = $item['Guid']; + } + + if (null !== ($itemGuid = ag($item, 'guid')) && false === $guid->isLocal($itemGuid)) { + $guids[] = ['id' => $itemGuid]; } $year = (int)ag($item, ['grandParentYear', 'parentYear', 'year'], 0); @@ -454,22 +456,14 @@ class Import $this->logger->debug('Processing [%(backend)] %(item.type) [%(item.title) (%(item.year))].', [ 'backend' => $context->backendName, ...$logContext, - 'trace' => $item, + 'body' => $item, ]); } - if (!$guid->has($item['Guid'])) { - if (null === ($item['Guid'] ?? null)) { - $item['Guid'] = []; - } - - if (null !== ($item['guid'] ?? null) && false === $guid->isLocal($item['guid'])) { - $item['Guid'][] = ['id' => $item['guid']]; - } - + if (!$guid->has($guids)) { $message = 'Ignoring [%(backend)] [%(item.title)]. %(item.type) has no valid/supported external ids.'; - if (empty($item['Guid'] ?? [])) { + if (empty($guids)) { $message .= ' Most likely unmatched %(item.type).'; } @@ -487,19 +481,19 @@ class Import $gContext = ag_set( $logContext, 'item.plex_id', - str_starts_with(ag($item, 'guid', ''), 'plex://') ? ag($item, 'guid') : 'none' + str_starts_with($itemGuid ?? 'None', 'plex://') ? ag($item, 'guid') : 'none' ); $context->cache->set( PlexClient::TYPE_SHOW . '.' . ag($logContext, 'item.id'), Guid::fromArray( - payload: $guid->get($item['Guid'], context: [...$gContext]), - context: ['backend' => $context->backendName, ...$logContext,] + payload: $guid->get($guids, context: [...$gContext]), + context: ['backend' => $context->backendName, ...$logContext] )->getAll() ); } - private function import( + protected function process( Context $context, iGuid $guid, ImportInterface $mapper, @@ -547,7 +541,7 @@ class Import $this->logger->debug('Processing [%(backend)] %(item.type) [%(item.title)]', [ 'backend' => $context->backendName, ...$logContext, - 'payload' => $item, + 'body' => $item, ]); } @@ -556,9 +550,7 @@ class Import 'backend' => $context->backendName, 'date_key' => true === (bool)ag($item, 'viewCount', false) ? 'lastViewedAt' : 'addedAt', ...$logContext, - 'response' => [ - 'body' => $item, - ], + 'body' => $item, ]); Data::increment($context->backendName, $type . '_ignored_no_date_is_set'); @@ -615,9 +607,7 @@ class Import $this->logger->info($message, [ 'backend' => $context->backendName, ...$logContext, - 'context' => [ - 'guids' => !empty($item['Guid']) ? $item['Guid'] : 'None' - ], + 'guids' => !empty($item['Guid']) ? $item['Guid'] : 'None' ]); Data::increment($context->backendName, $type . '_ignored_no_supported_guid'); From 538a54a878a4b7c00bc257cf47840412229b652a Mon Sep 17 00:00:00 2001 From: "Abdulmhsen B. A. A" Date: Mon, 20 Jun 2022 19:04:44 +0300 Subject: [PATCH 2/3] As we moved logging to stderr it's make sense to output stderr before stdout. --- src/Commands/Scheduler/RunCommand.php | 4 ++-- src/Libs/Scheduler/Task.php | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Commands/Scheduler/RunCommand.php b/src/Commands/Scheduler/RunCommand.php index 51436c6e..8c5e9b2a 100644 --- a/src/Commands/Scheduler/RunCommand.php +++ b/src/Commands/Scheduler/RunCommand.php @@ -79,7 +79,7 @@ final class RunCommand extends Command if (0 === $count) { $this->write( - sprintf('[%s] No Tasks Scheduled to run at this time.', makeDate()), + sprintf('[%s] No Tasks Scheduled to run at this time.', makeDate()), $input, $output, OutputInterface::VERBOSITY_VERY_VERBOSE @@ -101,7 +101,7 @@ final class RunCommand extends Command $this->write('Command: ' . $task->getCommand() . ' ' . $task->getArgs(), $input, $output); $this->write('Date: ' . makeDate(), $input, $output); $this->write('--------------------------', $input, $output); - $this->write(sprintf('Task %s Output.', $task->getName()), $input, $output); + $this->write(sprintf('Task [%s] Output.', $task->getName()), $input, $output); $this->write('--------------------------', $input, $output); $this->write('', $input, $output); } diff --git a/src/Libs/Scheduler/Task.php b/src/Libs/Scheduler/Task.php index 62603410..3d3ec0da 100644 --- a/src/Libs/Scheduler/Task.php +++ b/src/Libs/Scheduler/Task.php @@ -316,15 +316,15 @@ final class Task $stdout = $this->process->getOutput(); $stderr = $this->process->getErrorOutput(); - if (!empty($stdout)) { - $this->output .= $stdout; + if (!empty($stderr)) { + $this->output .= $stderr; } - if (!empty($stderr)) { - if (!empty($this->output)) { + if (!empty($stdout)) { + if (!empty($stderr)) { $this->output .= PHP_EOL; } - $this->output .= $stderr; + $this->output .= $stdout; } $this->process = null; From 7d471af2e1ac9dceda299e5b3118617ddcfcc106 Mon Sep 17 00:00:00 2001 From: "Abdulmhsen B. A. A" Date: Mon, 20 Jun 2022 19:05:29 +0300 Subject: [PATCH 3/3] Do not update last sync date if backend reported error during import/export. --- src/Backends/Jellyfin/Action/Import.php | 10 +++++----- src/Backends/Plex/Action/Import.php | 10 +++++----- src/Commands/State/ExportCommand.php | 4 ++-- src/Commands/State/ImportCommand.php | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Backends/Jellyfin/Action/Import.php b/src/Backends/Jellyfin/Action/Import.php index 8485eb9e..940060ae 100644 --- a/src/Backends/Jellyfin/Action/Import.php +++ b/src/Backends/Jellyfin/Action/Import.php @@ -105,7 +105,7 @@ class Import 'status_code' => $response->getStatusCode(), ] ); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } @@ -122,7 +122,7 @@ class Import 'backend' => $context->backendName, 'body' => $json, ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } } catch (ExceptionInterface $e) { @@ -135,7 +135,7 @@ class Import 'message' => $e->getMessage(), ], ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } catch (JsonException $e) { $this->logger->error('Request for [%(backend)] libraries returned with invalid body.', [ @@ -145,7 +145,7 @@ class Import 'message' => $e->getMessage(), ], ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } @@ -309,7 +309,7 @@ class Import 'unsupported' => $unsupported, ], ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } diff --git a/src/Backends/Plex/Action/Import.php b/src/Backends/Plex/Action/Import.php index 8b788cbf..01f43cf1 100644 --- a/src/Backends/Plex/Action/Import.php +++ b/src/Backends/Plex/Action/Import.php @@ -106,7 +106,7 @@ class Import ] ); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } @@ -123,7 +123,7 @@ class Import 'backend' => $context->backendName, 'body' => $json, ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } } catch (ExceptionInterface $e) { @@ -136,7 +136,7 @@ class Import 'message' => $e->getMessage(), ], ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } catch (JsonException $e) { $this->logger->error('Request for [%(backend)] libraries returned with invalid body.', [ @@ -146,7 +146,7 @@ class Import 'message' => $e->getMessage(), ], ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } @@ -331,7 +331,7 @@ class Import ], ]); - Data::add($context->backendName, 'no_import_update', true); + Data::add($context->backendName, 'has_errors', true); return []; } diff --git a/src/Commands/State/ExportCommand.php b/src/Commands/State/ExportCommand.php index dca3f1ec..eb0698ce 100644 --- a/src/Commands/State/ExportCommand.php +++ b/src/Commands/State/ExportCommand.php @@ -329,7 +329,7 @@ class ExportCommand extends Command continue; } - if (true === (bool)Data::get(sprintf('%s.no_export_update', $name))) { + if (true === (bool)Data::get(sprintf('%s.has_errors', $name))) { $this->logger->notice( sprintf('%s: Not updating last export date. Backend reported an error.', $name) ); @@ -444,7 +444,7 @@ class ExportCommand extends Command array_push($requests, ...$backend['class']->export($this->mapper, $this->queue, $after)); if (false === $input->getOption('dry-run')) { - if (true === (bool)Data::get(sprintf('%s.no_export_update', $name))) { + if (true === (bool)Data::get(sprintf('%s.has_errors', $name))) { $this->logger->notice('Not updating last export date. [%(backend)] report an error.', [ 'backend' => $name, ]); diff --git a/src/Commands/State/ImportCommand.php b/src/Commands/State/ImportCommand.php index 5a6df87d..50547a2b 100644 --- a/src/Commands/State/ImportCommand.php +++ b/src/Commands/State/ImportCommand.php @@ -248,7 +248,7 @@ class ImportCommand extends Command $inDryMode = $this->mapper->inDryRunMode() || ag($server, 'options.' . Options::DRY_RUN); - if (false === Data::get(sprintf('%s.no_import_update', $name)) && false === $inDryMode) { + if (false === Data::get(sprintf('%s.has_errors', $name)) && false === $inDryMode) { Config::save(sprintf('servers.%s.import.lastSync', $name), time()); } }