Allow adding jellyfin/emby backends via API.
This commit is contained in:
@@ -44,10 +44,16 @@ final class Add
|
||||
return api_error('No type was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$type = strtolower($type);
|
||||
|
||||
if (null === ($name = $data->get('name'))) {
|
||||
return api_error('No name was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === isValidName($name)) {
|
||||
return api_error('Invalid name was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$backend = $this->getBackends(name: $name);
|
||||
|
||||
if (!empty($backend)) {
|
||||
@@ -56,10 +62,6 @@ final class Add
|
||||
]), HTTP_STATUS::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
if (false === isValidName($name)) {
|
||||
return api_error('Invalid name was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === ($url = $data->get('url'))) {
|
||||
return api_error('No url was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
@@ -73,9 +75,8 @@ final class Add
|
||||
}
|
||||
|
||||
if (null === ($class = Config::get("supported.{$type}", null))) {
|
||||
throw api_error(r("Unexpected client type '{type}' was given.", [
|
||||
'type' => $type
|
||||
]), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error(r("Unexpected client type '{type}' was given.", ['type' => $type]),
|
||||
HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$instance = Container::getNew($class);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Backends\Common;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
class Context
|
||||
readonly class Context
|
||||
{
|
||||
/**
|
||||
* Make backend context for classes to work with.
|
||||
@@ -23,16 +23,16 @@ class Context
|
||||
* @param array $options optional options.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $clientName,
|
||||
public readonly string $backendName,
|
||||
public readonly UriInterface $backendUrl,
|
||||
public readonly Cache $cache,
|
||||
public readonly string|int|null $backendId = null,
|
||||
public readonly string|int|null $backendToken = null,
|
||||
public readonly string|int|null $backendUser = null,
|
||||
public readonly array $backendHeaders = [],
|
||||
public readonly bool $trace = false,
|
||||
public readonly array $options = []
|
||||
public string $clientName,
|
||||
public string $backendName,
|
||||
public UriInterface $backendUrl,
|
||||
public Cache $cache,
|
||||
public string|int|null $backendId = null,
|
||||
public string|int|null $backendToken = null,
|
||||
public string|int|null $backendUser = null,
|
||||
public array $backendHeaders = [],
|
||||
public bool $trace = false,
|
||||
public array $options = []
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Backends\Common;
|
||||
use Stringable;
|
||||
use Throwable;
|
||||
|
||||
final class Error implements Stringable
|
||||
final readonly class Error implements Stringable
|
||||
{
|
||||
/**
|
||||
* Wrap error in easy to consume way.
|
||||
@@ -18,10 +18,10 @@ final class Error implements Stringable
|
||||
* @param Throwable|null $previous Previous exception stack trace.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $message,
|
||||
public readonly array $context = [],
|
||||
public readonly Levels $level = Levels::ERROR,
|
||||
public readonly Throwable|null $previous = null,
|
||||
public string $message,
|
||||
public array $context = [],
|
||||
public Levels $level = Levels::ERROR,
|
||||
public Throwable|null $previous = null,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -549,7 +549,7 @@ class EmbyClient implements iClient
|
||||
*/
|
||||
public function validateContext(Context $context): bool
|
||||
{
|
||||
return false;
|
||||
return Container::get(EmbyValidateContext::class)($context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
11
src/Backends/Emby/EmbyValidateContext.php
Normal file
11
src/Backends/Emby/EmbyValidateContext.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Backends\Emby;
|
||||
|
||||
use App\Backends\Jellyfin\JellyfinValidateContext;
|
||||
|
||||
class EmbyValidateContext extends JellyfinValidateContext
|
||||
{
|
||||
}
|
||||
@@ -11,9 +11,9 @@ use App\Backends\Common\Levels;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Libs\Options;
|
||||
use JsonException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LoggerInterface as iLogger;
|
||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
||||
|
||||
/**
|
||||
* Class GetUsersList
|
||||
@@ -32,10 +32,10 @@ class GetUsersList
|
||||
/**
|
||||
* Class Constructor.
|
||||
*
|
||||
* @param HttpClientInterface $http The HTTP client instance.
|
||||
* @param LoggerInterface $logger The logger instance.
|
||||
* @param iHttp $http The HTTP client instance.
|
||||
* @param iLogger $logger The logger instance.
|
||||
*/
|
||||
public function __construct(protected HttpClientInterface $http, protected LoggerInterface $logger)
|
||||
public function __construct(protected iHttp $http, protected iLogger $logger)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -71,7 +71,17 @@ class GetUsersList
|
||||
'url' => (string)$url,
|
||||
]);
|
||||
|
||||
$response = $this->http->request('GET', (string)$url, $context->backendHeaders);
|
||||
$headers = $context->backendHeaders;
|
||||
|
||||
if (empty($headers)) {
|
||||
$headers = [
|
||||
'headers' => [
|
||||
'X-MediaBrowser-Token' => $context->backendToken,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$response = $this->http->request('GET', (string)$url, $headers);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
return new Response(
|
||||
|
||||
@@ -582,7 +582,7 @@ class JellyfinClient implements iClient
|
||||
*/
|
||||
public function validateContext(Context $context): bool
|
||||
{
|
||||
return false;
|
||||
return Container::get(JellyfinValidateContext::class)($context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
124
src/Backends/Jellyfin/JellyfinValidateContext.php
Normal file
124
src/Backends/Jellyfin/JellyfinValidateContext.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Backends\Jellyfin;
|
||||
|
||||
use App\Backends\Common\Context;
|
||||
use App\Backends\Jellyfin\Action\GetUsersList;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Exceptions\Backends\InvalidContextException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
||||
|
||||
class JellyfinValidateContext
|
||||
{
|
||||
public function __construct(private readonly iHttp $http)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backend context.
|
||||
*
|
||||
* @param Context $context Backend context.
|
||||
* @throws InvalidContextException on failure.
|
||||
*/
|
||||
public function __invoke(Context $context): bool
|
||||
{
|
||||
$data = json_decode($this->validateUrl($context), true);
|
||||
$backendId = ag($data, 'Id');
|
||||
|
||||
if (empty($backendId)) {
|
||||
throw new InvalidContextException('Failed to get backend id.');
|
||||
}
|
||||
|
||||
if (null !== $context->backendId && $backendId !== $context->backendId) {
|
||||
throw new InvalidContextException(
|
||||
r("Backend id mismatch. Expected '{expected}', server responded with '{actual}'.", [
|
||||
'expected' => $context->backendId,
|
||||
'actual' => $backendId,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$action = Container::get(GetUsersList::class)($context);
|
||||
if ($action->hasError()) {
|
||||
throw new InvalidContextException(r('Failed to get user info. {error}', [
|
||||
'error' => $action->error->format()
|
||||
]));
|
||||
}
|
||||
|
||||
$found = false;
|
||||
$list = [];
|
||||
|
||||
foreach ($action->response as $user) {
|
||||
$list[ag($user, 'name')] = ag($user, 'id');
|
||||
if ((string)ag($user, 'id') === (string)$context->backendUser) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === $found) {
|
||||
throw new InvalidContextException(
|
||||
r("User id '{uid}' was not found in list of users. '{user_list}'.", [
|
||||
'uid' => $context->backendUser,
|
||||
'actual' => arrayToString($list),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backend url.
|
||||
*
|
||||
* @param Context $context
|
||||
*
|
||||
* @return string
|
||||
* @throws InvalidContextException
|
||||
*/
|
||||
private function validateUrl(Context $context): string
|
||||
{
|
||||
try {
|
||||
$url = $context->backendUrl->withPath('/system/Info');
|
||||
$request = $this->http->request('GET', (string)$url, [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'X-MediaBrowser-Token' => $context->backendToken,
|
||||
],
|
||||
]);
|
||||
|
||||
if (HTTP_STATUS::HTTP_UNAUTHORIZED->value === $request->getStatusCode()) {
|
||||
throw new InvalidContextException('Backend responded with 401. Most likely means token is invalid.');
|
||||
}
|
||||
|
||||
if (HTTP_STATUS::HTTP_NOT_FOUND->value === $request->getStatusCode()) {
|
||||
throw new InvalidContextException('Backend responded with 404. Most likely means url is incorrect.');
|
||||
}
|
||||
|
||||
return $request->getContent(true);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new InvalidContextException(r('Failed to connect to backend. {error}', ['error' => $e->getMessage()]),
|
||||
previous: $e);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
throw new InvalidContextException(r('Got non 200 response. {error}', ['error' => $e->getMessage()]),
|
||||
previous: $e);
|
||||
} catch (RedirectionExceptionInterface $e) {
|
||||
throw new InvalidContextException(
|
||||
r('Redirection recursion detected. {error}', ['error' => $e->getMessage()]),
|
||||
previous: $e
|
||||
);
|
||||
} catch (ServerExceptionInterface $e) {
|
||||
throw new InvalidContextException(
|
||||
r('Backend responded with 5xx error. {error}', ['error' => $e->getMessage()]),
|
||||
previous: $e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user