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);
|
return api_error('No type was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$type = strtolower($type);
|
||||||
|
|
||||||
if (null === ($name = $data->get('name'))) {
|
if (null === ($name = $data->get('name'))) {
|
||||||
return api_error('No name was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
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);
|
$backend = $this->getBackends(name: $name);
|
||||||
|
|
||||||
if (!empty($backend)) {
|
if (!empty($backend)) {
|
||||||
@@ -56,10 +62,6 @@ final class Add
|
|||||||
]), HTTP_STATUS::HTTP_CONFLICT);
|
]), 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'))) {
|
if (null === ($url = $data->get('url'))) {
|
||||||
return api_error('No url was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
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))) {
|
if (null === ($class = Config::get("supported.{$type}", null))) {
|
||||||
throw api_error(r("Unexpected client type '{type}' was given.", [
|
return api_error(r("Unexpected client type '{type}' was given.", ['type' => $type]),
|
||||||
'type' => $type
|
HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||||
]), HTTP_STATUS::HTTP_BAD_REQUEST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$instance = Container::getNew($class);
|
$instance = Container::getNew($class);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace App\Backends\Common;
|
|||||||
|
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
class Context
|
readonly class Context
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Make backend context for classes to work with.
|
* Make backend context for classes to work with.
|
||||||
@@ -23,16 +23,16 @@ class Context
|
|||||||
* @param array $options optional options.
|
* @param array $options optional options.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $clientName,
|
public string $clientName,
|
||||||
public readonly string $backendName,
|
public string $backendName,
|
||||||
public readonly UriInterface $backendUrl,
|
public UriInterface $backendUrl,
|
||||||
public readonly Cache $cache,
|
public Cache $cache,
|
||||||
public readonly string|int|null $backendId = null,
|
public string|int|null $backendId = null,
|
||||||
public readonly string|int|null $backendToken = null,
|
public string|int|null $backendToken = null,
|
||||||
public readonly string|int|null $backendUser = null,
|
public string|int|null $backendUser = null,
|
||||||
public readonly array $backendHeaders = [],
|
public array $backendHeaders = [],
|
||||||
public readonly bool $trace = false,
|
public bool $trace = false,
|
||||||
public readonly array $options = []
|
public array $options = []
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace App\Backends\Common;
|
|||||||
use Stringable;
|
use Stringable;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class Error implements Stringable
|
final readonly class Error implements Stringable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Wrap error in easy to consume way.
|
* Wrap error in easy to consume way.
|
||||||
@@ -18,10 +18,10 @@ final class Error implements Stringable
|
|||||||
* @param Throwable|null $previous Previous exception stack trace.
|
* @param Throwable|null $previous Previous exception stack trace.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $message,
|
public string $message,
|
||||||
public readonly array $context = [],
|
public array $context = [],
|
||||||
public readonly Levels $level = Levels::ERROR,
|
public Levels $level = Levels::ERROR,
|
||||||
public readonly Throwable|null $previous = null,
|
public Throwable|null $previous = null,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ class EmbyClient implements iClient
|
|||||||
*/
|
*/
|
||||||
public function validateContext(Context $context): bool
|
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\Backends\Common\Response;
|
||||||
use App\Libs\Options;
|
use App\Libs\Options;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface as iLogger;
|
||||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class GetUsersList
|
* Class GetUsersList
|
||||||
@@ -32,10 +32,10 @@ class GetUsersList
|
|||||||
/**
|
/**
|
||||||
* Class Constructor.
|
* Class Constructor.
|
||||||
*
|
*
|
||||||
* @param HttpClientInterface $http The HTTP client instance.
|
* @param iHttp $http The HTTP client instance.
|
||||||
* @param LoggerInterface $logger The logger 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,
|
'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()) {
|
if (200 !== $response->getStatusCode()) {
|
||||||
return new Response(
|
return new Response(
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ class JellyfinClient implements iClient
|
|||||||
*/
|
*/
|
||||||
public function validateContext(Context $context): bool
|
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