Skip to content

Commit 57ee7e0

Browse files
committed
feat: add UsersCommand for viewing Plex users
1 parent 01e6c2c commit 57ee7e0

File tree

2 files changed

+197
-6
lines changed

2 files changed

+197
-6
lines changed

src/Backends/Plex/Action/GetUsersList.php

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,14 @@ private function action(Context $context, array $opts = []): Response
7171
$callback = ag($opts, Options::RAW_RESPONSE_CALLBACK, null);
7272
$this->logRequests = $callback && ag($opts, Options::RAW_RESPONSE, false);
7373

74+
$opts[Options::LOG_TO_WRITER] = ag($opts, Options::LOG_TO_WRITER, static fn() => static function (string $log) {});
75+
7476
if (true === (bool) ag($opts, Options::PLEX_EXTERNAL_USER, false)) {
7577
$cls = fn() => $this->getExternalUsers($context, $opts);
78+
$opts[Options::LOG_TO_WRITER](r('Reading external user from cache? {state}', [
79+
'state' => true === (bool) ag($opts, Options::NO_CACHE) ? 'no' : 'yes',
80+
]));
81+
7682
return true === (bool) ag($opts, Options::NO_CACHE)
7783
? $cls()
7884
: $this->tryCache(
@@ -89,6 +95,10 @@ private function action(Context $context, array $opts = []): Response
8995

9096
$cls = fn() => $this->getHomeUsers($this->getExternalUsers($context, $opts), $context, $opts);
9197

98+
$opts[Options::LOG_TO_WRITER](r('Reading data from cache? {state}', [
99+
'state' => true === (bool) ag($opts, Options::NO_CACHE) ? 'no' : 'yes',
100+
]));
101+
92102
$data = true === (bool) ag($opts, Options::NO_CACHE)
93103
? $cls()
94104
: $this->tryCache(
@@ -192,7 +202,7 @@ private function getExternalUsers(Context $context, array $opts = []): Response
192202
'headers' => [
193203
'Accept' => 'application/xml',
194204
],
195-
]), $context, $url);
205+
]), $context, $url, $opts);
196206

197207
if (true !== (bool) ag($opts, Options::GET_TOKENS) || count($users) < 1) {
198208
return new Response(status: true, response: $users);
@@ -249,7 +259,7 @@ private function getExternalUsers(Context $context, array $opts = []): Response
249259
);
250260
}
251261

252-
return $this->externalUsersTokens($users, $context, $url, $response);
262+
return $this->externalUsersTokens($users, $context, $url, $response, $opts);
253263
}
254264

