Allow adding jellyfin/emby backends via API.

This commit is contained in:
Abdulmhsen B. A. A
2024-04-20 20:57:52 +03:00
parent 7149540c74
commit 1ca6c1bc48
8 changed files with 177 additions and 31 deletions

View File

@@ -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);

View File

@@ -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 = []
) {
}
}

View File

@@ -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,
) {
}

View File

@@ -549,7 +549,7 @@ class EmbyClient implements iClient
*/
public function validateContext(Context $context): bool
{
return false;
return Container::get(EmbyValidateContext::class)($context);
}
/**

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Backends\Emby;
use App\Backends\Jellyfin\JellyfinValidateContext;
class EmbyValidateContext extends JellyfinValidateContext
{
}

View File

@@ -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(

View File

@@ -582,7 +582,7 @@ class JellyfinClient implements iClient
*/
public function validateContext(Context $context): bool
{
return false;
return Container::get(JellyfinValidateContext::class)($context);
}
/**

View 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
);
}
}
}