Implemented Get sessions for plex.

This commit is contained in:
abdulmohsen
2024-01-17 17:04:52 +03:00
parent abda2b0273
commit 69a4008c19
4 changed files with 336 additions and 0 deletions

View File

@@ -210,6 +210,14 @@ interface ClientInterface
*/
public static function manage(array $backend, array $opts = []): array;
/**
* Return list of active sessions.
*
* @param array $opts (Optional) options.
* @return array{sessions: array<array>}
*/
public function getSessions(array $opts = []): array;
/**
* Return user access token.
*

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace App\Backends\Plex\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Error;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Libs\Options;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class GetSessions
{
use CommonTrait;
private string $action = 'plex.getSessions';
public function __construct(
protected HttpClientInterface $http,
protected LoggerInterface $logger,
) {
}
/**
* Get Backend unique identifier.
*
* @param Context $context
* @param array $opts optional options.
*
* @return Response
*/
public function __invoke(Context $context, array $opts = []): Response
{
return $this->tryResponse(
context: $context,
fn: function () use ($context, $opts) {
$url = $context->backendUrl->withPath('/status/sessions');
$this->logger->debug('Requesting [{client}: {backend}] play sessions.', [
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => $url
]);
$response = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
);
$content = $response->getContent(false);
if (200 !== $response->getStatusCode()) {
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] {action} returned with unexpected [{status_code}] status code.',
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
'url' => (string)$url,
'response' => $content,
],
level: Levels::WARNING
)
);
}
if (empty($content)) {
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] {action} returned with empty response.',
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => (string)$url,
'response' => $content,
],
level: Levels::ERROR
)
);
}
$item = json_decode(
json: $content,
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
if (true === $context->trace) {
$this->logger->debug('Processing [{client}: {backend}] {action} payload.', [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'trace' => $item,
]);
}
$data = ag($item, 'MediaContainer.Metadata', []);
$ret = [
'sessions' => [],
];
if (true === ag_exists($opts, Options::RAW_RESPONSE)) {
$ret[Options::RAW_RESPONSE] = $item;
}
foreach ($data as $session) {
$uuid = preg_match(
'#/users/(.+?)/avatar#i',
ag($session, 'User.thumb'),
$matches
) ? $matches[1] : null;
$ret['sessions'][] = [
'user_uid' => (int)ag($session, 'User.id'),
'user_uuid' => $uuid,
'item_id' => (int)ag($session, 'ratingKey'),
'item_title' => ag($session, 'title'),
'item_type' => ag($session, 'type'),
'item_offset_at' => ag($session, 'viewOffset'),
'session_state' => ag($session, 'Player.state'),
'session_id' => ag($session, 'Session.id', ag($session, 'sessionKey')),
];
}
return new Response(status: true, response: $ret);
},
action: $this->action
);
}
}

View File

@@ -16,6 +16,7 @@ use App\Backends\Plex\Action\GetInfo;
use App\Backends\Plex\Action\GetLibrariesList;
use App\Backends\Plex\Action\GetLibrary;
use App\Backends\Plex\Action\GetMetaData;
use App\Backends\Plex\Action\GetSessions;
use App\Backends\Plex\Action\GetUsersList;
use App\Backends\Plex\Action\GetUserToken;
use App\Backends\Plex\Action\GetVersion;
@@ -459,6 +460,24 @@ class PlexClient implements iClient
return $response->response;
}
/**
* @inheritdoc
*/
public function getSessions(array $opts = []): array
{
$response = Container::get(GetSessions::class)($this->context, $opts);
if (false === $response->isSuccessful()) {
if ($response->hasError()) {
$this->logger->log($response->error->level(), $response->error->message, $response->error->context);
}
$this->throwError($response);
}
return $response->response;
}
/**
* @inheritdoc
*/

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace App\Commands\Backend\Users;
use App\Command;
use App\Libs\Config;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Options;
use App\Libs\Routable;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
/**
* Class ListCommand
*
*/
#[Routable(command: self::ROUTE)]
final class SessionsCommand extends Command
{
public const ROUTE = 'backend:users:sessions';
private const REMAP_FIELDS = [
'user_uid' => 'User ID',
'user_uuid' => 'User UUID',
'item_id' => 'Item ID',
'item_title' => 'Title',
'item_type' => 'Type',
'item_offset_at' => 'Progress',
'session_id' => 'Session ID',
'session_updated_at' => 'Session Activity',
'session_state' => 'Play State',
];
public function __construct(private iDB $db)
{
parent::__construct();
}
/**
* Configures the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
->setDescription('Get backend active sessions.')
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
->addOption('select-backends', 's', InputOption::VALUE_REQUIRED, 'Select backends.')
->setHelp(
r(
<<<HELP
Get active sessions for all users in a backend.
-------
<notice>[ FAQ ]</notice>
-------
<question># How to see the raw response?</question>
{cmd} <cmd>{route}</cmd> <flag>--output</flag> <value>yaml</value> <flag>--include-raw-response</flag> <flag>-s</flag> <value>backend_name</value>
HELP,
[
'cmd' => trim(commandContext()),
'route' => self::ROUTE,
]
)
);
}
/**
* Runs the command.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit status code.
* @throws ExceptionInterface When the request fails.
* @throws \JsonException When the response cannot be parsed.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$mode = $input->getOption('output');
$backend = $input->getOption('select-backends');
if (null === $backend) {
$output->writeln(r('<error>ERROR: Backend not specified. Please use [-s, --select-backends].</error>'));
return self::FAILURE;
}
$backend = explode(',', $backend, 1)[0];
// -- Use Custom servers.yaml file.
if (($config = $input->getOption('config'))) {
try {
Config::save('servers', Yaml::parseFile($this->checkCustomBackendsFile($config)));
} catch (\App\Libs\Exceptions\RuntimeException $e) {
$output->writeln(r('<error>{message}</error>', ['message' => $e->getMessage()]));
return self::FAILURE;
}
}
if (null === ag(Config::get('servers', []), $backend, null)) {
$output->writeln(r("<error>ERROR: Backend '{backend}' not found.</error>", ['backend' => $backend]));
return self::FAILURE;
}
$opts = $backendOpts = [];
if ($input->getOption('include-raw-response')) {
$opts[Options::RAW_RESPONSE] = true;
}
if ($input->getOption('trace')) {
$backendOpts = ag_set($opts, 'options.' . Options::DEBUG_TRACE, true);
}
$sessions = $this->getBackend($backend, $backendOpts)->getSessions(opts: $opts);
if (count($sessions) < 1) {
$output->writeln(
r("<notice>No active sessions were found for '{backend}'.</notice>", ['backend' => $backend])
);
return self::FAILURE;
}
if ('table' === $mode) {
$items = [];
foreach (ag($sessions, 'sessions', []) as $item) {
$item['item_offset_at'] = formatDuration($item['item_offset_at']);
$item['item_type'] = ucfirst($item['item_type']);
$entity = $this->db->findByBackendId(
backend: $backend,
id: $item['item_id'],
type: 'Episode' === $item['item_type'] ? iState::TYPE_EPISODE : iState::TYPE_MOVIE
);
if (null !== $entity) {
$item['item_title'] = $entity->getName();
}
$i = [];
foreach ($item as $key => $val) {
$i[self::REMAP_FIELDS[$key] ?? $key] = $val;
}
$items[] = $i;
}
$sessions = $items;
}
$this->displayContent($sessions, $output, $mode);
return self::SUCCESS;
}
}