Merge pull request #543 from arabcoders/dev

Initial support for adding plex users with PIN.
This commit is contained in:
Abdulmohsen
2024-08-26 21:00:24 +03:00
committed by GitHub
9 changed files with 110 additions and 25 deletions

View File

@@ -173,5 +173,11 @@ return [
'visible' => false,
'description' => 'Whether the token has limited access.',
],
[
'key' => 'options.PLEX_USER_PIN',
'type' => 'int',
'visible' => false,
'description' => 'Plex user PIN.',
],
];

View File

@@ -175,6 +175,16 @@
<NuxtLink @click="getUsers" v-text="'Retrieve User ids from backend.'" v-if="stage < 4"/>
</p>
</div>
<template v-if="'plex' === backend.type">
<label class="label">User PIN</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.options.PLEX_USER_PIN" :disabled="stage > 3">
<div class="icon is-left"><i class="fas fa-key"></i></div>
<p class="help">
If the selected user is using <code>PIN</code> to login, enter the PIN here.
</p>
</div>
</template>
</div>
<template v-if="stage >= 4">
@@ -272,11 +282,17 @@
</div>
<div class="card-footer">
<div class="card-footer-item" v-if="stage >= 1">
<button class="button is-fullwidth is-warning" type="button" @click="stage = stage-1">
<span class="icon"><i class="fas fa-arrow-left"></i></span>
<span>Previous Step</span>
</button>
</div>
<div class="card-footer-item" v-if="stage < maxStages">
<button class="button is-fullwidth is-primary" type="submit" @click="changeStep()">
<span class="icon">
<i class="fas fa-arrow-right"></i>
</span>
<button class="button is-fullwidth is-info" type="submit" @click="changeStep()">
<span class="icon"><i class="fas fa-arrow-right"></i></span>
<span>Next Step</span>
</button>
</div>
@@ -500,10 +516,6 @@ onMounted(async () => {
backend.value.type = supported.value[0]
})
watch(stage, v => {
console.log(v);
}, {immediate: true})
const changeStep = async () => {
let _

View File

@@ -590,6 +590,12 @@ const getServers = async () => {
url: window.location.origin,
};
if (backend.value?.options && backend.value.options?.ADMIN_TOKEN) {
data.options = {
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
}
}
const response = await request(`/backends/discover/${backend.value.type}`, {
method: 'POST',
body: JSON.stringify(data)

View File

@@ -8,6 +8,7 @@ use App\Backends\Plex\PlexClient;
use App\Libs\Attributes\Route\Get;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
@@ -42,7 +43,14 @@ final class Discover
assert($client instanceof PlexClient);
$list = $client::discover($this->http, $client->getContext()->backendToken);
$context = $client->getContext();
$opts = [];
if (null !== ($adminToken = ag($context->options, Options::ADMIN_TOKEN))) {
$opts[Options::ADMIN_TOKEN] = $adminToken;
}
$list = $client::discover($this->http, $context->backendToken, $opts);
return api_response(Status::OK, ag($list, 'list', []));
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);

View File

@@ -9,6 +9,7 @@ use App\Libs\Attributes\Route\Route;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
@@ -42,7 +43,13 @@ final class Discover
}
try {
$list = $client::discover($this->http, $client->getContext()->backendToken);
$opts = [];
if (null !== ($adminToken = ag($request->getParsedBody(), 'options.' . Options::ADMIN_TOKEN))) {
$opts[Options::ADMIN_TOKEN] = $adminToken;
}
$list = $client::discover($this->http, $client->getContext()->backendToken, $opts);
return api_response(Status::OK, ag($list, 'list', []));
} catch (Throwable $e) {
return api_error($e->getMessage(), Status::INTERNAL_SERVER_ERROR);

View File

@@ -10,6 +10,7 @@ use App\Backends\Common\Error;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Libs\Container;
use App\Libs\Options;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\RetryableHttpClient;
@@ -71,6 +72,10 @@ final class GetUserToken
'url' => (string)$url,
]);
if (null !== ($pin = ag($context->options, Options::PLEX_USER_PIN))) {
$url = $url->withQuery(http_build_query(['pin' => $pin]));
}
$response = $this->http->request('POST', (string)$url, [
'headers' => [
'Accept' => 'application/json',
@@ -83,13 +88,14 @@ final class GetUserToken
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.',
message: "Request for temporary access token for '{backend}' user '{username}'{pin} failed due to rate limit. error 429.",
context: [
'backend' => $context->backendName,
'username' => $username,
'user_id' => $userId,
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'pin' => null !== $pin ? ' with pin' : '',
],
level: Levels::ERROR
),
@@ -100,13 +106,14 @@ final class GetUserToken
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] user [{username}] temporary access token responded with unexpected [{status_code}] status code.',
message: "Request for '{backend}' user '{username}'{pin} 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(),
'pin' => null !== $pin ? ' with pin' : '',
],
level: Levels::ERROR
),
@@ -120,13 +127,14 @@ final class GetUserToken
);
if ($context->trace) {
$this->logger->debug('Parsing temporary access token for [{backend}] user [{username}] payload.', [
$this->logger->debug("Parsing temporary access token for '{backend}' user '{username}'{pin} payload.", [
'backend' => $context->backendName,
'username' => $username,
'user_id' => $userId,
'url' => (string)$url,
'trace' => $json,
'headers' => $response->getHeaders(),
'pin' => null !== $pin ? ' with pin' : '',
]);
}
@@ -141,11 +149,12 @@ final class GetUserToken
])
);
$this->logger->debug('Requesting permanent access token for [{backend}] user [{username}].', [
$this->logger->debug("Requesting permanent access token for '{backend}' user '{username}'{pin}.", [
'backend' => $context->backendName,
'username' => $username,
'user_id' => $userId,
'url' => (string)$url,
'pin' => null !== $pin ? ' with pin' : '',
]);
$response = $this->http->request('GET', (string)$url, [
@@ -163,12 +172,13 @@ final class GetUserToken
);
if ($context->trace) {
$this->logger->debug('Parsing permanent access token for [{backend}] user [{username}] payload.', [
$this->logger->debug("Parsing permanent access token for '{backend}' user '{username}'{pin} payload.", [
'backend' => $context->backendName,
'username' => $username,
'user_id' => $userId,
'url' => (string)$url,
'trace' => $json,
'pin' => null !== $pin ? ' with pin' : '',
]);
}
@@ -189,7 +199,7 @@ final class GetUserToken
}
$this->logger->error(
'Response had [{count}] associated servers, non match [{backend} - [{backend_id}] unique identifier.',
"Response had '{count}' associated servers, non match '{backend}: {backend_id}' unique identifier.",
[
'count' => count(($json)),
'backend' => $context->backendName,
@@ -201,11 +211,12 @@ final class GetUserToken
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 [--debug] flag for more information.',
message: "No permanent access token was found for '{username}'{pin} 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 [--debug] flag for more information.",
context: [
'backend' => $context->backendName,
'username' => $username,
'user_id' => $userId,
'pin' => null !== $pin ? ' with pin' : '',
],
level: Levels::ERROR
),
@@ -214,10 +225,11 @@ final class GetUserToken
return new Response(
status: false,
error: new Error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] request for [{username}] access token. Error [{error.message} @ {error.file}:{error.line}].',
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for '{username}'{pin} access token. Error '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'pin' => isset($pin) ? ' with pin' : '',
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),

View File

@@ -33,6 +33,7 @@ use App\Libs\Config;
use App\Libs\Container;
use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Exceptions\HttpException;
use App\Libs\Mappers\ImportInterface as iImport;
@@ -671,28 +672,38 @@ class PlexClient implements iClient
$payload = $response->getContent(false);
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::from($response->getStatusCode())) {
if (Status::UNAUTHORIZED === Status::from($response->getStatusCode())) {
if (null !== ($adminToken = ag($opts, Options::ADMIN_TOKEN))) {
$opts['with_admin'] = true;
return self::discover($http, $adminToken, ag_delete($opts, Options::ADMIN_TOKEN));
}
}
throw new RuntimeException(
r(
text: 'PlexClient: Request for servers list returned with unexpected [{status_code}] status code. {context}',
text: "PlexClient: Request for servers list returned with unexpected '{status_code}' status code. {context}",
context: [
'status_code' => $response->getStatusCode(),
'context' => arrayToString(['payload' => $payload]),
'context' => arrayToString([
'with_admin' => true === ag($opts, 'with_admin'),
'payload' => $payload
]),
]
)
), $response->getStatusCode()
);
}
} catch (TransportExceptionInterface $e) {
throw new RuntimeException(
r(
text: 'PlexClient: Exception [{kind}] was thrown unhandled during request for plex servers list, likely network related error. [{error} @ {file}:{line}]',
text: "PlexClient: Exception '{kind}' was thrown unhandled during request for plex servers list, likely network related error. '{error}' at '{file}:{line}'.",
context: [
'kind' => $e::class,
'error' => $e->getMessage(),
'line' => $e->getLine(),
'file' => after($e->getFile(), ROOT_PATH),
]
)
), code: 500, previous: $e
);
}

