diff --git a/FAQ.md b/FAQ.md index 30d1c13d..b597e78c 100644 --- a/FAQ.md +++ b/FAQ.md @@ -341,10 +341,10 @@ $ docker exec -ti watchstate console system:tasks ### How to add webhooks? -The Webhook URL is backend specific, the request path is `/v1/api/backends/[BACKEND_NAME]/webhook?apikey=[APIKEY]`, +The Webhook URL is backend specific, the request path is `/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]`, Where `[BACKEND_NAME]` is the name of the backend you want to add webhook for, and `[APIKEY]` is the global api key which you can get via the `system:apikey` command. Typically, the full path -is `http://localhost:8080/v1/api/backends/[BACKEND_NAME]/webhook?apikey=[APIKEY]`. if the tool +is `http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]`. if the tool port is directly exposed or via the reverse proxy you have setup. If your media backend support sending headers then remove query parameter `?apikey=[APIKEY]`, and add this header @@ -371,7 +371,7 @@ Go to your Manage Emby Server > Server > Webhooks > (Click Add Webhook) ##### Webhook/Notifications URL: -`http://localhost:8080/v1/api/backends/[BACKEND_NAME]/webhook?apikey=[APIKEY]` +`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]` * Replace `[BACKEND_NAME]` with the name you have chosen for your backend. * Replace `[APIKEY]` with the global apikey. @@ -412,7 +412,7 @@ Go to your Plex Web UI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK ##### URL: -`http://localhost:8080/v1/api/backends/[BACKEND_NAME]/webhook?apikey=[APIKEY]` +`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]` * Replace `[BACKEND_NAME]` with the name you have chosen for your backend. * Replace `[APIKEY]` with the global apikey. @@ -441,7 +441,7 @@ go back again to dashboard > plugins > webhook. Add `Add Generic Destination`, ##### Webhook Url: -`http://localhost:8080/v1/api/backends/[BACKEND_NAME]/webhook` +`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook` * Replace `[BACKEND_NAME]` with the name you have chosen for your backend. diff --git a/README.md b/README.md index 7c23be43..636320ed 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,12 @@ out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers. ### 2024-04-30 - [BREAKING CHANGE] -We are going to retire the old webhooks endpoint, please refer to the [FAQ](FAQ.md#how-to-add-webhooks) to know how to update -to the new API endpoint. We are going to include `WebUI` for alpha testing after two weeks from today `2024-05-15`. Which most likely means the old webhooks -endpoint will be removed. We will try to preseve the old endpoint for a while, but it's not guaranteed we will be able to. +We are going to retire the old webhooks endpoint, please refer to the [FAQ](FAQ.md#how-to-add-webhooks) to know how to +update +to the new API endpoint. We are going to include `WebUI` for alpha testing after two weeks from today `2024-05-15`. +Which most likely means the old webhooks +endpoint will be removed. We will try to preserve the old endpoint for a while, but it's not guaranteed we will be able +to. Refer to [NEWS](NEWS.md) for old updates. diff --git a/frontend/pages/backends/[backend]/edit.vue b/frontend/pages/backends/[backend]/edit.vue new file mode 100644 index 00000000..65a25503 --- /dev/null +++ b/frontend/pages/backends/[backend]/edit.vue @@ -0,0 +1,130 @@ + + + diff --git a/src/API/Backends/Ignore.php b/src/API/Backend/Ignore.php similarity index 99% rename from src/API/Backends/Ignore.php rename to src/API/Backend/Ignore.php index 7e818840..d427b823 100644 --- a/src/API/Backends/Ignore.php +++ b/src/API/Backend/Ignore.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\API\Backends; +namespace App\API\Backend; use App\Libs\Attributes\Route\Delete; use App\Libs\Attributes\Route\Get; diff --git a/src/API/Backend/Index.php b/src/API/Backend/Index.php new file mode 100644 index 00000000..baa5d34e --- /dev/null +++ b/src/API/Backend/Index.php @@ -0,0 +1,45 @@ +getBackends(name: $name); + + if (empty($data)) { + 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]); + } +} diff --git a/src/API/Backends/Info.php b/src/API/Backend/Info.php similarity index 98% rename from src/API/Backends/Info.php rename to src/API/Backend/Info.php index f8faaa76..53dda85d 100644 --- a/src/API/Backends/Info.php +++ b/src/API/Backend/Info.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\API\Backends; +namespace App\API\Backend; use App\Libs\Attributes\Route\Get; use App\Libs\DataUtil; diff --git a/src/API/Backends/Library/Ignore.php b/src/API/Backend/Library/Ignore.php similarity index 97% rename from src/API/Backends/Library/Ignore.php rename to src/API/Backend/Library/Ignore.php index 699b0ef4..5a183065 100644 --- a/src/API/Backends/Library/Ignore.php +++ b/src/API/Backend/Library/Ignore.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\API\Backends\Library; +namespace App\API\Backend\Library; -use App\API\Backends\Index as BackendsIndex; +use App\API\Backend\Index as BackendsIndex; use App\Libs\Attributes\Route\Route; use App\Libs\Config; use App\Libs\ConfigFile; diff --git a/src/API/Backends/Library/Index.php b/src/API/Backend/Library/Index.php similarity index 94% rename from src/API/Backends/Library/Index.php rename to src/API/Backend/Library/Index.php index 62a4eb22..ab103335 100644 --- a/src/API/Backends/Library/Index.php +++ b/src/API/Backend/Library/Index.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\API\Backends\Library; +namespace App\API\Backend\Library; -use App\API\Backends\Index as BackendsIndex; +use App\API\Backend\Index as BackendsIndex; use App\Libs\Attributes\Route\Get; use App\Libs\Config; use App\Libs\Exceptions\RuntimeException; diff --git a/src/API/Backends/Library/Mismatched.php b/src/API/Backend/Library/Mismatched.php similarity index 97% rename from src/API/Backends/Library/Mismatched.php rename to src/API/Backend/Library/Mismatched.php index 3f102821..ba1cb6fc 100644 --- a/src/API/Backends/Library/Mismatched.php +++ b/src/API/Backend/Library/Mismatched.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\API\Backends\Library; +namespace App\API\Backend\Library; -use App\API\Backends\Index as BackendsIndex; +use App\API\Backend\Index as BackendsIndex; use App\Commands\Backend\Library\MismatchCommand; use App\Libs\Attributes\Route\Get; use App\Libs\DataUtil; diff --git a/src/API/Backends/Library/Unmatched.php b/src/API/Backend/Library/Unmatched.php similarity index 96% rename from src/API/Backends/Library/Unmatched.php rename to src/API/Backend/Library/Unmatched.php index a3ba8036..ba16045d 100644 --- a/src/API/Backends/Library/Unmatched.php +++ b/src/API/Backend/Library/Unmatched.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\API\Backends\Library; +namespace App\API\Backend\Library; -use App\API\Backends\Index as BackendsIndex; +use App\API\Backend\Index as BackendsIndex; use App\Libs\Attributes\Route\Get; use App\Libs\DataUtil; use App\Libs\Exceptions\RuntimeException; diff --git a/src/API/Backend/PartialUpdate.php b/src/API/Backend/PartialUpdate.php new file mode 100644 index 00000000..c24a47f8 --- /dev/null +++ b/src/API/Backend/PartialUpdate.php @@ -0,0 +1,71 @@ +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)), + ], + ]); + } +} diff --git a/src/API/Backends/Search.php b/src/API/Backend/Search.php similarity index 98% rename from src/API/Backends/Search.php rename to src/API/Backend/Search.php index 30d0ae66..1f8d0d7a 100644 --- a/src/API/Backends/Search.php +++ b/src/API/Backend/Search.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\API\Backends; +namespace App\API\Backend; use App\Libs\Attributes\Route\Get; use App\Libs\DataUtil; diff --git a/src/API/Backends/Sessions.php b/src/API/Backend/Sessions.php similarity index 98% rename from src/API/Backends/Sessions.php rename to src/API/Backend/Sessions.php index 1dd8417e..eb95e486 100644 --- a/src/API/Backends/Sessions.php +++ b/src/API/Backend/Sessions.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\API\Backends; +namespace App\API\Backend; use App\Libs\Attributes\Route\Get; use App\Libs\DataUtil; diff --git a/src/API/Backends/Users.php b/src/API/Backend/Users.php similarity index 98% rename from src/API/Backends/Users.php rename to src/API/Backend/Users.php index 92efab7f..11590fc4 100644 --- a/src/API/Backends/Users.php +++ b/src/API/Backend/Users.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\API\Backends; +namespace App\API\Backend; use App\Libs\Attributes\Route\Get; use App\Libs\DataUtil; diff --git a/src/API/Backends/Version.php b/src/API/Backend/Version.php similarity index 98% rename from src/API/Backends/Version.php rename to src/API/Backend/Version.php index 1581c0e5..21dbf8ea 100644 --- a/src/API/Backends/Version.php +++ b/src/API/Backend/Version.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\API\Backends; +namespace App\API\Backend; use App\Libs\Attributes\Route\Get; use App\Libs\DataUtil; diff --git a/src/API/Backends/Webhooks.php b/src/API/Backend/Webhooks.php similarity index 96% rename from src/API/Backends/Webhooks.php rename to src/API/Backend/Webhooks.php index eece6825..46bc788a 100644 --- a/src/API/Backends/Webhooks.php +++ b/src/API/Backend/Webhooks.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\API\Backends; +namespace App\API\Backend; use App\Libs\Attributes\Route\Route; use App\Libs\Config; @@ -27,20 +27,20 @@ final class Webhooks { use APITraits; - private iLogger $accesslog; + private iLogger $accessLog; public function __construct(private iCache $cache) { - $this->accesslog = new Logger(name: 'http', processors: [new LogMessageProcessor()]); + $this->accessLog = new Logger(name: 'http', 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->accessLog = $this->accessLog->pushHandler(new StreamHandler($logfile, $level, true)); } if (true === inContainer()) { - $this->accesslog->pushHandler(new StreamHandler('php://stderr', $level, true)); + $this->accessLog->pushHandler(new StreamHandler('php://stderr', $level, true)); } } @@ -257,9 +257,9 @@ final class Webhooks } if (true === (Config::get('logs.context') || $forceContext)) { - $this->accesslog->log($level, $message, $context); + $this->accessLog->log($level, $message, $context); } else { - $this->accesslog->log($level, r($message, $context)); + $this->accessLog->log($level, r($message, $context)); } } } diff --git a/src/API/Backends/Index.php b/src/API/Backends/Index.php index d7e46dc0..7e486ff2 100644 --- a/src/API/Backends/Index.php +++ b/src/API/Backends/Index.php @@ -5,13 +5,9 @@ declare(strict_types=1); namespace App\API\Backends; use App\Libs\Attributes\Route\Get; -use App\Libs\Attributes\Route\Patch; -use App\Libs\Config; -use App\Libs\ConfigFile; 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; @@ -28,7 +24,7 @@ final class Index ]; #[Get(self::URL . '[/]', name: 'backends.index')] - public function backendsIndex(iRequest $request): iResponse + public function __invoke(iRequest $request): iResponse { $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme(''); $urlPath = $request->getUri()->getPath(); @@ -48,7 +44,9 @@ final class Index ); $backend['links'] = [ - 'self' => (string)$apiUrl->withPath($urlPath . '/' . $backend['name']), + 'self' => (string)$apiUrl->withPath( + parseConfigValue(\App\API\Backend\Index::URL) . '/' . $backend['name'] + ), ]; $response['backends'][] = $backend; @@ -57,82 +55,4 @@ final class Index return api_response(HTTP_STATUS::HTTP_OK, $response); } - #[Get(Index::URL . '/{name:backend}[/]', name: 'backends.view')] - public function backendsView(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); - } - - $data = $this->getBackends(name: $name); - - if (empty($data)) { - 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]); - } - - #[Patch(Index::URL . '/{name:backend}[/]', name: 'backends.view')] - public function backendsUpdatePartial(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)), - ], - ]); - } } diff --git a/src/API/System/Supported.php b/src/API/System/Supported.php new file mode 100644 index 00000000..54138adc --- /dev/null +++ b/src/API/System/Supported.php @@ -0,0 +1,24 @@ + array_keys(Config::get('supported')), + ]); + } +} diff --git a/src/Libs/Initializer.php b/src/Libs/Initializer.php index 2366b126..d0835f6e 100644 --- a/src/Libs/Initializer.php +++ b/src/Libs/Initializer.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Libs; -use App\API\Backends\Webhooks; +use App\API\Backend\Webhooks; use App\Cli; use App\Libs\Exceptions\Backends\RuntimeException; use App\Libs\Exceptions\HttpException;