diff --git a/src/API/Backends/Library/Mismatched.php b/src/API/Backends/Library/Mismatched.php new file mode 100644 index 00000000..3f102821 --- /dev/null +++ b/src/API/Backends/Library/Mismatched.php @@ -0,0 +1,88 @@ +getQueryParams()); + + $backendOpts = $opts = $list = []; + + if ($params->get('timeout')) { + $backendOpts = ag_set($backendOpts, 'client.timeout', (float)$params->get('timeout')); + } + + if ($params->get('raw')) { + $opts[Options::RAW_RESPONSE] = true; + } + + $percentage = (float)$params->get('percentage', MismatchCommand::DEFAULT_PERCENT); + $method = $params->get('method', MismatchCommand::METHODS[0]); + + if (false === in_array($method, MismatchCommand::METHODS, true)) { + return api_error('Invalid comparison method.', HTTP_STATUS::HTTP_BAD_REQUEST); + } + + try { + $client = $this->getClient(name: $name, config: $backendOpts); + } catch (RuntimeException $e) { + return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND); + } + + $ids = []; + if (null !== ($id = ag($args, 'id'))) { + $ids[] = $id; + } else { + foreach ($client->listLibraries() as $library) { + if (false === (bool)ag($library, 'supported') || true === (bool)ag($library, 'ignored')) { + continue; + } + $ids[] = ag($library, 'id'); + } + } + + foreach ($ids as $libraryId) { + foreach ($client->getLibrary(id: $libraryId, opts: $opts) as $item) { + $processed = MismatchCommand::compare(item: $item, method: $method); + + if (empty($processed) || $processed['percent'] >= $percentage) { + continue; + } + + $list[] = $processed; + } + } + + $response = [ + 'items' => $list, + 'links' => [ + 'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''), + 'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"), + ], + ]; + + return api_response(HTTP_STATUS::HTTP_OK, $response); + } +} diff --git a/src/API/Backends/Library/Unmatched.php b/src/API/Backends/Library/Unmatched.php new file mode 100644 index 00000000..a3ba8036 --- /dev/null +++ b/src/API/Backends/Library/Unmatched.php @@ -0,0 +1,76 @@ +getQueryParams()); + + $backendOpts = $opts = $list = []; + + if ($params->get('timeout')) { + $backendOpts = ag_set($backendOpts, 'client.timeout', (float)$params->get('timeout')); + } + + if ($params->get('raw')) { + $opts[Options::RAW_RESPONSE] = true; + } + + try { + $client = $this->getClient(name: $name, config: $backendOpts); + } catch (RuntimeException $e) { + return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND); + } + + $ids = []; + if (null !== ($id = ag($args, 'id'))) { + $ids[] = $id; + } else { + foreach ($client->listLibraries() as $library) { + if (false === (bool)ag($library, 'supported') || true === (bool)ag($library, 'ignored')) { + continue; + } + $ids[] = ag($library, 'id'); + } + } + + foreach ($ids as $libraryId) { + foreach ($client->getLibrary(id: $libraryId, opts: $opts) as $item) { + if (null === ($externals = ag($item, 'guids', null)) || empty($externals)) { + $list[] = $item; + } + } + } + + $response = [ + 'items' => $list, + 'links' => [ + 'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''), + 'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"), + ], + ]; + + return api_response(HTTP_STATUS::HTTP_OK, $response); + } +} diff --git a/src/Commands/Backend/Library/MismatchCommand.php b/src/Commands/Backend/Library/MismatchCommand.php index 4aceff1d..d9efce66 100644 --- a/src/Commands/Backend/Library/MismatchCommand.php +++ b/src/Commands/Backend/Library/MismatchCommand.php @@ -23,16 +23,16 @@ use Symfony\Component\Console\Output\OutputInterface; #[Cli(command: self::ROUTE)] final class MismatchCommand extends Command { - public const ROUTE = 'backend:library:mismatch'; + public const string ROUTE = 'backend:library:mismatch'; - protected array $methods = [ + public const array METHODS = [ 'similarity', 'levenshtein', ]; - private const DEFAULT_PERCENT = 50.0; + public const float DEFAULT_PERCENT = 50.0; - private const REMOVED_CHARS = [ + public const array REMOVED_CHARS = [ '?', ':', '(', @@ -56,7 +56,7 @@ final class MismatchCommand extends Command '*', ]; - private const CUTOFF = 30; + private const int CUTOFF = 30; /** * Configures the command. @@ -71,8 +71,8 @@ final class MismatchCommand extends Command 'method', 'm', InputOption::VALUE_OPTIONAL, - r('Comparison method. Can be [{list}].', ['list' => implode(', ', $this->methods)]), - $this->methods[0] + r('Comparison method. Can be [{list}].', ['list' => implode(', ', self::METHODS)]), + self::METHODS[0] ) ->addOption( 'timeout', @@ -130,9 +130,9 @@ final class MismatchCommand extends Command 'route' => self::ROUTE, 'methodsList' => implode( ', ', - array_map(fn($val) => '' . $val . '', $this->methods) + array_map(fn($val) => '' . $val . '', self::METHODS) ), - 'DefaultMethod' => $this->methods[0], + 'DefaultMethod' => self::METHODS[0], 'defaultPercent' => self::DEFAULT_PERCENT, 'removedList' => implode( ', ', @@ -264,7 +264,7 @@ final class MismatchCommand extends Command * @param string $method The method to use for comparison (similarity or levenshtein). * @return array The updated array item with comparison results. */ - private function compare(array $item, string $method): array + public static function compare(array $item, string $method): array { if (empty($item)) { return []; @@ -297,8 +297,8 @@ final class MismatchCommand extends Command foreach ($titles as $title) { $isASCII = mb_detect_encoding($pathShort, 'ASCII') && mb_detect_encoding($title, 'ASCII'); - $title = $toLower($this->formatName(name: $title), isASCII: $isASCII); - $pathShort = $toLower($this->formatName(name: $pathShort), isASCII: $isASCII); + $title = $toLower(self::formatName(name: $title), isASCII: $isASCII); + $pathShort = $toLower(self::formatName(name: $pathShort), isASCII: $isASCII); if (1 === preg_match('/\((\d{4})\)/', basename($pathFull), $match)) { $withYear = true; @@ -317,11 +317,11 @@ final class MismatchCommand extends Command similar_text($pathShort, $title, $similarity); $levenshtein = levenshtein($pathShort, $title); } else { - $this->mb_similar_text($pathShort, $title, $similarity); - $levenshtein = $this->mb_levenshtein($pathShort, $title); + self::mb_similar_text($pathShort, $title, $similarity); + $levenshtein = self::mb_levenshtein($pathShort, $title); } - $levenshtein = $this->toPercentage($levenshtein, $pathShort, $title); + $levenshtein = self::toPercentage($levenshtein, $pathShort, $title); switch ($method) { default: @@ -399,7 +399,7 @@ final class MismatchCommand extends Command * * @return string The formatted name. */ - private function formatName(string $name): string + public static function formatName(string $name): string { $name = preg_replace('#[\[{].+?[]}]#', '', $name); return trim(preg_replace('/\s+/', ' ', str_replace(self::REMOVED_CHARS, ' ', $name))); @@ -419,7 +419,7 @@ final class MismatchCommand extends Command * * @return int */ - private function mb_similar_text(string $str1, string $str2, float|null &$percent = null): int + public static function mb_similar_text(string $str1, string $str2, float|null &$percent = null): int { if (0 === mb_strlen($str1) + mb_strlen($str2)) { $percent = 0.0; @@ -455,10 +455,10 @@ final class MismatchCommand extends Command $similarity = $max; if ($similarity) { if ($pos1 && $pos2) { - $similarity += $this->mb_similar_text(mb_substr($str1, 0, $pos1), mb_substr($str2, 0, $pos2)); + $similarity += self::mb_similar_text(mb_substr($str1, 0, $pos1), mb_substr($str2, 0, $pos2)); } if (($pos1 + $max < $l1) && ($pos2 + $max < $l2)) { - $similarity += $this->mb_similar_text( + $similarity += self::mb_similar_text( mb_substr($str1, $pos1 + $max, $l1 - $pos1 - $max), mb_substr($str2, $pos2 + $max, $l2 - $pos2 - $max) ); @@ -478,13 +478,13 @@ final class MismatchCommand extends Command * * @return int The Levenshtein distance between the two strings. */ - private function mb_levenshtein(string $str1, string $str2): int + public static function mb_levenshtein(string $str1, string $str2): int { $length1 = mb_strlen($str1, 'UTF-8'); $length2 = mb_strlen($str2, 'UTF-8'); if ($length1 < $length2) { - return $this->mb_levenshtein($str2, $str1); + return self::mb_levenshtein($str2, $str1); } if (0 === $length1) { @@ -525,7 +525,7 @@ final class MismatchCommand extends Command * * @return float The percentage value calculated based on the base value and the lengths of the strings. */ - private function toPercentage(int $base, string $str1, string $str2, bool $isASCII = false): float + public static function toPercentage(int $base, string $str1, string $str2, bool $isASCII = false): float { $length = fn(string $text) => $isASCII ? mb_strlen($text, 'UTF-8') : strlen($text); diff --git a/src/Commands/Backend/Library/UnmatchedCommand.php b/src/Commands/Backend/Library/UnmatchedCommand.php index 921ca0d5..5e6bfb86 100644 --- a/src/Commands/Backend/Library/UnmatchedCommand.php +++ b/src/Commands/Backend/Library/UnmatchedCommand.php @@ -21,9 +21,9 @@ use Symfony\Component\Console\Output\OutputInterface; #[Cli(command: self::ROUTE)] final class UnmatchedCommand extends Command { - public const ROUTE = 'backend:library:unmatched'; + public const string ROUTE = 'backend:library:unmatched'; - private const CUTOFF = 30; + private const int CUTOFF = 30; /** * Configures the command. @@ -99,11 +99,11 @@ final class UnmatchedCommand extends Command $backendOpts = $opts = $list = []; if ($input->getOption('timeout')) { - $backendOpts = ag_set($opts, 'client.timeout', (float)$input->getOption('timeout')); + $backendOpts = ag_set($backendOpts, 'client.timeout', (float)$input->getOption('timeout')); } if ($input->getOption('trace')) { - $backendOpts = ag_set($opts, 'options.' . Options::DEBUG_TRACE, true); + $backendOpts = ag_set($backendOpts, 'options.' . Options::DEBUG_TRACE, true); } if ($input->getOption('include-raw-response')) {