Remapped backend endpoints to /backend instead of /backends.

This commit is contained in:
abdulmohsen
2024-05-04 10:57:34 +03:00
parent f1417a554b
commit 041a325e3e
19 changed files with 307 additions and 114 deletions

10
FAQ.md
View File

@@ -341,10 +341,10 @@ $ docker exec -ti watchstate console system:tasks
### How to add webhooks? ### 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 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 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. 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 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: ##### 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 `[BACKEND_NAME]` with the name you have chosen for your backend.
* Replace `[APIKEY]` with the global apikey. * Replace `[APIKEY]` with the global apikey.
@@ -412,7 +412,7 @@ Go to your Plex Web UI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK
##### URL: ##### 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 `[BACKEND_NAME]` with the name you have chosen for your backend.
* Replace `[APIKEY]` with the global apikey. * Replace `[APIKEY]` with the global apikey.
@@ -441,7 +441,7 @@ go back again to dashboard > plugins > webhook. Add `Add Generic Destination`,
##### Webhook Url: ##### 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. * Replace `[BACKEND_NAME]` with the name you have chosen for your backend.

View File

@@ -11,9 +11,12 @@ out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers.
### 2024-04-30 - [BREAKING CHANGE] ### 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 We are going to retire the old webhooks endpoint, please refer to the [FAQ](FAQ.md#how-to-add-webhooks) to know how to
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 update
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. 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. Refer to [NEWS](NEWS.md) for old updates.

View File

@@ -0,0 +1,130 @@
<template>
<div class="columns is-multiline">
<div class="column is-12 is-clearfix">
<span class="title is-4">
<NuxtLink href="/backends">Backends</NuxtLink>
: Edit -
<NuxtLink :href="'/backends/' + id">{{ id }}</NuxtLink>
</span>
<div class="is-pulled-right">
<div class="field is-grouped"></div>
</div>
</div>
<div class="column is-12">
<form id="backend_edit_form" @submit.prevent="saveContent">
<div class="box">
<div class="field">
<label class="label">Backend Name</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.name" required readonly disabled>
<div class="icon is-small is-left">
<i class="fas fa-user"></i>
</div>
<p class="help">
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
case a-z, 0-9 and _</code> only.
</p>
</div>
</div>
<div class="field">
<label class="label">Backend Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth" disabled>
<select v-model="backend.type" disabled class="is-capitalized">
<option v-for="(bType, index) in supported" :key="'btype-'+index" :value="bType">
{{ bType }}
</option>
</select>
</div>
<div class="icon is-small is-left">
<i class="fas fa-globe"></i>
</div>
<p class="help">
Select the correct backend type.
</p>
</div>
</div>
<div class="field">
<label class="label">Backend URL</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.url" required>
<div class="icon is-small is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
Enter the URL of the backend.
<a v-if="'plex' === backend.type" href="javascript:void(0)">Get associated servers with token.</a>
</p>
</div>
</div>
<div class="field">
<label class="label">Backend Token</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.token" required>
<div class="icon is-small is-left">
<i class="fas fa-key"></i>
</div>
<p class="help">
Enter the token of the backend.
</p>
</div>
</div>
<div class="field">
<label class="label">Backend User ID</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.user" required>
<div class="icon is-small is-left">
<i class="fas fa-user-tie"></i>
</div>
<p class="help">
The user ID of the backend. <a href="javascript:void(0)">Pull User ids from backend.</a>
</p>
</div>
</div>
<div class="field">
<label class="label">Backend Unique ID</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.uuid" required>
<div class="icon is-small is-left">
<i class="fas fa-server"></i>
</div>
<p class="help">
The Unique identifier for the backend.
<a href="javascript:void(0)">Pull from the backend</a>
</p>
</div>
</div>
</div>
</form>
</div>
</div>
</template>
<script setup>
const id = useRoute().params.backend
const backend = ref({})
const supported = ref([])
const loadContent = async () => {
let content = await request('/system/supported')
let json = await content.json()
supported.value = json.supported
content = await request(`/backends/${id}`)
json = await content.json()
backend.value = json.backend
}
onMounted(() => loadContent())
</script>

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backend;
use App\Libs\Attributes\Route\Delete; use App\Libs\Attributes\Route\Delete;
use App\Libs\Attributes\Route\Get; use App\Libs\Attributes\Route\Get;

45
src/API/Backend/Index.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\API\Backend;
use App\Libs\Attributes\Route\Get;
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;
public const string URL = '%{api.prefix}/backend';
#[Get(self::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 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]);
}
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backend;
use App\Libs\Attributes\Route\Get; use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil; use App\Libs\DataUtil;

