diff --git a/src/Backends/Common/ClientInterface.php b/src/Backends/Common/ClientInterface.php
index a6a44381..c125f801 100644
--- a/src/Backends/Common/ClientInterface.php
+++ b/src/Backends/Common/ClientInterface.php
@@ -190,4 +190,14 @@ interface ClientInterface
* @return array
*/
public static function manage(array $backend, array $opts = []): array;
+
+ /**
+ * Return user access token.
+ *
+ * @param int|string $userId
+ * @param string $username
+ *
+ * @return string|bool return user token as string or bool(FALSE) if not supported.
+ */
+ public function getUserToken(int|string $userId, string $username): string|bool;
}
diff --git a/src/Backends/Emby/EmbyClient.php b/src/Backends/Emby/EmbyClient.php
index f1ca562e..e5b5935c 100644
--- a/src/Backends/Emby/EmbyClient.php
+++ b/src/Backends/Emby/EmbyClient.php
@@ -335,6 +335,11 @@ class EmbyClient implements iClient
return $response->response;
}
+ public function getUserToken(int|string $userId, string $username): string|bool
+ {
+ return false;
+ }
+
public function listLibraries(array $opts = []): array
{
$response = Container::get(GetLibrariesList::class)(context: $this->context, opts: $opts);
diff --git a/src/Backends/Jellyfin/JellyfinClient.php b/src/Backends/Jellyfin/JellyfinClient.php
index c8096890..5cd69e14 100644
--- a/src/Backends/Jellyfin/JellyfinClient.php
+++ b/src/Backends/Jellyfin/JellyfinClient.php
@@ -350,6 +350,11 @@ class JellyfinClient implements iClient
return $response->response;
}
+ public function getUserToken(int|string $userId, string $username): string|bool
+ {
+ return false;
+ }
+
public function listLibraries(array $opts = []): array
{
$response = Container::get(GetLibrariesList::class)(context: $this->context, opts: $opts);
diff --git a/src/Backends/Plex/Action/GetUserToken.php b/src/Backends/Plex/Action/GetUserToken.php
new file mode 100644
index 00000000..92787fbb
--- /dev/null
+++ b/src/Backends/Plex/Action/GetUserToken.php
@@ -0,0 +1,220 @@
+tryResponse(
+ context: $context,
+ fn: fn() => $this->getUserToken($context, $userId, $username)
+ );
+ }
+
+ /**
+ * Request tokens from plex.tv api.
+ *
+ * @param Context $context
+ * @param int|string $userId
+ * @param string $username
+ *
+ * @return Response
+ */
+ private function getUserToken(Context $context, int|string $userId, string $username): Response
+ {
+ try {
+ $url = Container::getNew(UriInterface::class)
+ ->withPort(443)->withScheme('https')->withHost('plex.tv')
+ ->withPath(r('/api/v2/home/users/{user_id}/switch', ['user_id' => $userId]));
+
+ $this->logger->debug('Requesting temporary access token for [%(backend)] user [%(username)].', [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ 'url' => (string)$url,
+ ]);
+
+ $response = $this->http->request('POST', (string)$url, [
+ 'headers' => [
+ 'Accept' => 'application/json',
+ 'X-Plex-Token' => $context->backendToken,
+ 'X-Plex-Client-Identifier' => $context->backendId,
+ ],
+ ]);
+
+ if (429 === $response->getStatusCode()) {
+ return new Response(
+ status: false,
+ error: new Error(
+ message: 'Request for temporary access token for [%(backend)] user [%(username)] failed due to rate limit. error 429.',
+ context: [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ 'status_code' => $response->getStatusCode(),
+ 'headers' => $response->getHeaders(),
+ ],
+ level: Levels::ERROR
+ ),
+ );
+ }
+
+ if (201 !== $response->getStatusCode()) {
+ return new Response(
+ status: false,
+ error: new Error(
+ message: 'Request for [%(backend)] user [%(username)] temporary access token responded with unexpected [%(status_code)] status code.',
+ context: [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ 'status_code' => $response->getStatusCode(),
+ 'headers' => $response->getHeaders(),
+ ],
+ level: Levels::ERROR
+ ),
+ );
+ }
+
+ $json = json_decode(
+ json: $response->getContent(),
+ associative: true,
+ flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
+ );
+
+ if ($context->trace) {
+ $this->logger->debug('Parsing temporary access token for [%(backend)] user [%(username)] payload.', [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ 'url' => (string)$url,
+ 'trace' => $json,
+ 'headers' => $response->getHeaders(),
+ ]);
+ }
+
+ $tempToken = ag($json, 'authToken', null);
+
+ $url = Container::getNew(UriInterface::class)->withPort(443)->withScheme('https')->withHost('plex.tv')
+ ->withPath('/api/v2/resources')->withQuery(
+ http_build_query([
+ 'includeIPv6' => 1,
+ 'includeHttps' => 1,
+ 'includeRelay' => 1
+ ])
+ );
+
+ $this->logger->debug('Requesting permanent access token for [%(backend)] user [%(username)].', [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ 'url' => (string)$url,
+ ]);
+
+ $response = $this->http->request('GET', (string)$url, [
+ 'headers' => [
+ 'Accept' => 'application/json',
+ 'X-Plex-Token' => $tempToken,
+ 'X-Plex-Client-Identifier' => $context->backendId,
+ ],
+ ]);
+
+ $json = json_decode(
+ json: $response->getContent(),
+ associative: true,
+ flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
+ );
+
+ if ($context->trace) {
+ $this->logger->debug('Parsing permanent access token for [%(backend)] user [%(username)] payload.', [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ 'url' => (string)$url,
+ 'trace' => $json,
+ ]);
+ }
+
+ foreach ($json ?? [] as $server) {
+ if (ag($server, 'clientIdentifier') !== $context->backendId) {
+ continue;
+ }
+ return new Response(status: true, response: ag($server, 'accessToken'));
+ }
+
+ $this->logger->error(
+ 'Response had [%(count)] of associated servers non match [%(backend)] unique identifier.',
+ [
+ 'backend' => $context->backendName,
+ 'count' => count(($json)),
+ 'backend_id' => $context->backendId,
+ ]
+ );
+
+ return new Response(
+ status: false,
+ error: new Error(
+ message: 'No permanent access token was found for [%(username)] in [%(backend)] response. Likely invalid unique identifier was selected or plex.tv API error, check https://status.plex.tv or try running same command with [-vvv --trace --context] flags for more information.',
+ context: [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ ],
+ level: Levels::ERROR
+ ),
+ );
+ } catch (Throwable $e) {
+ return new Response(
+ status: false,
+ error: new Error(
+ message: 'Unhandled exception was thrown during request for [%(backend)] [%(username)] access token.',
+ context: [
+ 'backend' => $context->backendName,
+ 'username' => $username,
+ 'user_id' => $userId,
+ 'exception' => [
+ 'file' => after($e->getFile(), ROOT_PATH),
+ 'line' => $e->getLine(),
+ 'kind' => get_class($e),
+ 'message' => $e->getMessage(),
+ 'trace' => $context->trace ? $e->getTrace() : [],
+ ],
+ ],
+ level: Levels::ERROR
+ ),
+ );
+ }
+ }
+}
diff --git a/src/Backends/Plex/Action/GetUsersList.php b/src/Backends/Plex/Action/GetUsersList.php
index d9772bc2..932dd88f 100644
--- a/src/Backends/Plex/Action/GetUsersList.php
+++ b/src/Backends/Plex/Action/GetUsersList.php
@@ -16,7 +16,6 @@ use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
-use Throwable;
final class GetUsersList
{
@@ -108,6 +107,7 @@ final class GetUsersList
foreach ($users as $user) {
$data = [
'id' => ag($user, 'admin') && $adminsCount <= 1 ? 1 : ag($user, 'id'),
+ 'uuid' => ag($user, 'uuid'),
'name' => ag($user, ['friendlyName', 'username', 'title', 'email'], '??'),
'admin' => (bool)ag($user, 'admin'),
'guest' => (bool)ag($user, 'guest'),
@@ -116,7 +116,7 @@ final class GetUsersList
];
if (true === (bool)ag($opts, 'tokens')) {
- $tokenRequest = $this->getUserToken(
+ $tokenRequest = Container::getNew(GetUserToken::class)(
context: $context,
userId: ag($user, 'uuid'),
username: ag($data, 'name'),
@@ -142,176 +142,4 @@ final class GetUsersList
return new Response(status: true, response: $list);
}
-
- /**
- * Request tokens from plex.tv api.
- *
- * @param Context $context
- * @param int|string $userId
- * @param string $username
- * @param int $retry
- *
- * @return Response
- */
- private function getUserToken(Context $context, int|string $userId, string $username, int $retry = 0): Response
- {
- try {
- $url = Container::getNew(UriInterface::class)
- ->withPort(443)->withScheme('https')->withHost('plex.tv')
- ->withPath(r('/api/v2/home/users/{user_id}/switch', ['user_id' => $userId]));
-
- $this->logger->debug('Requesting temporary access token for [%(backend)] user [%(username)].', [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- 'url' => (string)$url,
- ]);
-
- $response = $this->http->request('POST', (string)$url, [
- 'headers' => [
- 'Accept' => 'application/json',
- 'X-Plex-Token' => $context->backendToken,
- 'X-Plex-Client-Identifier' => $context->backendId,
- ],
- ]);
-
- if ($retry < $this->maxRetry && 429 === $response->getStatusCode()) {
- $retry++;
- $sleepFor = ($retry * 1_000_000) + random_int(100000, 999999);
- $this->logger->warning(
- 'Request for temporary access token for [%(backend)] user [%(username)] failed due to rate limit. waiting for few secs. before retrying. [%(attempt)/%(out_of)]',
- [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- 'url' => (string)$url,
- 'headers' => $response->getHeaders(),
- 'retry_in' => $sleepFor,
- 'attempt' => $retry,
- 'out_of' => $this->maxRetry,
- ]
- );
- usleep((int)$sleepFor);
- return $this->getUserToken($context, $userId, $username, $retry);
- }
-
- if (201 !== $response->getStatusCode()) {
- return new Response(
- status: false,
- error: new Error(
- message: 'Request for [%(backend)] user [%(username)] temporary access token responded with unexpected [%(status_code)] status code.',
- context: [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- 'status_code' => $response->getStatusCode(),
- 'headers' => $response->getHeaders(),
- ],
- level: Levels::ERROR
- ),
- );
- }
-
- $json = json_decode(
- json: $response->getContent(),
- associative: true,
- flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
- );
-
- if ($context->trace) {
- $this->logger->debug('Parsing temporary access token for [%(backend)] user [%(username)] payload.', [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- 'url' => (string)$url,
- 'trace' => $json,
- 'headers' => $response->getHeaders(),
- ]);
- }
-
- $tempToken = ag($json, 'authToken', null);
-
- $url = Container::getNew(UriInterface::class)->withPort(443)->withScheme('https')->withHost('plex.tv')
- ->withPath('/api/v2/resources')->withQuery(
- http_build_query(
- [
- 'includeIPv6' => 1,
- 'includeHttps' => 1,
- 'includeRelay' => 1
- ]
- )
- );
-
- $this->logger->debug('Requesting permanent access token for [%(backend)] user [%(username)].', [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- 'url' => (string)$url,
- ]);
-
- $response = $this->http->request('GET', (string)$url, [
- 'headers' => [
- 'Accept' => 'application/json',
- 'X-Plex-Token' => $tempToken,
- 'X-Plex-Client-Identifier' => $context->backendId,
- ],
- ]);
-
- $json = json_decode(
- json: $response->getContent(),
- associative: true,
- flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
- );
-
- if ($context->trace) {
- $this->logger->debug('Parsing permanent access token for [%(backend)] user [%(username)] payload.', [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- 'url' => (string)$url,
- 'trace' => $json,
- ]);
- }
-
- foreach ($json ?? [] as $server) {
- if (ag($server, 'clientIdentifier') !== $context->backendId) {
- continue;
- }
- return new Response(status: true, response: ag($server, 'accessToken'));
- }
-
- return new Response(
- status: false,
- error: new Error(
- message: 'No permanent access token was found for [%(username)] in [%(backend)] response. Likely plex.tv API error, check https://status.plex.tv or try running same command with [-vvv --trace --context] flags for more information.',
- context: [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- ],
- level: Levels::ERROR
- ),
- );
- } catch (Throwable $e) {
- return new Response(
- status: false,
- error: new Error(
- message: 'Unhandled exception was thrown during request for [%(backend)] [%(username)] access token.',
- context: [
- 'backend' => $context->backendName,
- 'username' => $username,
- 'user_id' => $userId,
- 'exception' => [
- 'file' => after($e->getFile(), ROOT_PATH),
- 'line' => $e->getLine(),
- 'kind' => get_class($e),
- 'message' => $e->getMessage(),
- 'trace' => $context->trace ? $e->getTrace() : [],
- ],
- ],
- level: Levels::ERROR
- ),
- );
- }
- }
}
diff --git a/src/Backends/Plex/Commands/AccessTokenCommand.php b/src/Backends/Plex/Commands/AccessTokenCommand.php
new file mode 100644
index 00000000..8805e99c
--- /dev/null
+++ b/src/Backends/Plex/Commands/AccessTokenCommand.php
@@ -0,0 +1,119 @@
+setName(self::ROUTE)
+ ->setDescription('Generate Access tokens for plex backend users.')
+ ->addArgument('backend', InputArgument::REQUIRED, 'Already Added Plex server with plex admin token.')
+ ->addArgument(
+ 'uuid',
+ InputArgument::REQUIRED,
+ 'User UUID as seen via [' . ListCommand::ROUTE . '] command.'
+ )
+ ->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
+ ->addOption('use-token', 'u', InputOption::VALUE_REQUIRED, 'Override backend token with this one.')
+ ->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
+ ->setHelp(
+ r(
+ <<generate limited tokens for users.
+
+ -------
+ [ FAQ ]
+ -------
+ # How to see the raw response?
+
+ {cmd} {route} --output yaml --include-raw-response -- backend_name plex_user_uuid
+
+ HELP,
+ [
+ 'cmd' => trim(commandContext()),
+ 'route' => self::ROUTE,
+ ]
+ )
+ );
+ }
+
+ /**
+ * @throws RedirectionExceptionInterface
+ * @throws ClientExceptionInterface
+ * @throws ServerExceptionInterface
+ */
+ protected function runCommand(InputInterface $input, OutputInterface $output, null|array $rerun = null): int
+ {
+ $uuid = $input->getArgument('uuid');
+ $backend = $input->getArgument('backend');
+
+ // -- Use Custom servers.yaml file.
+ if (($config = $input->getOption('config'))) {
+ try {
+ Config::save('servers', Yaml::parseFile($this->checkCustomBackendsFile($config)));
+ } catch (RuntimeException $e) {
+ $this->logger->error($e->getMessage());
+ return self::FAILURE;
+ }
+ }
+
+ $opts = $backendOpts = [];
+
+ if ($input->getOption('include-raw-response')) {
+ $opts[Options::RAW_RESPONSE] = true;
+ }
+
+ if ($input->getOption('use-token')) {
+ $backendOpts = ag_set($backendOpts, 'token', $input->getOption('use-token'));
+ }
+
+ if ($input->getOption('trace')) {
+ $backendOpts = ag_set($opts, 'options.' . Options::DEBUG_TRACE, true);
+ }
+
+ $client = $this->getBackend($backend, $backendOpts);
+
+ $token = $client->getUserToken(userId: $uuid, username: $client->getContext()->backendName . '_user');
+
+ $output->writeln(
+ r(
+ 'The access token for ({backend}) user id ({uuid}) is [{token}].',
+ [
+ 'uuid' => $uuid,
+ 'backend' => $client->getContext()->backendName,
+ 'token' => $token,
+ ]
+ )
+ );
+
+ return self::SUCCESS;
+ }
+}
diff --git a/src/Backends/Plex/PlexClient.php b/src/Backends/Plex/PlexClient.php
index 8ffc2069..abf451b4 100644
--- a/src/Backends/Plex/PlexClient.php
+++ b/src/Backends/Plex/PlexClient.php
@@ -15,6 +15,7 @@ use App\Backends\Plex\Action\GetLibrariesList;
use App\Backends\Plex\Action\GetLibrary;
use App\Backends\Plex\Action\GetMetaData;
use App\Backends\Plex\Action\GetUsersList;
+use App\Backends\Plex\Action\GetUserToken;
use App\Backends\Plex\Action\Import;
use App\Backends\Plex\Action\InspectRequest;
use App\Backends\Plex\Action\ParseWebhook;
@@ -55,7 +56,7 @@ class PlexClient implements iClient
PlexClient::TYPE_MOVIE => iState::TYPE_MOVIE,
PlexClient::TYPE_EPISODE => iState::TYPE_EPISODE,
];
-
+
public const SUPPORTED_AGENTS = [
'com.plexapp.agents.imdb',
'com.plexapp.agents.tmdb',
@@ -349,6 +350,23 @@ class PlexClient implements iClient
return $response->response;
}
+ public function getUserToken(int|string $userId, string $username): string|bool
+ {
+ $response = Container::get(GetUserToken::class)($this->context, $userId, $username);
+
+ if (false === $response->isSuccessful()) {
+ if ($response->hasError()) {
+ $this->logger->log($response->error->level(), $response->error->message, $response->error->context);
+ }
+
+ throw new RuntimeException(
+ ag($response->extra, 'message', fn() => $response->error->format())
+ );
+ }
+
+ return $response->response;
+ }
+
public function listLibraries(array $opts = []): array
{
$response = Container::get(GetLibrariesList::class)(context: $this->context, opts: $opts);
diff --git a/src/Backends/Plex/PlexManage.php b/src/Backends/Plex/PlexManage.php
index 288a72d6..b3dcacc2 100644
--- a/src/Backends/Plex/PlexManage.php
+++ b/src/Backends/Plex/PlexManage.php
@@ -208,10 +208,12 @@ class PlexManage implements ManageInterface
------------------
The Unique identifier is randomly generated string on server setup.
------------------
- It's mainly used to differentiate between plex servers installations as Plex way of doing webhooks
- is tied to Plex account not the server itself. While you may want to select specfic server
- Plex instead will send events from all servers that are associated with your Plex account.
- To prevent other servers events diluting your watch state, we need this to scope the events to selected server.
+ Backend unique identifier is used for two purposes.
+ 1. To generate access tokens to access the server content for given user.
+ 2. To deny other servers webhook events from reaching yor watch state installation if enabled.
+ ------------------
+ If you select invalid or given invalid unique identifier, the access token generation will fails.
+ and Webhooks will be non-functional.
HELP. PHP_EOL . '> ',
[
'name' => ag($backend, 'name'),
@@ -245,14 +247,12 @@ class PlexManage implements ManageInterface
$this->output->writeln('');
// -- $backend.user
-
- // -- $name.user
(function () use (&$backend, $opts) {
$chosen = ag($backend, 'user');
try {
$this->output->writeln(
- 'Trying to get users list from backend. Please wait...'
+ 'Trying to get users list from plex.tv api. Please wait...'
);
$list = $map = $ids = $userInfo = [];
@@ -267,7 +267,7 @@ class PlexManage implements ManageInterface
]);
try {
- $users = makeBackend($custom, ag($backend, 'name'))->getUsersList(['tokens' => true]);
+ $users = makeBackend($custom, ag($backend, 'name'))->getUsersList();
} catch (Throwable $e) {
// -- Check admin token.
$adminToken = ag($backend, 'options.' . Options::ADMIN_TOKEN);
@@ -283,7 +283,7 @@ class PlexManage implements ManageInterface
$backend['token'] = $adminToken;
$custom['token'] = $adminToken;
- $users = makeBackend($custom, ag($backend, 'name'))->getUsersList(['tokens' => true]);
+ $users = makeBackend($custom, ag($backend, 'name'))->getUsersList([]);
} else {
throw $e;
}
@@ -334,7 +334,8 @@ class PlexManage implements ManageInterface
<<{user}] is not the main user of the server.
Thus syncing the user watch state using the provided token is not possible, as Plex
- use tokens to identify users rather than user ids. We replaced the token with the one reported from the server.
+ use tokens to identify users rather than user ids. We are going to attempt to generate access
+ token for the [{user}] and replace the given token with it.
------------------
This might lead to some functionality to not work as expected, like listing backend users.
this is expected as the managed user token is rather limited compared to the admin user token.
@@ -344,6 +345,11 @@ class PlexManage implements ManageInterface
]
)
);
+
+ $userInfo[$map[$user]]['token'] = makeBackend($custom, ag($backend, 'name'))->getUserToken(
+ ag($userInfo[$map[$user]], 'uuid', $map[$user]),
+ $user
+ );
$backend = ag_set($backend, 'options.' . Options::ADMIN_TOKEN, ag($backend, 'token'));
} else {
$backend = ag_delete($backend, 'options.' . Options::ADMIN_TOKEN);
diff --git a/src/Commands/Backend/Users/ListCommand.php b/src/Commands/Backend/Users/ListCommand.php
index 9d136f7b..60926fed 100644
--- a/src/Commands/Backend/Users/ListCommand.php
+++ b/src/Commands/Backend/Users/ListCommand.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Commands\Backend\Users;
+use App\Backends\Plex\Commands\AccessTokenCommand;
use App\Command;
use App\Libs\Config;
use App\Libs\Options;
@@ -25,7 +26,12 @@ final class ListCommand extends Command
{
$this->setName(self::ROUTE)
->setDescription('Get backend users list.')
- ->addOption('with-tokens', 't', InputOption::VALUE_NONE, 'Include access tokens in response.')
+ ->addOption(
+ 'with-tokens',
+ 't',
+ InputOption::VALUE_NONE,
+ 'Include access tokens in response. NOTE: if you have many plex users you will be rate limited.'
+ )
->addOption('use-token', 'u', InputOption::VALUE_REQUIRED, 'Use this given token.')
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
@@ -45,6 +51,14 @@ final class ListCommand extends Command
{cmd} {route} --with-tokens -- backend_name
+ Notice: If you have many plex users and request tokens for all of them you may get rate-limited by plex api,
+ you shouldn't do this unless you have good reason. In most cases you dont need to, and can use
+ {plex_accesstoken_command} command to generate tokens for specific user. for example:
+
+ {cmd} {plex_accesstoken_command} -- backend_name plex_user_uuid
+
+ plex_user_uuid: is what can be seen using this list command.
+
# How to see the raw response?
{cmd} {route} --output yaml --include-raw-response -- backend_name
@@ -61,6 +75,7 @@ final class ListCommand extends Command
[
'cmd' => trim(commandContext()),
'route' => self::ROUTE,
+ 'plex_accesstoken_command' => AccessTokenCommand::ROUTE,
]
)
);