65
FAQ.md
65
FAQ.md
@@ -947,3 +947,68 @@ In my docker host the group id for `video` is `44` and for `render` is `105`. ch
|
||||
file to match your setup.
|
||||
|
||||
Note: the tip about adding the group_add came from the user `binarypancakes` in discord.
|
||||
|
||||
---
|
||||
|
||||
### Advanced: How to extend the GUID parser to support more GUIDs or custom ones?
|
||||
|
||||
You can extend the parser by creating new file at `/config/config/guid.yaml` with the following content.
|
||||
|
||||
```yaml
|
||||
# The version of the guid file. right now in beta so it's 0.0. not required to be present.
|
||||
version: 0.0
|
||||
|
||||
# The key must be in lower case. and it's an array.
|
||||
guids:
|
||||
- type: string # must be exactly string do not change it.
|
||||
name: guid_mydb # the name must start with guid_ with no spaces and lower case.
|
||||
description: "My custom database guid" # description of the guid.
|
||||
# Validator object. to validate the guid.
|
||||
validator:
|
||||
pattern: /^[0-9\/]+$/i # regex pattern to match the guid. The pattern must also support / being in the guid. as we use the same object to generate relative guid.
|
||||
example: "(number)" # example of the guid.
|
||||
tests:
|
||||
valid:
|
||||
- "1234567" # valid guid examples.
|
||||
invalid:
|
||||
- "1234567a" # invalid guid examples.
|
||||
- "a111234567" # invalid guid examples.
|
||||
|
||||
# Extend Plex client to support the new guid.
|
||||
plex:
|
||||
- legacy: true # Tag the mapper as legacy GUID for mapping.
|
||||
# Required map object. to map the new guid to WatchState guid.
|
||||
map:
|
||||
from: com.plexapp.agents.foo # map.from this string.
|
||||
to: guid_mydb # map.to this guid.
|
||||
# (Optional) Replace helper. Sometimes you need to replace the guid identifier to another.
|
||||
# The replacement happens before the mapping, so if you replace the guid identifier, you should also
|
||||
# update the map.from to match the new identifier.
|
||||
replace:
|
||||
from: com.plexapp.agents.foobar:// # Replace from this string
|
||||
to: com.plexapp.agents.foo:// # Into this string.
|
||||
|
||||
# Extend Jellyfin client to support the new guid.
|
||||
jellyfin:
|
||||
# Required map object. to map the new guid to WatchState guid.
|
||||
- map:
|
||||
from: foo # map.from this string.
|
||||
to: guid_mydb # map.to this guid.
|
||||
|
||||
# Extend Emby client to support the new guid.
|
||||
emby:
|
||||
# Required map object. to map the new guid to WatchState guid.
|
||||
- map:
|
||||
from: foo # map.from this string.
|
||||
to: guid_mydb # map.to this guid.
|
||||
```
|
||||
|
||||
As you can see from the config, it's roughly how we expected it to be. The `guids` array is where you define your new
|
||||
guids. The `plex`, `jellyfin` and `emby` objects are where you map the new guid to the WatchState guid.
|
||||
|
||||
Everything in this file should be in lower case. If error occurs, the tool will log a warning and ignore the guid,
|
||||
By default, we only show `ERROR` levels in log file, You can lower it by setting `WS_LOGGER_FILE_LEVEL` environment variable
|
||||
to `WARNING`.
|
||||
|
||||
If you added or removed a guid from the `guid.yaml` file, you should run `system:reindex --force-reindex` command to update the
|
||||
database indexes with the new guids.
|
||||
|
||||
@@ -81,6 +81,11 @@ return (function () {
|
||||
],
|
||||
];
|
||||
|
||||
$config['guid'] = [
|
||||
'version' => '0.0',
|
||||
'file' => fixPath(env('WS_GUID_FILE', ag($config, 'path') . '/config/guid.yaml')),
|
||||
];
|
||||
|
||||
$config['backends_file'] = fixPath(env('WS_BACKENDS_FILE', ag($config, 'path') . '/config/servers.yaml'));
|
||||
|
||||
date_default_timezone_set(ag($config, 'tz', 'UTC'));
|
||||
|
||||
@@ -5,10 +5,12 @@ declare(strict_types=1);
|
||||
namespace App\API\System;
|
||||
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use App\Libs\Guid;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class Guids
|
||||
{
|
||||
@@ -19,6 +21,7 @@ final class Guids
|
||||
{
|
||||
$list = [];
|
||||
|
||||
Guid::setLogger(Container::get(LoggerInterface::class));
|
||||
$validator = Guid::getValidators();
|
||||
|
||||
foreach (Guid::getSupported() as $guid => $type) {
|
||||
|
||||
@@ -6,18 +6,25 @@ namespace App\Backends\Jellyfin;
|
||||
|
||||
use App\Backends\Common\Context;
|
||||
use App\Backends\Common\GuidInterface as iGuid;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Exceptions\Backends\InvalidArgumentException;
|
||||
use App\Libs\Guid;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
|
||||
class JellyfinGuid implements iGuid
|
||||
{
|
||||
private const array GUID_MAPPER = [
|
||||
private string $type;
|
||||
|
||||
private array $guidMapper = [
|
||||
'imdb' => Guid::GUID_IMDB,
|
||||
'tmdb' => Guid::GUID_TMDB,
|
||||
'tvdb' => Guid::GUID_TVDB,
|
||||
'tvmaze' => Guid::GUID_TVMAZE,
|
||||
'tvrage' => Guid::GUID_TVRAGE,
|
||||
'anidb' => Guid::GUID_ANIDB,
|
||||
'ytinforeader' => Guid::GUID_YOUTUBE,
|
||||
'cmdb' => Guid::GUID_CMDB,
|
||||
];
|
||||
@@ -31,6 +38,149 @@ class JellyfinGuid implements iGuid
|
||||
*/
|
||||
public function __construct(protected LoggerInterface $logger)
|
||||
{
|
||||
$this->type = str_contains(static::class, 'EmbyGuid') ? 'emby' : 'jellyfin';
|
||||
|
||||
$file = Config::get('guid.file', null);
|
||||
|
||||
try {
|
||||
if (null !== $file && true === file_exists($file)) {
|
||||
$this->parseGUIDFile($file);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error("{class}: Failed to read or parse '{guid}' file. Error '{error}'.", [
|
||||
'class' => afterLast(static::class, '\\'),
|
||||
'guid' => $file,
|
||||
'error' => $e->getMessage(),
|
||||
'exception' => [
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTrace(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends WatchState GUID parsing to include external GUIDs.
|
||||
*
|
||||
* @param string $file The path to the external GUID mapping file.
|
||||
*
|
||||
* @throws InvalidArgumentException if the file does not exist or is not readable.
|
||||
* @throws InvalidArgumentException if the GUIDs file cannot be parsed.
|
||||
* @throws InvalidArgumentException if the file version is not supported.
|
||||
*/
|
||||
public function parseGUIDFile(string $file): void
|
||||
{
|
||||
if (false === file_exists($file) || false === is_readable($file)) {
|
||||
throw new InvalidArgumentException(r("The file '{file}' does not exist or is not readable.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
|
||||
if (filesize($file) < 1) {
|
||||
$this->logger->info("The external GUID mapping file '{file}' is empty.", ['file' => $file]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$yaml = Yaml::parseFile($file);
|
||||
if (false === is_array($yaml)) {
|
||||
throw new InvalidArgumentException(r("The GUIDs file '{file}' is not an array.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
} catch (ParseException $e) {
|
||||
throw new InvalidArgumentException(r("Failed to parse GUIDs file. Error '{error}'.", [
|
||||
'error' => $e->getMessage(),
|
||||
]), code: (int)$e->getCode(), previous: $e);
|
||||
}
|
||||
|
||||
$supported = array_keys(Guid::getSupported());
|
||||
$supportedVersion = Config::get('guid.version', '0.0');
|
||||
$guidVersion = (string)ag($yaml, 'version', $supportedVersion);
|
||||
|
||||
if (true === version_compare($supportedVersion, $guidVersion, '<')) {
|
||||
throw new InvalidArgumentException(r("Unsupported file version '{version}'. Expecting '{supported}'.", [
|
||||
'version' => $guidVersion,
|
||||
'supported' => $supportedVersion,
|
||||
]));
|
||||
}
|
||||
|
||||
$mapping = ag($yaml, $this->type, []);
|
||||
|
||||
if (false === is_array($mapping)) {
|
||||
throw new InvalidArgumentException(r("The GUIDs file '{file}' {type} sub key is not an array.", [
|
||||
'type' => $this->type,
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
|
||||
if (count($mapping) < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($mapping as $key => $map) {
|
||||
if (false === is_array($map)) {
|
||||
$this->logger->warning("Ignoring '{type}.{key}'. Value must be an object. '{given}' is given.", [
|
||||
'key' => $key,
|
||||
'type' => $this->type,
|
||||
'given' => get_debug_type($map),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapper = ag($map, 'map', null);
|
||||
|
||||
if (false === is_array($mapper)) {
|
||||
$this->logger->warning("Ignoring '{type}.{key}'. map value must be an object. '{given}' is given.", [
|
||||
'key' => $key,
|
||||
'type' => $this->type,
|
||||
'given' => get_debug_type($mapper),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$from = ag($mapper, 'from', null);
|
||||
$to = ag($mapper, 'to', null);
|
||||
|
||||
if (empty($from) || false === is_string($from)) {
|
||||
$this->logger->warning("Ignoring '{type}.{key}'. map.from field is empty or not a string.", [
|
||||
'type' => $this->type,
|
||||
'key' => $key,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($to) || false === is_string($to)) {
|
||||
$this->logger->warning("Ignoring '{type}.{key}'. map.to field is empty or not a string.", [
|
||||
'type' => $this->type,
|
||||
'key' => $key,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === Guid::validateGUIDName($to)) {
|
||||
$this->logger->warning("Ignoring '{type}.{key}'. map.to '{to}' field does not starts with 'guid_'.", [
|
||||
'type' => $this->type,
|
||||
'key' => $key,
|
||||
'to' => $to,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === in_array($to, $supported)) {
|
||||
$this->logger->warning("Ignoring '{type}.{key}'. map.to field is not a supported GUID type.", [
|
||||
'type' => $this->type,
|
||||
'key' => $key,
|
||||
'to' => $to,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->guidMapper[$from] = $to;
|
||||
}
|
||||
}
|
||||
|
||||
public function withContext(Context $context): self
|
||||
@@ -79,7 +229,7 @@ class JellyfinGuid implements iGuid
|
||||
$type = JellyfinClient::TYPE_MAPPER[$type] ?? $type;
|
||||
|
||||
foreach (array_change_key_case($guids, CASE_LOWER) as $key => $value) {
|
||||
if (null === (self::GUID_MAPPER[$key] ?? null) || empty($value)) {
|
||||
if (null === ($this->guidMapper[$key] ?? null) || empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -87,8 +237,10 @@ class JellyfinGuid implements iGuid
|
||||
if (true === isIgnoredId($this->context->backendName, $type, $key, $value, $id)) {
|
||||
if (true === $log) {
|
||||
$this->logger->debug(
|
||||
'Ignoring [{backend}] external id [{source}] for {item.type} [{item.title}] as requested.',
|
||||
"{class}: Ignoring '{client}: {backend}' external id '{source}' for {item.type} '{item.id}: {item.title}' as requested.",
|
||||
[
|
||||
'class' => afterLast(static::class, '\\'),
|
||||
'client' => $this->context->clientName,
|
||||
'backend' => $this->context->backendName,
|
||||
'source' => $key . '://' . $value,
|
||||
'guid' => [
|
||||
@@ -102,12 +254,13 @@ class JellyfinGuid implements iGuid
|
||||
continue;
|
||||
}
|
||||
|
||||
$guid[self::GUID_MAPPER[$key]] = $value;
|
||||
$guid[$this->guidMapper[$key]] = $value;
|
||||
} catch (Throwable $e) {
|
||||
if (true === $log) {
|
||||
$this->logger->error(
|
||||
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] parsing [{agent}] identifier. Error [{error.message} @ {error.file}:{error.line}].',
|
||||
message: "{class}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing '{agent}' identifier. Error '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
'class' => afterLast(static::class, '\\'),
|
||||
'backend' => $this->context->backendName,
|
||||
'client' => $this->context->clientName,
|
||||
'error' => [
|
||||
@@ -136,4 +289,16 @@ class JellyfinGuid implements iGuid
|
||||
|
||||
return $guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'guidMapper' => $this->guidMapper,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ namespace App\Backends\Plex;
|
||||
|
||||
use App\Backends\Common\Context;
|
||||
use App\Backends\Common\GuidInterface as iGuid;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Exceptions\Backends\InvalidArgumentException;
|
||||
use App\Libs\Guid;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LoggerInterface as iLogger;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
|
||||
final class PlexGuid implements iGuid
|
||||
@@ -15,7 +19,7 @@ final class PlexGuid implements iGuid
|
||||
/**
|
||||
* @var array<string,string> Map plex guids to our guids.
|
||||
*/
|
||||
private const array GUID_MAPPER = [
|
||||
private array $guidMapper = [
|
||||
'imdb' => Guid::GUID_IMDB,
|
||||
'tmdb' => Guid::GUID_TMDB,
|
||||
'tvdb' => Guid::GUID_TVDB,
|
||||
@@ -29,7 +33,7 @@ final class PlexGuid implements iGuid
|
||||
/**
|
||||
* @var array<array-key,string> List of legacy plex agents.
|
||||
*/
|
||||
private const array GUID_LEGACY = [
|
||||
private array $guidLegacy = [
|
||||
'com.plexapp.agents.imdb',
|
||||
'com.plexapp.agents.tmdb',
|
||||
'com.plexapp.agents.themoviedb',
|
||||
@@ -44,7 +48,7 @@ final class PlexGuid implements iGuid
|
||||
/**
|
||||
* @var array<array-key,string> List of local plex agents.
|
||||
*/
|
||||
private const array GUID_LOCAL = [
|
||||
private array $guidLocal = [
|
||||
'plex',
|
||||
'local',
|
||||
'com.plexapp.agents.none',
|
||||
@@ -54,7 +58,7 @@ final class PlexGuid implements iGuid
|
||||
/**
|
||||
* @var array<string,string> Map guids to their replacement.
|
||||
*/
|
||||
private const array GUID_LEGACY_REPLACER = [
|
||||
private array $guidReplacer = [
|
||||
'com.plexapp.agents.themoviedb://' => 'com.plexapp.agents.tmdb://',
|
||||
'com.plexapp.agents.xbmcnfotv://' => 'com.plexapp.agents.tvdb://',
|
||||
'com.plexapp.agents.thetvdb://' => 'com.plexapp.agents.tvdb://',
|
||||
@@ -72,10 +76,189 @@ final class PlexGuid implements iGuid
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param LoggerInterface $logger Logger instance.
|
||||
* @param iLogger $logger Logger instance.
|
||||
*/
|
||||
public function __construct(protected LoggerInterface $logger)
|
||||
public function __construct(private readonly iLogger $logger)
|
||||
{
|
||||
$file = Config::get('guid.file', null);
|
||||
|
||||
try {
|
||||
if (null !== $file && true === file_exists($file)) {
|
||||
$this->parseGUIDFile($file);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error("Failed to read or parse '{guid}' file. Error '{error}'.", [
|
||||
'guid' => $file,
|
||||
'error' => $e->getMessage(),
|
||||
'exception' => [
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTrace(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends WatchState GUID parsing to include external GUIDs.
|
||||
*
|
||||
* @param string $file The path to the external GUID mapping file.
|
||||
*
|
||||
* @throws InvalidArgumentException if the file does not exist or is not readable.
|
||||
* @throws InvalidArgumentException if the GUIDs file cannot be parsed.
|
||||
* @throws InvalidArgumentException if the file version is not supported.
|
||||
*/
|
||||
public function parseGUIDFile(string $file): void
|
||||
{
|
||||
if (false === file_exists($file) || false === is_readable($file)) {
|
||||
throw new InvalidArgumentException(r("The file '{file}' does not exist or is not readable.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
|
||||
if (filesize($file) < 1) {
|
||||
$this->logger->info("The external GUID mapping file '{file}' is empty.", ['file' => $file]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$yaml = Yaml::parseFile($file);
|
||||
if (false === is_array($yaml)) {
|
||||
throw new InvalidArgumentException(r("The GUIDs file '{file}' is not an array.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
} catch (ParseException $e) {
|
||||
throw new InvalidArgumentException(r("Failed to parse GUIDs file. Error '{error}'.", [
|
||||
'error' => $e->getMessage(),
|
||||
]), code: (int)$e->getCode(), previous: $e);
|
||||
}
|
||||
|
||||
$supported = array_keys(Guid::getSupported());
|
||||
$supportedVersion = Config::get('guid.version', '0.0');
|
||||
$guidVersion = (string)ag($yaml, 'version', $supportedVersion);
|
||||
|
||||
if (true === version_compare($supportedVersion, $guidVersion, '<')) {
|
||||
throw new InvalidArgumentException(r("Unsupported file version '{version}'. Expecting '{supported}'.", [
|
||||
'version' => $guidVersion,
|
||||
'supported' => $supportedVersion,
|
||||
]));
|
||||
}
|
||||
|
||||
$mapping = ag($yaml, 'plex', []);
|
||||
|
||||
if (false === is_array($mapping)) {
|
||||
throw new InvalidArgumentException(r("The GUIDs file '{file}' plex sub key is not an array.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
|
||||
if (count($mapping) < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($mapping as $key => $map) {
|
||||
if (false === is_array($map)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. Value must be an object. '{given}' is given.", [
|
||||
'key' => $key,
|
||||
'given' => get_debug_type($map),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== ($replace = ag($map, 'replace', null))) {
|
||||
if (false === is_array($replace)) {
|
||||
$this->logger->warning(
|
||||
"Ignoring 'plex.{key}'. replace value must be an object. '{given}' is given.",
|
||||
[
|
||||
'key' => $key,
|
||||
'given' => get_debug_type($replace),
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$from = ag($replace, 'from', null);
|
||||
$to = ag($replace, 'to', null);
|
||||
|
||||
if (empty($from) || false === is_string($from)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. replace.from field is empty or not a string.", [
|
||||
'key' => $key,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === is_string($to)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. replacer.to field is not a string.", [
|
||||
'key' => $key,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->guidReplacer[$from] = $to;
|
||||
}
|
||||
|
||||
if (null !== ($mapper = ag($map, 'map', null))) {
|
||||
if (false === is_array($mapper)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. map value must be an object. '{given}' is given.", [
|
||||
'key' => $key,
|
||||
'given' => get_debug_type($mapper),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$from = ag($mapper, 'from', null);
|
||||
$to = ag($mapper, 'to', null);
|
||||
|
||||
if (empty($from) || false === is_string($from)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. map.from field is empty or not a string.", [
|
||||
'key' => $key,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($to) || false === is_string($to)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. map.to field is empty or not a string.", [
|
||||
'key' => $key,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === str_starts_with($to, 'guid_')) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. map.to '{to}' field does not starts with 'guid_'.", [
|
||||
'key' => $key,
|
||||
'to' => $to,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === in_array($to, $supported)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. map.to field is not a supported GUID type.", [
|
||||
'key' => $key,
|
||||
'to' => $to,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === (bool)ag($map, 'legacy', true)) {
|
||||
$this->guidMapper[$from] = $to;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (true === in_array($from, $this->guidLegacy)) {
|
||||
$this->logger->warning("Ignoring 'plex.{key}'. map.from already exists.", [
|
||||
'key' => $key,
|
||||
'from' => $from,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
$this->guidLegacy[] = $from;
|
||||
$agentGuid = explode('://', after($from, 'agents.'));
|
||||
$this->guidMapper[$agentGuid[0]] = $to;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +305,7 @@ final class PlexGuid implements iGuid
|
||||
*/
|
||||
public function isLocal(string $guid): bool
|
||||
{
|
||||
return true === in_array(before(strtolower($guid), '://'), self::GUID_LOCAL);
|
||||
return true === in_array(before(strtolower($guid), '://'), $this->guidLocal);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,14 +342,11 @@ final class PlexGuid implements iGuid
|
||||
|
||||
if (false === str_contains($val, '://')) {
|
||||
if (true === $log) {
|
||||
$this->logger->info(
|
||||
'PlexGuid: Unable to parse [{backend}] [{agent}] identifier.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'agent' => $val,
|
||||
...$context
|
||||
]
|
||||
);
|
||||
$this->logger->info("PlexGuid: Unable to parse '{backend}: {agent}' identifier.", [
|
||||
'backend' => $this->context->backendName,
|
||||
'agent' => $val,
|
||||
...$context
|
||||
]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -174,15 +354,16 @@ final class PlexGuid implements iGuid
|
||||
[$key, $value] = explode('://', $val);
|
||||
$key = strtolower($key);
|
||||
|
||||
if (null === (self::GUID_MAPPER[$key] ?? null) || empty($value)) {
|
||||
if (null === ($this->guidMapper[$key] ?? null) || empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (true === isIgnoredId($this->context->backendName, $type, $key, $value, $id)) {
|
||||
if (true === $log) {
|
||||
$this->logger->debug(
|
||||
'PlexGuid: Ignoring [{backend}] external id [{source}] for {item.type} [{item.title}] as requested.',
|
||||
"PlexGuid: Ignoring '{client}: {backend}' external id '{source}' for {item.type} '{item.id}: {item.title}' as requested.",
|
||||
[
|
||||
'client' => $this->context->clientName,
|
||||
'backend' => $this->context->backendName,
|
||||
'source' => $val,
|
||||
'guid' => [
|
||||
@@ -197,14 +378,15 @@ final class PlexGuid implements iGuid
|
||||
}
|
||||
|
||||
// -- Plex in their infinite wisdom, sometimes report two keys for same data source.
|
||||
if (null !== ($guid[self::GUID_MAPPER[$key]] ?? null)) {
|
||||
if (null !== ($guid[$this->guidMapper[$key]] ?? null)) {
|
||||
if (true === $log) {
|
||||
$this->logger->debug(
|
||||
'PlexGuid: [{backend}] reported multiple ids for same data source [{key}: {ids}] for {item.type} [{item.title}].',
|
||||
$this->logger->warning(
|
||||
"PlexGuid: '{client}: {backend}' reported multiple ids for same data source '{key}: {ids}' for {item.type} '{item.id}: {item.title}'.",
|
||||
[
|
||||
'client' => $this->context->clientName,
|
||||
'backend' => $this->context->backendName,
|
||||
'key' => $key,
|
||||
'ids' => sprintf('%s, %s', $guid[self::GUID_MAPPER[$key]], $value),
|
||||
'ids' => sprintf('%s, %s', $guid[$this->guidMapper[$key]], $value),
|
||||
...$context
|
||||
]
|
||||
);
|
||||
@@ -214,16 +396,16 @@ final class PlexGuid implements iGuid
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int)$guid[self::GUID_MAPPER[$key]] < (int)$value) {
|
||||
if ((int)$guid[$this->guidMapper[$key]] < (int)$value) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$guid[self::GUID_MAPPER[$key]] = $value;
|
||||
$guid[$this->guidMapper[$key]] = $value;
|
||||
} catch (Throwable $e) {
|
||||
if (true === $log) {
|
||||
$this->logger->error(
|
||||
message: 'PlexGuid: Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] parsing [{agent}] identifier. Error [{error.message} @ {error.file}:{error.line}].',
|
||||
message: "PlexGuid: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing '{agent}' identifier. Error '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
'backend' => $this->context->backendName,
|
||||
'client' => $this->context->clientName,
|
||||
@@ -261,12 +443,12 @@ final class PlexGuid implements iGuid
|
||||
* @param array $context Context data.
|
||||
* @param bool $log Log errors. default true.
|
||||
*
|
||||
* @return string Parsed guid.
|
||||
* @return string The parsed GUID.
|
||||
* @see https://github.com/ZeroQI/Hama.bundle/issues/510
|
||||
*/
|
||||
private function parseLegacyAgent(string $guid, array $context = [], bool $log = true): string
|
||||
{
|
||||
if (false === in_array(before($guid, '://'), self::GUID_LEGACY)) {
|
||||
if (false === in_array(before($guid, '://'), $this->guidLegacy)) {
|
||||
return $guid;
|
||||
}
|
||||
|
||||
@@ -286,15 +468,19 @@ final class PlexGuid implements iGuid
|
||||
return str_replace('tsdb', 'tmdb', $source) . '://' . before($sourceId, '?');
|
||||
}
|
||||
|
||||
$guid = strtr($guid, self::GUID_LEGACY_REPLACER);
|
||||
$guid = strtr($guid, $this->guidReplacer);
|
||||
|
||||
$agentGuid = explode('://', after($guid, 'agents.'));
|
||||
|
||||
if (false === isset($agentGuid[1])) {
|
||||
return $guid;
|
||||
}
|
||||
|
||||
return $agentGuid[0] . '://' . before($agentGuid[1], '?');
|
||||
} catch (Throwable $e) {
|
||||
if (true === $log) {
|
||||
$this->logger->error(
|
||||
message: 'PlexGuid: Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] parsing legacy agent [{agent}] identifier. Error [{error.message} @ {error.file}:{error.line}].',
|
||||
message: "PlexGuid: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing legacy agent '{agent}' identifier. Error '{error.message}' at '{error.file}:{error.line}.",
|
||||
context: [
|
||||
'backend' => $this->context->backendName,
|
||||
'client' => $this->context->clientName,
|
||||
@@ -319,4 +505,19 @@ final class PlexGuid implements iGuid
|
||||
return $guid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Plex Guid configuration.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'guidMapper' => $this->guidMapper,
|
||||
'guidLegacy' => $this->guidLegacy,
|
||||
'guidLocal' => $this->guidLocal,
|
||||
'guidReplacer' => $this->guidReplacer,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ namespace App\Libs;
|
||||
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface as iLogger;
|
||||
use Stringable;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The Guid class is the parser for external ids for different databases.
|
||||
@@ -29,14 +33,11 @@ final class Guid implements JsonSerializable, Stringable
|
||||
public const string GUID_ANIDB = 'guid_anidb';
|
||||
public const string GUID_YOUTUBE = 'guid_youtube';
|
||||
public const string GUID_CMDB = 'guid_cmdb';
|
||||
|
||||
/**
|
||||
* Constant array of supported GUID types.
|
||||
*
|
||||
* This array contains the supported GUID types as keys and their respective data types as values.
|
||||
*
|
||||
* @var array
|
||||
* @var array GUID types and their respective data types.
|
||||
*/
|
||||
private const array SUPPORTED = [
|
||||
private static array $supported = [
|
||||
Guid::GUID_IMDB => 'string',
|
||||
Guid::GUID_TVDB => 'string',
|
||||
Guid::GUID_TMDB => 'string',
|
||||
@@ -46,6 +47,7 @@ final class Guid implements JsonSerializable, Stringable
|
||||
Guid::GUID_YOUTUBE => 'string',
|
||||
Guid::GUID_CMDB => 'string',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constant array for validating GUIDs.
|
||||
*
|
||||
@@ -54,94 +56,111 @@ final class Guid implements JsonSerializable, Stringable
|
||||
* - 'pattern' (string): The regular expression pattern to match against the GUID.
|
||||
* - 'example' (string): An example format of the GUID value.
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, array{ pattern: string, example: string, tests: array{ valid: array<string|int>, invalid: array<string|int> } }>
|
||||
*/
|
||||
private const array VALIDATE_GUID = [
|
||||
private static array $validateGuid = [
|
||||
Guid::GUID_IMDB => [
|
||||
'pattern' => '/tt(\d+)/i',
|
||||
'description' => 'IMDB ID Parser.',
|
||||
'pattern' => '/^(?<guid>tt[0-9\/]+)$/i',
|
||||
'example' => 'tt(number)',
|
||||
'tests' => [
|
||||
'valid' => ['tt1234567'],
|
||||
'invalid' => ['tt1234567a', '111234567'],
|
||||
],
|
||||
],
|
||||
Guid::GUID_TMDB => [
|
||||
'pattern' => '/^[0-9\/]+$/i',
|
||||
'description' => 'The tmdb ID Parser.',
|
||||
'pattern' => '/^(?<guid>[0-9\/]+)$/i',
|
||||
'example' => '(number)',
|
||||
'tests' => [
|
||||
'valid' => ['123456'],
|
||||
'invalid' => ['123456a'],
|
||||
],
|
||||
],
|
||||
Guid::GUID_TVDB => [
|
||||
'pattern' => '/^[0-9\/]+$/i',
|
||||
'description' => 'The tvdb ID Parser.',
|
||||
'pattern' => '/^(?<guid>[0-9\/]+)$/i',
|
||||
'example' => '(number)',
|
||||
'tests' => [
|
||||
'valid' => ['123456'],
|
||||
'invalid' => ['123456a', 'd123456'],
|
||||
],
|
||||
],
|
||||
Guid::GUID_TVMAZE => [
|
||||
'pattern' => '/^[0-9\/]+$/i',
|
||||
'description' => 'The tvMaze ID Parser.',
|
||||
'pattern' => '/^(?<guid>[0-9\/]+)$/i',
|
||||
'example' => '(number)',
|
||||
'tests' => [
|
||||
'valid' => ['123456'],
|
||||
'invalid' => ['123456a', 'd123456'],
|
||||
],
|
||||
],
|
||||
Guid::GUID_TVRAGE => [
|
||||
'pattern' => '/^[0-9\/]+$/i',
|
||||
'description' => 'The tvRage ID Parser.',
|
||||
'pattern' => '/^(?<guid>[0-9\/]+)$/i',
|
||||
'example' => '(number)',
|
||||
'tests' => [
|
||||
'valid' => ['123456'],
|
||||
'invalid' => ['123456a', 'd123456'],
|
||||
],
|
||||
],
|
||||
Guid::GUID_ANIDB => [
|
||||
'pattern' => '/^[0-9\/]+$/i',
|
||||
'description' => 'The anidb ID Parser.',
|
||||
'pattern' => '/^(?<guid>[0-9\/]+)$/i',
|
||||
'example' => '(number)',
|
||||
'tests' => [
|
||||
'valid' => ['123456'],
|
||||
'invalid' => ['123456a', 'd123456'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string LOOKUP_KEY is how we format external ids to look up a record.
|
||||
*/
|
||||
private const string LOOKUP_KEY = '{db}://{id}';
|
||||
|
||||
/**
|
||||
* @var array $data Holds the list of supported external ids.
|
||||
*/
|
||||
private array $data = [];
|
||||
|
||||
/**
|
||||
* @var null|iLogger $logger The logger instance used for logging.
|
||||
*/
|
||||
private static iLogger|null $logger = null;
|
||||
|
||||
private static bool $checkedExternalFile = false;
|
||||
|
||||
/**
|
||||
* Create list of db => external id list.
|
||||
*
|
||||
* @param array $guids A key/value a pair of db => external id. For example, [ "guid_imdb" => "tt123456789" ]
|
||||
* @param array $context
|
||||
* @param iLogger|null $logger
|
||||
*/
|
||||
public function __construct(array $guids, array $context = [])
|
||||
public function __construct(array $guids, array $context = [], iLogger|null $logger = null)
|
||||
{
|
||||
if (null !== $logger) {
|
||||
self::$logger = $logger;
|
||||
}
|
||||
|
||||
if (false === self::$checkedExternalFile) {
|
||||
self::loadExternalGUID();
|
||||
}
|
||||
|
||||
foreach ($guids as $key => $value) {
|
||||
if (null === $value || null === (self::SUPPORTED[$key] ?? null)) {
|
||||
if (null === $value || null === (self::$supported[$key] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === ($this->data[$key] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === is_string($key)) {
|
||||
$this->getLogger()->info(
|
||||
"Ignoring '{backend}' {item.type} '{item.title}' external id. Unexpected key type '{given}' was given.",
|
||||
[
|
||||
'key' => (string)$key,
|
||||
'given' => get_debug_type($key),
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === (self::SUPPORTED[$key] ?? null)) {
|
||||
$this->getLogger()->info(
|
||||
"Ignoring '{backend}' {item.type} '{item.title}' '{key}' external id. Not supported.",
|
||||
[
|
||||
'key' => $key,
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::SUPPORTED[$key] !== ($valueType = get_debug_type($value))) {
|
||||
if (self::$supported[$key] !== ($valueType = get_debug_type($value))) {
|
||||
$this->getLogger()->info(
|
||||
"Ignoring '{backend}' {item.type} '{item.title}' '{key}' external id. Unexpected value type.",
|
||||
[
|
||||
'key' => $key,
|
||||
'condition' => [
|
||||
'expecting' => self::SUPPORTED[$key],
|
||||
'expecting' => self::$supported[$key],
|
||||
'actual' => $valueType,
|
||||
],
|
||||
...$context,
|
||||
@@ -150,19 +169,23 @@ final class Guid implements JsonSerializable, Stringable
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== (self::VALIDATE_GUID[$key] ?? null)) {
|
||||
if (1 !== preg_match(self::VALIDATE_GUID[$key]['pattern'], $value)) {
|
||||
if (null !== (self::$validateGuid[$key] ?? null)) {
|
||||
if (1 !== @preg_match(self::$validateGuid[$key]['pattern'], $value, $matches)) {
|
||||
$this->getLogger()->info(
|
||||
"Ignoring '{backend}' {item.type} '{item.title}' '{key}' external id. Unexpected value '{given}'. Expecting '{expected}'.",
|
||||
[
|
||||
'key' => $key,
|
||||
'expected' => self::VALIDATE_GUID[$key]['example'],
|
||||
'expected' => self::$validateGuid[$key]['example'],
|
||||
'given' => $value,
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($matches['guid'])) {
|
||||
$value = $matches['guid'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->data[$key] = $value;
|
||||
@@ -179,6 +202,201 @@ final class Guid implements JsonSerializable, Stringable
|
||||
self::$logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends WatchState GUID parsing to include external GUIDs.
|
||||
*
|
||||
* @param string $file The path to the external GUID mapping file.
|
||||
*
|
||||
* @throws InvalidArgumentException if the file does not exist or is not readable.
|
||||
* @throws InvalidArgumentException if the GUIDs file cannot be parsed.
|
||||
* @throws InvalidArgumentException if the file version is not supported.
|
||||
*/
|
||||
public static function parseGUIDFile(string $file): void
|
||||
{
|
||||
if (false === file_exists($file) || false === is_readable($file)) {
|
||||
throw new InvalidArgumentException(r("The file '{file}' does not exist or is not readable.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
|
||||
if (filesize($file) < 1) {
|
||||
self::$logger?->info(r("The external GUID mapping file '{file}' is empty.", ['file' => $file]));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$yaml = Yaml::parseFile($file);
|
||||
if (false === is_array($yaml)) {
|
||||
throw new InvalidArgumentException(r("The GUIDs file '{file}' is not an array.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
} catch (ParseException $e) {
|
||||
throw new InvalidArgumentException(r("Failed to parse GUIDs file. Error '{error}'.", [
|
||||
'error' => $e->getMessage(),
|
||||
]), (int)$e->getCode(), $e);
|
||||
}
|
||||
|
||||
$supportedVersion = ag(require __DIR__ . '/../../config/config.php', 'guid.version', '0.0');
|
||||
$guidVersion = (string)ag($yaml, 'version', Config::get('guid.version', '0.0'));
|
||||
|
||||
if (true === version_compare($supportedVersion, $guidVersion, '<')) {
|
||||
throw new InvalidArgumentException(r("Unsupported file version '{version}'. Expecting '{supported}'.", [
|
||||
'version' => $guidVersion,
|
||||
'supported' => $supportedVersion,
|
||||
]));
|
||||
}
|
||||
|
||||
$guids = ag($yaml, 'guids', null);
|
||||
|
||||
if (null === $guids || false === is_array($guids) || count($guids) < 1) {
|
||||
throw new InvalidArgumentException(r("The GUIDs file '{file}' does not contain any GUIDs mapping.", [
|
||||
'file' => $file,
|
||||
]));
|
||||
}
|
||||
|
||||
foreach ($guids as $key => $def) {
|
||||
if (false === is_array($def)) {
|
||||
self::$logger?->warning(
|
||||
"Ignoring 'guids.{key}'. Value must be an object. '{given}' is given.",
|
||||
[
|
||||
'key' => $key,
|
||||
'given' => get_debug_type($def),
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = ag($def, 'name');
|
||||
if (null === $name || false === self::validateGUIDName($name)) {
|
||||
self::$logger?->warning(
|
||||
"Ignoring 'guids.{key}'. name must start with 'guid_'. '{given}' is given.",
|
||||
[
|
||||
'key' => $key,
|
||||
'given' => $name ?? 'null',
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = ag($def, 'type');
|
||||
if (null === $type || false === is_string($type)) {
|
||||
self::$logger?->warning(
|
||||
"Ignoring 'guids.{key}.{name}'. type must be a string. '{given}' is given.",
|
||||
[
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
'given' => get_debug_type($type),
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$validator = ag($def, 'validator', null);
|
||||
if (null === $validator || false === is_array($validator) || count($validator) < 1) {
|
||||
self::$logger?->warning(
|
||||
"Ignoring 'guids.{key}.{name}'. validator key must be an object. '{given}' is given.",
|
||||
[
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
'given' => get_debug_type($validator),
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = ag($validator, 'pattern');
|
||||
if (null === $pattern || false === @preg_match($pattern, '')) {
|
||||
self::$logger?->warning("Ignoring 'guids.{key}.{name}'. validator.pattern is empty or invalid.", [
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$example = ag($validator, 'example');
|
||||
|
||||
if (empty($example) || false === is_string($example)) {
|
||||
self::$logger?->warning("Ignoring 'guids.{key}.{name}'. validator.example is empty or not a string.", [
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$tests = ag($validator, 'tests', []);
|
||||
if (empty($tests) || false === is_array($tests)) {
|
||||
self::$logger?->warning("Ignoring 'guids.{key}.{name}'. validator.tests key must be an object.", [
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$valid = ag($tests, 'valid', []);
|
||||
if (empty($valid) || false === is_array($valid) || count($valid) < 1) {
|
||||
self::$logger?->warning("Ignoring 'guids.{key}.{name}'. validator.tests.valid key must be an array.", [
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($valid as $val) {
|
||||
if (1 !== @preg_match($pattern, $val)) {
|
||||
self::$logger?->warning(
|
||||
"Ignoring 'guids.{key}.{name}'. validator.tests.valid value '{val}' does not match pattern.",
|
||||
[
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
'val' => $val,
|
||||
]
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$invalid = ag($tests, 'invalid', []);
|
||||
if (empty($invalid) || false === is_array($invalid) || count($invalid) < 1) {
|
||||
self::$logger?->warning(
|
||||
"Ignoring 'guids.{key}.{name}'. validator.tests.invalid key must be an array.",
|
||||
[
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($invalid as $val) {
|
||||
if (1 === @preg_match($pattern, $val)) {
|
||||
self::$logger?->warning(
|
||||
"Ignoring 'guids.{key}.{name}'. validator.tests.invalid value '{val}' matches pattern.",
|
||||
[
|
||||
'key' => $key,
|
||||
'name' => $name,
|
||||
'val' => $val,
|
||||
]
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
self::$supported[$name] = $type;
|
||||
self::$validateGuid[$name] = [
|
||||
'description' => ag($validator, 'description', fn() => r("The {name} ID Parser.", [
|
||||
'name' => after($name, 'guid_')
|
||||
])),
|
||||
'pattern' => $pattern,
|
||||
'example' => $example,
|
||||
'tests' => [
|
||||
'valid' => $valid,
|
||||
'invalid' => $invalid,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported external ids sources.
|
||||
*
|
||||
@@ -186,17 +404,29 @@ final class Guid implements JsonSerializable, Stringable
|
||||
*/
|
||||
public static function getSupported(): array
|
||||
{
|
||||
return self::SUPPORTED;
|
||||
if (false === self::$checkedExternalFile) {
|
||||
self::loadExternalGUID();
|
||||
}
|
||||
|
||||
return self::$supported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validators for external ids.
|
||||
*
|
||||
* @return array<string,array{pattern:string, example:string}>
|
||||
* @return array<string, array{
|
||||
* pattern: string,
|
||||
* example: string,
|
||||
* tests: array{ valid: array<string|int>, invalid: array<string|int> }
|
||||
* }>
|
||||
*/
|
||||
public static function getValidators(): array
|
||||
{
|
||||
return self::VALIDATE_GUID;
|
||||
if (false === self::$checkedExternalFile) {
|
||||
self::loadExternalGUID();
|
||||
}
|
||||
|
||||
return self::$validateGuid;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,12 +434,13 @@ final class Guid implements JsonSerializable, Stringable
|
||||
*
|
||||
* @param array $payload array of [ 'key' => 'value' ] pairs of [ 'db_source' => 'external id' ].
|
||||
* @param array $context context data.
|
||||
* @param Logger|null $logger logger instance.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function fromArray(array $payload, array $context = []): self
|
||||
public static function fromArray(array $payload, array $context = [], Logger|null $logger = null): self
|
||||
{
|
||||
return new self(guids: $payload, context: $context);
|
||||
return new self(guids: $payload, context: $context, logger: $logger);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,22 +459,22 @@ final class Guid implements JsonSerializable, Stringable
|
||||
|
||||
$lookup = 'guid_' . $db;
|
||||
|
||||
if (false === array_key_exists($lookup, self::SUPPORTED)) {
|
||||
if (false === array_key_exists($lookup, self::$supported)) {
|
||||
throw new InvalidArgumentException(r("Invalid db '{db}' source was given. Expecting '{db_list}'.", [
|
||||
'db' => $db,
|
||||
'db_list' => implode(', ', array_map(fn($f) => after($f, 'guid_'), array_keys(self::SUPPORTED))),
|
||||
'db_list' => implode(', ', array_map(fn($f) => after($f, 'guid_'), array_keys(self::$supported))),
|
||||
]));
|
||||
}
|
||||
|
||||
if (null === (self::VALIDATE_GUID[$lookup] ?? null)) {
|
||||
if (null === (self::$validateGuid[$lookup] ?? null)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (1 !== @preg_match(self::VALIDATE_GUID[$lookup]['pattern'], $id)) {
|
||||
if (1 !== @preg_match(self::$validateGuid[$lookup]['pattern'], $id)) {
|
||||
throw new InvalidArgumentException(r("Invalid value '{value}' for '{db}' GUID. Expecting '{example}'.", [
|
||||
'db' => $db,
|
||||
'value' => $id,
|
||||
'example' => self::VALIDATE_GUID[$lookup]['example'],
|
||||
'example' => self::$validateGuid[$lookup]['example'],
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -309,4 +540,50 @@ final class Guid implements JsonSerializable, Stringable
|
||||
{
|
||||
return json_encode($this->getAll());
|
||||
}
|
||||
|
||||
private static function loadExternalGUID(): void
|
||||
{
|
||||
$file = Config::get('guid.file', null);
|
||||
|
||||
try {
|
||||
if (null !== $file && true === file_exists($file)) {
|
||||
self::parseGUIDFile($file);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
self::$logger?->error("Failed to read or parse '{guid}' file. Error '{error}'.", [
|
||||
'guid' => $file,
|
||||
'error' => $e->getMessage(),
|
||||
'exception' => [
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTrace(),
|
||||
],
|
||||
]);
|
||||
} finally {
|
||||
self::$checkedExternalFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for testing purposes only. do not use in production.
|
||||
* @return void
|
||||
*/
|
||||
public static function reparse(): void
|
||||
{
|
||||
self::$checkedExternalFile = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Externally Added GUID Names.
|
||||
*
|
||||
* @param string $name The name to validate.
|
||||
*
|
||||
* @return bool True if the name is valid, false otherwise.
|
||||
*/
|
||||
public static function validateGUIDName(string $name): bool
|
||||
{
|
||||
return str_starts_with($name, 'guid_') && isValidName($name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,14 @@ class TestCase extends \PHPUnit\Framework\TestCase
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->handler->getRecords() as $logs) {
|
||||
fwrite(STDOUT, $logs['formatted'] . PHP_EOL);
|
||||
}
|
||||
$d = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[0];
|
||||
$getScript = ag($d, 'file');
|
||||
$line = ag($d, 'line');
|
||||
|
||||
dump(
|
||||
$getScript . ':' . $line,
|
||||
array_map(fn($v) => $v['formatted'] ?? $v['message'], $this->handler->getRecords())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
400
tests/Backends/Emby/EmbyGuidTest.php
Normal file
400
tests/Backends/Emby/EmbyGuidTest.php
Normal file
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Backends\Emby;
|
||||
|
||||
use App\Backends\Common\Cache;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Backends\Emby\EmbyClient;
|
||||
use App\Backends\Emby\EmbyGuid;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Exceptions\Backends\InvalidArgumentException;
|
||||
use App\Libs\Extends\LogMessageProcessor;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\TestCase;
|
||||
use App\Libs\Uri;
|
||||
use Monolog\Handler\TestHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class EmbyGuidTest extends TestCase
|
||||
{
|
||||
protected Logger|null $logger = null;
|
||||
|
||||
private function logged(Level $level, string $message, bool $clear = false): bool
|
||||
{
|
||||
try {
|
||||
foreach ($this->handler->getRecords() as $record) {
|
||||
if ($level !== $record->level) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $record->formatted && true === str_contains($record->formatted, $message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (true === str_contains($record->message, $message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (true === $clear) {
|
||||
$this->handler->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getClass(): EmbyGuid
|
||||
{
|
||||
$this->handler->clear();
|
||||
return (new EmbyGuid($this->logger))->withContext(
|
||||
new Context(
|
||||
clientName: EmbyClient::CLIENT_NAME,
|
||||
backendName: 'test_emby',
|
||||
backendUrl: new Uri('http://127.0.0.1:8096'),
|
||||
cache: new Cache($this->logger, new Psr16Cache(new ArrayAdapter())),
|
||||
logger: $this->logger,
|
||||
backendId: 's000000000000000000000000000000e',
|
||||
backendToken: 't000000000000000000000000000000e',
|
||||
backendUser: 'u000000000000000000000000000000e',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->handler = new TestHandler();
|
||||
$this->logger = new Logger('logger', processors: [new LogMessageProcessor()]);
|
||||
$this->logger->pushHandler($this->handler);
|
||||
|
||||
Guid::setLogger($this->logger);
|
||||
}
|
||||
|
||||
public function test__construct()
|
||||
{
|
||||
$oldGuidFile = Config::get('guid.file');
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
file_put_contents($tmpFile, "{'foo' => 'too' }");
|
||||
Config::save('guid.file', $tmpFile);
|
||||
$this->getClass();
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Error, 'Failed to parse GUIDs file', true),
|
||||
"Assert message logged when the value type does not match the expected type."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
Config::save('guid.file', $oldGuidFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_parseGUIDFile()
|
||||
{
|
||||
Config::save('guid.file', null);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'version: 2.0');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file version is not supported.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Unsupported file version'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkException(
|
||||
closure: fn() => $this->getClass()->parseGUIDFile('not_set.yml'),
|
||||
reason: "Failed to assert that the GUID file is not found.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'does not exist'
|
||||
);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'fff: {_]');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Failed to parse GUIDs file'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'invalid');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'is not an array'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'is empty', true),
|
||||
"Failed to assert that the GUID file is empty."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, Yaml::dump(['emby' => 'foo']));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Should throw an exception when there are no GUIDs mapping.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'emby sub key is not an array'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->handler->clear();
|
||||
$yaml = ['emby' => []];
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->assertCount(0, $this->handler->getRecords(), "There should be no messages logged for empty list.");
|
||||
$this->handler->clear();
|
||||
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump(ag_set($yaml, 'emby.0', 'ff')));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'Value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->handler->clear();
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$yaml = ag_set(['emby' => []], 'emby.0.map', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'emby.0', ['map' => []]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.from field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'emby.0.map.from', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.to field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'emby.0.map.to', 'foobar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'field does not starts with', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'emby.0.map.to', 'guid_foobar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.to field is not a supported', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'emby.0.map', [
|
||||
'from' => 'tsdb',
|
||||
'to' => Guid::GUID_IMDB,
|
||||
]);
|
||||
|
||||
$this->handler->clear();
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$class = $this->getClass();
|
||||
$class->parseGUIDFile($tmpFile);
|
||||
$this->assertArrayHasKey(
|
||||
'tsdb',
|
||||
ag($class->getConfig(), 'guidMapper', []),
|
||||
'Assert that the GUID mapping has been added.'
|
||||
);
|
||||
$this->handler->clear();
|
||||
|
||||
$yaml = ag_set($yaml, 'emby.0', [
|
||||
'legacy' => false,
|
||||
'map' => [
|
||||
'from' => 'imthedb',
|
||||
'to' => 'guid_imdb',
|
||||
]
|
||||
]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$class = $this->getClass();
|
||||
$class->parseGUIDFile($tmpFile);
|
||||
$this->assertArrayHasKey(
|
||||
'imthedb',
|
||||
ag($class->getConfig(), 'guidMapper', []),
|
||||
'Assert that the GUID mapping has been added.'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_isLocal()
|
||||
{
|
||||
$this->assertFalse(
|
||||
$this->getClass()->isLocal('test://123456/1/1'),
|
||||
'Should always return false, as emby does not have local GUIDs.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_has()
|
||||
{
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => 123,
|
||||
'type' => EmbyClient::TYPE_EPISODE,
|
||||
'title' => 'Test title',
|
||||
'year' => 2021
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertTrue($this->getClass()->has([
|
||||
'imdb' => '123456',
|
||||
'tvdb' => '123456',
|
||||
], $context), 'Assert that the GUID exists.');
|
||||
|
||||
$this->assertFalse($this->getClass()->has([
|
||||
['none' => '123456'],
|
||||
['imdb' => ''],
|
||||
], $context), 'Assert that the GUID does not exist.');
|
||||
}
|
||||
|
||||
public function test_parse()
|
||||
{
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => 123,
|
||||
'type' => 'episode',
|
||||
'title' => 'Test title',
|
||||
'year' => 2021,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals([
|
||||
Guid::GUID_IMDB => '123456',
|
||||
Guid::GUID_TMDB => '123456',
|
||||
Guid::GUID_ANIDB => '123456',
|
||||
],
|
||||
$this->getClass()->parse([
|
||||
'imdb' => '123456',
|
||||
'tmdb' => '123456',
|
||||
'anidb' => '123456',
|
||||
], $context),
|
||||
'Assert that the GUID exists.'
|
||||
);
|
||||
|
||||
$this->assertEquals([], $this->getClass()->parse([
|
||||
'' => '',
|
||||
'none' => '123456',
|
||||
'imdb' => ''
|
||||
], $context), 'Assert that the GUID does not exist. for invalid GUIDs.');
|
||||
}
|
||||
|
||||
public function test_get()
|
||||
{
|
||||
$context = ['item' => ['id' => 123, 'type' => 'episode', 'title' => 'Test title', 'year' => 2021]];
|
||||
|
||||
$this->assertEquals([], $this->getClass()->get([
|
||||
['imdb' => ''],
|
||||
], $context), 'Assert invalid guid return empty array.');
|
||||
|
||||
$this->assertEquals([Guid::GUID_IMDB => '1', Guid::GUID_CMDB => 'afa', Guid::GUID_TVDB => '123'],
|
||||
$this->getClass()->get([
|
||||
'imdb' => '1',
|
||||
'cmdb' => 'afa',
|
||||
'tvdb' => '123',
|
||||
'none' => '123',
|
||||
], $context),
|
||||
'Assert only the the oldest ID is returned for numeric GUIDs.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_get_ignore()
|
||||
{
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => 123,
|
||||
'type' => EmbyClient::TYPE_SHOW,
|
||||
'title' => 'Test title',
|
||||
'year' => 2021
|
||||
]
|
||||
];
|
||||
Config::save('ignore', [(string)makeIgnoreId('show://imdb:123@test_emby') => 1]);
|
||||
|
||||
$this->assertEquals([],
|
||||
$this->getClass()->get(['imdb' => '123'], $context),
|
||||
'Assert only the the oldest ID is returned for numeric GUIDs.');
|
||||
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Debug, 'EmbyGuid: Ignoring', true),
|
||||
'Assert that a log is raised when the GUID is ignored by user choice.'
|
||||
);
|
||||
}
|
||||
}
|
||||
400
tests/Backends/Jellyfin/JellyfinGuidTest.php
Normal file
400
tests/Backends/Jellyfin/JellyfinGuidTest.php
Normal file
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Backends\Jellyfin;
|
||||
|
||||
use App\Backends\Common\Cache;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Backends\Jellyfin\JellyfinClient;
|
||||
use App\Backends\Jellyfin\JellyfinGuid;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Exceptions\Backends\InvalidArgumentException;
|
||||
use App\Libs\Extends\LogMessageProcessor;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\TestCase;
|
||||
use App\Libs\Uri;
|
||||
use Monolog\Handler\TestHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class JellyfinGuidTest extends TestCase
|
||||
{
|
||||
protected Logger|null $logger = null;
|
||||
|
||||
private function logged(Level $level, string $message, bool $clear = false): bool
|
||||
{
|
||||
try {
|
||||
foreach ($this->handler->getRecords() as $record) {
|
||||
if ($level !== $record->level) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $record->formatted && true === str_contains($record->formatted, $message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (true === str_contains($record->message, $message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (true === $clear) {
|
||||
$this->handler->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getClass(): JellyfinGuid
|
||||
{
|
||||
$this->handler->clear();
|
||||
return (new JellyfinGuid($this->logger))->withContext(
|
||||
new Context(
|
||||
clientName: JellyfinClient::CLIENT_NAME,
|
||||
backendName: 'test_jellyfin',
|
||||
backendUrl: new Uri('http://127.0.0.1:8096'),
|
||||
cache: new Cache($this->logger, new Psr16Cache(new ArrayAdapter())),
|
||||
logger: $this->logger,
|
||||
backendId: 's000000000000000000000000000000j',
|
||||
backendToken: 't000000000000000000000000000000j',
|
||||
backendUser: 'u000000000000000000000000000000j',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->handler = new TestHandler();
|
||||
$this->logger = new Logger('logger', processors: [new LogMessageProcessor()]);
|
||||
$this->logger->pushHandler($this->handler);
|
||||
|
||||
Guid::setLogger($this->logger);
|
||||
}
|
||||
|
||||
public function test__construct()
|
||||
{
|
||||
$oldGuidFile = Config::get('guid.file');
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
file_put_contents($tmpFile, "{'foo' => 'too' }");
|
||||
Config::save('guid.file', $tmpFile);
|
||||
$this->getClass();
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Error, 'Failed to parse GUIDs file', true),
|
||||
"Assert message logged when the value type does not match the expected type."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
Config::save('guid.file', $oldGuidFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_parseGUIDFile()
|
||||
{
|
||||
Config::save('guid.file', null);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'version: 2.0');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file version is not supported.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Unsupported file version'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkException(
|
||||
closure: fn() => $this->getClass()->parseGUIDFile('not_set.yml'),
|
||||
reason: "Failed to assert that the GUID file is not found.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'does not exist'
|
||||
);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'fff: {_]');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Failed to parse GUIDs file'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'invalid');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'is not an array'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'is empty', true),
|
||||
"Failed to assert that the GUID file is empty."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, Yaml::dump(['jellyfin' => 'foo']));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Should throw an exception when there are no GUIDs mapping.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'jellyfin sub key is not an array'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->handler->clear();
|
||||
$yaml = ['jellyfin' => []];
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->assertCount(0, $this->handler->getRecords(), "There should be no messages logged for empty list.");
|
||||
$this->handler->clear();
|
||||
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump(ag_set($yaml, 'jellyfin.0', 'ff')));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'Value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->handler->clear();
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$yaml = ag_set(['jellyfin' => []], 'jellyfin.0.map', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'jellyfin.0', ['map' => []]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.from field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'jellyfin.0.map.from', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.to field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'jellyfin.0.map.to', 'foobar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'field does not starts with', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'jellyfin.0.map.to', 'guid_foobar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.to field is not a supported', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'jellyfin.0.map', [
|
||||
'from' => 'tsdb',
|
||||
'to' => Guid::GUID_IMDB,
|
||||
]);
|
||||
|
||||
$this->handler->clear();
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$class = $this->getClass();
|
||||
$class->parseGUIDFile($tmpFile);
|
||||
$this->assertArrayHasKey(
|
||||
'tsdb',
|
||||
ag($class->getConfig(), 'guidMapper', []),
|
||||
'Assert that the GUID mapping has been added.'
|
||||
);
|
||||
$this->handler->clear();
|
||||
|
||||
$yaml = ag_set($yaml, 'jellyfin.0', [
|
||||
'legacy' => false,
|
||||
'map' => [
|
||||
'from' => 'imthedb',
|
||||
'to' => 'guid_imdb',
|
||||
]
|
||||
]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$class = $this->getClass();
|
||||
$class->parseGUIDFile($tmpFile);
|
||||
$this->assertArrayHasKey(
|
||||
'imthedb',
|
||||
ag($class->getConfig(), 'guidMapper', []),
|
||||
'Assert that the GUID mapping has been added.'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_isLocal()
|
||||
{
|
||||
$this->assertFalse(
|
||||
$this->getClass()->isLocal('test://123456/1/1'),
|
||||
'Should always return false, as Jellyfin does not have local GUIDs.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_has()
|
||||
{
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => 123,
|
||||
'type' => JellyfinClient::TYPE_EPISODE,
|
||||
'title' => 'Test title',
|
||||
'year' => 2021
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertTrue($this->getClass()->has([
|
||||
'imdb' => '123456',
|
||||
'tvdb' => '123456',
|
||||
], $context), 'Assert that the GUID exists.');
|
||||
|
||||
$this->assertFalse($this->getClass()->has([
|
||||
['none' => '123456'],
|
||||
['imdb' => ''],
|
||||
], $context), 'Assert that the GUID does not exist.');
|
||||
}
|
||||
|
||||
public function test_parse()
|
||||
{
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => 123,
|
||||
'type' => 'episode',
|
||||
'title' => 'Test title',
|
||||
'year' => 2021,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals([
|
||||
Guid::GUID_IMDB => '123456',
|
||||
Guid::GUID_TMDB => '123456',
|
||||
Guid::GUID_ANIDB => '123456',
|
||||
],
|
||||
$this->getClass()->parse([
|
||||
'imdb' => '123456',
|
||||
'tmdb' => '123456',
|
||||
'anidb' => '123456',
|
||||
], $context),
|
||||
'Assert that the GUID exists.'
|
||||
);
|
||||
|
||||
$this->assertEquals([], $this->getClass()->parse([
|
||||
'' => '',
|
||||
'none' => '123456',
|
||||
'imdb' => ''
|
||||
], $context), 'Assert that the GUID does not exist. for invalid GUIDs.');
|
||||
}
|
||||
|
||||
public function test_get()
|
||||
{
|
||||
$context = ['item' => ['id' => 123, 'type' => 'episode', 'title' => 'Test title', 'year' => 2021]];
|
||||
|
||||
$this->assertEquals([], $this->getClass()->get([
|
||||
['imdb' => ''],
|
||||
], $context), 'Assert invalid guid return empty array.');
|
||||
|
||||
$this->assertEquals([Guid::GUID_IMDB => '1', Guid::GUID_CMDB => 'afa', Guid::GUID_TVDB => '123'],
|
||||
$this->getClass()->get([
|
||||
'imdb' => '1',
|
||||
'cmdb' => 'afa',
|
||||
'tvdb' => '123',
|
||||
'none' => '123',
|
||||
], $context),
|
||||
'Assert only the the oldest ID is returned for numeric GUIDs.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_get_ignore()
|
||||
{
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => 123,
|
||||
'type' => JellyfinClient::TYPE_SHOW,
|
||||
'title' => 'Test title',
|
||||
'year' => 2021
|
||||
]
|
||||
];
|
||||
Config::save('ignore', [(string)makeIgnoreId('show://imdb:123@test_jellyfin') => 1]);
|
||||
|
||||
$this->assertEquals([],
|
||||
$this->getClass()->get(['imdb' => '123'], $context),
|
||||
'Assert only the the oldest ID is returned for numeric GUIDs.');
|
||||
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Debug, 'JellyfinGuid: Ignoring', true),
|
||||
'Assert that a log is raised when the GUID is ignored by user choice.'
|
||||
);
|
||||
}
|
||||
}
|
||||
446
tests/Backends/Plex/PlexGuidTest.php
Normal file
446
tests/Backends/Plex/PlexGuidTest.php
Normal file
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Backends\Plex;
|
||||
|
||||
use App\Backends\Common\Cache;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Backends\Plex\PlexClient;
|
||||
use App\Backends\Plex\PlexGuid;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Exceptions\Backends\InvalidArgumentException;
|
||||
use App\Libs\Extends\LogMessageProcessor;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\TestCase;
|
||||
use App\Libs\Uri;
|
||||
use Monolog\Handler\TestHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class PlexGuidTest extends TestCase
|
||||
{
|
||||
protected Logger|null $logger = null;
|
||||
|
||||
private function logged(Level $level, string $message, bool $clear = false): bool
|
||||
{
|
||||
try {
|
||||
foreach ($this->handler->getRecords() as $record) {
|
||||
if ($level !== $record->level) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $record->formatted && true === str_contains($record->formatted, $message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (true === str_contains($record->message, $message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (true === $clear) {
|
||||
$this->handler->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getClass(): PlexGuid
|
||||
{
|
||||
$this->handler->clear();
|
||||
return (new PlexGuid($this->logger))->withContext(
|
||||
new Context(
|
||||
clientName: PlexClient::CLIENT_NAME,
|
||||
backendName: 'test_plex',
|
||||
backendUrl: new Uri('http://127.0.0.1:34000'),
|
||||
cache: new Cache($this->logger, new Psr16Cache(new ArrayAdapter())),
|
||||
logger: $this->logger,
|
||||
backendId: 's00000000000000000000000000000000000000p',
|
||||
backendToken: 't000000000000000000p',
|
||||
backendUser: '11111111',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->handler = new TestHandler();
|
||||
$this->logger = new Logger('logger', processors: [new LogMessageProcessor()]);
|
||||
$this->logger->pushHandler($this->handler);
|
||||
|
||||
Guid::setLogger($this->logger);
|
||||
}
|
||||
|
||||
public function test__construct()
|
||||
{
|
||||
$oldGuidFile = Config::get('guid.file');
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
file_put_contents($tmpFile, "{'foo' => 'too' }");
|
||||
Config::save('guid.file', $tmpFile);
|
||||
$this->getClass();
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Error, 'Failed to parse GUIDs file', true),
|
||||
"Assert message logged when the value type does not match the expected type."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
Config::save('guid.file', $oldGuidFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_parseGUIDFile()
|
||||
{
|
||||
Config::save('guid.file', null);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'version: 2.0');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file version is not supported.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Unsupported file version'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkException(
|
||||
closure: fn() => $this->getClass()->parseGUIDFile('not_set.yml'),
|
||||
reason: "Failed to assert that the GUID file is not found.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'does not exist'
|
||||
);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'fff: {_]');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Failed to parse GUIDs file'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'invalid');
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'is not an array'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'is empty', true),
|
||||
"Failed to assert that the GUID file is empty."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, Yaml::dump(['plex' => 'foo']));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Should throw an exception when there are no GUIDs mapping.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'plex sub key is not an array'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->handler->clear();
|
||||
$yaml = ['plex' => [[]]];
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertCount(0, $this->handler->getRecords(), "There should be no messages logged for empty list.");
|
||||
$this->handler->clear();
|
||||
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump(ag_set($yaml, 'plex.0', 'ff')));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'Value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.replace', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'replace value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0', ['replace' => []]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'replace.from field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.replace.from', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'replacer.to field is not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.replace.to', 'bar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertCount(0, $this->handler->getRecords(), "There should be no error messages logged.");
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->handler->clear();
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$yaml = ag_set(['plex' => []], 'plex.0.map', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0', ['map' => []]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.from field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.map.from', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.to field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.map.to', 'foobar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'field does not starts with', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.map.to', 'guid_foobar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.to field is not a supported', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.map', [
|
||||
'from' => 'com.plexapp.agents.imdb',
|
||||
'to' => 'guid_imdb',
|
||||
]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map.from already exists.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0.map', [
|
||||
'from' => 'com.plexapp.agents.ccdb',
|
||||
'to' => 'guid_imdb',
|
||||
]);
|
||||
|
||||
$this->handler->clear();
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$class = $this->getClass();
|
||||
$class->parseGUIDFile($tmpFile);
|
||||
$this->assertArrayHasKey(
|
||||
'ccdb',
|
||||
ag($class->getConfig(), 'guidMapper', []),
|
||||
'Assert that the GUID mapping has been added.'
|
||||
);
|
||||
$this->handler->clear();
|
||||
|
||||
$yaml = ag_set($yaml, 'plex.0', [
|
||||
'legacy' => false,
|
||||
'map' => [
|
||||
'from' => 'com.plexapp.agents.imthedb',
|
||||
'to' => 'guid_imdb',
|
||||
]
|
||||
]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$class = $this->getClass();
|
||||
$class->parseGUIDFile($tmpFile);
|
||||
$this->assertArrayHasKey(
|
||||
'com.plexapp.agents.imthedb',
|
||||
ag($class->getConfig(), 'guidMapper', []),
|
||||
'Assert that the GUID mapping has been added.'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_isLocal()
|
||||
{
|
||||
$this->assertTrue(
|
||||
$this->getClass()->isLocal('com.plexapp.agents.none://123456/1/1'),
|
||||
'Assert that the GUID is local.'
|
||||
);
|
||||
$this->assertFalse(
|
||||
$this->getClass()->isLocal('com.plexapp.agents.imdb://123456/1/1'),
|
||||
'Assert that the GUID is not local.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_has()
|
||||
{
|
||||
$context = ['item' => ['id' => 123, 'type' => 'episode', 'title' => 'Test title', 'year' => 2021]];
|
||||
|
||||
$this->assertTrue($this->getClass()->has([
|
||||
['id' => 'com.plexapp.agents.imdb://123456'],
|
||||
['id' => 'com.plexapp.agents.tvdb://123456'],
|
||||
], $context), 'Assert that the GUID exists.');
|
||||
|
||||
$this->assertFalse($this->getClass()->has([
|
||||
['id' => ''],
|
||||
['id' => 'com.plexapp.agents.none://123456'],
|
||||
['id' => 'com.plexapp.agents.imdb'],
|
||||
], $context), 'Assert that the GUID does not exist.');
|
||||
}
|
||||
|
||||
public function test_parse()
|
||||
{
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => 123,
|
||||
'type' => 'episode',
|
||||
'title' => 'Test title',
|
||||
'year' => 2021,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals([
|
||||
Guid::GUID_IMDB => '123456',
|
||||
Guid::GUID_TMDB => '123456',
|
||||
Guid::GUID_ANIDB => '123456',
|
||||
],
|
||||
$this->getClass()->parse([
|
||||
['id' => 'com.plexapp.agents.imdb://123456'],
|
||||
['id' => 'com.plexapp.agents.tmdb://123456'],
|
||||
['id' => 'com.plexapp.agents.hama://anidb-123456'],
|
||||
], $context),
|
||||
'Assert that the GUID exists.');
|
||||
|
||||
$this->assertEquals([], $this->getClass()->parse([
|
||||
['id' => ''],
|
||||
['id' => 'com.plexapp.agents.none://123456'],
|
||||
['id' => 'com.plexapp.agents.imdb'],
|
||||
], $context), 'Assert that the GUID does not exist. for invalid GUIDs.');
|
||||
}
|
||||
|
||||
public function test_get()
|
||||
{
|
||||
$context = ['item' => ['id' => 123, 'type' => 'episode', 'title' => 'Test title', 'year' => 2021]];
|
||||
|
||||
$this->assertEquals([], $this->getClass()->get([
|
||||
['id' => 'com.plexapp.agents.imdb'],
|
||||
], $context), 'Assert invalid guid return empty array.');
|
||||
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'Unable to parse', true),
|
||||
'Assert that the invalid GUID is logged.'
|
||||
);
|
||||
$this->assertEquals([Guid::GUID_IMDB => '1', Guid::GUID_CMDB => 'afa', Guid::GUID_TVDB => '123'],
|
||||
$this->getClass()->get([
|
||||
['id' => 'com.plexapp.agents.imdb://2'],
|
||||
['id' => 'com.plexapp.agents.imdb://1'],
|
||||
['id' => 'com.plexapp.agents.cmdb://afa'],
|
||||
['id' => 'com.plexapp.agents.cmdb://faf'],
|
||||
['id' => 'com.plexapp.agents.hama://tvdb-123'],
|
||||
['id' => 'com.plexapp.agents.hama://notSet-123'],
|
||||
['id' => 'com.plexapp.agents.hama://notSet-'],
|
||||
], $context),
|
||||
'Assert only the the oldest ID is returned for numeric GUIDs.'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'reported multiple ids', true),
|
||||
'Assert that a log is raised when multiple GUIDs for the same provider are found.'
|
||||
);
|
||||
|
||||
$this->assertEquals([Guid::GUID_IMDB => '1'], $this->getClass()->get([
|
||||
['id' => 'com.plexapp.agents.imdb://1'],
|
||||
['id' => 'com.plexapp.agents.imdb://2'],
|
||||
], $context), 'Assert only the the oldest ID is returned for numeric GUIDs.');
|
||||
|
||||
Config::save('ignore', [(string)makeIgnoreId('show://imdb:123@test_plex') => 1]);
|
||||
|
||||
$this->assertEquals([],
|
||||
$this->getClass()->get([
|
||||
['id' => 'com.plexapp.agents.imdb://123'],
|
||||
], ag_set($context, 'item.type', 'show')),
|
||||
'Assert only the the oldest ID is returned for numeric GUIDs.');
|
||||
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Debug, 'PlexGuid: Ignoring', true),
|
||||
'Assert that a log is raised when the GUID is ignored by user choice.'
|
||||
);
|
||||
}
|
||||
}
|
||||
450
tests/Libs/GuidTest.php
Normal file
450
tests/Libs/GuidTest.php
Normal file
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Libs;
|
||||
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use App\Libs\Extends\LogMessageProcessor;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\TestCase;
|
||||
use Monolog\Handler\TestHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface as iLogger;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class GuidTest extends TestCase
|
||||
{
|
||||
protected Logger|null $logger = null;
|
||||
|
||||
private function logged(Level $level, string $message, bool $clear = false): bool
|
||||
{
|
||||
try {
|
||||
foreach ($this->handler->getRecords() as $record) {
|
||||
if ($level !== $record->level) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $record->formatted && true === str_contains($record->formatted, $message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (true === str_contains($record->message, $message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (true === $clear) {
|
||||
$this->handler->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->handler = new TestHandler();
|
||||
$this->logger = new Logger('logger', processors: [new LogMessageProcessor()]);
|
||||
$this->logger->pushHandler($this->handler);
|
||||
|
||||
Container::init();
|
||||
Container::add(iLogger::class, $this->logger);
|
||||
}
|
||||
|
||||
public function test__construct()
|
||||
{
|
||||
$guid = Guid::fromArray(['guid_test' => 'ztt1234567']);
|
||||
$this->assertCount(0, $guid->getAll(), "Count should be 0 when the GUID is not supported.");
|
||||
|
||||
$guid = Guid::fromArray(['guid_imdb' => null]);
|
||||
$this->assertCount(0, $guid->getAll(), "Count should be 0 when value of guid is null.");
|
||||
|
||||
Guid::fromArray(['guid_tvdb' => INF], logger: $this->logger);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'external id. Unexpected value type.', true),
|
||||
"Assert message logged when the value type does not match the expected type."
|
||||
);
|
||||
|
||||
Guid::fromArray(['guid_tvdb' => 'tt1234567']);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, "external id. Unexpected value '", true),
|
||||
"Assert message logged when the value does not match the expected pattern."
|
||||
);
|
||||
}
|
||||
|
||||
public function test_validation()
|
||||
{
|
||||
foreach (Guid::getValidators() as $guid => $validator) {
|
||||
foreach (ag($validator, 'tests.valid', []) as $value) {
|
||||
$this->assertTrue(
|
||||
Guid::validate($guid, $value),
|
||||
r("Failed to assert that '{value} test for '{guid}' returns true.", [
|
||||
'guid' => $guid,
|
||||
'value' => $value,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
foreach (ag($validator, 'tests.invalid', []) as $value) {
|
||||
$this->checkException(
|
||||
closure: fn() => Guid::validate($guid, $value),
|
||||
reason: r("Failed to assert that invalid '{value}' test for '{guid}' throws an exception.", [
|
||||
'guid' => $guid,
|
||||
'value' => $value,
|
||||
]),
|
||||
exception: InvalidArgumentException::class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkException(
|
||||
closure: fn() => Guid::validate('guid_not_set', '12345678'),
|
||||
reason: 'Failed to assert that an exception is thrown when the GUID is not supported.',
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Invalid db'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
Guid::validate('guid_cmdb', '12345678'),
|
||||
'Assert supported guid with no validator returns true.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_jsonSerialize()
|
||||
{
|
||||
Guid::reparse();
|
||||
$guid = Guid::fromArray(['guid_imdb' => 'tt1234567', 'guid_tvdb' => '123']);
|
||||
$this->assertJsonStringEqualsJsonString(
|
||||
json_encode(['guid_imdb' => 'tt1234567', 'guid_tvdb' => '123',]),
|
||||
json_encode($guid),
|
||||
"Failed to assert that the JSON serialization of the Guid object is correct."
|
||||
);
|
||||
}
|
||||
|
||||
public function test__toString()
|
||||
{
|
||||
$guid = Guid::fromArray(['guid_imdb' => 'tt1234567', 'guid_tvdb' => '123']);
|
||||
$this->assertJsonStringEqualsJsonString(
|
||||
json_encode(['guid_imdb' => 'tt1234567', 'guid_tvdb' => '123',]),
|
||||
(string)$guid,
|
||||
"Failed to assert that the string representation of the Guid object is correct. {records}"
|
||||
);
|
||||
}
|
||||
|
||||
public function test_parseGUIDFile()
|
||||
{
|
||||
Guid::setLogger($this->logger);
|
||||
|
||||
$this->checkException(
|
||||
closure: fn() => Guid::parseGUIDFile('not_set.yml'),
|
||||
reason: "Failed to assert that the GUID file is not found.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'does not exist'
|
||||
);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'fff: {_]');
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Failed to parse GUIDs file'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'invalid');
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file is invalid.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'is not an array'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, 'version: 2.0');
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Failed to throw exception when the GUID file version is not supported.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'Unsupported file version'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'is empty', true),
|
||||
"Failed to assert that the GUID file is empty."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
$this->checkException(
|
||||
closure: function () use ($tmpFile) {
|
||||
file_put_contents($tmpFile, Yaml::dump(['guids' => []]));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
},
|
||||
reason: "Should throw an exception when there are no GUIDs mapping.",
|
||||
exception: InvalidArgumentException::class,
|
||||
exceptionMessage: 'does not contain any GUIDs mapping'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
try {
|
||||
file_put_contents($tmpFile, Yaml::dump(['guids' => ['guid_imdb' => 'tt1234567']]));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'Value must be an object', true),
|
||||
'Assert that GUID key is an array.'
|
||||
);
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump(['guids' => [['name' => 'imdb']]]));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, "name must start with 'guid_'", true),
|
||||
'Assert that GUID name starts with guid_'
|
||||
);
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump(['guids' => [['name' => 'guid_imdb', 'type' => INF]]]));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'type must be a string', true),
|
||||
'Assert guid type is string.'
|
||||
);
|
||||
|
||||
$yaml = [
|
||||
'guids' => [
|
||||
[
|
||||
'name' => 'guid_foobar',
|
||||
'type' => 'string',
|
||||
'validator' => []
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'validator key must be an object', true),
|
||||
'Assert validator key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator', ['pattern' => '\d']);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'validator.pattern is empty or invalid', true),
|
||||
'Assert a message is logged when the pattern is invalid.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator', ['pattern' => '/^\d+$/']);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'validator.example is empty or not a string', true),
|
||||
'Assert a message is logged when the example is empty or not a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator', ['pattern' => '/^\d+$/', 'example' => '(number)']);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'validator.tests key must be an object', true),
|
||||
'Assert a message is logged when the test key is not an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator.tests', ['valid' => 'foo']);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'validator.tests.valid key must be an array', true),
|
||||
'Assert a message is logged when the test key is not an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator.tests.valid', ['d12345678']);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'does not match pattern', true),
|
||||
'Assert a message is logged when valid test does not match the pattern.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator.tests', [
|
||||
'valid' => ['12345678'],
|
||||
'invalid' => 'foo',
|
||||
]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'validator.tests.invalid key must be an array', true),
|
||||
'Assert a message is logged when invalid test is not an array.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator.tests', [
|
||||
'valid' => ['12345678'],
|
||||
'invalid' => ['12345678'],
|
||||
]);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'validator.tests.invalid value', true),
|
||||
'Assert a message is logged when invalid test match the pattern.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'guids.0.validator.tests', [
|
||||
'valid' => ['12345678'],
|
||||
'invalid' => ['d12345678'],
|
||||
]);
|
||||
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Guid::parseGUIDFile($tmpFile);
|
||||
|
||||
$this->assertArrayHasKey(
|
||||
'guid_foobar',
|
||||
Guid::getValidators(),
|
||||
'Assert that the GUID is added to the validators.'
|
||||
);
|
||||
$this->assertArrayHasKey(
|
||||
'guid_foobar',
|
||||
Guid::getSupported(),
|
||||
'Assert that the GUID is added to the supported GUIDs.'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_reparse()
|
||||
{
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
$oldGuidFile = Config::get('guid.file');
|
||||
try {
|
||||
file_put_contents($tmpFile, "{'foo' => 'too' }");
|
||||
Config::save('guid.file', $tmpFile);
|
||||
Guid::setLogger($this->logger);
|
||||
Guid::reparse();
|
||||
Guid::getSupported();
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Error, 'Failed to read or parse', true),
|
||||
"Failed to assert that the GUID file is empty."
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
Config::save('guid.file', $oldGuidFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_getPointers()
|
||||
{
|
||||
$guid = Guid::fromArray(['guid_imdb' => 'tt1234567', 'guid_tvdb' => '123']);
|
||||
$this->assertEquals(
|
||||
['guid_imdb://tt1234567', 'guid_tvdb://123'],
|
||||
$guid->getPointers(),
|
||||
"Failed to assert that the GUID pointers are correct."
|
||||
);
|
||||
}
|
||||
|
||||
public function test_getValidators()
|
||||
{
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'guid');
|
||||
$oldGuidFile = Config::get('guid.file');
|
||||
|
||||
$yaml = [
|
||||
'guids' => [
|
||||
[
|
||||
'name' => 'guid_foobar',
|
||||
'type' => 'string',
|
||||
'validator' => [
|
||||
'pattern' => '/^\d+$/',
|
||||
'example' => '(number)',
|
||||
'tests' => [
|
||||
'valid' => ['12345678'],
|
||||
'invalid' => ['d12345678'],
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
try {
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
Config::save('guid.file', $tmpFile);
|
||||
Guid::reparse();
|
||||
$this->assertArrayHasKey(
|
||||
'guid_foobar',
|
||||
Guid::getValidators(),
|
||||
'Assert that the GUID is added to the validators.'
|
||||
);
|
||||
} finally {
|
||||
if (file_exists($tmpFile)) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
Config::save('guid.file', $oldGuidFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_guid_logger_from_container()
|
||||
{
|
||||
Guid::setLogger($this->logger);
|
||||
Guid::fromArray(['guid_tvdb' => INF]);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'external id. Unexpected value type.', true),
|
||||
"Assert message logged when the value type does not match the expected type."
|
||||
);
|
||||
}
|
||||
|
||||
public function test_guid_logger_from__constructor()
|
||||
{
|
||||
new Guid(['guid_tvdb' => INF], logger: $this->logger);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Info, 'external id. Unexpected value type.', true),
|
||||
"Assert message logged when the value type does not match the expected type."
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace Tests\Libs;
|
||||
use App\Libs\Entity\StateEntity;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Extends\LogMessageProcessor;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\TestCase;
|
||||
use Monolog\Handler\TestHandler;
|
||||
use Monolog\Logger;
|
||||
@@ -22,7 +23,9 @@ class StateEntityTest extends TestCase
|
||||
$this->testMovie = require __DIR__ . '/../Fixtures/MovieEntity.php';
|
||||
$this->testEpisode = require __DIR__ . '/../Fixtures/EpisodeEntity.php';
|
||||
$logger = new Logger('logger', processors: [new LogMessageProcessor()]);
|
||||
$logger->pushHandler(new TestHandler());
|
||||
$this->handler = new TestHandler();
|
||||
$logger->pushHandler($this->handler);
|
||||
Guid::setLogger($logger);
|
||||
}
|
||||
|
||||
public function test_init_bad_type(): void
|
||||
@@ -443,11 +446,13 @@ class StateEntityTest extends TestCase
|
||||
'rguid_tvdb://520/1/2',
|
||||
];
|
||||
|
||||
$po = $entity->getRelativePointers();
|
||||
$this->assertSame(
|
||||
$pointers,
|
||||
$entity->getRelativePointers(),
|
||||
$po,
|
||||
'When entity is episode, and has supported GUIDs, getRelativePointers() returns list of all supported GUIDs in format of rguid_<provider>://<id>/<season>/<episode>'
|
||||
);
|
||||
|
||||
$this->assertSame([],
|
||||
(new StateEntity($this->testMovie))->getRelativePointers(),
|
||||
'When entity is movie, getRelativePointers() returns empty array regardless.'
|
||||
|
||||
Reference in New Issue
Block a user