Migrated backends push method into separate action.
This commit is contained in:
9
src/Backends/Emby/Action/Push.php
Normal file
9
src/Backends/Emby/Action/Push.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Backends\Emby\Action;
|
||||
|
||||
class Push extends \App\Backends\Jellyfin\Action\Push
|
||||
{
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class GetMetaData
|
||||
}
|
||||
|
||||
$url = $context->backendUrl
|
||||
->withPath(sprintf('/Users/%s/items/' . $id, $context->backendUser))
|
||||
->withPath(sprintf('/Users/%s/items/%s', $context->backendUser, $id))
|
||||
->withQuery(
|
||||
http_build_query(
|
||||
array_merge_recursive(
|
||||
|
||||
300
src/Backends/Jellyfin/Action/Push.php
Normal file
300
src/Backends/Jellyfin/Action/Push.php
Normal file
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Backends\Jellyfin\Action;
|
||||
|
||||
use App\Backends\Common\CommonTrait;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Backends\Jellyfin\JellyfinClient;
|
||||
use App\Libs\Entity\StateInterface as iFace;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\QueueRequests;
|
||||
use DateTimeInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Throwable;
|
||||
|
||||
class Push
|
||||
{
|
||||
use CommonTrait;
|
||||
|
||||
public function __construct(protected HttpClientInterface $http, protected LoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Push Play state.
|
||||
*
|
||||
* @param Context $context
|
||||
* @param array<iState> $entities
|
||||
* @param QueueRequests $queue
|
||||
* @param DateTimeInterface|null $after
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(
|
||||
Context $context,
|
||||
array $entities,
|
||||
QueueRequests $queue,
|
||||
DateTimeInterface|null $after = null
|
||||
): Response {
|
||||
return $this->tryResponse(context: $context, fn: fn() => $this->action($context, $entities, $queue, $after));
|
||||
}
|
||||
|
||||
private function action(
|
||||
Context $context,
|
||||
array $entities,
|
||||
QueueRequests $queue,
|
||||
DateTimeInterface|null $after = null
|
||||
): Response {
|
||||
$requests = [];
|
||||
|
||||
foreach ($entities as $key => $entity) {
|
||||
if (true !== ($entity instanceof iFace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $after && false === (bool)ag($context->options, Options::IGNORE_DATE, false)) {
|
||||
if ($after->getTimestamp() > $entity->updated) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = $entity->getMetadata($context->backendName);
|
||||
|
||||
$logContext = [
|
||||
'item' => [
|
||||
'id' => $entity->id,
|
||||
'type' => $entity->type,
|
||||
'title' => $entity->getName(),
|
||||
],
|
||||
];
|
||||
|
||||
if (null === ag($metadata, iFace::COLUMN_ID, null)) {
|
||||
$this->logger->warning(
|
||||
'Ignoring [%(item.title)] for [%(backend)]. No metadata was found.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$logContext['remote']['id'] = ag($metadata, iFace::COLUMN_ID);
|
||||
|
||||
try {
|
||||
$url = $context->backendUrl->withPath(
|
||||
sprintf('/Users/%s/items/%s', $context->backendUser, ag($metadata, iFace::COLUMN_ID))
|
||||
)->withQuery(
|
||||
http_build_query(
|
||||
[
|
||||
'fields' => implode(',', JellyfinClient::EXTRA_FIELDS),
|
||||
'enableUserData' => 'true',
|
||||
'enableImages' => 'false',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$logContext['remote']['url'] = (string)$url;
|
||||
|
||||
$this->logger->debug('Requesting [%(backend)] %(item.type) [%(item.title)] metadata.', [
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
]);
|
||||
|
||||
$requests[] = $this->http->request(
|
||||
'GET',
|
||||
(string)$url,
|
||||
array_replace_recursive($context->backendHeaders, [
|
||||
'user_data' => [
|
||||
'id' => $key,
|
||||
'context' => $logContext,
|
||||
]
|
||||
])
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during request for [%(backend)] %(item.type) [%(item.title)] metadata.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$logContext = null;
|
||||
|
||||
foreach ($requests as $response) {
|
||||
$logContext = ag($response->getInfo('user_data'), 'context', []);
|
||||
|
||||
try {
|
||||
if (null === ($id = ag($response->getInfo('user_data'), 'id'))) {
|
||||
$this->logger->error('Unable to get entity object id.', [
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity = $entities[$id];
|
||||
|
||||
assert($entity instanceof iFace);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
if (404 === $response->getStatusCode()) {
|
||||
$this->logger->warning(
|
||||
'Request for [%(backend)] %(item.type) [%(item.title)] metadata returned with (Not Found) status code.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
'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,
|
||||
'status_code' => $response->getStatusCode(),
|
||||
...$logContext
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$json = json_decode(
|
||||
json: $response->getContent(),
|
||||
associative: true,
|
||||
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
|
||||
);
|
||||
|
||||
if ($context->trace) {
|
||||
$this->logger->debug(
|
||||
'Parsing [%(backend)] %(item.type) [%(item.title)] payload.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'trace' => $json,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$isWatched = (int)(bool)ag($json, 'UserData.Played', false);
|
||||
|
||||
if ($entity->watched === $isWatched) {
|
||||
$this->logger->info(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Play state is identical.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$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) {
|
||||
$this->logger->error(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. No %(date_key) is set on backend object.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
'date_key' => $dateKey,
|
||||
...$logContext,
|
||||
'response' => [
|
||||
'body' => $json,
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = makeDate($date);
|
||||
|
||||
$timeExtra = (int)(ag($context->options, Options::EXPORT_ALLOWED_TIME_DIFF, 10));
|
||||
|
||||
if ($date->getTimestamp() >= ($timeExtra + $entity->updated)) {
|
||||
$this->logger->notice(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Storage date is older than backend date.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'comparison' => [
|
||||
'storage' => makeDate($entity->updated),
|
||||
'backend' => $date,
|
||||
'difference' => $date->getTimestamp() - $entity->updated,
|
||||
'extra_margin' => [
|
||||
Options::EXPORT_ALLOWED_TIME_DIFF => $timeExtra,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$url = $context->backendUrl->withPath(
|
||||
sprintf('/Users/%s/PlayedItems/%s', $context->backendUser, ag($json, 'Id'))
|
||||
);
|
||||
|
||||
$logContext['remote']['url'] = $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',
|
||||
...$logContext,
|
||||
]
|
||||
);
|
||||
|
||||
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, [
|
||||
'user_data' => [
|
||||
'context' => $logContext + [
|
||||
'backend' => $context->backendName,
|
||||
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during handling of [%(backend)] %(item.type) [%(item.title)].',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(status: true, response: $queue);
|
||||
}
|
||||
}
|
||||
310
src/Backends/Plex/Action/Push.php
Normal file
310
src/Backends/Plex/Action/Push.php
Normal file
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Backends\Plex\Action;
|
||||
|
||||
use App\Backends\Common\CommonTrait;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\QueueRequests;
|
||||
use DateTimeInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Throwable;
|
||||
|
||||
final class Push
|
||||
{
|
||||
use CommonTrait;
|
||||
|
||||
public function __construct(protected HttpClientInterface $http, protected LoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Push Play state.
|
||||
*
|
||||
* @param Context $context
|
||||
* @param array<iState> $entities
|
||||
* @param QueueRequests $queue
|
||||
* @param DateTimeInterface|null $after
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(
|
||||
Context $context,
|
||||
array $entities,
|
||||
QueueRequests $queue,
|
||||
DateTimeInterface|null $after = null
|
||||
): Response {
|
||||
return $this->tryResponse(context: $context, fn: fn() => $this->action($context, $entities, $queue, $after));
|
||||
}
|
||||
|
||||
private function action(
|
||||
Context $context,
|
||||
array $entities,
|
||||
QueueRequests $queue,
|
||||
DateTimeInterface|null $after = null
|
||||
): Response {
|
||||
$requests = [];
|
||||
|
||||
foreach ($entities as $key => $entity) {
|
||||
if (true !== ($entity instanceof iState)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $after && false === (bool)ag($context->options, Options::IGNORE_DATE, false)) {
|
||||
if ($after->getTimestamp() > $entity->updated) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = $entity->getMetadata($context->backendName);
|
||||
|
||||
$logContext = [
|
||||
'item' => [
|
||||
'id' => $entity->id,
|
||||
'type' => $entity->type,
|
||||
'title' => $entity->getName(),
|
||||
],
|
||||
];
|
||||
|
||||
if (null === ag($metadata, iState::COLUMN_ID)) {
|
||||
$this->logger->warning(
|
||||
'Ignoring [%(item.title)] for [%(backend)]. No metadata was found.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$logContext['remote']['id'] = ag($metadata, iState::COLUMN_ID);
|
||||
|
||||
try {
|
||||
$url = $context->backendUrl->withPath('/library/metadata/' . ag($metadata, iState::COLUMN_ID));
|
||||
|
||||
$logContext['remote']['url'] = (string)$url;
|
||||
|
||||
$this->logger->debug('Requesting [%(backend)] %(item.type) [%(item.title)] metadata.', [
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
]);
|
||||
|
||||
$requests[] = $this->http->request(
|
||||
'GET',
|
||||
(string)$url,
|
||||
$context->backendHeaders + [
|
||||
'user_data' => [
|
||||
'id' => $key,
|
||||
'context' => $logContext,
|
||||
]
|
||||
]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during request for [%(backend)] %(item.type) [%(item.title)] metadata.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'exception' => [
|
||||
'file' => after($e->getFile(), ROOT_PATH),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$logContext = null;
|
||||
|
||||
foreach ($requests as $response) {
|
||||
$logContext = ag($response->getInfo('user_data'), 'context', []);
|
||||
|
||||
try {
|
||||
if (null === ($id = ag($response->getInfo('user_data'), 'id'))) {
|
||||
$this->logger->error('Unable to get entity object id.', [
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity = $entities[$id];
|
||||
|
||||
assert($entity instanceof iState);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
if (404 === $response->getStatusCode()) {
|
||||
$this->logger->warning(
|
||||
'Request for [%(backend)] %(item.type) [%(item.title)] metadata returned with (Not Found) status code.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
'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,
|
||||
'status_code' => $response->getStatusCode(),
|
||||
...$logContext
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$body = json_decode(
|
||||
json: $response->getContent(),
|
||||
associative: true,
|
||||
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
|
||||
);
|
||||
|
||||
if ($context->trace) {
|
||||
$this->logger->debug(
|
||||
'Parsing [%(backend)] %(item.type) [%(item.title)] payload.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'trace' => $body,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$json = ag($body, 'MediaContainer.Metadata.0', []);
|
||||
|
||||
if (empty($json)) {
|
||||
$this->logger->error(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Returned with unexpected body.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'response' => [
|
||||
'body' => $body,
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$isWatched = 0 === (int)ag($json, 'viewCount', 0) ? 0 : 1;
|
||||
|
||||
if ($entity->watched === $isWatched) {
|
||||
$this->logger->info(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Play state is identical.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === (bool)ag($context->options, Options::IGNORE_DATE, false)) {
|
||||
$dateKey = 1 === $isWatched ? 'lastViewedAt' : 'addedAt';
|
||||
$date = ag($json, $dateKey);
|
||||
|
||||
if (null === $date) {
|
||||
$this->logger->error(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. No %(date_key) is set on backend object.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
'date_key' => $dateKey,
|
||||
...$logContext,
|
||||
'response' => [
|
||||
'body' => $body,
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = makeDate($date);
|
||||
|
||||
$timeExtra = (int)(ag($context->options, Options::EXPORT_ALLOWED_TIME_DIFF, 10));
|
||||
|
||||
if ($date->getTimestamp() >= ($entity->updated + $timeExtra)) {
|
||||
$this->logger->notice(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Storage date is older than backend date.',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'comparison' => [
|
||||
'storage' => makeDate($entity->updated),
|
||||
'backend' => $date,
|
||||
'difference' => $date->getTimestamp() - $entity->updated,
|
||||
'extra_margin' => [
|
||||
Options::EXPORT_ALLOWED_TIME_DIFF => $timeExtra,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$url = $context->backendUrl
|
||||
->withPath($entity->isWatched() ? '/:/scrobble' : '/:/unscrobble')->withQuery(
|
||||
http_build_query(
|
||||
[
|
||||
'identifier' => 'com.plexapp.plugins.library',
|
||||
'key' => ag($json, 'ratingKey'),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$logContext['remote']['url'] = $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',
|
||||
...$logContext,
|
||||
]
|
||||
);
|
||||
|
||||
if (false === (bool)ag($context->options, Options::DRY_RUN)) {
|
||||
$queue->add(
|
||||
$this->http->request(
|
||||
'GET',
|
||||
(string)$url,
|
||||
array_replace_recursive($context->backendHeaders, [
|
||||
'user_data' => [
|
||||
'context' => $logContext + [
|
||||
'backend' => $context->backendName,
|
||||
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
|
||||
],
|
||||
]
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during handling of [%(backend)] %(item.type) [%(item.title)].',
|
||||
[
|
||||
'backend' => $context->backendName,
|
||||
...$logContext,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(status: true, response: $queue);
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,10 @@ class PushCommand extends Command
|
||||
$opts[Options::DRY_RUN] = true;
|
||||
}
|
||||
|
||||
if ($input->getOption('trace')) {
|
||||
$opts[Options::DEBUG_TRACE] = true;
|
||||
}
|
||||
|
||||
$server['options'] = $opts;
|
||||
$server['class'] = makeServer(server: $server, name: $name);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Backends\Jellyfin\Action\GetUsersList;
|
||||
use App\Backends\Jellyfin\Action\InspectRequest;
|
||||
use App\Backends\Jellyfin\Action\GetIdentifier;
|
||||
use App\Backends\Jellyfin\Action\ParseWebhook;
|
||||
use App\Backends\Jellyfin\Action\Push;
|
||||
use App\Backends\Jellyfin\Action\SearchId;
|
||||
use App\Backends\Jellyfin\Action\SearchQuery;
|
||||
use App\Backends\Jellyfin\JellyfinActionTrait;
|
||||
@@ -243,9 +244,6 @@ class JellyfinServer implements ServerInterface
|
||||
return $response->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getMetadata(string|int $id, array $opts = []): array
|
||||
{
|
||||
return $this->getItemDetails(context: $this->context, id: $id, opts: $opts);
|
||||
@@ -580,258 +578,21 @@ class JellyfinServer implements ServerInterface
|
||||
|
||||
public function push(array $entities, QueueRequests $queue, DateTimeInterface|null $after = null): array
|
||||
{
|
||||
$this->checkConfig(true);
|
||||
$response = Container::get(Push::class)(
|
||||
context: $this->context,
|
||||
entities: $entities,
|
||||
queue: $queue,
|
||||
after: $after
|
||||
);
|
||||
|
||||
$requests = [];
|
||||
|
||||
foreach ($entities as $key => $entity) {
|
||||
if (true !== ($entity instanceof iFace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === (bool)ag($this->options, Options::IGNORE_DATE, false)) {
|
||||
if (null !== $after && $after->getTimestamp() > $entity->updated) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = $entity->getMetadata($this->context->backendName);
|
||||
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => $entity->id,
|
||||
'type' => $entity->type,
|
||||
'title' => $entity->getName(),
|
||||
],
|
||||
];
|
||||
|
||||
if (null === ag($metadata, iFace::COLUMN_ID, null)) {
|
||||
$this->logger->warning(
|
||||
'Ignoring [%(item.title)] for [%(backend)] no backend metadata was found.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$context['remote']['id'] = ag($metadata, iFace::COLUMN_ID);
|
||||
|
||||
try {
|
||||
$url = $this->url->withPath(sprintf('/Users/%s/items', $this->user))->withQuery(
|
||||
http_build_query(
|
||||
[
|
||||
'ids' => ag($metadata, iFace::COLUMN_ID),
|
||||
'fields' => implode(',', self::FIELDS),
|
||||
'enableUserData' => 'true',
|
||||
'enableImages' => 'false',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$context['remote']['url'] = (string)$url;
|
||||
|
||||
$this->logger->debug('Requesting [%(backend)] %(item.type) [%(item.title)] play state.', [
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
]);
|
||||
|
||||
$requests[] = $this->http->request(
|
||||
'GET',
|
||||
(string)$url,
|
||||
array_replace_recursive($this->getHeaders(), [
|
||||
'user_data' => [
|
||||
'id' => $key,
|
||||
'context' => $context,
|
||||
]
|
||||
])
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during requesting of [%(backend)] %(item.type) [%(item.title)].',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
if ($response->hasError()) {
|
||||
$this->logger->log($response->error->level(), $response->error->message, $response->error->context);
|
||||
}
|
||||
|
||||
$context = null;
|
||||
|
||||
foreach ($requests as $response) {
|
||||
$context = ag($response->getInfo('user_data'), 'context', []);
|
||||
|
||||
try {
|
||||
if (null === ($id = ag($response->getInfo('user_data'), 'id'))) {
|
||||
$this->logger->error('Unable to get entity object id.', [
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity = $entities[$id];
|
||||
|
||||
assert($entity instanceof iFace);
|
||||
|
||||
switch ($response->getStatusCode()) {
|
||||
case 200:
|
||||
break;
|
||||
case 404:
|
||||
$this->logger->warning(
|
||||
'Request for [%(backend)] %(item.type) [%(item.title)] returned with 404 (Not Found) status code.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
...$context
|
||||
]
|
||||
);
|
||||
continue 2;
|
||||
default:
|
||||
$this->logger->error(
|
||||
'Request for [%(backend)] %(item.type) [%(item.title)] returned with unexpected [%(status_code)] status code.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'status_code' => $response->getStatusCode(),
|
||||
...$context
|
||||
]
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$body = json_decode(
|
||||
json: $response->getContent(false),
|
||||
associative: true,
|
||||
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
|
||||
);
|
||||
|
||||
$json = ag($body, 'Items', [])[0] ?? [];
|
||||
|
||||
if (empty($json)) {
|
||||
$this->logger->error(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. responded with empty metadata.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
'response' => [
|
||||
'body' => $body,
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$isWatched = (int)(bool)ag($json, 'UserData.Played', false);
|
||||
|
||||
if ($entity->watched === $isWatched) {
|
||||
$this->logger->info(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Play state is identical.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === (bool)ag($this->options, Options::IGNORE_DATE, false)) {
|
||||
$dateKey = 1 === $isWatched ? 'UserData.LastPlayedDate' : 'DateCreated';
|
||||
$date = ag($json, $dateKey);
|
||||
|
||||
if (null === $date) {
|
||||
$this->logger->error(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. No %(date_key) is set on backend object.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'date_key' => $dateKey,
|
||||
...$context,
|
||||
'response' => [
|
||||
'body' => $body,
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = makeDate($date);
|
||||
|
||||
$timeExtra = (int)(ag($this->options, Options::EXPORT_ALLOWED_TIME_DIFF, 10));
|
||||
|
||||
if ($date->getTimestamp() >= ($timeExtra + $entity->updated)) {
|
||||
$this->logger->notice(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Storage date is older than backend date.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
'comparison' => [
|
||||
'storage' => makeDate($entity->updated),
|
||||
'backend' => $date,
|
||||
'difference' => $date->getTimestamp() - $entity->updated,
|
||||
'extra_margin' => [
|
||||
Options::EXPORT_ALLOWED_TIME_DIFF => $timeExtra,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$url = $this->url->withPath(sprintf('/Users/%s/PlayedItems/%s', $this->user, ag($json, 'Id')));
|
||||
|
||||
$context['remote']['url'] = $url;
|
||||
|
||||
$this->logger->debug(
|
||||
'Queuing request to change [%(backend)] %(item.type) [%(item.title)] play state to [%(play_state)].',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
|
||||
if (false === (bool)ag($this->options, Options::DRY_RUN, false)) {
|
||||
$queue->add(
|
||||
$this->http->request(
|
||||
$entity->isWatched() ? 'POST' : 'DELETE',
|
||||
(string)$url,
|
||||
array_replace_recursive($this->getHeaders(), [
|
||||
'user_data' => [
|
||||
'context' => $context + [
|
||||
'backend' => $this->context->backendName,
|
||||
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during handling of [%(backend)] %(item.type) [%(item.title)].',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
...$context,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
if (false === $response->isSuccessful()) {
|
||||
throw new HttpException(ag($response->extra, 'message', fn() => $response->error->format()));
|
||||
}
|
||||
|
||||
unset($requests);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Backends\Plex\Action\GetIdentifier;
|
||||
use App\Backends\Plex\Action\GetUsersList;
|
||||
use App\Backends\Plex\Action\InspectRequest;
|
||||
use App\Backends\Plex\Action\ParseWebhook;
|
||||
use App\Backends\Plex\Action\Push;
|
||||
use App\Backends\Plex\Action\SearchId;
|
||||
use App\Backends\Plex\Action\SearchQuery;
|
||||
use App\Backends\Plex\PlexActionTrait;
|
||||
@@ -642,256 +643,21 @@ class PlexServer implements ServerInterface
|
||||
|
||||
public function push(array $entities, QueueRequests $queue, DateTimeInterface|null $after = null): array
|
||||
{
|
||||
$this->checkConfig();
|
||||
$response = Container::get(Push::class)(
|
||||
context: $this->context,
|
||||
entities: $entities,
|
||||
queue: $queue,
|
||||
after: $after
|
||||
);
|
||||
|
||||
$requests = [];
|
||||
|
||||
foreach ($entities as $key => $entity) {
|
||||
if (true !== ($entity instanceof iFace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === (bool)ag($this->options, Options::IGNORE_DATE, false)) {
|
||||
if (null !== $after && $after->getTimestamp() > $entity->updated) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = $entity->getMetadata($this->getName());
|
||||
|
||||
$context = [
|
||||
'item' => [
|
||||
'id' => $entity->id,
|
||||
'type' => $entity->type,
|
||||
'title' => $entity->getName(),
|
||||
],
|
||||
];
|
||||
|
||||
if (null === ag($metadata, iFace::COLUMN_ID)) {
|
||||
$this->logger->warning(
|
||||
'Ignoring [%(item.title)] for [%(backend)] no backend metadata was found.',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$context['remote']['id'] = ag($metadata, iFace::COLUMN_ID);
|
||||
|
||||
try {
|
||||
$url = $this->url->withPath('/library/metadata/' . ag($metadata, iFace::COLUMN_ID));
|
||||
|
||||
$context['remote']['url'] = (string)$url;
|
||||
|
||||
$this->logger->debug('Requesting [%(backend)] %(item.type) [%(item.title)] play state.', [
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
]);
|
||||
|
||||
$requests[] = $this->http->request(
|
||||
'GET',
|
||||
(string)$url,
|
||||
array_replace_recursive($this->getHeaders(), [
|
||||
'user_data' => [
|
||||
'id' => $key,
|
||||
'context' => $context,
|
||||
]
|
||||
])
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during requesting of [%(backend)] %(item.type) [%(item.title)].',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
if ($response->hasError()) {
|
||||
$this->logger->log($response->error->level(), $response->error->message, $response->error->context);
|
||||
}
|
||||
|
||||
$context = null;
|
||||
|
||||
foreach ($requests as $response) {
|
||||
$context = ag($response->getInfo('user_data'), 'context', []);
|
||||
|
||||
try {
|
||||
if (null === ($id = ag($response->getInfo('user_data'), 'id'))) {
|
||||
$this->logger->error('Unable to get entity object id.', [
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity = $entities[$id];
|
||||
|
||||
assert($entity instanceof iFace);
|
||||
|
||||
switch ($response->getStatusCode()) {
|
||||
case 200:
|
||||
break;
|
||||
case 404:
|
||||
$this->logger->warning(
|
||||
'Request for [%(backend)] %(item.type) [%(item.title)] returned with 404 (Not Found) status code.',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
...$context
|
||||
]
|
||||
);
|
||||
continue 2;
|
||||
default:
|
||||
$this->logger->error(
|
||||
'Request for [%(backend)] %(item.type) [%(item.title)] returned with unexpected [%(status_code)] status code.',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
'status_code' => $response->getStatusCode(),
|
||||
...$context
|
||||
]
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$body = json_decode(
|
||||
json: $response->getContent(false),
|
||||
associative: true,
|
||||
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
|
||||
);
|
||||
|
||||
$json = ag($body, 'MediaContainer.Metadata.0', []);
|
||||
|
||||
if (empty($json)) {
|
||||
$this->logger->error(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. responded with empty metadata.',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
'response' => [
|
||||
'body' => $body,
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$isWatched = 0 === (int)ag($json, 'viewCount', 0) ? 0 : 1;
|
||||
|
||||
if ($entity->watched === $isWatched) {
|
||||
$this->logger->info(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Play state is identical.',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === (bool)ag($this->options, Options::IGNORE_DATE, false)) {
|
||||
$dateKey = 1 === $isWatched ? 'lastViewedAt' : 'addedAt';
|
||||
$date = ag($json, $dateKey);
|
||||
|
||||
if (null === $date) {
|
||||
$this->logger->error(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. No %(date_key) is set on backend object.',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
'date_key' => $dateKey,
|
||||
...$context,
|
||||
'response' => [
|
||||
'body' => $body,
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = makeDate($date);
|
||||
|
||||
$timeExtra = (int)(ag($this->options, Options::EXPORT_ALLOWED_TIME_DIFF, 10));
|
||||
|
||||
if ($date->getTimestamp() >= ($entity->updated + $timeExtra)) {
|
||||
$this->logger->notice(
|
||||
'Ignoring [%(backend)] %(item.type) [%(item.title)]. Storage date is older than backend date.',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
'comparison' => [
|
||||
'storage' => makeDate($entity->updated),
|
||||
'backend' => $date,
|
||||
'difference' => $date->getTimestamp() - $entity->updated,
|
||||
'extra_margin' => [
|
||||
Options::EXPORT_ALLOWED_TIME_DIFF => $timeExtra,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$url = $this->url->withPath($entity->isWatched() ? '/:/scrobble' : '/:/unscrobble')->withQuery(
|
||||
http_build_query(
|
||||
[
|
||||
'identifier' => 'com.plexapp.plugins.library',
|
||||
'key' => ag($json, 'ratingKey'),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$context['remote']['url'] = $url;
|
||||
|
||||
$this->logger->debug(
|
||||
'Queuing request to change [%(backend)] %(item.type) [%(item.title)] play state to [%(play_state)].',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
|
||||
if (false === (bool)ag($this->options, Options::DRY_RUN)) {
|
||||
$queue->add(
|
||||
$this->http->request(
|
||||
'GET',
|
||||
(string)$url,
|
||||
array_replace_recursive($this->getHeaders(), [
|
||||
'user_data' => [
|
||||
'context' => $context + [
|
||||
'backend' => $this->getName(),
|
||||
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
|
||||
],
|
||||
]
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown during handling of [%(backend)] %(item.type) [%(item.title)].',
|
||||
[
|
||||
'backend' => $this->getName(),
|
||||
...$context,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
if (false === $response->isSuccessful()) {
|
||||
throw new HttpException(ag($response->extra, 'message', fn() => $response->error->format()));
|
||||
}
|
||||
|
||||
unset($requests);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user