From 69a4008c19c0d3e7728b4cfcb00103e2f79e6829 Mon Sep 17 00:00:00 2001 From: abdulmohsen Date: Wed, 17 Jan 2024 17:04:52 +0300 Subject: [PATCH] Implemented Get sessions for plex. --- src/Backends/Common/ClientInterface.php | 8 + src/Backends/Plex/Action/GetSessions.php | 141 +++++++++++++++ src/Backends/Plex/PlexClient.php | 19 ++ .../Backend/Users/SessionsCommand.php | 168 ++++++++++++++++++ 4 files changed, 336 insertions(+) create mode 100644 src/Backends/Plex/Action/GetSessions.php create mode 100644 src/Commands/Backend/Users/SessionsCommand.php diff --git a/src/Backends/Common/ClientInterface.php b/src/Backends/Common/ClientInterface.php index c1d83ff9..43c4ac70 100644 --- a/src/Backends/Common/ClientInterface.php +++ b/src/Backends/Common/ClientInterface.php @@ -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} + */ + public function getSessions(array $opts = []): array; + /** * Return user access token. * diff --git a/src/Backends/Plex/Action/GetSessions.php b/src/Backends/Plex/Action/GetSessions.php new file mode 100644 index 00000000..96b47147 --- /dev/null +++ b/src/Backends/Plex/Action/GetSessions.php @@ -0,0 +1,141 @@ +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 + ); + } +} diff --git a/src/Backends/Plex/PlexClient.php b/src/Backends/Plex/PlexClient.php index cb86acb5..8db39f69 100644 --- a/src/Backends/Plex/PlexClient.php +++ b/src/Backends/Plex/PlexClient.php @@ -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 */ diff --git a/src/Commands/Backend/Users/SessionsCommand.php b/src/Commands/Backend/Users/SessionsCommand.php new file mode 100644 index 00000000..fc19ba65 --- /dev/null +++ b/src/Commands/Backend/Users/SessionsCommand.php @@ -0,0 +1,168 @@ + '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( + <<[ FAQ ] + ------- + + # How to see the raw response? + + {cmd} {route} --output yaml --include-raw-response -s backend_name + + 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: Backend not specified. Please use [-s, --select-backends].')); + 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('{message}', ['message' => $e->getMessage()])); + return self::FAILURE; + } + } + + if (null === ag(Config::get('servers', []), $backend, null)) { + $output->writeln(r("ERROR: Backend '{backend}' not found.", ['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("No active sessions were found for '{backend}'.", ['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; + } + +}