Cleaned up and updated jellyfin/emby log messages.

This commit is contained in:
ArabCoders
2025-02-14 00:56:30 +03:00
parent bce50ef53b
commit 510c54fbd3
30 changed files with 828 additions and 887 deletions

View File

@@ -15,6 +15,7 @@ use App\Backends\Emby\EmbyClient;
use App\Backends\Jellyfin\JellyfinActionTrait;
use App\Libs\Config;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Options;
use Psr\Http\Message\ServerRequestInterface as iRequest;
@@ -102,13 +103,20 @@ final class ParseWebhook
*/
private function parse(Context $context, iGuid $guid, iRequest $request): Response
{
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
];
if (null === ($json = $request->getParsedBody())) {
return new Response(status: false, extra: [
'http_code' => 400,
'message' => r('[{client}: {backend}] No payload was found in request body.', [
'client' => $context->clientName,
'backend' => $context->backendName,
])
'http_code' => Status::BAD_REQUEST->value,
'message' => r(
text: "Ignoring '{client}: {user}@{backend}' request. Invalid request, no payload.",
context: $logContext
),
]);
}
@@ -118,34 +126,28 @@ final class ParseWebhook
if (null === $type || false === in_array($type, self::WEBHOOK_ALLOWED_TYPES)) {
return new Response(status: false, extra: [
'http_code' => 200,
'message' => r('[{client}: {backend}]: Webhook content type [{type}] is not supported.', [
'client' => $context->clientName,
'backend' => $context->backendName,
'type' => $type
])
'http_code' => Status::OK->value,
'message' => r(
text: "{user}@{backend}: Webhook content type '{type}' is not supported.",
context: [...$logContext, 'type' => $type]
)
]);
}
if (null === $event || false === in_array($event, self::WEBHOOK_ALLOWED_EVENTS)) {
return new Response(status: false, extra: [
'http_code' => 200,
'message' => r('[{client}: {backend}]: Webhook event type [{event}] is not supported.', [
'client' => $context->clientName,
'backend' => $context->backendName,
'event' => $event,
])
'http_code' => Status::OK->value,
'message' => r(
text: "{user}@{backend}: Webhook event type '{event}' is not supported.",
context: [...$logContext, 'event' => $event]
)
]);
}
if (null === $id) {
return new Response(status: false, extra: [
'http_code' => 400,
'message' => r('[{client}: {backend}]: No item id was found in request body.', [
'client' => $context->clientName,
'backend' => $context->backendName,
'event' => $event,
])
'http_code' => Status::BAD_REQUEST->value,
'message' => r('{user}@{backend}: No item id was found in body.', $logContext),
]);
}
@@ -169,6 +171,7 @@ final class ParseWebhook
}
$logContext = [
...$logContext,
'item' => [
'id' => ag($obj, 'Id'),
'type' => ag($obj, 'Type'),
@@ -257,11 +260,10 @@ final class ParseWebhook
return new Response(
status: false,
error: new Error(
message: 'Ignoring [{client}: {backend}] [{title}] webhook event. No valid/supported external ids.',
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{title}' webhook event. No valid/supported external ids.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'title' => $entity->getName(),
...$logContext,
'context' => [
'attributes' => $request->getAttributes(),
'parsed' => $entity->getAll(),
@@ -271,11 +273,8 @@ final class ParseWebhook
level: Levels::ERROR
),
extra: [
'http_code' => 200,
'message' => r('[{client}: {backend}] Import ignored. No valid/supported external ids.', [
'client' => $context->clientName,
'backend' => $context->backendName,
])
'http_code' => Status::OK->value,
'message' => r("{user}@{backend}: No valid/supported external ids.", $logContext)
],
);
}
@@ -285,37 +284,37 @@ final class ParseWebhook
return new Response(
status: false,
error: new Error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] webhook event parsing. Error [{error.message} @ {error.file}:{error.line}].',
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' webhook event parsing. {error.message} at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
'context' => [
'attributes' => $request->getAttributes(),
'payload' => $request->getParsedBody(),
],
'trace' => $e->getTrace(),
],
level: Levels::ERROR,
previous: $e
),
extra: [
'http_code' => 200,
'message' => r('[{client}: {backend}] Failed to handle webhook event payload. Check logs.', [
'http_code' => Status::OK->value,
'message' => r("{user}@{backend}: Failed to process event check logs.", [
'client' => $context->clientName,
'backend' => $context->backendName,
]),
'user' => $context->userContext->name,
])
],
);
}

View File

@@ -11,6 +11,7 @@ use App\Backends\Common\Response;
use App\Backends\Emby\EmbyActionTrait;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Options;
@@ -103,6 +104,10 @@ class Progress
$metadata = $entity->getMetadata($context->backendName);
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'item' => [
'id' => $entity->id,
'type' => $entity->type,
@@ -112,26 +117,16 @@ class Progress
if ($context->backendName === $entity->via) {
$this->logger->info(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. Event originated from this backend.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
context: $logContext,
);
continue;
}
if (null === ag($metadata, iState::COLUMN_ID, null)) {
$this->logger->warning(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. No metadata was found.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
context: $logContext,
);
continue;
}
@@ -139,13 +134,8 @@ class Progress
$senderDate = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_DATE);
if (null === $senderDate) {
$this->logger->warning(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. The event originator did not set a date.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
context: $logContext,
);
continue;
}
@@ -155,16 +145,13 @@ class Progress
$datetime = ag($entity->getExtra($context->backendName), iState::COLUMN_EXTRA_DATE, null);
if (false === $ignoreDate && null !== $datetime && makeDate($datetime)->getTimestamp() > $senderDate) {
$this->logger->warning(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. Event date is older than backend local item date.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
context: [
...$logContext,
'compare' => [
'remote' => makeDate($datetime),
'sender' => makeDate($senderDate),
],
...$logContext,
]
);
continue;
@@ -174,13 +161,8 @@ class Progress
if (array_key_exists($logContext['remote']['id'], $sessions)) {
$this->logger->notice(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. The item is playing right now.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
context: $logContext,
);
continue;
}
@@ -190,23 +172,18 @@ class Progress
$context,
$guid,
$this->getItemDetails($context, $logContext['remote']['id'], [Options::NO_CACHE => true]),
[
'latest_date' => true,
]
['latest_date' => true]
);
if (false === $ignoreDate && makeDate($remoteItem->updated)->getTimestamp() > $senderDate) {
$this->logger->info(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. Event date is older than backend remote item date.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
context: [
...$logContext,
'compare' => [
'remote' => makeDate($remoteItem->updated),
'sender' => makeDate($senderDate),
],
...$logContext,
]
);
continue;
@@ -214,24 +191,16 @@ class Progress
if ($remoteItem->isWatched()) {
$this->logger->info(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. The backend says the item is marked as watched.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The backend says the item is marked as watched.",
context: $logContext,
);
continue;
}
} catch (\App\Libs\Exceptions\RuntimeException|RuntimeException|InvalidArgumentException $e) {
$this->logger->error(
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
'error' => [
'kind' => $e::class,
@@ -264,44 +233,40 @@ class Progress
$logContext['remote']['url'] = (string)$url;
$this->logger->debug(
"{action}: Updating '{client}: {backend}' {item.type} '{item.title}' watch progress to '{progress}'.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
message: "{action}: Updating '{client}: {user}@{backend}' {item.type} '{item.title}' watch progress to '{progress}'.",
context: [
...$logContext,
'progress' => formatDuration($entity->getPlayProgress()),
// -- convert secs to ms for emby to understand it.
'time' => floor($entity->getPlayProgress() * 1_00_00),
...$logContext,
]
);
if (false === (bool)ag($context->options, Options::DRY_RUN, false)) {
$queue->add(
$this->http->request('POST', (string)$url, array_replace_recursive($context->backendHeaders, [
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'PlaybackPositionTicks' => (string)floor($entity->getPlayProgress() * 1_00_00),
],
'user_data' => [
'id' => $key,
'context' => $logContext + [
'backend' => $context->backendName,
],
],
]))
$this->http->request(
method: Method::POST->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, [
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'PlaybackPositionTicks' => (string)floor($entity->getPlayProgress() * 1_00_00),
],
'user_data' => [
'id' => $key,
'context' => $logContext,
],
])
)
);
}
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),

View File

@@ -235,9 +235,7 @@ class EmbyClient implements iClient
guid: $this->guid,
mapper: $mapper,
after: $after,
opts: [
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
]
opts: [Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid')]
);
if ($response->hasError()) {
@@ -260,10 +258,10 @@ class EmbyClient implements iClient
context: $this->context,
guid: $this->guid,
mapper: $mapper,
opts: $opts + [
opts: ag_sets($opts, [
'writer' => $writer,
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
]
])
);
if ($response->hasError()) {
@@ -424,11 +422,10 @@ class EmbyClient implements iClient
guid: $this->guid,
mapper: $mapper,
after: null,
opts: [
opts: ag_sets($opts, [
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
Options::ONLY_LIBRARY_ID => $libraryId,
...$opts,
]
])
);
if ($response->hasError()) {

View File

@@ -7,6 +7,7 @@ namespace App\Backends\Jellyfin\Action;
use App\Backends\Common\Context;
use App\Backends\Common\GuidInterface as iGuid;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Entity\StateEntity;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Mappers\ImportInterface as iImport;
@@ -18,8 +19,6 @@ use Throwable;
* Class Backup
*
* This class is responsible for performing backup operations on Jellyfin backend.
*
* @extends Import
*/
class Backup extends Import
{
@@ -48,6 +47,11 @@ class Backup extends Import
array $logContext = [],
array $opts = [],
): void {
$logContext['action'] = $this->action;
$logContext['client'] = $context->clientName;
$logContext['backend'] = $context->backendName;
$logContext['user'] = $context->userContext->name;
if (JFC::TYPE_SHOW === ($type = ag($item, 'Type'))) {
$this->processShow(context: $context, guid: $guid, item: $item, logContext: $logContext);
return;
@@ -57,10 +61,9 @@ class Backup extends Import
try {
if ($context->trace) {
$this->logger->debug('Processing [{backend}] payload.', [
'backend' => $context->backendName,
$this->logger->debug("{action}: Processing '{client}: {user}@{backend}' payload.", [
...$logContext,
'payload' => $item,
'response' => ['body' => $item],
]);
}
@@ -88,7 +91,6 @@ class Backup extends Import
];
} catch (InvalidArgumentException $e) {
$this->logger->info($e->getMessage(), [
'backend' => $context->backendName,
...$logContext,
'body' => $item,
]);
@@ -102,6 +104,8 @@ class Backup extends Import
opts: $opts + ['library' => ag($logContext, 'library.id')]
);
assert($entity instanceof StateEntity);
$arr = [
iState::COLUMN_TYPE => $entity->type,
iState::COLUMN_WATCHED => (int)$entity->isWatched(),
@@ -161,7 +165,7 @@ class Backup extends Import
fn($key) => str_contains($key, 'guid_'),
ARRAY_FILTER_USE_KEY
),
$arr[iState::COLUMN_PARENT]
$arr[iState::COLUMN_PARENT] ?? []
);
}
}
@@ -171,10 +175,8 @@ class Backup extends Import
}
} catch (Throwable $e) {
$this->logger->error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] backup. Error [{error.message} @ {error.file}:{error.line}].',
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' backup. {error.message} at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),

View File

