Massive API & WebUI changes.

This commit is contained in:
abdulmohsen
2024-05-04 19:09:19 +03:00
parent af41ac6171
commit 9d966b9b40
50 changed files with 1117 additions and 662 deletions

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\API\Backend;
use App\Libs\Attributes\Route\Post;
use App\Libs\DataUtil;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\HTTP_STATUS;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Throwable;
final class AccessToken
{
use APITraits;
public function __construct(private readonly iHttp $http)
{
}
#[Post(Index::URL . '/{name:backend}/accesstoken[/]', name: 'backend.accesstoken')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$data = DataUtil::fromArray($request->getParsedBody());
if (null === ($id = $data->get('id'))) {
return api_error('No id was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
$client = $this->getClient(name: $name);
$token = $client->getUserToken(
userId: $id,
username: $data->get('username', $client->getContext()->backendName . '_user'),
);
if (!is_string($token)) {
return api_error('Failed to generate access token.', HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
$arr = [
'token' => $token,
];
if ($data->get('username')) {
$arr['username'] = $data->get('username');
}
return api_response(HTTP_STATUS::HTTP_OK, $arr);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\API\Backend;
use App\Backends\Plex\PlexClient;
use App\Libs\Attributes\Route\Get;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\HTTP_STATUS;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Throwable;
final class Discover
{
use APITraits;
public function __construct(private readonly iHttp $http)
{
}
#[Get(Index::URL . '/{name:backend}/discover[/]', name: 'backend.discover')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
$client = $this->getClient(name: $name);
if (PlexClient::CLIENT_NAME !== $client->getType()) {
return api_error('Discover is only available for Plex backends.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
assert($client instanceof PlexClient);
$list = $client::discover($this->http, $client->getContext()->backendToken);
return api_response(HTTP_STATUS::HTTP_OK, ag($list, 'list', []));
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -27,11 +27,11 @@ final class Ignore
$this->file = new ConfigFile(Config::get('path') . '/config/ignore.yaml', type: 'yaml', autoCreate: true);
}
#[Get(Index::URL . '/{name:backend}/ignore[/]', name: 'backends.backend.ignoredIds')]
#[Get(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds')]
public function ignoredIds(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list = [];
@@ -61,22 +61,23 @@ final class Ignore
'created' => makeDate($date),
];
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$response = [
'ignore' => $list,
'links' => [
'self' => (string)$apiUrl,
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
return api_response(HTTP_STATUS::HTTP_OK, $list);
}
#[Delete(Index::URL . '/{name:backend}/ignore[/]', name: 'backends.backend.ignoredIds.delete')]
#[Delete(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds.delete')]
public function deleteRule(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$data = $this->getBackends(name: $name);
if (empty($data)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$params = DataUtil::fromRequest($request);
if (null === ($rule = $params->get('rule'))) {
@@ -98,11 +99,11 @@ final class Ignore
return api_response(HTTP_STATUS::HTTP_OK);
}
#[Post(Index::URL . '/{name:backend}/ignore[/]', name: 'backends.backend.ignoredIds.add')]
#[Post(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds.add')]
public function addRule(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$data = $this->getBackends(name: $name);

View File

@@ -16,11 +16,11 @@ final class Index
public const string URL = '%{api.prefix}/backend';
#[Get(self::URL . '/{name:backend}[/]', name: 'backends.view')]
#[Get(self::URL . '/{name:backend}[/]', name: 'backend.view')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$data = $this->getBackends(name: $name);
@@ -29,17 +29,8 @@ final class Index
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$data = array_pop($data);
$response = [
...$data,
'links' => [
'self' => (string)$apiUrl,
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
];
return api_response(HTTP_STATUS::HTTP_OK, ['backend' => $response]);
return api_response(HTTP_STATUS::HTTP_OK, $data);
}
}

View File

@@ -18,11 +18,11 @@ final class Info
{
use APITraits;
#[Get(Index::URL . '/{name:backend}/info[/]', name: 'backends.backend.info')]
public function backendsView(iRequest $request, array $args = []): iResponse
#[Get(Index::URL . '/{name:backend}/info[/]', name: 'backend.info')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
@@ -40,20 +40,9 @@ final class Info
try {
$data = $client->getInfo($opts);
return api_response(HTTP_STATUS::HTTP_OK, $data);
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$response = [
'data' => $data,
'links' => [
'self' => (string)$apiUrl,
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
}
}

View File

@@ -2,32 +2,51 @@
declare(strict_types=1);
namespace App\API\Backend\Library;
namespace App\API\Backend;
use App\API\Backend\Index as BackendsIndex;
use App\Libs\Attributes\Route\Get;
use App\Libs\Attributes\Route\Route;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\DataUtil;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\HTTP_STATUS;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Throwable;
final class Ignore
final class Library
{
use APITraits;
#[Route(['POST', 'DELETE'], BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backends.library.ignore')]
public function _invoke(iRequest $request, array $args = []): iResponse
#[Get(BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backend.library')]
public function listLibraries(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (null === ($id = DataUtil::fromRequest($request, true)->get('id', null))) {
return api_error('No library id was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
try {
$client = $this->getClient(name: $name);
return api_response(HTTP_STATUS::HTTP_OK, $client->listLibraries());
} catch (RuntimeException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route(['POST', 'DELETE'], BackendsIndex::URL . '/{name:backend}/library/{id}[/]', name: 'backend.library.ignore')]
public function ignoreLibrary(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (null === ($id = ag($args, 'id'))) {
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$remove = 'DELETE' === $request->getMethod();
@@ -75,13 +94,6 @@ final class Ignore
$config->set("{$name}.options." . Options::IGNORE, implode(',', array_values($ignoreIds)))->persist();
return api_response(HTTP_STATUS::HTTP_OK, [
'type' => $config->get("{$name}.type"),
'libraries' => $libraries,
'links' => [
'self' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}/library"),
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
],
]);
return api_response(HTTP_STATUS::HTTP_OK, $libraries);
}
}

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\API\Backend\Library;
use App\API\Backend\Index as BackendsIndex;
use App\Libs\Attributes\Route\Get;
use App\Libs\Config;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\HTTP_STATUS;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
final class Index
{
use APITraits;
#[Get(BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backends.library.list')]
public function listLibraries(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
$client = $this->getClient(name: $name);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
}
$response = [
'type' => ag(array_flip(Config::get('supported')), $client::class),
'libraries' => $client->listLibraries(),
'links' => [
'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''),
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
}
}

View File

@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\API\Backend\Library;
namespace App\API\Backend;
use App\API\Backend\Index as BackendsIndex;
use App\API\Backend\Index as backendIndex;
use App\Commands\Backend\Library\MismatchCommand;
use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil;
@@ -19,11 +19,11 @@ final class Mismatched
{
use APITraits;
#[Get(BackendsIndex::URL . '/{name:backend}/mismatched[/[{id}[/]]]', name: 'backends.library.mismatched')]
public function listLibraries(iRequest $request, array $args = []): iResponse
#[Get(backendIndex::URL . '/{name:backend}/mismatched[/[{id}[/]]]', name: 'backend.mismatched')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$params = DataUtil::fromArray($request->getQueryParams());
@@ -74,15 +74,6 @@ final class Mismatched
$list[] = $processed;
}
}
$response = [
'items' => $list,
'links' => [
'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''),
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
return api_response(HTTP_STATUS::HTTP_OK, $list);
}
}

171
src/API/Backend/Option.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace App\API\Backend;
use App\Libs\Attributes\Route\Delete;
use App\Libs\Attributes\Route\Get;
use App\Libs\Attributes\Route\Patch;
use App\Libs\Attributes\Route\Post;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\DataUtil;
use App\Libs\HTTP_STATUS;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
final class Option
{
use APITraits;
#[Get(Index::URL . '/{name:backend}/option/{option}[/]', name: 'backend.option')]
public function viewOption(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (null === ($option = ag($args, 'option'))) {
return api_error('Invalid value for option path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
if (false === $list->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$key = $name . '.options.' . $option;
if (false === $list->has($key)) {
return api_error(r("Option '{option}' not found in backend '{name}'.", [
'option' => $option,
'name' => $name
]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$value = $list->get($key);
return api_response(HTTP_STATUS::HTTP_OK, [
'key' => $option,
'value' => $value,
'type' => get_debug_type($value),
]);
}
#[Post(Index::URL . '/{name:backend}/option[/]', name: 'backend.option.add')]
public function addOption(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
if (false === $list->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$data = DataUtil::fromRequest($request);
if (null === ($option = $data->get('key'))) {
return api_error('Invalid value for key.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$spec = require __DIR__ . '/../../../config/backend.spec.php';
$found = false;
foreach ($spec as $supportedKey => $_) {
if (str_ends_with($supportedKey, 'options.' . $option)) {
$found = true;
break;
}
}
if (false === $found) {
return api_error(r("Option '{key}' is not supported.", ['key' => $option]), HTTP_STATUS::HTTP_BAD_REQUEST);
}
$value = $data->get('value');
$list->set($name . '.options.' . $option, $value)->persist();
return api_response(HTTP_STATUS::HTTP_OK, [
'key' => $option,
'value' => $value,
'type' => get_debug_type($value),
]);
}
#[Patch(Index::URL . '/{name:backend}/option/{option}[/]', name: 'backend.option.update')]
public function updateOption(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (null === ($option = ag($args, 'option'))) {
return api_error('Invalid value for option parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
if (false === $list->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$key = $name . '.options.' . $option;
if (false === $list->has($key)) {
return api_error(r("Option '{option}' not found in backend '{name}'.", [
'option' => $option,
'name' => $name
]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$data = DataUtil::fromRequest($request);
if (null === ($value = $data->get('value'))) {
return api_error(r("No value was provided for '{key}'.", [
'key' => $key,
]), HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list->set($key, $value)->persist();
return api_response(HTTP_STATUS::HTTP_OK, [
'key' => $option,
'value' => $value,
'type' => get_debug_type($value),
]);
}
#[Delete(Index::URL . '/{name:backend}/option/{option}[/]', name: 'backend.option.delete')]
public function deleteOption(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (null === ($option = ag($args, 'option'))) {
return api_error('Invalid value for option option parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
if (false === $list->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$key = $name . '.options.' . $option;
$value = $list->get($key);
$list->delete($key)->persist();
return api_response(HTTP_STATUS::HTTP_OK, [
'key' => $option,
'value' => $value,
'type' => get_debug_type($value),
]);
}
}

View File

@@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
namespace App\API\Backend;
use App\Libs\Attributes\Route\Patch;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\HTTP_STATUS;
use App\Libs\Traits\APITraits;
use JsonException;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
final class PartialUpdate
{
use APITraits;
#[Patch(Index::URL . '/{name:backend}[/]', name: 'backends.view')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
if (false === $list->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
try {
$data = json_decode((string)$request->getBody(), true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
return api_error(r('Invalid JSON data. {error}', ['error' => $e->getMessage()]),
HTTP_STATUS::HTTP_BAD_REQUEST);
}
foreach ($data as $update) {
if (!ag_exists($update, 'key')) {
return api_error('No key to update was present.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$list->set($name . '.' . ag($update, 'key'), ag($update, 'value'));
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$list->persist();
$backend = $this->getBackends(name: $name);
if (empty($backend)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$backend = array_pop($backend);
return api_response(HTTP_STATUS::HTTP_OK, [
'backend' => array_filter(
$backend,
fn($key) => false === in_array($key, ['options', 'webhook'], true),
ARRAY_FILTER_USE_KEY
),
'links' => [
'self' => (string)$apiUrl,
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
]);
}
}

View File

@@ -17,8 +17,8 @@ final class Search
{
use APITraits;
#[Get(Index::URL . '/{name:backend}/search[/[{id}[/]]]', name: 'backends.backend.search.id')]
public function searchById(iRequest $request, array $args = []): iResponse
#[Get(Index::URL . '/{name:backend}/search[/[{id}[/]]]', name: 'backend.search')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
@@ -55,14 +55,8 @@ final class Search
]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$response = [
'results' => $id ? [$data] : $data,
'links' => [
'self' => (string)$apiUrl,
'backend' => (string)$apiUrl->withPath(parseConfigValue(Index::URL . '/' . $name)),
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
'options' => [
'raw' => (bool)$params->get('raw', false),
],

View File

@@ -18,11 +18,11 @@ final class Sessions
{
use APITraits;
#[Get(Index::URL . '/{name:backend}/sessions[/]', name: 'backends.backend.sessions')]
public function backendsView(iRequest $request, array $args = []): iResponse
#[Get(Index::URL . '/{name:backend}/sessions[/]', name: 'backend.sessions')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
@@ -40,20 +40,9 @@ final class Sessions
try {
$sessions = $client->getSessions($opts);
return api_response(HTTP_STATUS::HTTP_OK, ag($sessions, 'sessions', []));
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$response = [
'sessions' => ag($sessions, 'sessions', []),
'links' => [
'self' => (string)$apiUrl,
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
}
}

View File

@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\API\Backend\Library;
namespace App\API\Backend;
use App\API\Backend\Index as BackendsIndex;
use App\API\Backend\Index as backendIndex;
use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil;
use App\Libs\Exceptions\RuntimeException;
@@ -18,11 +18,11 @@ final class Unmatched
{
use APITraits;
#[Get(BackendsIndex::URL . '/{name:backend}/unmatched[/[{id}[/]]]', name: 'backends.library.unmatched')]
public function listLibraries(iRequest $request, array $args = []): iResponse
#[Get(backendIndex::URL . '/{name:backend}/unmatched[/[{id}[/]]]', name: 'backend.unmatched')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
$params = DataUtil::fromArray($request->getQueryParams());
@@ -63,14 +63,6 @@ final class Unmatched
}
}
$response = [
'items' => $list,
'links' => [
'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''),
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
return api_response(HTTP_STATUS::HTTP_OK, $list);
}
}

144
src/API/Backend/Update.php Normal file
View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\API\Backend;
use App\Backends\Common\ClientInterface as iClient;
use App\Libs\Attributes\Route\Patch;
use App\Libs\Attributes\Route\Put;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\DataUtil;
use App\Libs\HTTP_STATUS;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use JsonException;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
final class Update
{
use APITraits;
private ConfigFile $backendFile;
public function __construct()
{
$this->backendFile = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
}
#[Put(Index::URL . '/{name:backend}[/]', name: 'backend.update')]
public function update(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (false === $this->backendFile->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$this->backendFile->set(
$name,
$this->fromRequest($this->backendFile->get($name), $request, $this->getClient($name))
)->persist();
$backend = $this->getBackends(name: $name);
if (empty($backend)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$backend = array_pop($backend);
return api_response(HTTP_STATUS::HTTP_OK, $backend);
}
#[Patch(Index::URL . '/{name:backend}[/]', name: 'backend.patch')]
public function patchUpdate(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (false === $this->backendFile->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
try {
$data = json_decode((string)$request->getBody(), true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
return api_error(r('Invalid JSON data. {error}', ['error' => $e->getMessage()]),
HTTP_STATUS::HTTP_BAD_REQUEST);
}
$spec = require __DIR__ . '/../../../config/backend.spec.php';
foreach ($data as $update) {
$key = ag($update, 'key');
$value = ag($update, 'value');
if (null === $key) {
return api_error('No key to update was present.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (false === in_array($key, $spec, true)) {
return api_error(r('Invalid key to update: {key}', ['key' => $key]), HTTP_STATUS::HTTP_BAD_REQUEST);
}
$this->backendFile->set("{$name}.{$key}", $value);
}
$this->backendFile->persist();
$backend = $this->getBackends(name: $name);
if (empty($backend)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
}
$backend = array_pop($backend);
return api_response(HTTP_STATUS::HTTP_OK, $backend);
}
private function fromRequest(array $config, iRequest $request, iClient $client): array
{
$data = DataUtil::fromArray($request->getParsedBody());
$newData = [
'url' => $data->get('url'),
'token' => $data->get('token'),
'user' => $data->get('user'),
'uuid' => $data->get('uuid'),
'export' => [
'enabled' => (bool)$data->get('export.enabled', false),
],
'import' => [
'enabled' => (bool)$data->get('import.enabled', false),
],
'webhook' => [
'match' => [
'user' => (bool)$data->get('webhook.match.user'),
'uuid' => (bool)$data->get('webhook.match.uuid'),
],
],
];
$optionals = [
Options::DUMP_PAYLOAD => 'bool',
Options::LIBRARY_SEGMENT => 'int',
Options::IGNORE => 'string',
];
foreach ($optionals as $key => $type) {
if (null !== ($value = $data->get('options.' . $key))) {
settype($value, $type);
$newData = ag_set($newData, "options.{$key}", $value);
}
}
return deepArrayMerge([$config, $client->fromRequest($newData, $request)]);
}
}

View File

@@ -18,19 +18,13 @@ final class Users
{
use APITraits;
#[Get(Index::URL . '/{name:backend}/users[/]', name: 'backends.backend.users')]
public function backendsView(iRequest $request, array $args = []): iResponse
#[Get(Index::URL . '/{name:backend}/users[/]', name: 'backend.users')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
$client = $this->getClient(name: $name);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
}
$opts = [];
$params = DataUtil::fromRequest($request, true);
@@ -43,21 +37,11 @@ final class Users
}
try {
$users = $client->getUsersList($opts);
return api_response(HTTP_STATUS::HTTP_OK, $this->getClient(name: $name)->getUsersList($opts));
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$response = [
'users' => $users,
'links' => [
'self' => (string)$apiUrl,
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
}
}

View File

@@ -5,10 +5,8 @@ declare(strict_types=1);
namespace App\API\Backend;
use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\HTTP_STATUS;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
@@ -18,42 +16,19 @@ final class Version
{
use APITraits;
#[Get(Index::URL . '/{name:backend}/version[/]', name: 'backends.backend.info')]
public function backendsView(iRequest $request, array $args = []): iResponse
#[Get(Index::URL . '/{name:backend}/version[/]', name: 'backend.version')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
$client = $this->getClient(name: $name);
return api_response(HTTP_STATUS::HTTP_OK, ['version' => $this->getClient(name: $name)->getVersion()]);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
}
$opts = [];
$params = DataUtil::fromRequest($request, true);
if (true === (bool)$params->get('raw', false)) {
$opts[Options::RAW_RESPONSE] = true;
}
try {
$version = $client->getVersion($opts);
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$response = [
'version' => $version,
'links' => [
'self' => (string)$apiUrl,
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
],
];
return api_response(HTTP_STATUS::HTTP_OK, $response);
}
}

View File

@@ -27,20 +27,20 @@ final class Webhooks
{
use APITraits;
private iLogger $accessLog;
private iLogger $logfile;
public function __construct(private iCache $cache)
{
$this->accessLog = new Logger(name: 'http', processors: [new LogMessageProcessor()]);
$this->logfile = new Logger(name: 'webhook', processors: [new LogMessageProcessor()]);
$level = Config::get('webhook.debug') ? Level::Debug : Level::Info;
if (null !== ($logfile = Config::get('webhook.logfile'))) {
$this->accessLog = $this->accessLog->pushHandler(new StreamHandler($logfile, $level, true));
$this->logfile = $this->logfile->pushHandler(new StreamHandler($logfile, $level, true));
}
if (true === inContainer()) {
$this->accessLog->pushHandler(new StreamHandler('php://stderr', $level, true));
$this->logfile->pushHandler(new StreamHandler('php://stderr', $level, true));
}
}
@@ -53,13 +53,27 @@ final class Webhooks
* @return iResponse The response object.
* @throws InvalidArgumentException if cache key is invalid.
*/
#[Route(['POST', 'PUT'], Index::URL . '/{name:backend}/webhook[/]', name: 'webhooks.receive')]
#[Route(['POST', 'PUT'], Index::URL . '/{name:backend}/webhook[/]', name: 'backend.webhook')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
return $this->process($name, $request)->withHeader('X-Log-Response', '0');
}
/**
* Process the incoming webhook request.
*
* @param string $name The backend name.
* @param iRequest $request The incoming request object.
*
* @return iResponse The response object.
* @throws InvalidArgumentException if cache key is invalid.
*/
private function process(string $name, iRequest $request): iResponse
{
try {
$backend = $this->getBackends(name: $name);
if (empty($backend)) {
@@ -84,7 +98,7 @@ final class Webhooks
if (null === ($requestUser = ag($attr, 'user.id'))) {
$message = "Request payload didn't contain a user id. Backend requires a user check.";
$this->write($request, Level::Info, $message);
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (false === hash_equals((string)$userId, (string)$requestUser)) {
@@ -93,7 +107,7 @@ final class Webhooks
'config_user' => $userId,
]);
$this->write($request, Level::Info, $message);
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
}
}
@@ -101,7 +115,7 @@ final class Webhooks
if (null === ($requestBackendId = ag($attr, 'backend.id'))) {
$message = "Request payload didn't contain the backend unique id.";
$this->write($request, Level::Info, $message);
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
}
if (false === hash_equals((string)$uuid, (string)$requestBackendId)) {
@@ -110,7 +124,7 @@ final class Webhooks
'config_uid' => $uuid,
]);
$this->write($request, Level::Info, $message);
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
}
}
@@ -128,7 +142,7 @@ final class Webhooks
'backend' => $client->getName(),
]), forceContext: true);
return $response->withHeader('X-Log-Response', '0');
return $response;
}
$entity = $client->parseWebhook($request);
@@ -151,7 +165,7 @@ final class Webhooks
]
);
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED)->withHeader('X-Log-Response', '0');
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED);
}
if ((0 === (int)$entity->episode || null === $entity->season) && $entity->isEpisode()) {
@@ -170,7 +184,7 @@ final class Webhooks
]
);
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED)->withHeader('X-Log-Response', '0');
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED);
}
$items = $this->cache->get('requests', []);
@@ -211,7 +225,7 @@ final class Webhooks
]
);
return api_response(HTTP_STATUS::HTTP_OK)->withHeader('X-Log-Response', '0');
return api_response(HTTP_STATUS::HTTP_OK);
}
/**
@@ -257,9 +271,9 @@ final class Webhooks
}
if (true === (Config::get('logs.context') || $forceContext)) {
$this->accessLog->log($level, $message, $context);
$this->logfile->log($level, $message, $context);
} else {
$this->accessLog->log($level, r($message, $context));
$this->logfile->log($level, r($message, $context));
}
}
}