472 lines
18 KiB
PHP
472 lines
18 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Backends\Plex;
|
|
|
|
use App\Backends\Common\ManageInterface;
|
|
use App\Libs\Options;
|
|
use Psr\Log\LoggerInterface as iLogger;
|
|
use RuntimeException;
|
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
|
use Symfony\Component\Console\Input\InputInterface as iInput;
|
|
use Symfony\Component\Console\Output\OutputInterface as iOutput;
|
|
use Symfony\Component\Console\Question\ChoiceQuestion;
|
|
use Symfony\Component\Console\Question\Question;
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Class PlexManage
|
|
*
|
|
* This class is responsible for creating and managing Plex backends configuration.
|
|
*
|
|
* @implements ManageInterface.
|
|
*/
|
|
class PlexManage implements ManageInterface
|
|
{
|
|
/**
|
|
* @var QuestionHelper The QuestionHelper object used for asking questions.
|
|
*/
|
|
private QuestionHelper $questionHelper;
|
|
|
|
/**
|
|
* Constructor for the class.
|
|
*
|
|
* @param iHttp $http The iHttp object used for HTTP requests.
|
|
* @param iOutput $output The iOutput object used for outputting data.
|
|
* @param iInput $input The iInput object used for retrieving user input.
|
|
* @param iLogger $logger The iLogger object used for logging.
|
|
*/
|
|
public function __construct(
|
|
private iHttp $http,
|
|
private iOutput $output,
|
|
private iInput $input,
|
|
protected iLogger $logger
|
|
) {
|
|
$this->questionHelper = new QuestionHelper();
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function manage(array $backend, array $opts = []): array
|
|
{
|
|
// -- $backend.token
|
|
(function () use (&$backend) {
|
|
$chosen = ag($backend, 'token');
|
|
|
|
$question = new Question(
|
|
r(
|
|
<<<HELP
|
|
<question>Enter [<value>{name}</value>] Plex admin token</question>. {default}
|
|
------------------
|
|
<info>Please log in your main plex account to extract the admin token.</info>
|
|
to find your plex token. follow the steps described in the following link.
|
|
<href={url}>{url}</>
|
|
------------------
|
|
<notice>Plex uses tokens to differentiate between users. As such the token you enter not necessarily the final
|
|
one that ends up in the config due to user selection.</notice>
|
|
HELP. PHP_EOL . '> ',
|
|
[
|
|
'name' => ag($backend, 'name'),
|
|
'default' => null !== $chosen ? "<value>[Default: {$chosen}]</value>" : '',
|
|
'url' => 'https://support.plex.tv/articles/204059436',
|
|
]
|
|
),
|
|
$chosen
|
|
);
|
|
|
|
$question->setValidator(function ($answer) {
|
|
if (empty($answer)) {
|
|
throw new RuntimeException('Plex admin token cannot be empty.');
|
|
}
|
|
|
|
if (!is_string($answer) && !is_int($answer)) {
|
|
throw new RuntimeException(
|
|
r(
|
|
'Invalid Plex token was given. Expecting string or integer, but got \'{type}\' instead.',
|
|
[
|
|
'type' => get_debug_type($answer)
|
|
]
|
|
)
|
|
);
|
|
}
|
|
return $answer;
|
|
});
|
|
|
|
$token = $this->questionHelper->ask($this->input, $this->output, $question);
|
|
$backend = ag_set($backend, 'token', $token);
|
|
})();
|
|
$this->output->writeln('');
|
|
|
|
// -- $backend.url
|
|
(function () use (&$backend) {
|
|
$chosen = ag($backend, 'url');
|
|
|
|
try {
|
|
re_select:
|
|
if (null === $chosen || 'http://choose' === $chosen) {
|
|
$this->output->writeln(
|
|
'<info>Trying to get list of servers associated with the token from plex.tv API. Please wait...</info>'
|
|
);
|
|
|
|
$response = PlexClient::discover(http: $this->http, token: ag($backend, 'token'));
|
|
|
|
$backends = ag($response, 'list', []);
|
|
|
|
if (empty($backends)) {
|
|
throw new RuntimeException('Plex API returned empty list of servers.');
|
|
}
|
|
|
|
$list = $map = [];
|
|
|
|
foreach ($backends as $server) {
|
|
$val = r('{name} - {uri}', [
|
|
'name' => ag($server, 'name'),
|
|
'uri' => ag($server, 'uri')
|
|
]);
|
|
|
|
$list[] = $val;
|
|
$map[$val] = ag($server, 'uri');
|
|
}
|
|
|
|
$list[] = 'Other. Enter manually.';
|
|
|
|
$question = new ChoiceQuestion(
|
|
r('<question>Select [<value>{name}</value>] URL.</question>', [
|
|
'name' => ag($backend, 'name'),
|
|
]), $list
|
|
);
|
|
|
|
$question->setAutocompleterValues($list);
|
|
|
|
$question->setErrorMessage('Invalid value [%s] was selected.');
|
|
|
|
$server = $this->questionHelper->ask($this->input, $this->output, $question);
|
|
|
|
if (true === ag_exists($map, $server)) {
|
|
$backend = ag_set($backend, 'url', ag($map, $server));
|
|
return;
|
|
}
|
|
}
|
|
} catch (Throwable $e) {
|
|
$this->output->writeln(
|
|
'<error>Failed to get list of servers associated with the token from plex.tv api.</error>'
|
|
);
|
|
$this->output->writeln(
|
|
r('<error>ERROR - {class}: {error}.</error>' . PHP_EOL, [
|
|
'class' => afterLast(get_class($e), '\\'),
|
|
'error' => $e->getMessage(),
|
|
])
|
|
);
|
|
}
|
|
|
|
$question = new Question(
|
|
r(
|
|
<<<HELP
|
|
<question>Enter [<value>{name}</value>] URL</question>. {default}
|
|
------------------
|
|
To see list of servers associated with this plex token, Please write the following as URL
|
|
<value>http://choose</value>
|
|
HELP. PHP_EOL . '> ',
|
|
[
|
|
'name' => ag($backend, 'name'),
|
|
'default' => null !== $chosen ? "[<value>Default: {$chosen}</value>]" : '',
|
|
]
|
|
),
|
|
$chosen
|
|
);
|
|
|
|
$question->setValidator(function ($answer) {
|
|
if (false === isValidURL($answer)) {
|
|
throw new RuntimeException(
|
|
'Invalid URL was selected/given. Expecting something like http://plex:32400.'
|
|
);
|
|
}
|
|
return $answer;
|
|
});
|
|
|
|
$url = $this->questionHelper->ask($this->input, $this->output, $question);
|
|
if (null === $url || 'http://choose' === $url) {
|
|
$chosen = $url;
|
|
goto re_select;
|
|
}
|
|
|
|
$backend = ag_set($backend, 'url', $url);
|
|
})();
|
|
$this->output->writeln('');
|
|
|
|
// -- $backend.uuid
|
|
(function () use (&$backend, $opts) {
|
|
try {
|
|
$this->output->writeln(
|
|
'<info>Attempting to automatically get the server unique identifier from API. Please wait...</info>'
|
|
);
|
|
|
|
$custom = array_replace_recursive($backend, [
|
|
'options' => [
|
|
'client' => [
|
|
'timeout' => 20
|
|
],
|
|
Options::DEBUG_TRACE => (bool)ag($opts, Options::DEBUG_TRACE, false),
|
|
]
|
|
]);
|
|
|
|
$chosen = ag($backend, 'uuid', fn() => makeBackend($custom, ag($custom, 'name'))->getIdentifier(true));
|
|
$this->output->writeln(
|
|
r(
|
|
'<notice>Backend responded with [{id}] as it\'s unique identifier. setting it as default value.</notice>',
|
|
[
|
|
'id' => $chosen
|
|
]
|
|
)
|
|
);
|
|
} catch (Throwable $e) {
|
|
$this->output->writeln(
|
|
r(
|
|
<<<ERROR
|
|
<error>Failed to automatically get server unique identifier.</error>
|
|
------------------
|
|
This most likely means the token that was given doesn't have access to the selected server.
|
|
------------------
|
|
{class}: {error}
|
|
ERROR,
|
|
[
|
|
'class' => afterLast(get_class($e), '\\'),
|
|
'error' => $e->getMessage(),
|
|
]
|
|
),
|
|
);
|
|
$chosen = null;
|
|
}
|
|
|
|
$question = new Question(
|
|
r(
|
|
<<<HELP
|
|
<question>Enter [<value>{name}</value>] Unique identifier</question>. {default}
|
|
------------------
|
|
The Server Unique identifier is randomly generated string on server setup.
|
|
------------------
|
|
<notice>If you select invalid or give incorrect server unique identifier, the access token generation will
|
|
fail. and Webhooks receiver will most likely be non-functional as well.</notice>
|
|
------------------
|
|
<error>DO NOT CHANGE the default value unless you know what you are doing, or was told by devs.</error>
|
|
HELP. PHP_EOL . '> ',
|
|
[
|
|
'name' => ag($backend, 'name'),
|
|
'default' => null !== $chosen ? "<value>[Default: {$chosen}]</value>" : '',
|
|
]
|
|
),
|
|
$chosen
|
|
);
|
|
|
|
$question->setValidator(function ($answer) {
|
|
if (empty($answer)) {
|
|
throw new RuntimeException('Backend unique identifier cannot be empty.');
|
|
}
|
|
|
|
if (!is_string($answer) && !is_int($answer)) {
|
|
throw new RuntimeException(
|
|
r(
|
|
'Backend unique identifier is invalid. Expecting string or integer, but got \'{type}\' instead.',
|
|
[
|
|
'type' => get_debug_type($answer)
|
|
]
|
|
)
|
|
);
|
|
}
|
|
return $answer;
|
|
});
|
|
|
|
$uuid = $this->questionHelper->ask($this->input, $this->output, $question);
|
|
$backend = ag_set($backend, 'uuid', $uuid);
|
|
})();
|
|
$this->output->writeln('');
|
|
|
|
// -- $backend.user
|
|
(function () use (&$backend, $opts) {
|
|
$chosen = ag($backend, 'user');
|
|
$errorType = 'none';
|
|
|
|
try {
|
|
$this->output->writeln(
|
|
'<info>Attempting to get users list from plex.tv API. Please wait...</info>'
|
|
);
|
|
|
|
$list = $map = $ids = $userInfo = [];
|
|
|
|
$custom = array_replace_recursive($backend, [
|
|
'options' => [
|
|
'client' => [
|
|
'timeout' => 10
|
|
],
|
|
Options::DEBUG_TRACE => (bool)ag($opts, Options::DEBUG_TRACE, false),
|
|
]
|
|
]);
|
|
|
|
try {
|
|
$users = makeBackend($custom, ag($backend, 'name'))->getUsersList();
|
|
} catch (Throwable $e) {
|
|
// -- Check admin token.
|
|
$adminToken = ag($backend, 'options.' . Options::ADMIN_TOKEN);
|
|
if (null !== $adminToken && $adminToken !== ag($backend, 'token')) {
|
|
$this->output->writeln(
|
|
r(
|
|
'<notice>The API returned an error \'{error}\'. Attempting to use admin token.</notice>',
|
|
[
|
|
'error' => $e->getMessage()
|
|
]
|
|
)
|
|
);
|
|
|
|
$backend['token'] = $adminToken;
|
|
$custom['token'] = $adminToken;
|
|
$users = makeBackend($custom, ag($backend, 'name'))->getUsersList([]);
|
|
} else {
|
|
$errorType = 'users_list';
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
if (empty($users)) {
|
|
throw new RuntimeException('plex.tv API returned empty list of users.');
|
|
}
|
|
|
|
foreach ($users as $user) {
|
|
$uid = ag($user, 'id');
|
|
$val = ag($user, 'name', '??');
|
|
$list[] = $val;
|
|
$ids[$uid] = $val;
|
|
$map[$val] = $uid;
|
|
$userInfo[$uid] = $user;
|
|
}
|
|
|
|
$choice = $ids[$chosen] ?? null;
|
|
|
|
$question = new ChoiceQuestion(
|
|
r(
|
|
<<<HELP
|
|
<question>Select [<value>{name}</value>] User</question>. {default}
|
|
------------------
|
|
HELP. PHP_EOL . '> ',
|
|
[
|
|
'name' => ag($backend, 'name'),
|
|
'default' => null !== $choice ? "<value>[Default: {$choice}]</value>" : ''
|
|
]
|
|
),
|
|
$list,
|
|
false === $choice ? null : $choice
|
|
);
|
|
|
|
$question->setAutocompleterValues($list);
|
|
|
|
$question->setErrorMessage('Invalid value [%s] was selected.');
|
|
|
|
$user = $this->questionHelper->ask($this->input, $this->output, $question);
|
|
$backend = ag_set($backend, 'user', $map[$user]);
|
|
|
|
$this->output->writeln(
|
|
r('<info>Requesting plex token for [{user}] from plex.tv API.</info>', [
|
|
'user' => ag($userInfo[$map[$user]], 'name') ?? 'None',
|
|
])
|
|
);
|
|
|
|
try {
|
|
$userInfo[$map[$user]]['token'] = makeBackend($custom, ag($backend, 'name'))->getUserToken(
|
|
ag($userInfo[$map[$user]], 'uuid', $map[$user]),
|
|
$user
|
|
);
|
|
|
|
if (null === ag($backend, 'options.' . Options::ADMIN_TOKEN)) {
|
|
$backend = ag_set($backend, 'options.' . Options::ADMIN_TOKEN, ag($backend, 'token'));
|
|
}
|
|
} catch (RuntimeException $e) {
|
|
$errorType = 'token';
|
|
throw $e;
|
|
}
|
|
|
|
if (null === ($userToken = ag($userInfo[$map[$user]], 'token'))) {
|
|
$this->output->writeln(
|
|
r(
|
|
'<error>Unable to get [{user}] access token. rerun the command with [-vvv --context --trace] flags for more info or check logs.</error>',
|
|
[
|
|
'user' => $user
|
|
]
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
$backend = ag_set($backend, 'token', $userToken);
|
|
|
|
return;
|
|
} catch (Throwable $e) {
|
|
$this->output->writeln(
|
|
r(
|
|
<<<ERROR
|
|
<error>Failed to get list of users from plex.tv API.</error>
|
|
------------------
|
|
This most likely means the token is not a valid admin token.
|
|
------------------
|
|
{class}: {error}
|
|
ERROR,
|
|
[
|
|
'class' => afterLast(get_class($e), '\\'),
|
|
'error' => $e->getMessage(),
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
$question = new Question(
|
|
r(
|
|
<<<HELP
|
|
<question>Enter [<value>{name}</value>] User ID</question>. {default}
|
|
------------------
|
|
The error occurred at [<notice>{errorType}</notice>] stage.
|
|
|
|
If you are seeing this, The reason is in the following order from most likely to unlikely:
|
|
|
|
* Invalid token was given. (<notice>users_list</notice>)
|
|
* You used a limited plex token instead of admin one. (<notice>token, users_list</notice>)
|
|
* the selected user doesn't have access to the selected server. (<notice>token</notice>)
|
|
* plex.tv API is having problems. (<notice>none</notice>)
|
|
* Plex.tv API made breaking changes, and thus the tool need to be updated. (<notice>none</notice>)
|
|
HELP. PHP_EOL . '> ',
|
|
[
|
|
'name' => ag($backend, 'name'),
|
|
'default' => null !== $chosen ? "- <value>[Default: {$chosen}]</value>" : '',
|
|
'reason' => $errorType,
|
|
]
|
|
),
|
|
$chosen
|
|
);
|
|
|
|
$question->setValidator(function ($answer) {
|
|
if (empty($answer)) {
|
|
throw new RuntimeException('User id cannot be empty.');
|
|
}
|
|
|
|
if (!is_string($answer) && !is_int($answer)) {
|
|
throw new RuntimeException(
|
|
r(
|
|
'Invalid user id type was given. Expecting a string or integer. but got \'{type}\' instead.',
|
|
[
|
|
'type' => get_debug_type($answer)
|
|
]
|
|
)
|
|
);
|
|
}
|
|
return $answer;
|
|
});
|
|
|
|
$user = $this->questionHelper->ask($this->input, $this->output, $question);
|
|
$backend = ag_set($backend, 'user', $user);
|
|
})();
|
|
$this->output->writeln('');
|
|
|
|
return $backend;
|
|
}
|
|
}
|