View File

@@ -409,9 +409,31 @@ class PlexManage implements ManageInterface
$backend = ag_set($backend, 'user', $map[$user]);
$backend = ag_set($backend, 'options.plex_user_uuid', $uuid[$user]);
$question = new Question(
r(
<<<HELP
<question>[<value>{name}</value>] User PIN</question>. {default}
------------------
<notice>Leave empty if the user doesn't have a PIN.</notice>
------------------
HELP. PHP_EOL . '> ',
[
'name' => ag($backend, 'name'),
'default' => null !== $choice ? "<value>[Default: {$choice}]</value>" : ''
]
),
ag($backend, 'options.' . Options::PLEX_USER_PIN)
);
$pin = $this->questionHelper->ask($this->input, $this->output, $question);
if (!empty($pin)) {
$backend = ag_set($backend, 'options.' . Options::PLEX_USER_PIN, $pin);
}
$this->output->writeln(
r('<info>Requesting plex token for [{user}] from plex.tv API.</info>', [
r("<info>Requesting plex token for '{user}'{pin} from plex.tv API.</info>", [
'user' => ag($userInfo[$map[$user]], 'name') ?? 'None',
'pin' => empty($pin) ? '' : ' with PIN'
])
);

View File

@@ -38,6 +38,7 @@ final class Options
public const string LIMIT_RESULTS = 'LIMIT_RESULTS';
public const string NO_CHECK = 'NO_CHECK';
public const string LOG_WRITER = 'LOG_WRITER';
public const string PLEX_USER_PIN = 'PLEX_USER_PIN';
private function __construct()
{