improved mismatch handling.

This commit is contained in:
abdulmohsen
2022-05-31 00:35:45 +03:00
parent fae191d97b
commit ffb7b2497e
4 changed files with 640 additions and 459 deletions

View File

@@ -245,19 +245,31 @@ final class RemoteCommand extends Command
try {
$result = $server->searchMismatch(id: $id, opts: ['coef' => $percentage]);
} catch (Throwable $e) {
$this->setOutputContent(['error' => $e->getMessage()], $output, $mode);
$this->setOutputContent(
[
[
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
],
],
$output,
$mode
);
return self::FAILURE;
}
if (empty($result)) {
$this->setOutputContent(
[
'info' => sprintf(
'We are %1$02.2f%3$s sure there are no mis-identified items in library \'%2$s\'.',
$percentage,
$id,
'%',
)
[
'info' => sprintf(
'We are %1$02.2f%3$s sure there are no mis-identified items in library \'%2$s\'.',
$percentage,
$id,
'%',
)
]
],
$output,
$mode

View File

@@ -508,256 +508,290 @@ class JellyfinServer implements ServerInterface
}
}
/**
* @throws Throwable
*/
public function searchMismatch(string|int $id, array $opts = []): array
{
$list = [];
$this->checkConfig();
try {
// -- Get Content type.
$url = $this->url->withPath(sprintf('/Users/%s/items/', $this->user))->withQuery(
http_build_query(
[
'Recursive' => 'false',
'enableUserData' => 'false',
'enableImages' => 'false',
]
)
);
$this->logger->debug(sprintf('%s: Requesting list of backend libraries.', $this->name), [
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
}
$json = json_decode(
json: $response->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
$type = null;
$found = false;
foreach (ag($json, 'Items', []) as $section) {
if ((string)ag($section, 'Id') !== (string)$id) {
continue;
}
$found = true;
$type = ag($section, 'CollectionType', 'unknown');
break;
}
if (false === $found) {
throw new RuntimeException(sprintf('%s: library id \'%s\' not found.', $this->name, $id));
}
if ('movies' !== $type && 'tvshows' !== $type) {
throw new RuntimeException(sprintf('%s: Library id \'%s\' is of unsupported type.', $this->name, $id));
}
$type = $type === 'movies' ? iFace::TYPE_MOVIE : 'Show';
$url = $this->url->withPath(sprintf('/Users/%s/items/', $this->user))->withQuery(
http_build_query(
[
'parentId' => $id,
'enableUserData' => 'false',
'enableImages' => 'false',
'ExcludeLocationTypes' => 'Virtual',
'include' => 'Series,Movie',
]
)
);
$this->logger->debug(sprintf('%s: Sending get library content for id \'%s\'.', $this->name, $id), [
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
}
$handleRequest = function (string $type, array $item) use (&$list, $opts) {
$this->logger->debug(sprintf('%s: Processing %s \'%s\'.', $this->name, $type, ag($item, 'Name')));
$url = $this->url->withPath(sprintf('/Users/%s/items/%s', $this->user, ag($item, 'Id')));
$possibleTitles = $paths = $locations = $guids = $matches = [];
$possibleTitlesList = ['Name', 'OriginalTitle', 'SortName', 'ForcedSortName'];
foreach ($possibleTitlesList as $title) {
if (null === ($title = ag($item, $title))) {
continue;
}
$title = formatName($title);
if (true === in_array($type, $possibleTitles)) {
continue;
}
$possibleTitles[] = formatName($title);
}
if (null !== ($path = ag($item, 'Path'))) {
if (ag($item, 'Type') === 'Movie') {
$paths[] = formatName(basename(dirname($path)));
$locations[] = dirname($path);
} else {
$paths[] = formatName(basename($path));
$locations[] = $path;
}
}
if (null !== ($providerIds = ag($item, 'ProviderIds'))) {
$guids = $providerIds;
}
foreach ($paths ?? [] as $location) {
foreach ($possibleTitles as $title) {
if (true === str_contains($location, $title)) {
return;
}
$locationIsAscii = mb_detect_encoding($location, 'ASCII', true);
$titleIsAscii = mb_detect_encoding($location, 'ASCII', true);
if (true === $locationIsAscii && $titleIsAscii) {
similar_text($location, $title, $percent);
} else {
mb_similar_text($location, $title, $percent);
}
if ($percent >= $opts['coef']) {
return;
}
$matches[] = [
'path' => $location,
'title' => $title,
'similarity' => $percent,
];
}
}
$metadata = [
'id' => ag($item, 'Id'),
'type' => ucfirst($type),
'url' => [(string)$url],
'title' => ag($item, $possibleTitlesList, '??'),
'year' => ag($item, 'ProductionYear', 0000),
'guids' => $guids,
'path' => $locations,
'matching' => $matches,
'comments' => (empty($paths)) ? 'No path found.' : 'Title does not match path.',
];
if (empty($guids)) {
$metadata['guids'] = 'None.';
}
if (count($locations) <= 1) {
$metadata['path'] = $locations[0];
}
$list[] = $metadata;
};
$it = Items::fromIterable(
httpClientChunks($this->http->stream($response)),
// -- Get Content type.
$url = $this->url->withPath(sprintf('/Users/%s/items/', $this->user))->withQuery(
http_build_query(
[
'pointer' => '/Items',
'decoder' => new ErrorWrappingDecoder(
new ExtJsonDecoder(assoc: true, options: JSON_INVALID_UTF8_IGNORE)
)
'Recursive' => 'false',
'enableUserData' => 'false',
'enableImages' => 'false',
]
)
);
$this->logger->debug(sprintf('%s: Requesting list of backend libraries.', $this->name), [
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
}
$requests = [];
$json = json_decode(
json: $response->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
foreach ($it as $entity) {
if ($entity instanceof DecodingError) {
$this->logger->warning(
sprintf(
'%s: Failed to decode one of library id \'%s\' items. %s',
$this->name,
$id,
$entity->getErrorMessage()
),
[
'payload' => $entity->getMalformedJson(),
]
);
$type = null;
$found = false;
foreach (ag($json, 'Items', []) as $section) {
if ((string)ag($section, 'Id') !== (string)$id) {
continue;
}
$found = true;
$type = ag($section, 'CollectionType', 'unknown');
break;
}
if (false === $found) {
throw new RuntimeException(sprintf('%s: library id \'%s\' not found.', $this->name, $id));
}
if ('movies' !== $type && 'tvshows' !== $type) {
throw new RuntimeException(sprintf('%s: Library id \'%s\' is of unsupported type.', $this->name, $id));
}
$type = $type === 'movies' ? iFace::TYPE_MOVIE : 'Show';
$url = $this->url->withPath(sprintf('/Users/%s/items/', $this->user))->withQuery(
http_build_query(
[
'parentId' => $id,
'enableUserData' => 'false',
'enableImages' => 'false',
'ExcludeLocationTypes' => 'Virtual',
'include' => 'Series,Movie',
]
)
);
$this->logger->debug(sprintf('%s: Sending get library content for id \'%s\'.', $this->name, $id), [
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
}
$handleRequest = function (string $type, array $item) use (&$list, $opts) {
$this->logger->debug(sprintf('%s: Processing %s \'%s\'.', $this->name, $type, ag($item, 'Name')));
$url = $this->url->withPath(sprintf('/Users/%s/items/%s', $this->user, ag($item, 'Id')));
$year = ag($item, 'ProductionYear', null);
$parseYear = '/\((\d{4})\)/';
$possibleTitles = $locations = $guids = $matches = [];
$possibleTitlesList = ['Name', 'OriginalTitle', 'SortName', 'ForcedSortName'];
foreach ($possibleTitlesList as $title) {
if (null === ($title = ag($item, $title))) {
continue;
}
$url = $this->url->withPath(sprintf('/Users/%s/items/%s', $this->user, ag($entity, 'Id')));
$title = formatName($title);
$this->logger->debug(sprintf('%s: get %s \'%s\' metadata.', $this->name, $type, ag($entity, 'Name')), [
'url' => $url
]);
if (true === in_array($type, $possibleTitles)) {
continue;
}
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($this->getHeaders(), [
'user_data' => [
'id' => ag($entity, 'Id'),
'title' => ag($entity, 'Name'),
'type' => $type,
]
])
);
$possibleTitles[] = formatName($title);
}
foreach ($requests as $response) {
if (200 !== $response->getStatusCode()) {
$this->logger->error(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
continue;
if (null !== ($path = ag($item, 'Path'))) {
if (ag($item, 'Type') === 'Movie') {
$locations[] = [
'l' => dirname($path),
'n' => formatName(basename(dirname($path))),
];
}
$handleRequest(
$response->getInfo('user_data')['type'],
json_decode(
json: $response->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
$locations[] = [
'l' => $path,
'n' => formatName(basename($path)),
];
}
if (null !== ($providerIds = ag($item, 'ProviderIds'))) {
$guids = $providerIds;
}
foreach ($locations ?? [] as $location) {
foreach ($possibleTitles as $title) {
if (true === str_contains($location['n'], $title)) {
return;
}
$isASCII = mb_detect_encoding($location['n'], 'ASCII', true) && mb_detect_encoding(
$title,
'ASCII',
true
);
if (1 === preg_match($parseYear, basename($location['l']), $match)) {
$withYear = true;
$title = !empty($year) && !str_contains($title, (string)$year) ? $title . ' ' . $year : $title;
} else {
$withYear = false;
}
if (true === $isASCII) {
similar_text($location['n'], $title, $percent);
} else {
mb_similar_text($location['n'], $title, $percent);
}
if ($percent >= $opts['coef']) {
return;
}
$matches[] = [
'path' => $location['n'],
'title' => $title,
'type' => $isASCII ? 'ascii' : 'unicode',
'methods' => [
'similarity' => round($percent, 3),
'levenshtein' => round(
$isASCII ? levenshtein($location['n'], $title) : mb_levenshtein($location['n'], $title),
3
),
'compareStrings' => compareStrings($location['n'], $title),
],
'year' => [
'inPath' => $withYear,
'parsed' => isset($match[1]) ? (int)$match[1] : 'No',
'source' => $year ?? 'No',
],
];
}
}
$metadata = [
'id' => ag($item, 'Id'),
'type' => ucfirst($type),
'url' => [(string)$url],
'title' => ag($item, $possibleTitlesList, '??'),
'year' => $year ?? 0000,
'guids' => $guids,
'path' => array_column($locations, 'l') ?? [],
'matching' => $matches,
'comments' => (empty($locations)) ? 'No path found.' : 'Title does not match path.',
];
if (empty($guids)) {
$metadata['guids'] = 'None.';
}
if (!empty($metadata['path']) && count($metadata['path']) <= 1) {
$metadata['path'] = $metadata['path'][0];
}
if ('movie' === $type && 2 === count($metadata['path'])) {
$metadata['path'] = $metadata['path'][1];
}
$list[] = $metadata;
};
$it = Items::fromIterable(
httpClientChunks($this->http->stream($response)),
[
'pointer' => '/Items',
'decoder' => new ErrorWrappingDecoder(
new ExtJsonDecoder(assoc: true, options: JSON_INVALID_UTF8_IGNORE)
)
]
);
$requests = [];
foreach ($it as $entity) {
if ($entity instanceof DecodingError) {
$this->logger->warning(
sprintf(
'%s: Failed to decode one of library id \'%s\' items. %s',
$this->name,
$id,
$entity->getErrorMessage()
),
[
'payload' => $entity->getMalformedJson(),
]
);
continue;
}
$url = $this->url->withPath(sprintf('/Users/%s/items/%s', $this->user, ag($entity, 'Id')));
$this->logger->debug(sprintf('%s: get %s \'%s\' metadata.', $this->name, $type, ag($entity, 'Name')), [
'url' => $url
]);
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($this->getHeaders(), [
'user_data' => [
'id' => ag($entity, 'Id'),
'title' => ag($entity, 'Name'),
'type' => $type,
]
])
);
}
foreach ($requests as $response) {
if (200 !== $response->getStatusCode()) {
$this->logger->error(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
continue;
}
} catch (ExceptionInterface|JsonException|\JsonMachine\Exception\InvalidArgumentException $e) {
throw new RuntimeException(get_class($e) . ': ' . $e->getMessage(), $e->getCode(), $e);
$handleRequest(
$response->getInfo('user_data')['type'],
json_decode(
json: $response->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
)
);
}
return $list;

View File

@@ -542,24 +542,280 @@ class PlexServer implements ServerInterface
}
}
/**
* @throws Throwable
*/
public function searchMismatch(string|int $id, array $opts = []): array
{
$list = [];
$this->checkConfig();
try {
// -- Get Content type.
$url = $this->url->withPath('/library/sections/');
// -- Get Content type.
$url = $this->url->withPath('/library/sections/');
$this->logger->debug(sprintf('%s: Sending get sections list.', $this->name), [
'url' => $url
]);
$this->logger->debug(sprintf('%s: Sending get sections list.', $this->name), [
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
}
$json = json_decode(
json: $response->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
$type = null;
$found = false;
foreach (ag($json, 'MediaContainer.Directory', []) as $section) {
if ((int)ag($section, 'key') !== (int)$id) {
continue;
}
$found = true;
$type = ag($section, 'type', 'unknown');
break;
}
if (false === $found) {
throw new RuntimeException(sprintf('%s: library id \'%s\' not found.', $this->name, $id));
}
if ('movie' !== $type && 'show' !== $type) {
throw new RuntimeException(sprintf('%s: Library id \'%s\' is of unsupported type.', $this->name, $id));
}
$query = [
'sort' => 'addedAt:asc',
'includeGuids' => 1,
];
if (iFace::TYPE_MOVIE === $type) {
$query['type'] = 1;
}
$url = $this->url->withPath(sprintf('/library/sections/%d/all', $id))->withQuery(http_build_query($query));
$this->logger->debug(sprintf('%s: Sending get library content for id \'%s\'.', $this->name, $id), [
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
}
$handleRequest = function (string $type, array $item) use (&$list, $opts) {
$this->logger->debug(sprintf('%s: Processing %s \'%s\'.', $this->name, $type, ag($item, 'title')));
$url = $this->url->withPath(sprintf('/library/metadata/%d', ag($item, 'ratingKey')));
$year = ag($item, 'year');
$parseYear = '/\((\d{4})\)/';
$locations = [];
$possibleTitles = $guids = $matches = [];
$possibleTitlesList = ['title', 'originalTitle', 'titleSort'];
foreach ($possibleTitlesList as $title) {
if (null === ($title = ag($item, $title))) {
continue;
}
$title = formatName($title);
if (true === in_array($title, $possibleTitles)) {
continue;
}
$possibleTitles[] = formatName($title);
}
foreach (ag($item, 'Location', []) as $path) {
$location = ag($path, 'path');
$locations[] = [
'l' => $location,
'n' => formatName(basename($location)),
];
}
foreach (ag($item, 'Media', []) as $leaf) {
foreach (ag($leaf, 'Part', []) as $path) {
$location = ag($path, 'file');
$locations[] = [
'l' => dirname($location),
'n' => formatName(basename(dirname($location))),
];
$locations[] = [
'l' => $location,
'n' => formatName(basename($location)),
];
}
}
$guids[] = ag($item, 'guid', []);
foreach (ag($item, 'Guid', []) as $guid) {
$guids[] = ag($guid, 'id');
}
foreach ($locations ?? [] as $location) {
foreach ($possibleTitles as $title) {
if (true === str_contains($location['n'], $title)) {
return;
}
$isASCII = mb_detect_encoding($location['n'], 'ASCII', true) && mb_detect_encoding(
$title,
'ASCII',
true
);
if (1 === preg_match($parseYear, basename($location['l']), $match)) {
$withYear = true;
$title = !empty($year) && !str_contains($title, (string)$year) ? $title . ' ' . $year : $title;
} else {
$withYear = false;
}
if (true === $isASCII) {
similar_text($location['n'], $title, $percent);
} else {
mb_similar_text($location['n'], $title, $percent);
}
if ($percent >= $opts['coef']) {
return;
}
$matches[] = [
'path' => $location['n'],
'title' => $title,
'type' => $isASCII ? 'ascii' : 'unicode',
'methods' => [
'similarity' => round($percent, 3),
'levenshtein' => round(
$isASCII ? levenshtein($location['n'], $title) : mb_levenshtein($location['n'], $title),
3
),
'compareStrings' => compareStrings($location['n'], $title),
],
'year' => [
'inPath' => $withYear,
'parsed' => isset($match[1]) ? (int)$match[1] : 'No',
'source' => $year ?? 'No',
],
];
}
}
$metadata = [
'id' => (int)ag($item, 'ratingKey'),
'type' => ucfirst($type),
'url' => [(string)$url],
'title' => ag($item, $possibleTitlesList, '??'),
'year' => $year ?? 0000,
'guids' => $guids,
'path' => array_column($locations, 'l') ?? [],
'matching' => $matches,
'comments' => (empty($locations)) ? 'No path found.' : 'Title does not match path.',
];
if (empty($guids)) {
$metadata['guids'] = 'No external ids found.';
}
if (!empty($metadata['path']) && count($metadata['path']) <= 1) {
$metadata['path'] = $metadata['path'][0];
}
if ('movie' === $type && 2 === count($metadata['path'])) {
$metadata['path'] = $metadata['path'][1];
}
$list[] = $metadata;
};
$it = Items::fromIterable(
httpClientChunks($this->http->stream($response)),
[
'pointer' => '/MediaContainer/Metadata',
'decoder' => new ErrorWrappingDecoder(
new ExtJsonDecoder(assoc: true, options: JSON_INVALID_UTF8_IGNORE)
)
]
);
$requests = [];
foreach ($it as $entity) {
if ($entity instanceof DecodingError) {
$this->logger->warning(
sprintf(
'%s: Failed to decode one of library id \'%s\' items. %s',
$this->name,
$id,
$entity->getErrorMessage()
),
[
'payload' => $entity->getMalformedJson(),
]
);
continue;
}
if (iFace::TYPE_MOVIE === $type) {
$handleRequest($type, $entity);
} else {
$url = $this->url->withPath(sprintf('/library/metadata/%d', ag($entity, 'ratingKey')));
$this->logger->debug(
sprintf('%s: get %s \'%s\' metadata.', $this->name, $type, ag($entity, 'title')),
[
'url' => $url
]
);
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($this->getHeaders(), [
'user_data' => [
'id' => ag($entity, 'ratingKey'),
'title' => ag($entity, 'title'),
'type' => $type,
]
])
);
}
}
foreach ($requests as $response) {
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
$this->logger->error(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
@@ -567,6 +823,7 @@ class PlexServer implements ServerInterface
$response->getStatusCode()
)
);
continue;
}
$json = json_decode(
@@ -575,224 +832,10 @@ class PlexServer implements ServerInterface
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
$type = null;
$found = false;
foreach (ag($json, 'MediaContainer.Directory', []) as $section) {
if ((int)ag($section, 'key') !== (int)$id) {
continue;
}
$found = true;
$type = ag($section, 'type', 'unknown');
break;
}
if (false === $found) {
throw new RuntimeException(sprintf('%s: library id \'%s\' not found.', $this->name, $id));
}
if ('movie' !== $type && 'show' !== $type) {
throw new RuntimeException(sprintf('%s: Library id \'%s\' is of unsupported type.', $this->name, $id));
}
$query = [
'sort' => 'addedAt:asc',
'includeGuids' => 1,
];
if (iFace::TYPE_MOVIE === $type) {
$query['type'] = 1;
}
$url = $this->url->withPath(sprintf('/library/sections/%d/all', $id))->withQuery(http_build_query($query));
$this->logger->debug(sprintf('%s: Sending get library content for id \'%s\'.', $this->name, $id), [
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $this->getHeaders());
if (200 !== $response->getStatusCode()) {
throw new RuntimeException(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
}
$handleRequest = function (string $type, array $item) use (&$list, $opts) {
$this->logger->debug(sprintf('%s: Processing %s \'%s\'.', $this->name, $type, ag($item, 'title')));
$url = $this->url->withPath(sprintf('/library/metadata/%d', ag($item, 'ratingKey')));
$possibleTitles = $paths = $locations = $guids = $matches = [];
$possibleTitlesList = ['title', 'originalTitle', 'titleSort'];
foreach ($possibleTitlesList as $title) {
if (null === ($title = ag($item, $title))) {
continue;
}
$title = formatName($title);
if (true === in_array($title, $possibleTitles)) {
continue;
}
$possibleTitles[] = formatName($title);
}
foreach (ag($item, 'Location', []) as $path) {
$paths[] = formatName(basename(ag($path, 'path')));
$locations[] = ag($path, 'path');
}
foreach (ag($item, 'Media', []) as $leaf) {
foreach (ag($leaf, 'Part', []) as $path) {
$paths[] = formatName(basename(dirname(ag($path, 'file'))));
$locations[] = dirname(ag($path, 'file'));
}
}
$guids[] = ag($item, 'guid', []);
foreach (ag($item, 'Guid', []) as $guid) {
$guids[] = ag($guid, 'id');
}
foreach ($paths ?? [] as $location) {
foreach ($possibleTitles as $title) {
if (true === str_contains($location, $title)) {
return;
}
$locationIsAscii = mb_detect_encoding($location, 'ASCII', true);
$titleIsAscii = mb_detect_encoding($location, 'ASCII', true);
if (true === $locationIsAscii && $titleIsAscii) {
similar_text($location, $title, $percent);
} else {
mb_similar_text($location, $title, $percent);
}
if ($percent >= $opts['coef']) {
return;
}
$matches[] = [
'path' => $location,
'title' => $title,
'similarity' => $percent,
];
}
}
$metadata = [
'id' => (int)ag($item, 'ratingKey'),
'type' => ucfirst($type),
'url' => [(string)$url],
'title' => ag($item, $possibleTitlesList, '??'),
'year' => ag($item, 'year', 0000),
'guids' => $guids,
'path' => $locations,
'matching' => $matches,
'comments' => (empty($paths)) ? 'No path found.' : 'Title does not match path.',
];
if (empty($guids)) {
$metadata['guids'] = 'No external ids found.';
}
if (count($locations) <= 1) {
$metadata['path'] = $locations[0];
}
$list[] = $metadata;
};
$it = Items::fromIterable(
httpClientChunks($this->http->stream($response)),
[
'pointer' => '/MediaContainer/Metadata',
'decoder' => new ErrorWrappingDecoder(
new ExtJsonDecoder(assoc: true, options: JSON_INVALID_UTF8_IGNORE)
)
]
$handleRequest(
$response->getInfo('user_data')['type'],
ag($json, 'MediaContainer.Metadata.0', [])
);
$requests = [];
foreach ($it as $entity) {
if ($entity instanceof DecodingError) {
$this->logger->warning(
sprintf(
'%s: Failed to decode one of library id \'%s\' items. %s',
$this->name,
$id,
$entity->getErrorMessage()
),
[
'payload' => $entity->getMalformedJson(),
]
);
continue;
}
if (iFace::TYPE_MOVIE === $type) {
$handleRequest($type, $entity);
} else {
$url = $this->url->withPath(sprintf('/library/metadata/%d', ag($entity, 'ratingKey')));
$this->logger->debug(
sprintf('%s: get %s \'%s\' metadata.', $this->name, $type, ag($entity, 'title')),
[
'url' => $url
]
);
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($this->getHeaders(), [
'user_data' => [
'id' => ag($entity, 'ratingKey'),
'title' => ag($entity, 'title'),
'type' => $type,
]
])
);
}
}
foreach ($requests as $response) {
if (200 !== $response->getStatusCode()) {
$this->logger->error(
sprintf(
'%s: Get metadata request for id \'%s\' responded with unexpected http status code \'%d\'.',
$this->name,
$id,
$response->getStatusCode()
)
);
continue;
}
$json = json_decode(
json: $response->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
$handleRequest(
$response->getInfo('user_data')['type'],
ag($json, 'MediaContainer.Metadata.0', [])
);
}
} catch (ExceptionInterface|JsonException|\JsonMachine\Exception\InvalidArgumentException $e) {
$this->logger->error($this->name . ': ' . $e->getMessage(), $e->getCode(), $e);
}
return $list;

View File

@@ -701,3 +701,95 @@ if (false === function_exists('mb_similar_text')) {
return $similarity;
}
}
if (false === function_exists('mb_levenshtein')) {
function mb_levenshtein(string $str1, string $str2)
{
$length1 = mb_strlen($str1, 'UTF-8');
$length2 = mb_strlen($str2, 'UTF-8');
if ($length1 < $length2) {
return mb_levenshtein($str2, $str1);
}
if (0 === $length1) {
return $length2;
}
if ($str1 === $str2) {
return 0;
}
$prevRow = range(0, $length2);
for ($i = 0; $i < $length1; $i++) {
$currentRow = [];
$currentRow[0] = $i + 1;
$c1 = mb_substr($str1, $i, 1, 'UTF-8');
for ($j = 0; $j < $length2; $j++) {
$c2 = mb_substr($str2, $j, 1, 'UTF-8');
$insertions = $prevRow[$j + 1] + 1;
$deletions = $currentRow[$j] + 1;
$substitutions = $prevRow[$j] + (($c1 !== $c2) ? 1 : 0);
$currentRow[] = min($insertions, $deletions, $substitutions);
}
$prevRow = $currentRow;
}
return $prevRow[$length2];
}
}
if (false === function_exists('compareStrings')) {
function compareStrings(string $str1, string $str2): float
{
if (empty($str1) || empty($str2)) {
return 0.000;
}
$length1 = mb_strlen($str1, 'UTF-8');
$length2 = mb_strlen($str2, 'UTF-8');
if ($length1 < $length2) {
return compareStrings($str2, $str1);
}
$ar1 = preg_split('/[^\w\-]+/u', strtolower($str1), flags: PREG_SPLIT_NO_EMPTY);
$ar2 = preg_split('/[^\w\-]+/u', strtolower($str2), flags: PREG_SPLIT_NO_EMPTY);
$ar2Copy = array_values($ar2);
$matchedIndices = [];
$wordMap = [];
foreach ($ar1 as $k => $w1) {
if (isset($wordMap[$w1])) {
if ($wordMap[$w1][0] >= $k) {
$matchedIndices[$k] = $wordMap[$w1][0];
}
array_splice($wordMap[$w1], 0, 1);
} else {
$indices = array_keys($ar2Copy, $w1);
$indexCount = count($indices);
if ($indexCount) {
$matchedIndices[$k] = $indices[0];
if (1 === $indexCount) {
// remove the word at given index from second array so that it won't repeat
unset($ar2Copy[$indices[0]]);
} else {
// remove the word at given indices from second array so that it won't repeat
foreach ($indices as $index) {
unset($ar2Copy[$index]);
}
array_splice($indices, 0, 1);
$wordMap[$w1] = $indices;
}
}
}
}
return round(count($matchedIndices) * 100 / count($ar1), 3);
}
}