diff --git a/src/Commands/State/CacheCommand.php b/src/Commands/State/CacheCommand.php index d535368a..188dd550 100644 --- a/src/Commands/State/CacheCommand.php +++ b/src/Commands/State/CacheCommand.php @@ -14,8 +14,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Yaml; -use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\ResponseInterface; +use Throwable; class CacheCommand extends Command { @@ -173,12 +173,8 @@ class CacheCommand extends Command foreach ($queue as $_key => $response) { $requestData = $response->getInfo('user_data'); try { - if (200 === $response->getStatusCode()) { - $requestData['ok']($response); - } else { - $requestData['error']($response); - } - } catch (ExceptionInterface $e) { + $requestData['ok']($response); + } catch (Throwable $e) { $requestData['error']($e); } diff --git a/src/Commands/State/ExportCommand.php b/src/Commands/State/ExportCommand.php index ebe26bfc..b71db3b2 100644 --- a/src/Commands/State/ExportCommand.php +++ b/src/Commands/State/ExportCommand.php @@ -9,7 +9,7 @@ use App\Libs\Config; use App\Libs\Data; use App\Libs\Extends\CliLogger; use App\Libs\Mappers\ExportInterface; -use App\Libs\Servers\ServerInterface; +use App\Libs\Options; use App\Libs\Storage\PDO\PDOAdapter; use App\Libs\Storage\StorageInterface; use Psr\Log\LoggerInterface; @@ -20,6 +20,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\HttpClient\Exception\ServerException; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; +use Throwable; class ExportCommand extends Command { @@ -174,7 +175,7 @@ class ExportCommand extends Command $opts = ag($server, 'options', []); if ($input->getOption('ignore-date')) { - $opts[ServerInterface::OPT_EXPORT_IGNORE_DATE] = true; + $opts[Options::IGNORE_DATE] = true; } if ($input->getOption('proxy')) { @@ -227,12 +228,8 @@ class ExportCommand extends Command foreach ($requests as $response) { $requestData = $response->getInfo('user_data'); try { - if (200 === $response->getStatusCode()) { - $requestData['ok']($response); - } else { - $requestData['error']($response); - } - } catch (ExceptionInterface $e) { + $requestData['ok']($response); + } catch (Throwable $e) { $requestData['error']($e); } } diff --git a/src/Commands/State/ImportCommand.php b/src/Commands/State/ImportCommand.php index 444f1a5e..560c19c1 100644 --- a/src/Commands/State/ImportCommand.php +++ b/src/Commands/State/ImportCommand.php @@ -10,6 +10,7 @@ use App\Libs\Data; use App\Libs\Entity\StateInterface; use App\Libs\Extends\CliLogger; use App\Libs\Mappers\ImportInterface; +use App\Libs\Options; use App\Libs\Storage\PDO\PDOAdapter; use App\Libs\Storage\StorageInterface; use Psr\Log\LoggerInterface; @@ -20,8 +21,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Yaml; -use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\ResponseInterface; +use Throwable; class ImportCommand extends Command { @@ -70,20 +71,12 @@ class ImportCommand extends Command 'Sync selected servers, comma seperated. \'s1,s2\'.', '' ) - ->addOption('stats-show', null, InputOption::VALUE_NONE, 'Show final status.') - ->addOption( - 'stats-filter', - null, - InputOption::VALUE_OPTIONAL, - 'Filter final status output e.g. (servername.key)', - null - ) ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not commit any changes.') ->addOption( 'deep-debug', null, InputOption::VALUE_NONE, - 'You should not use this flag unless told by the team.' + 'You should not use this flag unless told by the team it will inflate your log output.' ) ->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.'); } @@ -129,11 +122,11 @@ class ImportCommand extends Command if ($input->getOption('dry-run')) { $output->writeln('Dry run mode. No changes will be committed to backend.'); - $mapperOpts[ImportInterface::DRY_RUN] = true; + $mapperOpts[Options::DRY_RUN] = true; } if ($input->getOption('deep-debug')) { - $mapperOpts[ImportInterface::DEEP_DEBUG] = true; + $mapperOpts[Options::DEEP_DEBUG] = true; } if (!empty($mapperOpts)) { @@ -207,6 +200,10 @@ class ImportCommand extends Command $opts['client']['proxy'] = $input->getOption('proxy'); } + if ($input->getOption('deep-debug')) { + $opts[Options::DEEP_DEBUG] = true; + } + if ($input->getOption('no-proxy')) { $opts['client']['no_proxy'] = $input->getOption('no-proxy'); } @@ -222,25 +219,27 @@ class ImportCommand extends Command $server['class'] = $server['class']->setLogger($logger); } - $after = true === $input->getOption('force-full') ? null : ag($server, 'import.lastSync', null); + $after = ag($server, 'import.lastSync', null); + + if (true === (bool)ag($opts, Options::FORCE_FULL, false) || true === $input->getOption('force-full')) { + $after = null; + } if (null === $after) { $this->logger->notice( - sprintf('Importing \'%s\' play state changes since beginning.', $name) + sprintf('%s: Importing play state changes since beginning.', $name) ); } else { $after = makeDate($after); $this->logger->notice( - sprintf('Importing \'%s\' play state changes since \'%s\'.', $name, $after) + sprintf('%s: Importing play state changes since \'%s\'.', $name, $after) ); } array_push($queue, ...$server['class']->pull($this->mapper, $after)); if (true === Data::get(sprintf('%s.no_import_update', $name))) { - $this->logger->notice( - sprintf('Not updating \'%s\' last sync time as the server reported an error.', $name) - ); + $this->logger->notice(sprintf('%s: Not updating last sync date backend reported an error.', $name)); } else { Config::save(sprintf('servers.%s.import.lastSync', $name), time()); } @@ -248,17 +247,14 @@ class ImportCommand extends Command unset($server); - $this->logger->notice(sprintf('Waiting on (%d) HTTP Requests.', count($queue))); + $this->logger->notice(sprintf('HTTP: Waiting on \'%d\' external requests.', count($queue))); foreach ($queue as $_key => $response) { $requestData = $response->getInfo('user_data'); + try { - if (200 === $response->getStatusCode()) { - $requestData['ok']($response); - } else { - $requestData['error']($response); - } - } catch (ExceptionInterface $e) { + $requestData['ok']($response); + } catch (Throwable $e) { $requestData['error']($e); } @@ -268,50 +264,40 @@ class ImportCommand extends Command } unset($queue); - $this->logger->notice('Finished waiting HTTP Requests.'); + $this->logger->notice('HTTP: Finished waiting external requests.'); $total = count($this->mapper); if ($total >= 1) { - $this->logger->notice(sprintf('Committing (%d) Changes.', $total)); + $this->logger->notice(sprintf('MAPPER: Committing \'%d\' recorded changes.', $total)); } $operations = $this->mapper->commit(); - if ($input->getOption('stats-show')) { - Data::add('operations', 'stats', $operations); - $output->writeln( - Yaml::dump(Data::get($input->getOption('stats-filter'), []), 8, 2) - ); - } else { - $a = [ - [ - 'Type' => ucfirst(StateInterface::TYPE_MOVIE), - 'Added' => $operations[StateInterface::TYPE_MOVIE]['added'] ?? 'None', - 'Updated' => $operations[StateInterface::TYPE_MOVIE]['updated'] ?? 'None', - 'Failed' => $operations[StateInterface::TYPE_MOVIE]['failed'] ?? 'None', - ], - new TableSeparator(), - [ - 'Type' => ucfirst(StateInterface::TYPE_EPISODE), - 'Added' => $operations[StateInterface::TYPE_EPISODE]['added'] ?? 'None', - 'Updated' => $operations[StateInterface::TYPE_EPISODE]['updated'] ?? 'None', - 'Failed' => $operations[StateInterface::TYPE_EPISODE]['failed'] ?? 'None', - ], - ]; + $a = [ + [ + 'Type' => ucfirst(StateInterface::TYPE_MOVIE), + 'Added' => $operations[StateInterface::TYPE_MOVIE]['added'] ?? '-', + 'Updated' => $operations[StateInterface::TYPE_MOVIE]['updated'] ?? '-', + 'Failed' => $operations[StateInterface::TYPE_MOVIE]['failed'] ?? '-', + ], + new TableSeparator(), + [ + 'Type' => ucfirst(StateInterface::TYPE_EPISODE), + 'Added' => $operations[StateInterface::TYPE_EPISODE]['added'] ?? '-', + 'Updated' => $operations[StateInterface::TYPE_EPISODE]['updated'] ?? '-', + 'Failed' => $operations[StateInterface::TYPE_EPISODE]['failed'] ?? '-', + ], + ]; - (new Table($output))->setHeaders(array_keys($a[0]))->setStyle('box')->setRows(array_values($a))->render(); - } + (new Table($output))->setHeaders(array_keys($a[0]))->setStyle('box')->setRows(array_values($a))->render(); foreach ($list as $server) { if (null === ($name = ag($server, 'name'))) { continue; } - Config::save( - sprintf('servers.%s.persist', $name), - $server['class']->getPersist() - ); + Config::save(sprintf('servers.%s.persist', $name), $server['class']->getPersist()); } if (false === $custom && is_writable(dirname($config))) { diff --git a/src/Commands/State/PushCommand.php b/src/Commands/State/PushCommand.php index 34dd3603..8b1e1e1f 100644 --- a/src/Commands/State/PushCommand.php +++ b/src/Commands/State/PushCommand.php @@ -10,7 +10,7 @@ use App\Libs\Container; use App\Libs\Data; use App\Libs\Entity\StateInterface; use App\Libs\Extends\CliLogger; -use App\Libs\Servers\ServerInterface; +use App\Libs\Options; use Psr\Log\LoggerInterface; use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\InvalidArgumentException; @@ -182,7 +182,7 @@ class PushCommand extends Command $opts = ag($server, 'server.options', []); if ($input->getOption('ignore-date')) { - $opts[ServerInterface::OPT_EXPORT_IGNORE_DATE] = true; + $opts[Options::IGNORE_DATE] = true; } if ($input->getOption('proxy')) { diff --git a/src/Libs/Mappers/Import/MemoryMapper.php b/src/Libs/Mappers/Import/MemoryMapper.php index c1419587..decc32a4 100644 --- a/src/Libs/Mappers/Import/MemoryMapper.php +++ b/src/Libs/Mappers/Import/MemoryMapper.php @@ -7,6 +7,7 @@ namespace App\Libs\Mappers\Import; use App\Libs\Data; use App\Libs\Entity\StateInterface; use App\Libs\Mappers\ImportInterface; +use App\Libs\Options; use App\Libs\Storage\StorageInterface; use DateTimeInterface; use PDOException; @@ -76,7 +77,7 @@ final class MemoryMapper implements ImportInterface Data::increment($bucket, $entity->type . '_added'); $this->addPointers($this->objects[$pointer], $pointer); - if (true === ($this->options[ImportInterface::DEEP_DEBUG] ?? false)) { + if ($this->inDeepDebugMode()) { $data = $entity->getAll(); unset($data['id']); $data['updated'] = makeDate($data['updated']); @@ -110,7 +111,7 @@ final class MemoryMapper implements ImportInterface $this->removePointers($cloned); $this->addPointers($this->objects[$pointer], $pointer); $this->logger->info( - sprintf('Updating %s. State changed.', $name), + sprintf('%s: Updating \'%s\'. State changed.', $entity->via, $entity->getName()), $this->objects[$pointer]->diff(all: true), ); return $this; @@ -163,17 +164,19 @@ final class MemoryMapper implements ImportInterface 0 === $count ? 'No changes detected.' : sprintf('Updating backend with \'%d\' changes.', $count) ); + $inDryRunMode = $this->inDryRunMode(); + foreach ($this->changed as $pointer) { try { $entity = &$this->objects[$pointer]; if (null === $entity->id) { - if (false === (bool)($this->options[ImportInterface::DRY_RUN] ?? false)) { + if (false === $inDryRunMode) { $storage->insert($entity); } $list[$entity->type]['added']++; } else { - if (false === (bool)($this->options[ImportInterface::DRY_RUN] ?? false)) { + if (false === $inDryRunMode) { $storage->update($entity); } $list[$entity->type]['updated']++; @@ -242,12 +245,12 @@ final class MemoryMapper implements ImportInterface public function inDryRunMode(): bool { - return true === ($this->options[ImportInterface::DRY_RUN] ?? false); + return true === (bool)ag($this->options, Options::DRY_RUN, false); } public function inDeepDebugMode(): bool { - return true === ($this->options[ImportInterface::DEEP_DEBUG] ?? false); + return true === (bool)ag($this->options, Options::DEEP_DEBUG, false); } /** diff --git a/src/Libs/Options.php b/src/Libs/Options.php new file mode 100644 index 00000000..4048c14e --- /dev/null +++ b/src/Libs/Options.php @@ -0,0 +1,17 @@ +setIsTainted($isTainted); if (!$entity->hasGuids() && !$entity->hasRelativeGuid()) { - $message = sprintf('%s: No valid/supported External ids.', self::NAME); + $message = sprintf('%s: No valid/supported external ids.', self::NAME); if (empty($providersId)) { - $message .= ' Most likely unmatched movie/episode or show.'; + $message .= sprintf(' Most likely unmatched %s.', $entity->type); } $message .= sprintf(' [%s].', arrayToString(['guids' => !empty($providersId) ? $providersId : 'None'])); @@ -212,9 +212,9 @@ class EmbyServer extends JellyfinServer } $json = json_decode( - json: $response->getContent(), + json: $response->getContent(), associative: true, - flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE + flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE ); if (null === ($itemType = ag($json, 'Type')) || 'Series' !== $itemType) { diff --git a/src/Libs/Servers/JellyfinServer.php b/src/Libs/Servers/JellyfinServer.php index 816e2ed4..34484969 100644 --- a/src/Libs/Servers/JellyfinServer.php +++ b/src/Libs/Servers/JellyfinServer.php @@ -13,6 +13,7 @@ use App\Libs\Guid; use App\Libs\HttpException; use App\Libs\Mappers\ExportInterface; use App\Libs\Mappers\ImportInterface; +use App\Libs\Options; use Closure; use DateInterval; use DateTimeInterface; @@ -352,7 +353,7 @@ class JellyfinServer implements ServerInterface $message = sprintf('%s: No valid/supported External ids.', self::NAME); if (empty($providersId)) { - $message .= ' Most likely unmatched movie/episode or show.'; + $message .= sprintf(' Most likely unmatched %s.', $entity->type); } $message .= sprintf(' [%s].', arrayToString(['guids' => !empty($providersId) ? $providersId : 'None'])); @@ -617,7 +618,7 @@ class JellyfinServer implements ServerInterface continue; } - if (false === (ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false))) { + if (false === (ag($this->options, Options::IGNORE_DATE, false))) { if (null !== $after && $after->getTimestamp() > $entity->updated) { continue; } @@ -719,7 +720,7 @@ class JellyfinServer implements ServerInterface continue; } - if (false === (ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false))) { + if (false === (ag($this->options, Options::IGNORE_DATE, false))) { $date = ag($json, ['UserData.LastPlayedDate', 'DateCreated', 'PremiereDate'], null); if (null === $date) { @@ -1296,10 +1297,23 @@ class JellyfinServer implements ServerInterface $message = sprintf('%s: Ignoring \'%s\'. No valid/supported external ids.', $this->name, $iName); if (empty($providerIds)) { - $message .= ' Most likely unmatched item.'; + $message .= sprintf(' Most likely unmatched %s.', $entity->type); } - $this->logger->info($message, ['guids' => !empty($providerIds) ? $providerIds : 'None']); + $kvStore = [ + 'guids' => !empty($providerIds) ? $providerIds : 'None' + ]; + + if (true === (bool)ag($this->options, Options::DEEP_DEBUG, false)) { + $kvStore['entity'] = $entity->getAll(); + $kvStore['payload'] = json_decode( + json: json_encode($item), + associative: true, + flags: JSON_INVALID_UTF8_IGNORE + ); + } + + $this->logger->info($message, $kvStore); Data::increment($this->name, $type . '_ignored_no_supported_guid'); @@ -1390,7 +1404,7 @@ class JellyfinServer implements ServerInterface $message = sprintf('%s: Ignoring \'%s\'. No valid/supported external ids.', $this->name, $iName); if (empty($providerIds)) { - $message .= ' Most likely unmatched item.'; + $message .= sprintf(' Most likely unmatched %s.', $rItem->type); } $this->logger->debug($message, ['guids' => !empty($providerIds) ? $providerIds : 'None']); @@ -1398,7 +1412,7 @@ class JellyfinServer implements ServerInterface return; } - if (false === ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false)) { + if (false === ag($this->options, Options::IGNORE_DATE, false)) { if (null !== $after && $rItem->updated >= $after->getTimestamp()) { $this->logger->debug( sprintf( @@ -1438,7 +1452,7 @@ class JellyfinServer implements ServerInterface return; } - if (false === ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false)) { + if (false === ag($this->options, Options::IGNORE_DATE, false)) { if ($rItem->updated >= $entity->updated) { $this->logger->debug( sprintf('%s: Ignoring \'%s\'. Date is newer or equal to backend entity.', $this->name, $iName), diff --git a/src/Libs/Servers/PlexServer.php b/src/Libs/Servers/PlexServer.php index befc4204..94b1740a 100644 --- a/src/Libs/Servers/PlexServer.php +++ b/src/Libs/Servers/PlexServer.php @@ -13,6 +13,7 @@ use App\Libs\Guid; use App\Libs\HttpException; use App\Libs\Mappers\ExportInterface; use App\Libs\Mappers\ImportInterface; +use App\Libs\Options; use Closure; use DateInterval; use DateTimeInterface; @@ -383,7 +384,7 @@ class PlexServer implements ServerInterface $message = sprintf('%s: No valid/supported external ids.', self::NAME); if (empty($item['Guid'])) { - $message .= ' Most likely unmatched movie/episode or show.'; + $message .= sprintf(' Most likely unmatched %s.', $entity->type); } $message .= sprintf(' [%s].', arrayToString(['guids' => ag($item, 'Guid', 'None')])); @@ -651,7 +652,7 @@ class PlexServer implements ServerInterface continue; } - if (false === (ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false))) { + if (false === (ag($this->options, Options::IGNORE_DATE, false))) { if (null !== $after && $after->getTimestamp() > $entity->updated) { continue; } @@ -747,7 +748,7 @@ class PlexServer implements ServerInterface continue; } - if (false === (ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false))) { + if (false === (ag($this->options, Options::IGNORE_DATE, false))) { $date = max( (int)ag($json, 'updatedAt', 0), (int)ag($json, 'lastViewedAt', 0), @@ -965,7 +966,7 @@ class PlexServer implements ServerInterface $this->processCache($entity, $type, $cName); } - } catch (PathNotFoundException $e) { + } catch (PathNotFoundException $e) { $this->logger->error( sprintf( '%s: Failed to find items in \'%s\' response. %s', @@ -1297,12 +1298,6 @@ class PlexServer implements ServerInterface $entity = $this->createEntity($item, $type); if (!$entity->hasGuids() && !$entity->hasRelativeGuid()) { - if (null === ($item->Guid ?? null)) { - $item->Guid = [['id' => $item->guid]]; - } else { - $item->Guid[] = ['id' => $item->guid]; - } - if (true === Config::get('debug.import')) { $name = Config::get('tmpDir') . '/debug/' . $this->name . '.' . $item->ratingKey . '.json'; @@ -1320,7 +1315,13 @@ class PlexServer implements ServerInterface $message = sprintf('%s: Ignoring \'%s\'. No valid/supported external ids.', $this->name, $iName); if (empty($item->Guid)) { - $message .= ' Most likely unmatched item.'; + $message .= sprintf(' Most likely unmatched %s.', $entity->type); + } + + if (null === ($item->Guid ?? null)) { + $item->Guid = [['id' => $item->guid]]; + } else { + $item->Guid[] = ['id' => $item->guid]; } $this->logger->info($message, ['guids' => !empty($item->Guid) ? $item->Guid : 'None']); @@ -1408,25 +1409,25 @@ class PlexServer implements ServerInterface $rItem = $this->createEntity($item, $type); if (!$rItem->hasGuids() && !$rItem->hasRelativeGuid()) { + $message = sprintf('%s: Ignoring \'%s\'. No valid/supported external ids.', $this->name, $iName); + + if (empty($item->Guid)) { + $message .= sprintf(' Most likely unmatched %s.', $rItem->type); + } + if (null === ($item->Guid ?? null)) { $item->Guid = [['id' => $item->guid]]; } else { $item->Guid[] = ['id' => $item->guid]; } - $message = sprintf('%s: Ignoring \'%s\'. No valid/supported external ids.', $this->name, $iName); - - if (empty($item->Guid)) { - $message .= ' Most likely unmatched item.'; - } - $this->logger->debug($message, ['guids' => !empty($item->Guid) ? $item->Guid : 'None']); Data::increment($this->name, $type . '_ignored_no_supported_guid'); return; } - if (false === ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false)) { + if (false === ag($this->options, Options::IGNORE_DATE, false)) { if (null !== $after && $rItem->updated >= $after->getTimestamp()) { $this->logger->debug( sprintf( @@ -1466,7 +1467,7 @@ class PlexServer implements ServerInterface return; } - if (false === ag($this->options, ServerInterface::OPT_EXPORT_IGNORE_DATE, false)) { + if (false === ag($this->options, Options::IGNORE_DATE, false)) { if ($rItem->updated >= $entity->updated) { $this->logger->debug( sprintf('%s: Ignoring \'%s\'. Date is newer or equal to backend entity.', $this->name, $iName), diff --git a/src/Libs/Servers/ServerInterface.php b/src/Libs/Servers/ServerInterface.php index 007926ac..d4a9d91f 100644 --- a/src/Libs/Servers/ServerInterface.php +++ b/src/Libs/Servers/ServerInterface.php @@ -17,8 +17,6 @@ use Symfony\Contracts\HttpClient\ResponseInterface; interface ServerInterface { - public const OPT_EXPORT_IGNORE_DATE = 'exportIgnoreDate'; - /** * Initiate server. It should return **NEW OBJECT** *