Added GetSessions for Emby & Jellyfin.
This commit is contained in:
10
src/Backends/Emby/Action/GetSessions.php
Normal file
10
src/Backends/Emby/Action/GetSessions.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Backends\Emby\Action;
|
||||
|
||||
class GetSessions extends \App\Backends\Jellyfin\Action\GetSessions
|
||||
{
|
||||
protected string $action = 'emby.getSessions';
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use App\Backends\Emby\Action\GetInfo;
|
||||
use App\Backends\Emby\Action\GetLibrariesList;
|
||||
use App\Backends\Emby\Action\GetLibrary;
|
||||
use App\Backends\Emby\Action\GetMetaData;
|
||||
use App\Backends\Emby\Action\GetSessions;
|
||||
use App\Backends\Emby\Action\GetUsersList;
|
||||
use App\Backends\Emby\Action\Import;
|
||||
use App\Backends\Emby\Action\InspectRequest;
|
||||
@@ -447,6 +448,24 @@ class EmbyClient 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* For Emby we do not generate api access token, thus we simply return
|
||||
* the given the access token.
|
||||
|
||||
151
src/Backends/Jellyfin/Action/GetSessions.php
Normal file
151
src/Backends/Jellyfin/Action/GetSessions.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Backends\Jellyfin\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 Psr\SimpleCache\CacheInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* Class GetInfo
|
||||
*
|
||||
* This class retrieves information from a jellyfin backend.
|
||||
*/
|
||||
class GetSessions
|
||||
{
|
||||
use CommonTrait;
|
||||
|
||||
protected string $action = 'jellyfin.getSessions';
|
||||
|
||||
public function __construct(
|
||||
protected HttpClientInterface $http,
|
||||
protected LoggerInterface $logger,
|
||||
protected CacheInterface $cache
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backend information.
|
||||
*
|
||||
* @param Context $context Backend 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('/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. Please make sure the container can communicate with the backend.',
|
||||
context: [
|
||||
'action' => $this->action,
|
||||
'client' => $context->clientName,
|
||||
'backend' => $context->backendName,
|
||||
'url' => (string)$url,
|
||||
'response' => $content,
|
||||
],
|
||||
level: Levels::ERROR
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$items = 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' => $items,
|
||||
]);
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (null === ag($item, 'NowPlayingItem')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered[] = $item;
|
||||
}
|
||||
|
||||
$ret = [
|
||||
'sessions' => [],
|
||||
];
|
||||
|
||||
foreach ($filtered as $item) {
|
||||
$ret['sessions'][] = [
|
||||
'user_uid' => ag($item, 'UserId'),
|
||||
'user_name' => ag($item, 'UserName'),
|
||||
'item_id' => ag($item, 'NowPlayingItem.Id'),
|
||||
'item_title' => ag($item, 'NowPlayingItem.Name'),
|
||||
'item_type' => ag($item, 'NowPlayingItem.Type'),
|
||||
'item_offset_at' => ag($item, 'PlayState.PositionTicks') / 1_00_00,
|
||||
'session_state' => (bool)ag($item, 'PlayState.IsPaused', false) === true ? 'paused' : 'playing',
|
||||
'session_updated_at' => makeDate(ag($item, 'LastActivityDate')),
|
||||
'session_id' => ag($item, 'Id'),
|
||||
];
|
||||
}
|
||||
|
||||
if (true === ag_exists($opts, Options::RAW_RESPONSE)) {
|
||||
$ret[Options::RAW_RESPONSE] = $items;
|
||||
}
|
||||
|
||||
return new Response(status: true, response: $ret);
|
||||
},
|
||||
action: $this->action,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use App\Backends\Jellyfin\Action\GetInfo;
|
||||
use App\Backends\Jellyfin\Action\GetLibrariesList;
|
||||
use App\Backends\Jellyfin\Action\GetLibrary;
|
||||
use App\Backends\Jellyfin\Action\GetMetaData;
|
||||
use App\Backends\Jellyfin\Action\GetSessions;
|
||||
use App\Backends\Jellyfin\Action\GetUsersList;
|
||||
use App\Backends\Jellyfin\Action\GetVersion;
|
||||
use App\Backends\Jellyfin\Action\Import;
|
||||
@@ -488,6 +489,24 @@ class JellyfinClient 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
|
||||
*/
|
||||
|
||||
@@ -121,8 +121,9 @@ final class GetSessions
|
||||
$matches
|
||||
) ? $matches[1] : null;
|
||||
|
||||
$ret['sessions'][] = [
|
||||
$item = [
|
||||
'user_uid' => (int)ag($session, 'User.id'),
|
||||
'user_name' => ag($session, 'User.title'),
|
||||
'user_uuid' => $uuid,
|
||||
'item_id' => (int)ag($session, 'ratingKey'),
|
||||
'item_title' => ag($session, 'title'),
|
||||
@@ -131,6 +132,12 @@ final class GetSessions
|
||||
'session_state' => ag($session, 'Player.state'),
|
||||
'session_id' => ag($session, 'Session.id', ag($session, 'sessionKey')),
|
||||
];
|
||||
|
||||
if ('playing' === $item['session_state']) {
|
||||
$item['session_updated_at'] = makeDate(date: time());
|
||||
}
|
||||
|
||||
$ret['sessions'][] = $item;
|
||||
}
|
||||
|
||||
return new Response(status: true, response: $ret);
|
||||
|
||||
@@ -297,7 +297,10 @@ class Command extends BaseCommand
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('select-backends') || $input->mustSuggestArgumentValuesFor('backend')) {
|
||||
if (
|
||||
$input->mustSuggestOptionValuesFor('select-backends') ||
|
||||
$input->mustSuggestOptionValuesFor('select-backend') ||
|
||||
$input->mustSuggestArgumentValuesFor('backend')) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
@@ -10,15 +10,14 @@ use App\Libs\Database\DatabaseInterface as iDB;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\Routable;
|
||||
use DateTimeInterface;
|
||||
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
|
||||
*
|
||||
* Get backend active sessions.
|
||||
*/
|
||||
#[Routable(command: self::ROUTE)]
|
||||
final class SessionsCommand extends Command
|
||||
@@ -26,15 +25,12 @@ 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',
|
||||
'user_name' => 'User',
|
||||
'item_title' => 'Title',
|
||||
'item_type' => 'Type',
|
||||
'item_offset_at' => 'Progress',
|
||||
'session_id' => 'Session ID',
|
||||
'session_updated_at' => 'Session Activity',
|
||||
'session_state' => 'Play State',
|
||||
'session_updated_at' => 'Last Activity',
|
||||
'session_state' => 'State',
|
||||
];
|
||||
|
||||
public function __construct(private iDB $db)
|
||||
@@ -51,7 +47,7 @@ final class SessionsCommand extends Command
|
||||
->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.')
|
||||
->addOption('select-backend', 's', InputOption::VALUE_REQUIRED, 'Select backend.')
|
||||
->setHelp(
|
||||
r(
|
||||
<<<HELP
|
||||
@@ -82,21 +78,9 @@ final class SessionsCommand extends Command
|
||||
* @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 {
|
||||
@@ -107,6 +91,15 @@ final class SessionsCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$mode = $input->getOption('output');
|
||||
|
||||
if (null === ($backend = $input->getOption('select-backend'))) {
|
||||
$output->writeln(r('<error>ERROR: Backend not specified. Please use [-s, --select-backends].</error>'));
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$backend = explode(',', $backend, 2)[0];
|
||||
|
||||
if (null === ag(Config::get('servers', []), $backend, null)) {
|
||||
$output->writeln(r("<error>ERROR: Backend '{backend}' not found.</error>", ['backend' => $backend]));
|
||||
return self::FAILURE;
|
||||
@@ -151,7 +144,15 @@ final class SessionsCommand extends Command
|
||||
$i = [];
|
||||
|
||||
foreach ($item as $key => $val) {
|
||||
$i[self::REMAP_FIELDS[$key] ?? $key] = $val;
|
||||
if (!array_key_exists($key, self::REMAP_FIELDS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('session_updated_at' === $key) {
|
||||
$val = $this->format_date($val);
|
||||
}
|
||||
|
||||
$i[self::REMAP_FIELDS[$key]] = $val;
|
||||
}
|
||||
|
||||
$items[] = $i;
|
||||
@@ -165,4 +166,44 @@ final class SessionsCommand extends Command
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function format_date(DateTimeInterface $date): string
|
||||
{
|
||||
$seconds = time() - $date->getTimestamp();
|
||||
|
||||
if ($seconds < 1) {
|
||||
return '0s';
|
||||
}
|
||||
|
||||
$string = "";
|
||||
|
||||
$years = (int)($seconds / 31536000);
|
||||
$months = (int)($seconds / 2678400);
|
||||
$days = (int)($seconds / (3600 * 24));
|
||||
$hours = (int)($seconds / 3600) % 24;
|
||||
$minutes = (int)($seconds / 60) % 60;
|
||||
$seconds = $seconds % 60;
|
||||
|
||||
if ($years > 0) {
|
||||
$string .= "{$days}y ";
|
||||
}
|
||||
|
||||
if ($months > 0) {
|
||||
$string .= "{$days}d ";
|
||||
}
|
||||
|
||||
if ($hours > 0) {
|
||||
$string .= "{$hours}h ";
|
||||
}
|
||||
|
||||
if ($days < 1 && $minutes > 0) {
|
||||
$string .= "{$minutes}m ";
|
||||
}
|
||||
|
||||
if ($minutes < 1 && $seconds > 0) {
|
||||
$string .= "{$seconds}s";
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user