Cleaned up and updated jellyfin/emby log messages.
This commit is contained in:
@@ -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,
|
||||
])
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user