@@ -9,6 +9,7 @@ use App\Backends\Common\GuidInterface as iGuid;
use App\Backends\Jellyfin\JellyfinClient;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Container;
use App\Libs\Enums\Http\Method;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Extends\Date;
use App\Libs\Mappers\ImportInterface as iImport;
@@ -22,8 +23,6 @@ use Throwable;
* Class Export
*
* Represents a class for exporting data to Jellyfin.
*
* @extends Import
*/
class Export extends Import
{
@@ -48,21 +47,23 @@ class Export extends Import
array $logContext = [],
array $opts = [],
): void {
$logContext['action'] = $this->action;
$logContext['client'] = $context->clientName;
$logContext['backend'] = $context->backendName;
$logContext['user'] = $context->userContext->name;
if (JFC::TYPE_SHOW === ($type = ag($item, 'Type'))) {
$this->processShow(context: $context, guid: $guid, item: $item, logContext: $logContext);
return;
}
$mappedType = JFC::TYPE_MAPPER[$type] ?? $type;
$mappedType = (string)(JFC::TYPE_MAPPER[$type] ?? $type);
try {
if ($context->trace) {
$this->logger->debug("Processing '{client}: {user}@{backend}' response payload.", [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
$this->logger->debug("{action}: Processing '{client}: {user}@{backend}' response payload.", [
...$logContext,
'body' => $item,
'response' => ['body' => $item],
]);
}
@@ -91,19 +92,10 @@ class Export extends Import
'type' => $type,
];
} catch (InvalidArgumentException $e) {
$this->logger->info(
...lw(
message: $e->getMessage(),
context: [
'client' => $context->clientName,
'user' => $context->userContext->name,
'backend' => $context->backendName,
...$logContext,
'body' => $item,
],
e: $e
)
);
$this->logger->info(...lw(message: $e->getMessage(), context: [
...$logContext,
'response' => ['body' => $item],
], e: $e));
return;
}
@@ -112,11 +104,8 @@ class Export extends Import
if (null === ag($item, $dateKey)) {
$this->logger->debug(
"Ignoring '{client}: {user}@{backend}' {item.type} '{item.title}'. No date is set on object.",
[
'client' => $context->clientName,
'user' => $context->userContext->name,
'backend' => $context->backendName,
message: "{action}: Ignoring '{client}: {user}@{backend}' {item.type} '{item.title}'. No date is set on object.",
context: [
'date_key' => $dateKey,
...$logContext,
'response' => [
@@ -139,16 +128,13 @@ class Export extends Import
if (!$rItem->hasGuids() && !$rItem->hasRelativeGuid()) {
$providerIds = (array)ag($item, 'ProviderIds', []);
$message = "Ignoring '{client}: {user}@{backend}' - '{item.title}'. No valid/supported external ids.";
$message = "{action}: Ignoring '{client}: {user}@{backend}' - '{item.title}'. No valid/supported external ids.";
if (empty($providerIds)) {
$message .= ' Most likely unmatched {item.type}.';
}
$this->logger->info($message, [
'client' => $context->clientName,
'user' => $context->userContext->name,
'backend' => $context->backendName,
...$logContext,
'context' => [
'guids' => !empty($providerIds) ? $providerIds : 'None'
@@ -162,11 +148,8 @@ class Export extends Import
if (false === ag($context->options, Options::IGNORE_DATE, false)) {
if (true === ($after instanceof DateTimeInterface) && $rItem->updated >= $after->getTimestamp()) {
$this->logger->debug(
"Ignoring '{client}: {user}@{backend}' - '{item.title}'. Backend date is equal or newer than last sync date.",
[
'client' => $context->clientName,
'user' => $context->userContext->name,
'backend' => $context->backendName,
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{item.title}'. Backend date is equal or newer than last sync date.",
context: [
...$logContext,
'comparison' => [
'lastSync' => makeDate($after),
@@ -182,13 +165,8 @@ class Export extends Import
if (null === ($entity = $mapper->get($rItem))) {
$this->logger->info(
"Ignoring '{client}: {user}@{backend}' - '{item.title}'. {item.type} Is not imported yet. Possibly because the backend was imported as metadata only.",
[
'client' => $context->clientName,
'user' => $context->userContext->name,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{item.title}'. {item.type} Is not imported yet. Possibly because the backend was imported as metadata only.",
context: $logContext,
);
Message::increment("{$context->backendName}.{$mappedType}.ignored_not_found_in_db");
return;
@@ -197,11 +175,8 @@ class Export extends Import
if ($rItem->watched === $entity->watched) {
if (true === (bool)ag($context->options, Options::DEBUG_TRACE)) {
$this->logger->debug(
"Ignoring '{client}: {user}@{backend}' - '{item.title}'. {item.type} play state is identical.",
[
'client' => $context->clientName,
'user' => $context->userContext->name,
'backend' => $context->backendName,
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{item.title}'. {item.type} play state is identical.",
context: [
...$logContext,
'comparison' => [
'backend' => $entity->isWatched() ? 'Played' : 'Unplayed',
@@ -217,11 +192,8 @@ class Export extends Import
if ($rItem->updated >= $entity->updated && false === ag($context->options, Options::IGNORE_DATE, false)) {
$this->logger->debug(
"Ignoring '{client}: {user}@{backend}' - '{item.title}'. Backend date is equal or newer than database date.",
[
'client' => $context->clientName,
'user' => $context->userContext->name,
'backend' => $context->backendName,
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{item.title}'. Backend date is equal or newer than database date.",
context: [
...$logContext,
'comparison' => [
'database' => makeDate($entity->updated),
@@ -234,19 +206,15 @@ class Export extends Import
return;
}
$url = $context->backendUrl->withPath(
r('/Users/{user}/PlayedItems/{id}', [
'user' => $context->backendUser,
'id' => ag($item, 'Id')
])
);
$url = $context->backendUrl->withPath(r('/Users/{user}/PlayedItems/{id}', [
'user' => $context->backendUser,
'id' => ag($item, 'Id')
]));
if ($context->clientName === JellyfinClient::CLIENT_NAME) {
$url = $url->withQuery(
http_build_query([
'DatePlayed' => makeDate($entity->updated)->format(Date::ATOM)
])
);
$url = $url->withQuery(http_build_query([
'DatePlayed' => makeDate($entity->updated)->format(Date::ATOM)
]));
}
$logContext['item']['url'] = $url;
@@ -255,13 +223,10 @@ class Export extends Import
if (true === (bool)ag($context->options, Options::DRY_RUN, false)) {
$this->logger->notice(
"Queuing request to change '{client}: {user}@{backend}' {item.type} '{item.title}' play state to '{play_state}'.",
[
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
message: "{action}: Queuing request to change '{client}: {user}@{backend}' {item.type} '{item.title}' play state to '{play_state}'.",
context: [
...$logContext,
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
]
);
return;
@@ -269,9 +234,9 @@ class Export extends Import
$queue->add(
$this->http->request(
$entity->isWatched() ? 'POST' : 'DELETE',
(string)$url,
$context->backendHeaders + [
method: ($entity->isWatched() ? Method::POST : Method::DELETE)->value,
url: (string)$url,
options: $context->backendHeaders + [
'user_data' => [
'context' => $logContext + [
'backend' => $context->backendName,
@@ -284,11 +249,8 @@ class Export extends Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' export. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' export. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'user' => $context->userContext->name,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),

View File

@@ -10,9 +10,12 @@ use App\Backends\Common\Error;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Libs\Config;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Options;
use JsonException;
use Psr\Log\LoggerInterface as iLogger;
use SensitiveParameter;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
@@ -36,7 +39,7 @@ class GenerateAccessToken
* @param iHttp $http The HTTP client instance.
* @param iLogger $logger The logger instance.
*/
public function __construct(protected iHttp $http, protected iLogger $logger)
public function __construct(protected readonly iHttp $http, protected readonly iLogger $logger)
{
}
@@ -74,48 +77,53 @@ class GenerateAccessToken
private function generateToken(
Context $context,
string|int $identifier,
string $password,
#[SensitiveParameter] string $password,
array $opts = []
): Response {
$url = $context->backendUrl->withPath('/Users/AuthenticateByName');
$this->logger->debug("Requesting '{backend}' to generate access token for '{username}'.", [
'username' => (string)$identifier,
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'url' => (string)$url,
]);
'username' => (string)$identifier,
];
$response = $this->http->request('POST', (string)$url, [
$this->logger->debug(
message: "{action}: Requesting '{client}: {user}@{backend}' to generate access token for '{username}'.",
context: $logContext
);
$response = $this->http->request(Method::POST->value, (string)$url, [
'json' => [
'Username' => (string)$identifier,
'Pw' => $password,
],
'headers' => [
'Accept' => 'application/json',
'Authorization' => r(
'{Agent} Client="{app}", Device="{os}", DeviceId="{id}", Version="{version}"',
[
'Agent' => 'Emby' == $context->clientName ? 'Emby' : 'MediaBrowser',
'app' => Config::get('name') . '/' . $context->clientName,
'os' => PHP_OS,
'id' => md5(Config::get('name') . '/' . $context->clientName),
'version' => getAppVersion(),
]
),
'Authorization' => r('{Agent} Client="{app}", Device="{os}", DeviceId="{id}", Version="{version}"', [
'Agent' => 'Emby' == $context->clientName ? 'Emby' : 'MediaBrowser',
'app' => Config::get('name') . '/' . $context->clientName,
'os' => PHP_OS,
'id' => md5(Config::get('name') . '/' . $context->clientName),
'version' => getAppVersion(),
]),
],
]);
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
return new Response(
status: false,
error: new Error(
message: "Request for '{client}: {backend}' to generate access for '{username}' token returned with unexpected '{status_code}' status code. {body}",
message: "{action}: Request for '{client}: {user}@{backend}' to generate access for '{username}' token returned with unexpected '{status_code}' status code. {body}",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'username' => (string)$identifier,
...$logContext,
'status_code' => $response->getStatusCode(),
'body' => $response->getContent(false),
'response' => [
'body' => $response->getContent(false),
],
],
level: Levels::ERROR
),
@@ -129,11 +137,13 @@ class GenerateAccessToken
);
if ($context->trace) {
$this->logger->debug("Parsing '{backend}' access token response payload.", [
'backend' => $context->backendName,
'url' => (string)$url,
'trace' => $json,
]);
$this->logger->debug(
message: "{action}: Parsing '{client}: {user}@{backend}' - '{username}' access token response payload.",
context: [
...$logContext,
'trace' => $json,
]
);
}
$info = [

View File

@@ -7,9 +7,8 @@ namespace App\Backends\Jellyfin\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Response;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
/**
* Class GetIdentifier
@@ -28,16 +27,12 @@ class GetIdentifier
/**
* Class constructor.
*
* @param HttpClientInterface $http The HTTP client instance to use.
* @param LoggerInterface $logger The logger instance to use.
* @param CacheInterface $cache The cache instance to use.
* @param iHttp $http The HTTP client instance to use.
* @param iLogger $logger The logger instance to use.
* @return void
*/
public function __construct(
protected HttpClientInterface $http,
protected LoggerInterface $logger,
protected CacheInterface $cache
) {
public function __construct(protected readonly iHttp $http, protected readonly iLogger $logger)
{
}
/**
@@ -53,7 +48,7 @@ class GetIdentifier
return $this->tryResponse(
context: $context,
fn: function () use ($context, $opts) {
$info = new GetInfo($this->http, $this->logger, $this->cache)(context: $context, opts: $opts);
$info = new GetInfo($this->http, $this->logger)(context: $context, opts: $opts);
if (false === $info->status) {
return $info;

View File

@@ -9,10 +9,11 @@ use App\Backends\Common\Context;
use App\Backends\Common\Error;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Options;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
/**
* Class GetInfo
@@ -25,11 +26,14 @@ class GetInfo
protected string $action = 'jellyfin.getInfo';
public function __construct(
protected HttpClientInterface $http,
protected LoggerInterface $logger,
protected CacheInterface $cache
) {
/**
* Class constructor.
*
* @param iHttp $http The HTTP client instance to use.
* @param iLogger $logger The logger instance to use.
*/
public function __construct(protected readonly iHttp $http, protected readonly iLogger $logger)
{
}
/**
@@ -46,34 +50,35 @@ class GetInfo
context: $context,
fn: function () use ($context, $opts) {
$url = $context->backendUrl->withPath('/system/Info');
$this->logger->debug("{action}: Requesting '{client}: {backend}' info.", [
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => $url
]);
'user' => $context->userContext->name,
'url' => (string)$url
];
$this->logger->debug("{action}: Requesting '{client}: {user}@{backend}' info.", $logContext);
$response = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
);
$content = $response->getContent(false);
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
return new Response(
status: false,
error: new Error(
message: "{action}: '{client}: {backend}' request returned with unexpected '{status_code}' status code.",
message: "{action}: '{client}: {user}@{backend}' request returned with unexpected '{status_code}' status code.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
'status_code' => $response->getStatusCode(),
'url' => (string)$url,
'response' => $content,
'response' => [
'body' => $content,
],
],
level: Levels::WARNING
)
@@ -84,13 +89,13 @@ class GetInfo
return new Response(
status: false,
error: new Error(
message: "{action}: '{client}: {backend}' request returned with empty response. Please make sure the container can communicate with the backend.",
message: "{action}: '{client}: {user}@{backend}' request 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,
...$logContext,
'response' => [
'status_code' => $response->getStatusCode(),
'body' => $content
],
],
level: Levels::ERROR
)
@@ -104,10 +109,8 @@ class GetInfo
);
if (true === $context->trace) {
$this->logger->debug("{action}: Processing '{client}: {backend}' request payload.", [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
$this->logger->debug("{action}: Processing '{client}: {user}@{backend}' request payload.", [
...$logContext,
'trace' => $item,
]);
}

View File

@@ -11,13 +11,15 @@ use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Options;
use DateInterval;
use JsonException;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
/**
* Class GetLibrariesList
@@ -33,10 +35,10 @@ class GetLibrariesList
/**
* Class constructor
*
* @param HttpClientInterface $http The HTTP client object.
* @param LoggerInterface $logger The logger object.
* @param iHttp $http The HTTP client object.
* @param iLogger $logger The logger object.
*/
public function __construct(protected HttpClientInterface $http, protected LoggerInterface $logger)
public function __construct(protected readonly iHttp $http, protected readonly iLogger $logger)
{
}
@@ -70,23 +72,29 @@ class GetLibrariesList
try {
$json = true === (bool)ag($opts, Options::NO_CACHE) ? $cls() : $this->tryCache(
$context,
'library_list',
$cls,
new DateInterval('PT1M'),
$this->logger
context: $context,
key: 'library_list',
fn: $cls,
ttl: new DateInterval('PT1M'),
logger: $this->logger
);
} catch (RuntimeException $e) {
return new Response(
status: false,
error: new Error(message: $e->getMessage(), level: Levels::ERROR, previous: $e)
status: false, error: new Error(message: $e->getMessage(), level: Levels::ERROR, previous: $e)
);
}
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
];
if ($context->trace) {
$this->logger->debug('Parsing [{backend}] libraries payload.', [
'backend' => $context->backendName,
'trace' => $json,
$this->logger->debug("{action}: Parsing '{client}: {user}@{backend}' libraries payload.", [
...$logContext,
'response' => ['body' => $json],
]);
}
@@ -96,9 +104,9 @@ class GetLibrariesList
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] libraries returned empty list.',
message: "{action}: Request for '{client}: {user}@{backend}' libraries returned empty list.",
context: [
'backend' => $context->backendName,
...$logContext,
'response' => ['body' => $json],
],
level: Levels::WARNING
@@ -165,19 +173,27 @@ class GetLibrariesList
{
$url = $context->backendUrl->withPath(r('/Users/{user_id}/items/', ['user_id' => $context->backendUser]));
$this->logger->debug('Requesting [{backend}] libraries list.', [
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'url' => (string)$url
]);
];
$response = $this->http->request('GET', (string)$url, $context->backendHeaders);
$this->logger->debug("{action}: Requesting '{client}: {user}@{backend}' libraries list.", $logContext);
if (200 !== $response->getStatusCode()) {
$response = $this->http->request(Method::GET->value, (string)$url, $context->backendHeaders);
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
throw new RuntimeException(
r('Request for [{backend}] libraries returned with unexpected [{status_code}] status code.', [
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
])
r(
text: "{action}: Request for '{client}: {user}@{backend}' libraries returned with unexpected '{status_code}' status code.",
context: [
...$logContext,
'status_code' => $response->getStatusCode(),
]
)
);
}

View File

@@ -11,8 +11,10 @@ use App\Backends\Common\GuidInterface as iGuid;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinActionTrait;
use App\Backends\Jellyfin\JellyfinClient;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Options;
@@ -86,10 +88,13 @@ class GetLibrary
return new Response(
status: false,
error: new Error(
message: 'No Library with id [{id}] found in [{backend}] response.',
message: "{action}: No library with id '{id}' found in '{client}: {user}@{backend}' response.",
context: [
'id' => $id,
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'id' => $id,
'response' => [
'body' => $libraries
],
@@ -102,6 +107,10 @@ class GetLibrary
unset($libraries);
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'library' => [
'id' => $id,
'type' => ag($section, 'CollectionType', 'unknown'),
@@ -109,18 +118,14 @@ class GetLibrary
],
];
if (true !== in_array(
ag($logContext, 'library.type'),
[JellyfinClient::COLLECTION_TYPE_MOVIES, JellyfinClient::COLLECTION_TYPE_SHOWS]
)) {
$types = [JFC::COLLECTION_TYPE_MOVIES, JFC::COLLECTION_TYPE_SHOWS];
if (true !== in_array(ag($logContext, 'library.type'), $types)) {
return new Response(
status: false,
error: new Error(
message: 'The Requested [{backend}] Library [{library.id}: {library.title}] returned with unsupported type [{library.type}].',
context: [
'backend' => $context->backendName,
...$logContext,
],
message: "{action}: The request for '{client}: {user}@{backend}' library '{library.id}: {library.title}' returned with unsupported type '{library.type}'.",
context: $logContext,
level: Levels::WARNING
),
);
@@ -132,36 +137,31 @@ class GetLibrary
$extraQueryParams['Limit'] = (int)$limit;
}
$url = $context->backendUrl->withPath(
r('/Users/{user_id}/items/', ['user_id' => $context->backendUser])
)->withQuery(
http_build_query([
'parentId' => $id,
'enableUserData' => 'false',
'enableImages' => 'false',
'excludeLocationTypes' => 'Virtual',
'include' => implode(',', [JellyfinClient::TYPE_SHOW, JellyfinClient::TYPE_MOVIE]),
'fields' => implode(',', JellyfinClient::EXTRA_FIELDS),
...$extraQueryParams,
])
);
$url = $context->backendUrl->withPath(r('/Users/{user_id}/items/', ['user_id' => $context->backendUser]))
->withQuery(
http_build_query([
'parentId' => $id,
'enableUserData' => 'false',
'enableImages' => 'false',
'excludeLocationTypes' => 'Virtual',
'include' => implode(',', [JFC::TYPE_SHOW, JFC::TYPE_MOVIE]),
'fields' => implode(',', JFC::EXTRA_FIELDS),
...$extraQueryParams,
])
);
$logContext['library']['url'] = (string)$url;
$this->logger->debug('Requesting [{backend}] library [{library.title}] content.', [
'backend' => $context->backendName,
...$logContext,
]);
$this->logger->debug("Requesting '{client}: {user}@{backend}' library '{library.title}' content.", $logContext);
$response = $this->http->request('GET', (string)$url, $context->backendHeaders);
$response = $this->http->request(Method::GET->value, (string)$url, $context->backendHeaders);
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] library [{library.title}] returned with unexpected [{status_code}] status code.',
message: "{action}: Request for '{client}: {user}@{backend}' library '{library.title}' items returned with unexpected '{status_code}' status code.",
context: [
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
...$logContext,
],
@@ -185,9 +185,8 @@ class GetLibrary
foreach ($it as $entity) {
if ($entity instanceof DecodingError) {
$this->logger->warning(
'Failed to decode one item of [{backend}] library [{library.title}] content.',
"{action}: Failed to decode one item of '{client}: {user}@{backend}' library '{library.title}' content.",
[
'backend' => $context->backendName,
...$logContext,
'error' => [
'message' => $entity->getErrorMessage(),
@@ -263,21 +262,21 @@ class GetLibrary
$url = $context->backendUrl->withPath(sprintf('/Users/%s/items/%s', $context->backendUser, ag($item, 'Id')));
$possibleTitlesList = ['Name', 'OriginalTitle', 'SortName', 'ForcedSortName'];
$data = [
'backend' => $context->backendName,
...$log,
];
$data = $log;
if ($context->trace) {
$data['trace'] = $item;
}
$this->logger->debug('Processing [{backend}] {item.type} [{item.title} ({item.year})].', $data);
$this->logger->debug(
message: "{action}: Processing '{client}: {user}@{backend}' {item.type} '{item.title} ({item.year})'.",
context: $data
);
$webUrl = $url->withPath('/web/index.html')->withFragment(r('!/{action}?id={id}&serverId={backend_id}', [
'backend_id' => $context->backendId,
'id' => ag($item, 'Id'),
'action' => JellyfinClient::CLIENT_NAME === $context->clientName ? 'details' : 'item',
'action' => JFC::CLIENT_NAME === $context->clientName ? 'details' : 'item',
]));
$metadata = [

View File

@@ -9,12 +9,13 @@ use App\Backends\Common\Context;
use App\Backends\Common\Error;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinClient;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Options;
use DateInterval;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface as iCache;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
/**
* Class GetMetaData
@@ -33,15 +34,12 @@ class GetMetaData
/**
* Class Constructor.
*
* @param HttpClientInterface $http The HTTP client instance.
* @param LoggerInterface $logger The logger instance.
* @param CacheInterface $cache The cache instance.
* @param iHttp $http The HTTP client instance.
* @param iLogger $logger The logger instance.
* @param iCache $cache The cache instance.
*/
public function __construct(
protected HttpClientInterface $http,
protected LoggerInterface $logger,
protected CacheInterface $cache
) {
public function __construct(protected iHttp $http, protected iLogger $logger, protected iCache $cache)
{
}
/**
@@ -49,7 +47,7 @@ class GetMetaData
*
* @param Context $context Backend context.
* @param string|int $id the backend id.
* @param array $opts (Optional) options.
* @param array{query?:array,headers?:array,CACHE_TTL?:DateInterval,NO_CACHE?:bool} $opts (Optional) options.
*
* @return Response The wrapped response.
*/
@@ -64,51 +62,50 @@ class GetMetaData
$cacheKey = $context->clientName . '_' . $context->backendName . '_' . $id . '_metadata';
}
$url = $context->backendUrl
->withPath(
r('/Users/{user_id}/items/{item_id}', [
'user_id' => $context->backendUser,
'item_id' => $id
])
$url = $context->backendUrl->withPath(
r('/Users/{user_id}/items/{item_id}', ['user_id' => $context->backendUser, 'item_id' => $id])
)->withQuery(
http_build_query(
array_merge_recursive([
'recursive' => 'false',
'fields' => implode(',', JellyfinClient::EXTRA_FIELDS),
'enableUserData' => 'true',
'enableImages' => 'false',
'includeItemTypes' => 'Episode,Movie,Series',
], $opts['query'] ?? []),
)
->withQuery(
http_build_query(
array_merge_recursive([
'recursive' => 'false',
'fields' => implode(',', JellyfinClient::EXTRA_FIELDS),
'enableUserData' => 'true',
'enableImages' => 'false',
'includeItemTypes' => 'Episode,Movie,Series',
], $opts['query'] ?? []),
)
);
);
$this->logger->debug("{client}: Requesting '{backend}: {id}' item metadata.", [
$logContext = [
'id' => $id,
'url' => $url,
'client' => $context->clientName,
'backend' => $context->backendName,
]);
'user' => $context->userContext->name,
'url' => (string)$url,
];
$this->logger->debug(
"{action}: Requesting '{client}: {user}@{backend}' - '{id}' item metadata.",
$logContext
);
if (null !== $cacheKey && $this->cache->has($cacheKey)) {
$item = $this->cache->get(key: $cacheKey);
$fromCache = true;
} else {
$response = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
);
if (Status::OK !== Status::from($response->getStatusCode())) {
$response = new Response(
status: false,
error: new Error(
message: "{client} Request for '{backend}: {id}' item returned with unexpected '{status_code}' status code.",
message: "{action}: Request for '{client}: {user}@{backend}' - '{id}' item returned with unexpected '{status_code}' status code.",
context: [
'id' => $id,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
'status_code' => $response->getStatusCode(),
]
)
@@ -135,12 +132,12 @@ class GetMetaData
}
if (true === $context->trace) {
$this->logger->debug("{client} Processing '{backend}: {id}' item payload.", [
'id' => $id,
'client' => $context->clientName,
'backend' => $context->backendName,
$this->logger->debug("{action}: Processing '{client}: {user}@{backend}' - '{id}' item payload.", [
...$logContext,
'cached' => $fromCache,
'trace' => $item,
'response' => [
'body' => $item
],
]);
}

View File

@@ -9,10 +9,12 @@ use App\Backends\Common\Context;
use App\Backends\Common\Error;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Options;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface as iCache;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
/**
* Class GetInfo
@@ -26,9 +28,9 @@ class GetSessions
protected string $action = 'jellyfin.getSessions';
public function __construct(
protected HttpClientInterface $http,
protected LoggerInterface $logger,
protected CacheInterface $cache
protected readonly iHttp $http,
protected readonly iLogger $logger,
protected readonly iCache $cache
) {
}
@@ -47,32 +49,33 @@ class GetSessions
fn: function () use ($context, $opts) {
$url = $context->backendUrl->withPath('/Sessions');
$this->logger->debug('Requesting [{client}: {backend}] play sessions.', [
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => $url
]);
'user' => $context->userContext->name,
'url' => (string)$url
];
$this->logger->debug("{action}: Requesting '{client}: {user}@{backend}' play sessions.", $logContext);
$response = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
);
$content = $response->getContent(false);
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] {action} returned with unexpected [{status_code}] status code.',
message: "{action}: Request for '{client}: {user}@{backend}' get sessions returned with unexpected '{status_code}' status code.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
'status_code' => $response->getStatusCode(),
'url' => (string)$url,
'response' => $content,
'response' => ['body' => $content],
],
level: Levels::WARNING
)
@@ -83,13 +86,10 @@ class GetSessions
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.',
message: "{action}: Request for '{client}: {user}@{backend}' get sessions returned with empty response.",
context: [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => (string)$url,
'response' => $content,
...$logContext,
'response' => ['status_code' => $response->getStatusCode(), 'body' => $content],
],
level: Levels::ERROR
)
@@ -103,11 +103,9 @@ class GetSessions
);
if (true === $context->trace) {
$this->logger->debug('Processing [{client}: {backend}] {action} payload.', [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'trace' => $items,
$this->logger->debug("Processing '{client}: {user}@{backend}' {action} payload.", [
...$logContext,
'response' => ['body' => $items],
]);
}

View File

@@ -9,6 +9,8 @@ use App\Backends\Common\Context;
use App\Backends\Common\Error;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Options;
use JsonException;
use Psr\Log\LoggerInterface as iLogger;
@@ -64,15 +66,19 @@ class GetUser
*/
private function getUser(Context $context, array $opts = []): Response
{
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
];
if (null === $context->backendUser) {
return new Response(
status: false,
error: new Error(
message: "Request for '{client}: {backend}' user info failed. User not set.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
],
message: "{action}: Request for '{client}: {user}@{backend}' user info failed. User not set.",
context: $logContext,
level: Levels::ERROR
),
);
@@ -80,12 +86,10 @@ class GetUser
$url = $context->backendUrl->withPath('/Users/' . $context->backendUser);
$this->logger->debug("Requesting '{client}: {backend}' user '{user}' info.", [
'user' => $context->backendUrl,
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => (string)$url,
]);
$logContext['url'] = (string)$url;
$logContext['userId'] = $context->backendUser;
$this->logger->debug("{action}: Requesting '{client}: {user}@{backend}' user '{userId}' info.", $logContext);
$headers = $context->backendHeaders;
@@ -97,24 +101,21 @@ class GetUser
];
}
$response = $this->http->request('GET', (string)$url, $headers);
$response = $this->http->request(Method::GET->value, (string)$url, $headers);
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
return new Response(
status: false,
error: new Error(
message: "Request for '{client}: {backend}' user '{user}' info returned with unexpected '{status_code}' status code.",
message: "{action}: Request for '{client}: {user}@{backend}' user '{userId}' info returned with unexpected '{status_code}' status code.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->backendUser,
...$logContext,
'status_code' => $response->getStatusCode(),
],
level: Levels::ERROR
),
);
}
$json = json_decode(
json: $response->getContent(),
associative: true,
@@ -122,12 +123,9 @@ class GetUser
);
if ($context->trace) {
$this->logger->debug("Parsing '{client}: {backend}' user '{user}' info payload.", [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->backendUser,
'url' => (string)$url,
'trace' => $json,
$this->logger->debug("{action}: Parsing '{client}: {user}@{backend}' user '{userId}' info payload.", [
...$logContext,
'response' => ['body' => $json],
]);
}

View File

@@ -10,6 +10,8 @@ use App\Backends\Common\Error;
use App\Backends\Common\Levels;
use App\Backends\Common\Response;
use App\Libs\Container;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Options;
use JsonException;
use Psr\Log\LoggerInterface as iLogger;
@@ -75,11 +77,15 @@ class GetUsersList
$url = $context->backendUrl->withPath('/Users/');
$this->logger->debug("Requesting '{client}: {backend}' users list.", [
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'url' => (string)$url,
]);
];
$this->logger->debug("{action}: Requesting '{client}: {user}@{backend}' users list.", $logContext);
$headers = $context->backendHeaders;
@@ -91,15 +97,15 @@ class GetUsersList
];
}
$response = $this->http->request('GET', (string)$url, $headers);
if (200 !== $response->getStatusCode()) {
$response = $this->http->request(Method::GET->value, (string)$url, $headers);
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
return new Response(
status: false,
error: new Error(
message: "Request for '{client}: {backend}' users list returned with unexpected '{status_code}' status code.",
message: "{action}: Request for '{client}: {user}@{backend}' users list returned with unexpected '{status_code}' status code.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
'status_code' => $response->getStatusCode(),
],
level: Levels::ERROR
@@ -114,10 +120,9 @@ class GetUsersList
);
if ($context->trace) {
$this->logger->debug('Parsing [{backend}] user list payload.', [
'backend' => $context->backendName,
'url' => (string)$url,
'trace' => $json,
$this->logger->debug("{action}: Parsing '{client}: {user}@{backend}' user list payload.", [
...$logContext,
'response' => ['body' => $json],
]);
}

View File

@@ -7,9 +7,8 @@ namespace App\Backends\Jellyfin\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Response;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
/**
* Class GetVersion
@@ -28,15 +27,11 @@ class GetVersion
/**
* Class Constructor.
*
* @param HttpClientInterface $http The HTTP client instance.
* @param LoggerInterface $logger The logger instance.
* @param CacheInterface $cache The cache instance.
* @param iHttp $http The HTTP client instance.
* @param iLogger $logger The logger instance.
*/
public function __construct(
protected HttpClientInterface $http,
protected LoggerInterface $logger,
protected CacheInterface $cache
) {
public function __construct(protected readonly iHttp $http, protected readonly iLogger $logger)
{
}
/**
@@ -52,7 +47,7 @@ class GetVersion
return $this->tryResponse(
context: $context,
fn: function () use ($context, $opts) {
$info = new GetInfo($this->http, $this->logger, $this->cache)(context: $context, opts: $opts);
$info = new GetInfo($this->http, $this->logger)(context: $context, opts: $opts);
if (false === $info->status) {
return $info;

View File

@@ -16,7 +16,7 @@ class GetWebUrl
{
use CommonTrait;
private string $action = 'jellyfin.getWebUrl';
protected string $action = 'jellyfin.getWebUrl';
private array $supportedTypes = [
iState::TYPE_MOVIE,
@@ -40,7 +40,14 @@ class GetWebUrl
return new Response(
status: false,
error: new Error(
message: r('Invalid Web url type "{type}".', ['type' => $type]),
message: "{action}: Invalid Web url type '{type}' for '{client}: {user}@{backend}'.",
context: [
'type' => $type,
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext,
],
level: Levels::WARNING,
)
);
@@ -48,17 +55,16 @@ class GetWebUrl
return $this->tryResponse(
context: $context,
fn: function () use ($context, $type, $id, $opts) {
$webUrl = $context->backendUrl->withPath('/web/index.html')->withFragment(
fn: fn() => new Response(
status: true,
response: $context->backendUrl->withPath('/web/index.html')->withFragment(
r('!/{action}?id={id}&serverId={backend_id}', [
'backend_id' => $context->backendId,
'id' => $id,
'action' => JellyfinClient::CLIENT_NAME === $context->clientName ? 'details' : 'item',
])
);
return new Response(status: true, response: $webUrl);
},
)
),
action: $this->action
);
}

View File

@@ -11,6 +11,7 @@ use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinActionTrait;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Guid;
use App\Libs\Mappers\ImportInterface as iImport;
@@ -25,7 +26,7 @@ use JsonMachine\JsonDecoder\DecodingError;
use JsonMachine\JsonDecoder\ErrorWrappingDecoder;
use JsonMachine\JsonDecoder\ExtJsonDecoder;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface as iException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Symfony\Contracts\HttpClient\ResponseInterface as iResponse;
@@ -95,8 +96,9 @@ class Import
logContext: $logContext
),
error: fn(array $logContext = []) => fn(Throwable $e) => $this->logger->error(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' library '{library.title}' request. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' library '{library.title}' request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => property_exists($this, 'action') ? $this->action : 'import',
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
@@ -133,49 +135,49 @@ class Import
*/
protected function getLibraries(Context $context, Closure $handle, Closure $error, array $opts = []): array
{
$rContext = [
'action' => property_exists($this, 'action') ? $this->action : 'import',
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
];
try {
$url = $context->backendUrl->withPath(r('/Users/{user_id}/items/', ['user_id' => $context->backendUser]));
$rContext['url'] = (string)$url;
$this->logger->debug("Requesting '{client}: {user}@{backend}' libraries.", [
'user' => $context->userContext->name,
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => $url
]);
$this->logger->debug("Requesting '{client}: {user}@{backend}' libraries.", $rContext);
$response = $this->http->request('GET', (string)$url, $context->backendHeaders);
$response = $this->http->request(Method::GET->value, (string)$url, $context->backendHeaders);
$payload = $response->getContent(false);
if ($context->trace) {
$this->logger->debug("Processing '{client}: {user}@{backend}' response.", [
'user' => $context->userContext->name,
'client' => $context->clientName,
'backend' => $context->backendName,
'url' => (string)$url,
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(false),
'response' => $payload,
$this->logger->debug("{action}: Processing '{client}: {user}@{backend}' response.", [
...$rContext,
'response' => [
'headers' => $response->getHeaders(false),
'status_code' => $response->getStatusCode(),
'body' => $payload
],
]);
}
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
$logContext = [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
$rContext = ag_sets($rContext, [
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(false),
];
]);
if ($context->trace) {
$logContext['trace'] = $response->getInfo('debug');
$rContext = ag_set($rContext, 'response.body', $response->getInfo('debug'));
}
$this->logger->error(
"Request for '{client}: {user}@{backend}' libraries returned with unexpected '{status_code}' status code.",
$logContext
message: "{action}: Request for '{client}: {user}@{backend}' libraries returned with unexpected '{status_code}' status code.",
context: $rContext
);
Message::add("{$context->backendName}.has_errors", true);
return [];
}
@@ -189,23 +191,22 @@ class Import
$listDirs = ag($json, 'Items', []);
if (empty($listDirs)) {
$this->logger->warning("Request for '{client}: {user}@{backend}' libraries returned with empty list.", [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'body' => $payload,
]);
$this->logger->warning(
message: "{action}: Request for '{client}: {user}@{backend}' libraries returned with empty list.",
context: [
...$rContext,
'response' => ['body' => $payload],
]
);
Message::add("{$context->backendName}.has_errors", true);
return [];
}
} catch (ExceptionInterface $e) {
} catch (iException $e) {
$this->logger->error(
...lw(
message: "Request for '{client}: {user}@{backend}' libraries has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Request for '{client}: {user}@{backend}' libraries has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
...$rContext,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
@@ -228,11 +229,9 @@ class Import
} catch (JsonException $e) {
$this->logger->error(
...lw(
message: "Request for '{client}: {user}@{backend}' libraries returned with invalid body. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Request for '{client}: {user}@{backend}' libraries returned with invalid body. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
...$rContext,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
@@ -254,11 +253,9 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' request for libraries. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
...$rContext,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -294,9 +291,7 @@ class Import
$libraryId = (string)ag($section, 'Id');
$logContext = [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
...$rContext,
'library' => [
'id' => $libraryId,
'title' => ag($section, 'Name', '??'),
@@ -317,9 +312,7 @@ class Import
}
$url = $context->backendUrl->withPath(
r('/Users/{user_id}/items/', [
'user_id' => $context->backendUser
])
r('/Users/{user_id}/items/', ['user_id' => $context->backendUser])
)->withQuery(
http_build_query([
'sortBy' => 'DateCreated',
@@ -336,26 +329,21 @@ class Import
$logContext['library']['url'] = (string)$url;
$this->logger->debug(
"Requesting '{client}: {user}@{backend}' - '{library.title}' items count.",
$logContext
message: "{action}: Requesting '{client}: {user}@{backend}' - '{library.title}' items count.",
context: $logContext
);
try {
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, [
'user_data' => $logContext
])
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, ['user_data' => $logContext])
);
} catch (ExceptionInterface $e) {
} catch (iException $e) {
$this->logger->error(
...lw(
message: "Request for '{client}: {user}@{backend}' - '{library.title}' items count failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Request for '{client}: {user}@{backend}' - '{library.title}' items count failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
@@ -378,11 +366,8 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' - '{library.title}' items count request. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' - '{library.title}' items count request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -412,10 +397,10 @@ class Import
try {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
$this->logger->error(
message: "Request for '{client}: {user}@{backend}' - '{library.title}' items count returned with unexpected '{status_code}' status code.",
message: "{action}: Request for '{client}: {user}@{backend}' - '{library.title}' items count returned with unexpected '{status_code}' status code.",
context: [
'status_code' => $response->getStatusCode(),
...$logContext,
'status_code' => $response->getStatusCode(),
]
);
continue;
@@ -427,24 +412,24 @@ class Import
if ($totalCount < 1) {
$this->logger->warning(
message: "Request for '{client}: {user}@{backend}' - '{library.title}' items count returned with 0 or less.",
message: "{action}: Request for '{client}: {user}@{backend}' - '{library.title}' items count returned with 0 or less.",
context: [
'headers' => $response->getHeaders(),
...$logContext,
'response' => [
'headers' => $response->getHeaders(),
'body' => $response->getContent(false)
],
]
);
continue;
}
$total[ag($logContext, 'library.id')] = $totalCount;
} catch (ExceptionInterface $e) {
} catch (iException $e) {
$this->logger->error(
...lw(
message: "Request for '{client}: {user}@{backend}' - '{library.title}' total items has failed. '{error.kind}' '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Request for '{client}: {user}@{backend}' - '{library.title}' total items has failed. '{error.kind}' '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->backendName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -469,9 +454,6 @@ class Import
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' requests for items count. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -499,6 +481,7 @@ class Import
// -- Episodes Parent external ids.
foreach ($listDirs as $section) {
$logContext = [
...$rContext,
'library' => [
'id' => (string)ag($section, 'Id'),
'title' => ag($section, 'Name', '??'),
@@ -538,29 +521,26 @@ class Import
$logContext['library']['url'] = (string)$url;
$this->logger->debug(
"Requesting '{client}: {user}@{backend}' - '{library.title}' series external ids.",
$logContext
message: "{action}: Requesting '{client}: {user}@{backend}' - '{library.title}' series external ids.",
context: $logContext
);
try {
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, [
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, [
'user_data' => [
'ok' => $handle($logContext),
'error' => $error($logContext),
]
])
);
} catch (ExceptionInterface $e) {
} catch (iException $e) {
$this->logger->error(
...lw(
message: "Request for '{client}: {user}@{backend}' - '{library.title}' series external ids has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Request for '{client}: {user}@{backend}' - '{library.title}' series external ids has failed. '{error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
@@ -583,11 +563,8 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' '{library.title}' series external ids request. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' '{library.title}' series external ids request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -613,9 +590,7 @@ class Import
// -- get paginated movies/episodes.
foreach ($listDirs as $section) {
$logContext = [
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
...$rContext,
'library' => [
'id' => (string)ag($section, 'Id'),
'title' => ag($section, 'Name', '??'),
@@ -626,8 +601,8 @@ class Import
if (true === in_array(ag($logContext, 'library.id'), $ignoreIds ?? [])) {
$ignored++;
$this->logger->info(
"Ignoring '{client}: {user}@{backend}' - '{library.title}'. Requested by user.",
$logContext
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{library.title}'. Requested by user.",
context: $logContext
);
continue;
}
@@ -635,8 +610,8 @@ class Import
if (!in_array(ag($logContext, 'library.type'), [JFC::COLLECTION_TYPE_SHOWS, JFC::COLLECTION_TYPE_MOVIES])) {
$unsupported++;
$this->logger->info(
"Ignoring '{client}: {user}@{backend}' - '{library.title}'. Library type '{library.type}' is not supported.",
$logContext,
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{library.title}'. Library type '{library.type}' is not supported.",
context: $logContext,
);
continue;
}
@@ -644,8 +619,8 @@ class Import
if (false === array_key_exists(ag($logContext, 'library.id'), $total)) {
$ignored++;
$this->logger->warning(
"Ignoring '{client}: {user}@{backend}' - '{library.title}'. No items count was found.",
$logContext
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{library.title}'. No items count was found.",
context: $logContext
);
continue;
}
@@ -685,28 +660,25 @@ class Import
$logContext['library']['url'] = (string)$url;
$this->logger->debug(
message: "Requesting '{client}: {user}@{backend}' '{library.title} {segment.number}/{segment.of}' content list.",
message: "{action}: Requesting '{client}: {user}@{backend}' '{library.title} {segment.number}/{segment.of}' content list.",
context: $logContext,
);
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, [
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, [
'user_data' => [
'ok' => $handle($logContext),
'error' => $error($logContext),
]
])
);
} catch (ExceptionInterface $e) {
} catch (iException $e) {
$this->logger->error(
...lw(
message: "Request for '{client}: {user}@{backend}' '{library.title} {segment.number}/{segment.of}' content list has failed. {error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Request for '{client}: {user}@{backend}' '{library.title} {segment.number}/{segment.of}' content list has failed. {error.kind}' with message '{error.message}' at '{error.file}:{error.line}'.",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'error' => [
'line' => $e->getLine(),
'kind' => $e::class,
@@ -729,11 +701,8 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' '{library.title} {segment.number}/{segment.of}' content list request. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' '{library.title} {segment.number}/{segment.of}' content list request. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -759,9 +728,7 @@ class Import
if (0 === count($requests)) {
$this->logger->warning("No requests for '{client}: {user}@{backend}' libraries were queued.", [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
...$rContext,
'context' => [
'total' => count($listDirs),
'ignored' => $ignored,
@@ -788,12 +755,12 @@ class Import
*/
protected function handle(Context $context, iResponse $response, Closure $callback, array $logContext = []): void
{
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
$this->logger->error(
"Request for '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' content returned with unexpected '{status_code}' status code.",
[
'status_code' => $response->getStatusCode(),
message: "{action}: Request for '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' content returned with unexpected '{status_code}' status code.",
context: [
...$logContext,
'status_code' => $response->getStatusCode(),
]
);
return;
@@ -801,8 +768,8 @@ class Import
$start = microtime(true);
$this->logger->info(
"Parsing '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' response.",
[
message: "{action}: Parsing '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' response.",
context: [
...$logContext,
'time' => [
'start' => $start,
@@ -816,10 +783,7 @@ class Import
options: [
'pointer' => '/Items',
'decoder' => new ErrorWrappingDecoder(
innerDecoder: new ExtJsonDecoder(
assoc: true,
options: JSON_INVALID_UTF8_IGNORE
)
innerDecoder: new ExtJsonDecoder(assoc: true, options: JSON_INVALID_UTF8_IGNORE)
)
]
);
@@ -828,7 +792,7 @@ class Import
try {
if ($entity instanceof DecodingError) {
$this->logger->warning(
"Failed to decode one item of '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' content.",
"{action}: Failed to decode one item of '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' content.",
[
...$logContext,
'error' => [
@@ -848,7 +812,7 @@ class Import
$range = range(ag($entity, 'IndexNumber'), $indexNumberEnd);
if (count($range) > $episodeRangeLimit) {
$this->logger->warning(
"Ignoring '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' {item.type} '{item.id}: {item.title}' episode range, and treating it as single episode. Backend says it covers '{item.indexNumber}-{item.indexNumberEnd}' '{item.rangeCount}' The limit is '{rangeLimit}' per record.",
"{action}: Ignoring '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' {item.type} '{item.id}: {item.title}' episode range, and treating it as single episode. Backend says it covers '{item.indexNumber}-{item.indexNumberEnd}' '{item.rangeCount}' The limit is '{rangeLimit}' per record.",
[
'rangeLimit' => $episodeRangeLimit,
'item' => [
@@ -866,7 +830,7 @@ class Import
} else {
foreach ($range as $i) {
$this->logger->debug(
"Making virtual episode for '{client}:{user}@{backend}' '{library.title}] [{segment.number}/{segment.of}' {item.type} '{item.id}: {item.title}' '{item.indexNumber} => {item.i} of {item.indexNumberEnd}'.",
"{action}: Making virtual episode for '{client}:{user}@{backend}' '{library.title}] [{segment.number}/{segment.of}' {item.type} '{item.id}: {item.title}' '{item.indexNumber} => {item.i} of {item.indexNumberEnd}'.",
[
...$logContext,
'item' => [
@@ -889,11 +853,8 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' parsing '{library.title} {segment.number}/{segment.of}' item response. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' parsing '{library.title} {segment.number}/{segment.of}' item response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -918,11 +879,8 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' parsing of '{library.title} {segment.number}/{segment.of}' response. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' parsing of '{library.title} {segment.number}/{segment.of}' response. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -945,7 +903,7 @@ class Import
$end = microtime(true);
$this->logger->info(
"Parsing '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' completed in '{time.duration}'s.",
"{action}: Parsing '{client}: {user}@{backend}' - '{library.title} {segment.number}/{segment.of}' completed in '{time.duration}'s.",
[
...$logContext,
'time' => [
@@ -981,10 +939,11 @@ class Import
if ($context->trace) {
$this->logger->debug(
"Processing '{client}: {user}@{backend}' {item.type} '{item.title} ({item.year})' payload.",
"{action}: Processing '{client}: {user}@{backend}' {item.type} '{item.title} ({item.year})' payload.",
[
'body' => $item,
...$logContext,
'response' => ['body' => $item],
]
);
}
@@ -992,7 +951,7 @@ class Import
$providersId = (array)ag($item, 'ProviderIds', []);
if (false === $guid->has(guids: $providersId, context: $logContext)) {
$message = "Ignoring '{client}: {user}@{backend}' - '{item.title}'. {item.type} has no valid/supported external ids.";
$message = "{action}: Ignoring '{client}: {user}@{backend}' - '{item.title}'. {item.type} has no valid/supported external ids.";
if (empty($providersId)) {
$message .= ' Most likely unmatched {item.type}.';
@@ -1010,10 +969,7 @@ class Import
JFC::TYPE_SHOW . '.' . ag($logContext, 'item.id'),
Guid::fromArray(
payload: $guid->get(guids: $providersId, context: $logContext),
context: [
'backend' => $context->backendName,
...$logContext,
]
context: $logContext,
)->getAll()
);
}
@@ -1045,9 +1001,9 @@ class Import
try {
if ($context->trace) {
$this->logger->debug("Processing '{client}: {user}@{backend}' response payload.", [
$this->logger->debug("{action}: Processing '{client}: {user}@{backend}' response payload.", [
...$logContext,
'body' => $item,
'response' => ['body' => $item],
]);
}
@@ -1077,18 +1033,15 @@ class Import
} catch (InvalidArgumentException $e) {
$this->logger->error(
...lw(
message: "Failed to parse '{client}: {user}@{backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
message: "{action}: Failed to parse '{client}: {user}@{backend}' item response. '{error.kind}' with '{error.message}' at '{error.file}:{error.line}' ",
context: [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
'body' => $item,
'response' => ['body' => $item],
...$logContext,
],
e: $e
@@ -1108,9 +1061,9 @@ class Import
if (true === $isPlayed && false === ag_exists($item, 'UserData.LastPlayedDate')) {
if ($context->trace) {
$this->logger->debug(
"The {item.type} '{client}: {user}@{backend}' - '{item.id}: {item.title}' is marked as played without LastPlayedDate field.",
"{action}: The {item.type} '{client}: {user}@{backend}' - '{item.id}: {item.title}' is marked as played without LastPlayedDate field.",
[
'body' => $item,
'response' => ['body' => $item],
...$logContext,
]
);
@@ -1120,10 +1073,10 @@ class Import
if (null === ag($item, $dateKey)) {
$this->logger->debug(
message: "Ignoring '{client}: {user}@{backend}' - '{item.id}: {item.title}'. No date key '{date_key}' is set on object. '{body}'",
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{item.id}: {item.title}'. No date key '{date_key}' is set on object. '{response.body}'",
context: [
'date_key' => $dateKey,
'body' => arrayToString($item),
'response' => ['body' => arrayToString($item)],
...$logContext,
]
);
@@ -1152,11 +1105,8 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' occurred during '{client}: {user}@{backend}' - '{library.title}' - '{item.id}: {item.title}' entity creation. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' occurred during '{client}: {user}@{backend}' - '{library.title}' - '{item.id}: {item.title}' entity creation. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -1164,6 +1114,13 @@ class Import
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
'file' => after($e->getFile(), ROOT_PATH),
],
],
e: $e
)
@@ -1175,7 +1132,7 @@ class Import
if (false === $entity->hasGuids() && false === $entity->hasRelativeGuid()) {
$providerIds = (array)ag($item, 'ProviderIds', []);
$message = "Ignoring '{client}: {user}@{backend}' - '{item.title}'. No valid/supported external ids.";
$message = "{action}: Ignoring '{client}: {user}@{backend}' - '{item.title}'. No valid/supported external ids.";
if (empty($providerIds)) {
$message .= " Most likely unmatched '{item.type}'.";
@@ -1199,18 +1156,15 @@ class Import
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' - '{library.title}' - '{item.title}' {action}. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' - '{library.title}' - '{item.title}' {action}. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => property_exists($this, 'action') ? $this->action : 'import',
'backend' => $context->backendName,
'client' => $context->clientName,
'user' => $context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
@@ -1218,7 +1172,6 @@ class Import
'message' => $e->getMessage(),
'trace' => $e->getTrace(),
],
...$logContext,
],
e: $e
)

View File

@@ -36,7 +36,7 @@ final class InspectRequest
return $this->tryResponse(
context: $context,
fn: function () use ($request) {
$userAgent = ag($request->getServerParams(), 'HTTP_USER_AGENT', '');
$userAgent = (string)ag($request->getServerParams(), 'HTTP_USER_AGENT', '');
if (false === str_starts_with($userAgent, 'Jellyfin-Server/')) {
return new Response(status: false);

View File

@@ -14,6 +14,7 @@ use App\Backends\Jellyfin\JellyfinActionTrait;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Config;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Options;
use Psr\Http\Message\ServerRequestInterface as iRequest;
@@ -86,10 +87,20 @@ final class ParseWebhook
*/
private function parse(Context $context, iGuid $guid, iRequest $request): Response
{
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
];
if (null === ($json = $request->getParsedBody())) {
return new Response(status: false, extra: [
'http_code' => 400,
'message' => r('{client}: No payload.', ['client' => $context->clientName]),
'http_code' => Status::BAD_REQUEST->value,
'message' => r(
text: "Ignoring '{client}: {user}@{backend}' request. Invalid request, no payload.",
context: $logContext
),
]);
}
@@ -99,28 +110,28 @@ final class ParseWebhook
if (null === $type || false === in_array($type, self::WEBHOOK_ALLOWED_TYPES)) {
return new Response(status: false, extra: [
'http_code' => 200,
'message' => r('{backend}: Webhook content type [{type}] is not supported.', [
'backend' => $context->backendName,
'type' => $type,
])
'http_code' => Status::OK->value,
'message' => r(
text: "{user}@{backend}: Webhook content type '{type}' is not supported.",
context: [...$logContext, 'type' => $type]
)
]);
}
if (null === $event || false === in_array($event, self::WEBHOOK_ALLOWED_EVENTS)) {
return new Response(status: false, extra: [
'http_code' => 200,
'message' => r('{backend}: Webhook event type [{event}] is not supported.', [
'backend' => $context->backendName,
'event' => $event
])
'http_code' => Status::OK->value,
'message' => r(
text: "{user}@{backend}: Webhook event type '{event}' is not supported.",
context: [...$logContext, 'event' => $event]
)
]);
}
if (null === $id) {
return new Response(status: false, extra: [
'http_code' => 400,
'message' => r('{backend}: No item id was found in body.', ['client' => $context->backendName]),
'http_code' => Status::BAD_REQUEST->value,
'message' => r('{user}@{backend}: No item id was found in body.', $logContext),
]);
}
@@ -131,6 +142,7 @@ final class ParseWebhook
$lastPlayedAt = true === $isPlayed ? makeDate() : null;
$logContext = [
...$logContext,
'item' => [
'id' => ag($obj, 'Id'),
'type' => ag($obj, 'Type'),
@@ -145,9 +157,7 @@ final class ParseWebhook
'episode' => str_pad((string)ag($obj, 'IndexNumber', 0), 3, '0', STR_PAD_LEFT),
]),
default => throw new InvalidArgumentException(
r('Unexpected Content type [{type}] was received.', [
'type' => $type
])
r('Unexpected Content type [{type}] was received.', ['type' => $type])
),
},
'year' => ag($obj, 'ProductionYear'),
@@ -219,10 +229,10 @@ final class ParseWebhook
return new Response(
status: false,
error: new Error(
message: 'Ignoring [{backend}] [{title}] webhook event. No valid/supported external ids.',
message: "{action}: Ignoring '{client}: {user}@{backend}' - '{title}' webhook event. No valid/supported external ids.",
context: [
'backend' => $context->backendName,
'title' => $entity->getName(),
...$logContext,
'context' => [
'attributes' => $request->getAttributes(),
'parsed' => $entity->getAll(),
@@ -232,8 +242,8 @@ final class ParseWebhook
level: Levels::ERROR
),
extra: [
'http_code' => 200,
'message' => $context->backendName . ': Import ignored. No valid/supported external ids.'
'http_code' => Status::OK->value,
'message' => r("{user}@{backend}: No valid/supported external ids.", $logContext)
],
);
}
@@ -243,16 +253,15 @@ final class ParseWebhook
return new Response(
status: false,
error: new Error(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] webhook event parsing. Error [{error.message} @ {error.file}:{error.line}].',
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' webhook event parsing. {error.message} at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
],
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
@@ -269,8 +278,12 @@ final class ParseWebhook
previous: $e
),
extra: [
'http_code' => 200,
'message' => $context->backendName . ': Failed to handle payload. Check logs.'
'http_code' => Status::OK->value,
'message' => r("{user}@{backend}: Failed to process event check logs.", [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
])
],
);
}

View File

@@ -115,6 +115,7 @@ class Progress
} catch (Throwable) {
// simply ignore this error as it's not important enough to interrupt the whole process.
}
foreach ($entities as $key => $entity) {
if (true !== ($entity instanceof iState)) {
continue;
@@ -129,6 +130,10 @@ class Progress
$metadata = $entity->getMetadata($context->backendName);
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'item' => [
'id' => $entity->id,
'type' => $entity->type,
@@ -138,26 +143,16 @@ class Progress
if ($context->backendName === $entity->via) {
$this->logger->info(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. Event originated from this backend.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
context: $logContext,
);
continue;
}
if (null === ag($metadata, iState::COLUMN_ID, null)) {
$this->logger->warning(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. No metadata was found.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
context: $logContext,
);
continue;
}
@@ -165,13 +160,9 @@ class Progress
$senderDate = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_DATE);
if (null === $senderDate) {
$this->logger->warning(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. The event originator did not set a date.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
context: $logContext,
);
continue;
}
@@ -181,16 +172,13 @@ class Progress
$datetime = ag($entity->getExtra($context->backendName), iState::COLUMN_EXTRA_DATE, null);
if (false === $ignoreDate && null !== $datetime && makeDate($datetime)->getTimestamp() > $senderDate) {
$this->logger->warning(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. Event date is older than backend local item date.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
context: [
...$logContext,
'compare' => [
'remote' => makeDate($datetime),
'sender' => makeDate($senderDate),
],
...$logContext,
]
);
continue;
@@ -200,13 +188,9 @@ class Progress
if (array_key_exists($logContext['remote']['id'], $sessions)) {
$this->logger->notice(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. The item is playing right now.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
context: $logContext,
);
continue;
}
@@ -215,9 +199,7 @@ class Progress
$remoteItem = $this->createEntity(
$context,
$guid,
$this->getItemDetails($context, $logContext['remote']['id'], [
Options::NO_CACHE => true,
]),
$this->getItemDetails($context, $logContext['remote']['id'], [Options::NO_CACHE => true,]),
[
'latest_date' => true,
]
@@ -225,16 +207,13 @@ class Progress
if (false === $ignoreDate && makeDate($remoteItem->updated)->getTimestamp() > $senderDate) {
$this->logger->info(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. Event date is older than backend remote item date.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
context: [
...$logContext,
'compare' => [
'remote' => makeDate($remoteItem->updated),
'sender' => makeDate($senderDate),
],
...$logContext,
]
);
continue;
@@ -242,24 +221,16 @@ class Progress
if ($remoteItem->isWatched()) {
$this->logger->info(
"{action}: Not processing '{item.title}' for '{client}: {backend}'. The backend says the item is marked as watched.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The backend says the item is marked as watched.",
context: $logContext,
);
continue;
}
} catch (\App\Libs\Exceptions\RuntimeException|RuntimeException|InvalidArgumentException $e) {
$this->logger->error(
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' get {item.type} '{item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -292,15 +263,12 @@ class Progress
$logContext['remote']['url'] = (string)$url;
$this->logger->debug(
"{action}: Updating '{client}: {backend}' {item.type} '{item.title}' watch progress to '{progress}'.",
[
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'progress' => $entity->hasPlayProgress() ? formatDuration($entity->getPlayProgress()) : '0:0:0',
// -- convert secs to ms for jellyfin to understand it.
'time' => floor($entity->getPlayProgress() * 1_00_00),
message: "{action}: Updating '{client}: {user}@{backend}' {item.type} '{item.title}' watch progress to '{progress}'.",
context: [
...$logContext,
'progress' => $entity->hasPlayProgress() ? formatDuration($entity->getPlayProgress()) : '0:0:0',
// -- convert secs to ms for jellyfin/emby to understand it.
'time' => floor($entity->getPlayProgress() * 1_00_00),
]
);
@@ -315,9 +283,7 @@ class Progress
],
'user_data' => [
'id' => $key,
'context' => $logContext + [
'backend' => $context->backendName,
],
'context' => $logContext,
],
]))
);
@@ -325,11 +291,8 @@ class Progress
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' change {item.type} '{item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $this->action,
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),

View File

@@ -7,14 +7,16 @@ namespace App\Backends\Jellyfin\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinClient;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Extends\Date;
use App\Libs\Options;
use App\Libs\QueueRequests;
use DateTimeInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Throwable;
/**
@@ -34,10 +36,10 @@ class Push
/**
* Class constructor.
*
* @param HttpClientInterface $http The HTTP client.
* @param LoggerInterface $logger The logger.
* @param iHttp $http The HTTP client.
* @param iLogger $logger The logger.
*/
public function __construct(protected HttpClientInterface $http, protected LoggerInterface $logger)
public function __construct(protected readonly iHttp $http, protected readonly iLogger $logger)
{
}
@@ -92,6 +94,10 @@ class Push
$metadata = $entity->getMetadata($context->backendName);
$logContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'item' => [
'id' => $entity->id,
'type' => $entity->type,
@@ -101,11 +107,8 @@ class Push
if (null === ag($metadata, iState::COLUMN_ID, null)) {
$this->logger->warning(
'Ignoring [{item.title}] for [{backend}]. No metadata was found.',
[
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Ignoring '{item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
context: $logContext
);
continue;
}
@@ -120,7 +123,7 @@ class Push
])
)->withQuery(
http_build_query([
'fields' => implode(',', JellyfinClient::EXTRA_FIELDS),
'fields' => implode(',', JFC::EXTRA_FIELDS),
'enableUserData' => 'true',
'enableImages' => 'false',
])
@@ -128,15 +131,15 @@ class Push
$logContext['remote']['url'] = (string)$url;
$this->logger->debug('Requesting [{backend}] {item.type} [{item.title}] metadata.', [
'backend' => $context->backendName,
...$logContext,
]);
$this->logger->debug(
message: "{action}: Requesting '{client}: {user}@{backend}' {item.type} '{item.title}' metadata.",
context: $logContext
);
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, [
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, [
'user_data' => [
'id' => $key,
'context' => $logContext,
@@ -146,10 +149,8 @@ class Push
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] request for {item.type} [{item.title}] metadata. Error [{error.message} @ {error.file}:{error.line}].',
message: "{action}: Exception '{error.kind}' unhandled during '{client}: {user}@{backend}' request for {item.type} '{item.title}' metadata. {error.message} at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -178,10 +179,10 @@ class Push
try {
if (null === ($id = ag($response->getInfo('user_data'), 'id'))) {
$this->logger->error('Unable to get entity object id.', [
'backend' => $context->backendName,
...$logContext,
]);
$this->logger->error(
message: "{action}: Unable to get entity object id for '{client}: {user}@{backend}'.",
context: $logContext
);
continue;
}
@@ -189,21 +190,19 @@ class Push
assert($entity instanceof iState);
if (200 !== $response->getStatusCode()) {
if (404 === $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
if (Status::NOT_FOUND === Status::tryFrom($response->getStatusCode())) {
$this->logger->warning(
'Request for [{backend}] {item.type} [{item.title}] metadata returned with (Not Found) status code.',
[
'backend' => $context->backendName,
message: "{action}: Request for '{client}: {user}@{backend}' {item.type} '{item.title}' metadata returned with (404: Not Found) status code.",
context: [
...$logContext,
'status_code' => $response->getStatusCode(),
...$logContext
]
);
} else {
$this->logger->error(
'Request for [{backend}] {item.type} [{item.title}] metadata returned with unexpected [{status_code}] status code.',
[
'backend' => $context->backendName,
message: "{action}: Request for '{client}: {user}@{backend}' {item.type} '{item.title}' metadata returned with unexpected '{status_code}' status code.",
context: [
'status_code' => $response->getStatusCode(),
...$logContext
]
@@ -221,11 +220,10 @@ class Push
if ($context->trace) {
$this->logger->debug(
'Parsing [{backend}] {item.type} [{item.title}] payload.',
[
'backend' => $context->backendName,
message: "{action}: Parsing '{client}: {user}@{backend}' {item.type} '{item.title}' payload.",
context: [
...$logContext,
'trace' => $json,
'response' => ['body' => $json],
]
);
}
@@ -234,24 +232,18 @@ class Push
if ($entity->watched === $isWatched) {
$this->logger->info(
'Ignoring [{backend}] {item.type} [{item.title}]. Play state is identical.',
[
'backend' => $context->backendName,
...$logContext,
]
message: "{action}: Ignoring '{client}: {user}@{backend}' {item.type} '{item.title}'. Play state is identical.",
context: $logContext,
);
continue;
}
if (false === (bool)ag($context->options, Options::IGNORE_DATE, false)) {
$dateKey = 1 === $isWatched ? 'UserData.LastPlayedDate' : 'DateCreated';
$date = ag($json, $dateKey);
if (null === $date) {
if (null === ($date = ag($json, $dateKey))) {
$this->logger->error(
'Ignoring [{backend}] {item.type} [{item.title}]. No {date_key} is set on backend object.',
[
'backend' => $context->backendName,
message: "{action}: Ignoring '{client}: {user}@{backend}' {item.type} '{item.title}'. No {date_key} is set on backend object.",
context: [
'date_key' => $dateKey,
...$logContext,
'response' => [
@@ -268,9 +260,8 @@ class Push
if ($date->getTimestamp() >= ($timeExtra + $entity->updated)) {
$this->logger->notice(
'Ignoring [{backend}] {item.type} [{item.title}]. Database date is older than backend date.',
[
'backend' => $context->backendName,
message: "{action}: Ignoring '{client}: {user}@{backend}' {item.type} '{item.title}'. Database date is older than backend date.",
context: [
...$logContext,
'comparison' => [
'database' => makeDate($entity->updated),
@@ -293,7 +284,7 @@ class Push
])
);
if ($context->clientName === JellyfinClient::CLIENT_NAME) {
if ($context->clientName === JFC::CLIENT_NAME) {
$url = $url->withQuery(
http_build_query([
'DatePlayed' => makeDate($entity->updated)->format(Date::ATOM)
@@ -301,26 +292,24 @@ class Push
);
}
$logContext['remote']['url'] = $url;
$logContext['remote']['url'] = (string)$url;
$this->logger->debug(
'Queuing request to change [{backend}] {item.type} [{item.title}] play state to [{play_state}].',
[
'backend' => $context->backendName,
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
message: "{action}: Queuing request to change '{client}: {user}@{backend}' {item.type} '{item.title}' play state to '{play_state}'.",
context: [
...$logContext,
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
]
);
if (false === (bool)ag($context->options, Options::DRY_RUN, false)) {
$queue->add(
$this->http->request(
$entity->isWatched() ? 'POST' : 'DELETE',
(string)$url,
array_replace_recursive($context->backendHeaders, [
method: ($entity->isWatched() ? Method::POST : Method::DELETE)->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, [
'user_data' => [
'context' => $logContext + [
'backend' => $context->backendName,
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
],
],
@@ -331,10 +320,8 @@ class Push
} catch (Throwable $e) {
$this->logger->error(
...lw(
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] parsing [{library.title}] [{segment.number}/{segment.of}] response. Error [{error.message} @ {error.file}:{error.line}].',
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' parsing '{library.title}: {segment.number}/{segment.of}' response. {error.message} at '{error.file}:{error.line}'.",
context: [
'backend' => $context->backendName,
'client' => $context->clientName,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),

View File

@@ -8,7 +8,7 @@ use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinActionTrait;
use App\Backends\Jellyfin\JellyfinGuid;
use App\Backends\Jellyfin\JellyfinGuid as iGuid;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Exceptions\Backends\RuntimeException;
@@ -31,15 +31,10 @@ class SearchId
*/
protected string $action = 'jellyfin.searchId';
public function __construct(
protected iHttp $http,
protected iLogger $logger,
private JellyfinGuid $jellyfinGuid,
private iDB $db
) {
public function __construct(protected iHttp $http, protected iLogger $logger, private iGuid $iGuid, private iDB $db)
{
}
/**
* Wrap the operation in a try response block.
*
@@ -73,7 +68,7 @@ class SearchId
{
$item = $this->getItemDetails($context, $id, $opts);
$entity = $this->createEntity($context, $this->jellyfinGuid->withContext($context), $item);
$entity = $this->createEntity($context, $this->iGuid->withContext($context), $item);
if (null !== ($localEntity = $this->db->get($entity))) {
$entity->id = $localEntity->id;

View File

@@ -13,6 +13,8 @@ use App\Backends\Jellyfin\JellyfinActionTrait;
use App\Backends\Jellyfin\JellyfinClient;
use App\Backends\Jellyfin\JellyfinGuid;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Options;
use JsonException;
use Psr\Log\LoggerInterface as iLogger;
@@ -75,9 +77,7 @@ class SearchQuery
private function search(Context $context, string $query, int $limit = 25, array $opts = []): Response
{
$url = $context->backendUrl->withPath(
r('/Users/{user_id}/items/', [
'user_id' => $context->backendUser
])
path: r('/Users/{user_id}/items/', ['user_id' => $context->backendUser])
)->withQuery(
http_build_query(
array_replace_recursive([
@@ -91,26 +91,31 @@ class SearchQuery
], $opts['query'] ?? [])
)
);
$this->logger->debug('Searching [{backend}] libraries for [{query}].', [
'backend' => $context->backendName,
$logContext = [
'query' => $query,
'url' => $url
]);
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'url' => (string)$url,
];
$this->logger->debug("{action}: Searching '{client}: {user}@{backend}' libraries for '{query}'.", $logContext);
$response = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
method: Method::GET->value,
url: (string)$url,
options: array_replace_recursive($context->backendHeaders, $opts['headers'] ?? [])
);
if (200 !== $response->getStatusCode()) {
if (Status::OK !== Status::tryFrom($response->getStatusCode())) {
return new Response(
status: false,
error: new Error(
message: 'Search request for [{query}] in [{backend}] returned with unexpected [{status_code}] status code.',
message: "{action}: Search request for '{query}' in '{client}: {user}@{backend}' returned with unexpected '{status_code}' status code.",
context: [
'backend' => $context->backendName,
'query' => $query,
...$logContext,
'status_code' => $response->getStatusCode(),
],
level: Levels::ERROR
@@ -125,21 +130,27 @@ class SearchQuery
);
if ($context->trace) {
$this->logger->debug('Parsing Searching [{backend}] libraries for [{query}] payload.', [
'backend' => $context->backendName,
'query' => $query,
'url' => (string)$url,
'trace' => $json,
]);
$this->logger->debug(
message: "{action}: Parsing Searching '{client}: {user}@{backend}' libraries for '{query}' payload.",
context: [
...$logContext,
'response' => ['body' => $json],
]
);
}
$list = [];
$jellyfinGuid = $this->jellyfinGuid->withContext($context);
foreach (ag($json, 'Items', []) as $item) {
try {
$entity = $this->createEntity($context, $jellyfinGuid, $item, $opts);
} catch (\Throwable $e) {
$this->logger->error('Error creating entity: {error}', ['error' => $e->getMessage()]);
$this->logger->error("{action}: Failed to map '{client}: {user}@{backend}' item to entity. {error}", [
...$logContext,
'error' => $e->getMessage()
]);
continue;
}

View File

@@ -9,6 +9,7 @@ use App\Backends\Common\Context;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinClient;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Extends\Date;
use App\Libs\Options;
use App\Libs\QueueRequests;
@@ -40,6 +41,13 @@ class UpdateState
return $this->tryResponse(
context: $context,
fn: function () use ($context, $entities, $opts, $queue) {
$rContext = [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->backendUser,
];
foreach ($entities as $entity) {
$meta = $entity->getMetadata($context->backendName);
if (count($meta) < 1) {
@@ -71,9 +79,9 @@ class UpdateState
if (true === (bool)ag($context->options, Options::DRY_RUN, false)) {
$this->logger->notice(
"Would mark '{backend}' {item.type} '{item.title}' as '{item.play_state}'.",
[
'backend' => $context->backendName,
message: "{action}: Would mark '{client}: {user}@{backend}' {item.type} '{item.title}' as '{item.play_state}'.",
context: [
...$rContext,
'item' => [
'id' => $itemId,
'title' => $entity->getName(),
@@ -87,12 +95,12 @@ class UpdateState
$queue->add(
$this->http->request(
method: $entity->isWatched() ? 'POST' : 'DELETE',
method: ($entity->isWatched() ? Method::POST : Method::DELETE)->value,
url: (string)$url,
options: $context->backendHeaders + [
'user_data' => [
'context' => [
'backend' => $context->backendName,
...$rContext,
'play_state' => $entity->isWatched() ? 'played' : 'unplayed',
'item' => [
'id' => $itemId,

View File

@@ -65,7 +65,9 @@ trait JellyfinActionTrait
}
$logContext = [
'client' => $context->clientName,
'backend' => $context->backendName,
'user' => $context->userContext->name,
'item' => [
'id' => (string)ag($item, 'Id'),
'type' => $type,

View File

@@ -69,7 +69,7 @@ class JellyfinClient implements iClient
public const string COLLECTION_TYPE_MOVIES = 'movies';
/**
* @var array<string> This constant represents a list of extra fields tobe included in the request.
* @var array<string> This constant represents a list of extra fields to be included in the request.
*/
public const array EXTRA_FIELDS = [
'ProviderIds',
@@ -255,9 +255,7 @@ class JellyfinClient implements iClient
guid: $this->guid,
mapper: $mapper,
after: $after,
opts: [
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
]
opts: [Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid')]
);
if ($response->hasError()) {
@@ -280,10 +278,10 @@ class JellyfinClient implements iClient
context: $this->context,
guid: $this->guid,
mapper: $mapper,
opts: $opts + [
opts: ag_sets($opts, [
'writer' => $writer,
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
]
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid')
])
);
if ($response->hasError()) {
@@ -359,8 +357,11 @@ class JellyfinClient implements iClient
response: new Response(
status: false,
error: new Error(
message: 'Jellyfin play progress support works on Jellyfin version {version.required} and above. You are currently running {version.current}.',
message: "Watch progress support works on {client} version {version.required} and above. '{user}@{backend}' is running {version.current}.",
context: [
'client' => static::CLIENT_NAME,
'user' => $this->context->userContext->name,
'backend' => $this->context->backendName,
'version' => [
'current' => $version,
'required' => '10.9.x',
@@ -465,11 +466,10 @@ class JellyfinClient implements iClient
guid: $this->guid,
mapper: $mapper,
after: null,
opts: [
opts: ag_sets($opts, [
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
Options::ONLY_LIBRARY_ID => $libraryId,
...$opts,
]
])
);
if ($response->hasError()) {

View File

@@ -246,10 +246,11 @@ class JellyfinGuid implements iGuid
if (true === isIgnoredId($this->context->userContext, $bName, $type, $key, $value, $id)) {
if (true === $log) {
$this->logger->debug(
"{class}: Ignoring '{client}: {backend}' external id '{source}' for {item.type} '{item.id}: {item.title}' as requested.",
"{class}: Ignoring '{client}: {user}@{backend}' external id '{source}' for {item.type} '{item.id}: {item.title}' as requested.",
[
'class' => afterLast(static::class, '\\'),
'client' => $this->context->clientName,
'user' => $this->context->userContext->name,
'backend' => $bName,
'source' => $key . '://' . $value,
'guid' => [
@@ -267,11 +268,12 @@ class JellyfinGuid implements iGuid
} catch (Throwable $e) {
if (true === $log) {
$this->logger->error(
message: "{class}: Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' parsing '{agent}' identifier. Error '{error.message}' at '{error.file}:{error.line}'.",
message: "{class}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' parsing '{agent}' identifier. Error '{error.message}' at '{error.file}:{error.line}'.",
context: [
'class' => afterLast(static::class, '\\'),
'backend' => $this->context->backendName,
'client' => $this->context->clientName,
'user' => $this->context->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -306,8 +308,6 @@ class JellyfinGuid implements iGuid
*/
public function getConfig(): array
{
return [
'guidMapper' => $this->guidMapper,
];
return ['guidMapper' => $this->guidMapper];
}
}

View File

@@ -7,6 +7,7 @@ namespace App\Backends\Jellyfin;
use App\Backends\Common\Context;
use App\Backends\Jellyfin\Action\GetUser;
use App\Libs\Container;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\Backends\InvalidContextException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
@@ -33,7 +34,9 @@ class JellyfinValidateContext
$backendId = ag($data, 'Id');
if (empty($backendId)) {
throw new InvalidContextException('Failed to get backend id.');
throw new InvalidContextException(r('Failed to get backend id. Check {client} logs for errors.', [
'client' => $context->clientName,
]));
}
if (null !== $context->backendId && $backendId !== $context->backendId) {
@@ -76,18 +79,18 @@ class JellyfinValidateContext
{
try {
$url = $context->backendUrl->withPath('/system/Info');
$request = $this->http->request('GET', (string)$url, [
$request = $this->http->request(Method::GET->value, (string)$url, [
'headers' => [
'Accept' => 'application/json',
'X-MediaBrowser-Token' => $context->backendToken,
],
]);
if (Status::UNAUTHORIZED->value === $request->getStatusCode()) {
if (Status::UNAUTHORIZED === Status::tryFrom($request->getStatusCode())) {
throw new InvalidContextException('Backend responded with 401. Most likely means token is invalid.');
}
if (Status::NOT_FOUND->value === $request->getStatusCode()) {
if (Status::NOT_FOUND === Status::tryFrom($request->getStatusCode())) {
throw new InvalidContextException('Backend responded with 404. Most likely means url is incorrect.');
}

View File

@@ -188,22 +188,47 @@ class CreateUsersCommand extends Command
/** @var array $info */
$info = $backend;
$info['user'] = ag($user, 'id', ag($info, 'user'));
$info['backendName'] = r("{backend}_{user}", [
$info['backendName'] = strtolower(r("{backend}_{user}", [
'backend' => ag($backend, 'name'),
'user' => ag($user, 'name'),
]);
]));
if (false === isValidName($info['backendName'])) {
$rename = substr(md5($info['backendName']), 0, 8);
$this->logger->error(
message: "SYSTEM: Renaming invalid backend name '{name}'. backend name must be in [a-z_0-9], renaming to '{renamed}'",
context: ['name' => $info['backendName'], 'renamed' => $rename]
);
$info['backendName'] = $rename;
}
$info['displayName'] = ag($user, 'name');
if (false === isValidName($info['displayName'])) {
$rename = substr(md5($info['displayName']), 0, 8);
$this->logger->error(
message: "SYSTEM: Renaming invalid username '{name}'. username must be in [a-z_0-9], renaming to '{renamed}'",
context: ['name' => $info['displayName'], 'renamed' => $rename]
);
$info['displayName'] = $rename;
}
$info = ag_delete($info, 'options.' . Options::PLEX_USER_PIN);
$info = ag_set($info, 'options.' . Options::ALT_NAME, ag($backend, 'name'));
$info = ag_set($info, 'options.' . Options::ALT_ID, ag($backend, 'user'));
$info = ag_sets($info, [
'options.' . Options::ALT_NAME => ag($backend, 'name'),
'options.' . Options::ALT_ID => ag($backend, 'user')
]);
// -- of course, Plex has to be special.
if (PlexClient::CLIENT_NAME === ucfirst(ag($backend, 'type'))) {
$info = ag_set($info, 'token', 'reuse_or_generate_token');
$info = ag_set($info, 'options.' . Options::PLEX_USER_NAME, ag($user, 'name'));
$info = ag_set($info, 'options.' . Options::PLEX_USER_UUID, ag($user, 'uuid'));
$info = ag_set($info, 'options.' . Options::ADMIN_TOKEN, ag($backend, [
'options.' . Options::ADMIN_TOKEN,
'token'
]));
$info = ag_sets($info, [
'token' => 'reuse_or_generate_token',
'options.' . Options::PLEX_USER_NAME => ag($user, 'name'),
'options.' . Options::PLEX_USER_UUID => ag($user, 'uuid'),
'options.' . Options::ADMIN_TOKEN => ag(
array: $backend,
path: ['options.' . Options::ADMIN_TOKEN, 'token']
)
]);
if (true === (bool)ag($user, 'guest', false)) {
$info = ag_set($info, 'options.' . Options::PLEX_EXTERNAL_USER, true);
}
@@ -215,10 +240,11 @@ class CreateUsersCommand extends Command
}
} catch (Throwable $e) {
$this->logger->error(
"Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' get users list. '{error.message}' at '{error.file}:{error.line}'.",
"Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' get users list. '{error.message}' at '{error.file}:{error.line}'.",
[
'backend' => $client->getContext()->backendName,
'client' => $client->getContext()->clientName,
'backend' => $client->getContext()->backendName,
'user' => $client->getContext()->userContext->name,
'error' => [
'kind' => $e::class,
'line' => $e->getLine(),
@@ -248,7 +274,15 @@ class CreateUsersCommand extends Command
]);
foreach ($users as $user) {
$userName = ag($user, 'name', 'Unknown');
$userName = strtolower(ag($user, 'name', 'Unknown'));
if (false === isValidName($userName)) {
$rename = substr(md5($userName), 0, 8);
$this->logger->error(
message: "SYSTEM: Renaming invalid username '{user}'. Username must be in [a-z_0-9], renaming to '{renamed}'",
context: ['user' => $userName, 'renamed' => $rename]
);
$userName = $rename;
}
$subUserPath = r(fixPath(Config::get('path') . '/users/{user}'), ['user' => $userName]);
@@ -257,8 +291,9 @@ class CreateUsersCommand extends Command
'user' => $userName,
'path' => $subUserPath
]);
if (false === mkdir($subUserPath, 0755, true)) {
$this->logger->error("SYSTEM: Failed to '{user}' directory '{path}'.", [
$this->logger->error("SYSTEM: Failed to create '{user}' directory '{path}'.", [
'user' => $userName,
'path' => $subUserPath
]);
@@ -278,13 +313,21 @@ class CreateUsersCommand extends Command
foreach (ag($user, 'backends', []) as $backend) {
$name = ag($backend, 'client_data.backendName');
if (false === isValidName($name)) {
$rename = substr(md5($name), 0, 8);
$this->logger->error(
message: "SYSTEM: Renaming invalid backend name '{name}'. backend name must be in [a-z_0-9], renaming to '{renamed}'",
context: ['name' => $name, 'renamed' => $rename]
);
$name = $rename;
}
$clientData = ag_delete(ag($backend, 'client_data'), 'class');
$clientData['name'] = $name;
if (false === $perUser->has($name)) {
$data = $clientData;
$data = ag_set($data, 'import.lastSync', null);
$data = ag_set($data, 'export.lastSync', null);
$data = ag_sets($data, ['import.lastSync' => null, 'export.lastSync' => null]);
$data = ag_delete($data, ['webhook', 'name', 'backendName', 'displayName']);
$perUser->set($name, $data);
} else {
@@ -312,7 +355,7 @@ class CreateUsersCommand extends Command
if (null !== ($val = ag($backend, 'client_data.options.' . Options::PLEX_EXTERNAL_USER))) {
$update['options.' . Options::PLEX_EXTERNAL_USER] = (bool)$val;
}
if (null !== ($val = ag($backend, 'client_data.options.use_old_progress_endpoint'))) {
$update['options.use_old_progress_endpoint'] = $val;
}
@@ -351,11 +394,8 @@ class CreateUsersCommand extends Command
);
if (false === $token) {
$this->logger->error(
"Failed to generate access token for '{user}@{backend}' backend.",
[
'user' => $userName,
'backend' => $name,
]
message: "Failed to generate access token for '{user}@{backend}' backend.",
context: ['user' => $userName, 'backend' => $name]
);
} else {
$perUser->set("{$name}.token", $token);
@@ -364,8 +404,8 @@ class CreateUsersCommand extends Command
}
} catch (Throwable $e) {
$this->logger->error(
"Failed to generate access token for '{user}@{name}' backend. '{error}' at '{file}:{line}'.",
[
message: "Failed to generate access token for '{user}@{name}' backend. {error} at '{file}:{line}'.",
context: [
'name' => $name,
'user' => $userName,
'error' => [
@@ -386,7 +426,7 @@ class CreateUsersCommand extends Command
}
}
$dbFile = r($subUserPath . "/{user}.db", ['user' => $userName]);
$dbFile = r($subUserPath . "/{user}.db", ['user' => 'user']);
if (false === file_exists($dbFile)) {
$this->logger->notice("SYSTEM: Creating '{user}' database '{db}'.", [
'user' => $userName,

View File

@@ -185,6 +185,25 @@ if (!function_exists('ag')) {
}
}
if (!function_exists('ag_sets')) {
/**
* Set multiple key paths in an array using "dot" notation.
*
* @param array $array The array to set the values in.
* @param array $path The key paths to set the values at.
* @param string $separator The separator used in the key paths (default is '.').
*
* @return array The modified array.
*/
function ag_sets(array $array, array $path, string $separator = '.'): array
{
foreach ($path as $key => $value) {
$array = ag_set($array, $key, $value, $separator);
}
return $array;
}
}
if (!function_exists('ag_set')) {
/**
* Set an array item to a given value using "dot" notation.