View File

@@ -2,9 +2,9 @@
declare(strict_types=1); 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\Attributes\Route\Route;
use App\Libs\Config; use App\Libs\Config;
use App\Libs\ConfigFile; use App\Libs\ConfigFile;

View File

@@ -2,9 +2,9 @@
declare(strict_types=1); 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\Attributes\Route\Get;
use App\Libs\Config; use App\Libs\Config;
use App\Libs\Exceptions\RuntimeException; use App\Libs\Exceptions\RuntimeException;

View File

@@ -2,9 +2,9 @@
declare(strict_types=1); 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\Commands\Backend\Library\MismatchCommand;
use App\Libs\Attributes\Route\Get; use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil; use App\Libs\DataUtil;

View File

@@ -2,9 +2,9 @@
declare(strict_types=1); 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\Attributes\Route\Get;
use App\Libs\DataUtil; use App\Libs\DataUtil;
use App\Libs\Exceptions\RuntimeException; use App\Libs\Exceptions\RuntimeException;

View File

@@ -0,0 +1,71 @@
<?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

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backend;
use App\Libs\Attributes\Route\Get; use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil; use App\Libs\DataUtil;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backend;
use App\Libs\Attributes\Route\Get; use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil; use App\Libs\DataUtil;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backend;
use App\Libs\Attributes\Route\Get; use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil; use App\Libs\DataUtil;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backend;
use App\Libs\Attributes\Route\Get; use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil; use App\Libs\DataUtil;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backend;
use App\Libs\Attributes\Route\Route; use App\Libs\Attributes\Route\Route;
use App\Libs\Config; use App\Libs\Config;
@@ -27,20 +27,20 @@ final class Webhooks
{ {
use APITraits; use APITraits;
private iLogger $accesslog; private iLogger $accessLog;
public function __construct(private iCache $cache) 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; $level = Config::get('webhook.debug') ? Level::Debug : Level::Info;
if (null !== ($logfile = Config::get('webhook.logfile'))) { 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()) { 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)) { if (true === (Config::get('logs.context') || $forceContext)) {
$this->accesslog->log($level, $message, $context); $this->accessLog->log($level, $message, $context);
} else { } else {
$this->accesslog->log($level, r($message, $context)); $this->accessLog->log($level, r($message, $context));
} }
} }
} }

View File

@@ -5,13 +5,9 @@ declare(strict_types=1);
namespace App\API\Backends; namespace App\API\Backends;
use App\Libs\Attributes\Route\Get; 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\HTTP_STATUS;
use App\Libs\Options; use App\Libs\Options;
use App\Libs\Traits\APITraits; use App\Libs\Traits\APITraits;
use JsonException;
use Psr\Http\Message\ResponseInterface as iResponse; use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest; use Psr\Http\Message\ServerRequestInterface as iRequest;
@@ -28,7 +24,7 @@ final class Index
]; ];
#[Get(self::URL . '[/]', name: 'backends.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(''); $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
$urlPath = $request->getUri()->getPath(); $urlPath = $request->getUri()->getPath();
@@ -48,7 +44,9 @@ final class Index
); );
$backend['links'] = [ $backend['links'] = [
'self' => (string)$apiUrl->withPath($urlPath . '/' . $backend['name']), 'self' => (string)$apiUrl->withPath(
parseConfigValue(\App\API\Backend\Index::URL) . '/' . $backend['name']
),
]; ];
$response['backends'][] = $backend; $response['backends'][] = $backend;
@@ -57,82 +55,4 @@ final class Index
return api_response(HTTP_STATUS::HTTP_OK, $response); 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)),
],
]);
}
} }

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\API\System;
use App\Libs\Attributes\Route\Get;
use App\Libs\Config;
use App\Libs\HTTP_STATUS;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
final class Supported
{
public const string URL = '%{api.prefix}/system/supported';
#[Get(self::URL . '[/]', name: 'system.supported')]
public function __invoke(iRequest $request): iResponse
{
return api_response(HTTP_STATUS::HTTP_OK, [
'supported' => array_keys(Config::get('supported')),
]);
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Libs; namespace App\Libs;
use App\API\Backends\Webhooks; use App\API\Backend\Webhooks;
use App\Cli; use App\Cli;
use App\Libs\Exceptions\Backends\RuntimeException; use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Exceptions\HttpException; use App\Libs\Exceptions\HttpException;