255265
/**
@@ -258,18 +268,18 @@ private function getExternalUsers(Context $context, array $opts = []): Response
258268
* @param iResponse $response
259269
* @param Context $context
260270
* @param iUri $url
271+
* @param array $opts The options.
261272
*
262273
* @return array Return processed response.
263274
* @throws iException if an error occurs during the request.
264275
*/
265-
private function processExternalUsers(iResponse $response, Context $context, iUri $url): array
276+
private function processExternalUsers(iResponse $response, Context $context, iUri $url, array $opts = []): array
266277
{
267278
$content = simplexml_load_string($response->getContent(false));
268279
$data = [];
269280
foreach ($content->User ?? [] as $_user) {
270281
$user = [];
271282
// @INFO: This workaround is needed, for some reason array_map() doesn't work correctly on xml objects.
272-
/** @noinspection PhpLoopCanBeConvertedToArrayMapInspection */
273283
foreach ($_user->attributes() as $k => $v) {
274284
$user[$k] = (string) $v;
275285
}
@@ -296,7 +306,7 @@ private function processExternalUsers(iResponse $response, Context $context, iUr
296306
$list = [];
297307
foreach ($data as $user) {
298308
$uuidStatus = preg_match('/\/users\/(?<uuid>.+?)\/avatar/', ag($user, 'thumb', ''), $matches);
299-
$list[] = [
309+
$_user = [
300310
'id' => (int) ag($user, 'id'),
301311
'uuid' => 1 === $uuidStatus ? ag($matches, 'uuid') : ag($user, 'invited_user'),
302312
'name' => ag($user, ['username', 'title', 'email', 'id'], '??'),
@@ -306,6 +316,17 @@ private function processExternalUsers(iResponse $response, Context $context, iUr
306316
'protected' => 1 === (int) ag($user, 'protected'),
307317
'updatedAt' => 'external_user',
308318
];
319+
320+
$list[] = $_user;
321+
322+
$opts[Options::LOG_TO_WRITER](r("Processed external user '{name}' with id '{id}': {data}.", [
323+
'name' => $_user['name'],
324+
'id' => $_user['id'],
325+
'data' => [
326+
'local' => array_to_json($_user),
327+
'remote' => array_to_json($user),
328+
],
329+
]));
309330
}
310331

311332
return $list;
@@ -318,12 +339,13 @@ private function processExternalUsers(iResponse $response, Context $context, iUr
318339
* @param Context $context The context.
319340
* @param iUri $url The URL.
320341
* @param iResponse $response The response.
342+
* @param array $opts The options.
321343
*
322344
* @return Response Return processed response.
323345
* @throws iException if an error occurs during the request.
324346
* @throws JsonException if an error occurs during the JSON parsing.
325347
*/
326-
private function externalUsersTokens(array $users, Context $context, iUri $url, iResponse $response): Response
348+
private function externalUsersTokens(array $users, Context $context, iUri $url, iResponse $response, array $opts = []): Response
327349
{
328350
if (count($users) < 1) {
329351
return new Response(status: true, response: $users);
@@ -356,6 +378,11 @@ private function externalUsersTokens(array $users, Context $context, iUri $url,
356378

357379
foreach ($users as &$user) {
358380
if ((int) ag($user, 'id') !== (int) ag($data, 'userID')) {
381+
$opts[Options::LOG_TO_WRITER](r("Skipping token for user '{name}' with id '{id}' because it doesn't match with userID '{userID}' in the response.", [
382+
'name' => ag($user, 'name'),
383+
'id' => ag($user, 'id'),
384+
'userID' => ag($data, 'userID'),
385+
]));
359386
continue;
360387
}
361388
$user['token'] = ag($data, 'accessToken');
@@ -470,6 +497,12 @@ private function processHomeUsers(
470497
continue;
471498
}
472499

500+
$opts[Options::LOG_TO_WRITER](r("Skipping external user '{name}' with id '{id}' because match a home user with id '{userId}' and name '{userName}'.", [
501+
'name' => ag($extUser, 'name'),
502+
'id' => ag($extUser, 'id'),
503+
'userId' => $user['id'],
504+
'userName' => $user['name'],
505+
]));
473506
unset($users[$key]);
474507
}
475508
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Backends\Plex\Commands;
6+
7+
use App\Backends\Plex\PlexClient;
8+
use App\Command;
9+
use App\Libs\Attributes\DI\Inject;
10+
use App\Libs\Attributes\Route\Cli;
11+
use App\Libs\LogSuppressor;
12+
use App\Libs\Mappers\Import\DirectMapper;
13+
use App\Libs\Mappers\ImportInterface as iImport;
14+
use App\Libs\Options;
15+
use App\Libs\UserContext;
16+
use Psr\Log\LoggerInterface as iLogger;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
21+
/**
22+
* Class UsersCommand
23+
*
24+
* This command manages Plex users.
25+
*/
26+
#[Cli(command: self::ROUTE)]
27+
class UsersCommand extends Command
28+
{
29+
public const string ROUTE = 'backend:plex:users';
30+
31+
/**
32+
* Class Constructor.
33+
*
34+
* @param iImport $mapper The import interface object.
35+
* @param iLogger $logger The logger interface object.
36+
* @param LogSuppressor $suppressor The log suppressor object.
37+
*
38+
*/
39+
public function __construct(
40+
#[Inject(DirectMapper::class)]
41+
private readonly iImport $mapper,
42+
private readonly iLogger $logger,
43+
) {
44+
set_time_limit(0);
45+
ini_set('memory_limit', '-1');
46+
47+
parent::__construct();
48+
}
49+
50+
/**
51+
* Configure the method.
52+
*/
53+
protected function configure(): void
54+
{
55+
$this
56+
->setName(self::ROUTE)
57+
->setDescription('View plex users.')
58+
->addOption('no-cache', 'N', InputOption::VALUE_NONE, 'Disable cache when loading data plex.')
59+
->addOption('raw', 'r', InputOption::VALUE_NONE, 'Show raw data from plex.')
60+
->addOption('log', 'l', InputOption::VALUE_NONE, 'Show logs from get users list process.')
61+
->addOption(
62+
'select-backend',
63+
's',
64+
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
65+
'Select backend.',
66+
);
67+
}
68+
69+
/**
70+
* Make sure the command is not running in parallel.
71+
*
72+
* @param InputInterface $input The input interface object.
73+
* @param OutputInterface $output The output interface object.
74+
*
75+
* @return int The status code of the command execution.
76+
*/
77+
protected function runCommand(InputInterface $input, OutputInterface $output): int
78+
{
79+
$userContext = get_user_context('main', mapper: $this->mapper, logger: $this->logger);
80+
81+
$backends = $input->getOption('select-backend');
82+
if (empty($backends)) {
83+
$output->writeln('<error>No backends selected. Use --select-backend option to select backend.</error>');
84+
return self::FAILURE;
85+
}
86+
87+
$conf = $name = null;
88+
foreach ($backends as $backend) {
89+
if (null === ($conf = $userContext->config->get($backend))) {
90+
continue;
91+
}
92+
$name = $backend;
93+
break;
94+
}
95+
96+
if (null === $conf) {
97+
$output->writeln('<error>No valid backends selected. Use --select-backend option to select backend.</error>');
98+
return self::FAILURE;
99+
}
100+
101+
if (strtolower(PlexClient::CLIENT_NAME) !== ag($conf, 'type')) {
102+
$output->writeln('<error>Selected backend is not a plex backend. Use --select-backend option to select plex backend.</error>');
103+
return self::FAILURE;
104+
}
105+
106+
$requests = $logs = $opts = [];
107+
108+
if ($input->getOption('log')) {
109+
$opts[Options::LOG_TO_WRITER] = static function (string $log) use (&$logs) {
110+
$logs[] = r('[{time}] {log}', [
111+
'time' => make_date(),
112+
'log' => $log,
113+
]);
114+
};
115+
}
116+
117+
if ($input->getOption('raw')) {
118+
$opts[Options::NO_CACHE] = true;
119+
$opts[Options::RAW_RESPONSE] = true;
120+
$opts[Options::RAW_RESPONSE_CALLBACK] = static function (array $r) use (&$requests) {
121+
$requests = $r;
122+
};
123+
}
124+
125+
if ($input->getOption('no-cache')) {
126+
$opts[Options::NO_CACHE] = true;
127+
}
128+
129+
$backend = make_backend(backend: $conf, name: $name, options: [
130+
UserContext::class => $userContext,
131+
]);
132+
133+
$users = $backend->getUsersList($opts);
134+
135+
foreach ($logs as $log) {
136+
$output->writeln($log);
137+
}
138+
139+
foreach ($requests as $request) {
140+
$output->writeln(r('URL: {url}', ['url' => ag($request, 'url')]));
141+
142+
foreach (ag($request, 'headers', []) as $key => $value) {
143+
$output->writeln(r('{key}: {value}', [
144+
'key' => $key,
145+
'value' => is_array($value) ? implode(', ', $value) : (string) $value,
146+
]));
147+
}
148+
$output->writeln('');
149+
$output->writeln(json_encode(ag($request, 'body', []), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
150+
151+
$output->writeln(str_repeat('-', 80));
152+
}
153+
154+
$this->displayContent($users, $output, 'table');
155+
156+
return self::SUCCESS;
157+
}
158+
}

0 commit comments

Comments
 (0)