diff --git a/src/Libs/Initializer.php b/src/Libs/Initializer.php index a0ae00f7..6092cd9f 100644 --- a/src/Libs/Initializer.php +++ b/src/Libs/Initializer.php @@ -342,7 +342,11 @@ final class Initializer ]); } - return new Response(status: 200, headers: ['X-Status' => 'Entity is unchanged.']); + return new Response(status: 200, headers: [ + 'X-Status' => 'Entity is unchanged.', + 'X-WH-Type' => $request->getAttribute('WH_TYPE', 'not_set'), + 'X-WH-Event' => $request->getAttribute('WH_EVENT', 'not_set'), + ]); } catch (HttpException $e) { if (200 === $e->getCode()) { return new Response(status: $e->getCode(), headers: [ diff --git a/src/Libs/Servers/EmbyServer.php b/src/Libs/Servers/EmbyServer.php index 4634a6a6..1bd8e0d6 100644 --- a/src/Libs/Servers/EmbyServer.php +++ b/src/Libs/Servers/EmbyServer.php @@ -102,7 +102,7 @@ class EmbyServer extends JellyfinServer throw new HttpException(sprintf('%s: Not allowed event [%s]', afterLast(__CLASS__, '\\'), $event), 200); } - $date = time(); + $isTainted = in_array($event, self::WEBHOOK_TAINTED_EVENTS); $meta = match ($type) { StateInterface::TYPE_MOVIE => [ @@ -145,16 +145,9 @@ class EmbyServer extends JellyfinServer $isWatched = (int)(bool)ag($json, 'Item.Played', ag($json, 'Item.PlayedToCompletion', 0)); } - $guids = ag($json, 'Item.ProviderIds', []); + $providersId = ag($json, 'Item.ProviderIds', []); - if (!$this->hasSupportedIds($guids)) { - throw new HttpException( - sprintf('%s: No supported GUID was given. [%s]', afterLast(__CLASS__, '\\'), arrayToString($guids)), - 400 - ); - } - - $guids = $this->getGuids($type, $guids); + $guids = $this->getGuids($providersId, $type); foreach (Guid::fromArray($guids)->getPointers() as $guid) { $this->cacheData[$guid] = ag($json, 'Item.Id'); @@ -162,19 +155,40 @@ class EmbyServer extends JellyfinServer $row = [ 'type' => $type, - 'updated' => $date, + 'updated' => time(), 'watched' => $isWatched, 'meta' => $meta, ...$guids ]; - if (true === Config::get('webhook.debug') || null !== ag($request->getQueryParams(), 'debug')) { - saveWebhookPayload($request, "{$this->name}.{$event}", $json + ['entity' => $row]); + $entity = Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); + + if (!$entity->hasGuids()) { + throw new HttpException( + sprintf( + '%s: No supported GUID was given. [%s]', + afterLast(__CLASS__, '\\'), + arrayToString( + [ + 'guids' => !empty($providersId) ? $providersId : 'None', + 'rGuids' => $entity->hasRelativeGuids() ? $entity->getRelativeGuids() : 'None', + ] + ) + ), 400 + ); } - return Container::get(StateInterface::class)::fromArray($row)->setIsTainted( - in_array($event, self::WEBHOOK_TAINTED_EVENTS) - ); + if (false === $isTainted && (true === Config::get('webhook.debug') || null !== ag( + $request->getQueryParams(), + 'debug' + ))) { + saveWebhookPayload($this->name . '.' . $event, $request, [ + 'entity' => $entity->getAll(), + 'payload' => $json, + ]); + } + + return $entity; } public function push(array $entities, DateTimeInterface|null $after = null): array diff --git a/src/Libs/Servers/JellyfinServer.php b/src/Libs/Servers/JellyfinServer.php index 0d1c9362..bc662699 100644 --- a/src/Libs/Servers/JellyfinServer.php +++ b/src/Libs/Servers/JellyfinServer.php @@ -286,8 +286,6 @@ class JellyfinServer implements ServerInterface $isTainted = in_array($event, self::WEBHOOK_TAINTED_EVENTS); - $date = time(); - $meta = match ($type) { StateInterface::TYPE_MOVIE => [ 'via' => $this->name, @@ -311,49 +309,65 @@ class JellyfinServer implements ServerInterface default => throw new HttpException(sprintf('%s: Invalid content type.', afterLast(__CLASS__, '\\')), 400), }; - $guids = []; + $providersId = []; foreach ($json as $key => $val) { - if (str_starts_with($key, 'Provider_')) { - $guids[self::afterString($key, 'Provider_')] = $val; + if (!str_starts_with($key, 'Provider_')) { + continue; } + $providersId[self::afterString($key, 'Provider_')] = $val; } - if (!$this->hasSupportedIds($guids)) { - throw new HttpException( - sprintf('%s: No supported GUID was given. [%s]', afterLast(__CLASS__, '\\'), arrayToString($guids)), - 400 - ); + // We use SeriesName to overcome jellyfin webhook limitation, it does not send series id. + if (StateInterface::TYPE_EPISODE === $type && null !== ag($json, 'SeriesName')) { + $meta['parent'] = $this->getEpisodeParent(ag($json, 'ItemId'), ag($json, 'SeriesName')); } - if (false === $isTainted && StateInterface::TYPE_EPISODE === $type) { - $meta['parent'] = $this->getParentGUIDs(ag($json, 'ItemId'), ag($json, 'SeriesName')); - } - - $guids = $this->getGuids($guids, $type); + $guids = $this->getGuids($providersId, $type); foreach (Guid::fromArray($guids)->getPointers() as $guid) { $this->cacheData[$guid] = ag($json, 'Item.ItemId'); } - $isWatched = (int)(bool)ag($json, 'Played', ag($json, 'PlayedToCompletion', 0)); - $row = [ 'type' => $type, - 'updated' => $date, - 'watched' => $isWatched, + 'updated' => time(), + 'watched' => (int)(bool)ag($json, 'Played', ag($json, 'PlayedToCompletion', 0)), 'meta' => $meta, ...$guids ]; - if (true === Config::get('webhook.debug') || null !== ag($request->getQueryParams(), 'debug')) { - saveWebhookPayload($request, "{$this->name}.{$event}", $json + ['entity' => $row]); + $entity = Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); + + if (!$entity->hasGuids()) { + throw new HttpException( + sprintf( + '%s: No supported GUID was given. [%s]', + afterLast(__CLASS__, '\\'), + arrayToString( + [ + 'guids' => !empty($providersId) ? $providersId : 'None', + 'rGuids' => $entity->hasRelativeGuids() ? $entity->getRelativeGuids() : 'None', + ] + ) + ), 400 + ); } - return Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); + if (false === $isTainted && (true === Config::get('webhook.debug') || null !== ag( + $request->getQueryParams(), + 'debug' + ))) { + saveWebhookPayload($this->name . '.' . $event, $request, [ + 'entity' => $entity->getAll(), + 'payload' => $json, + ]); + } + + return $entity; } - protected function getParentGUIDs(mixed $id, string|null $series): array + protected function getEpisodeParent(mixed $id, string|null $series): array { if (null !== $series && array_key_exists($series, $this->cacheShow)) { return $this->cacheShow[$series]; diff --git a/src/Libs/Servers/PlexServer.php b/src/Libs/Servers/PlexServer.php index f04b6a58..94819f7d 100644 --- a/src/Libs/Servers/PlexServer.php +++ b/src/Libs/Servers/PlexServer.php @@ -366,49 +366,56 @@ class PlexServer implements ServerInterface ]; } - $isWatched = (int)(bool)ag($json, 'Metadata.viewCount', 0); - - $date = time(); - - if (!$this->hasSupportedIds($json['Metadata']['Guid'] ?? [])) { - throw new HttpException( - sprintf( - '%s: No supported GUID was given. [%s]', - afterLast(__CLASS__, '\\'), - arrayToString($json['Metadata']['Guid'] ?? []) - ), - 400 - ); - } - $guids = $this->getGuids($json['Metadata']['Guid'] ?? [], $type); foreach (Guid::fromArray($guids)->getPointers() as $guid) { $this->cacheData[$guid] = ag($json, 'Metadata.guid'); } - if (false === $isTainted && StateInterface::TYPE_EPISODE === $type) { - $meta['parent'] = $this->getParentGUIDs( - $json['Metadata']['grandparentRatingKey'] ?? $json['Metadata']['parentRatingKey'] - ); + if (StateInterface::TYPE_EPISODE === $type) { + $parentId = ag($json, 'Metadata.grandparentRatingKey', fn() => ag($json, 'Metadata.parentRatingKey')); + $meta['parent'] = null !== $parentId ? $this->getEpisodeParent($parentId) : []; } $row = [ 'type' => $type, - 'updated' => $date, - 'watched' => $isWatched, + 'updated' => time(), + 'watched' => (int)(bool)ag($json, 'Metadata.viewCount', 0), 'meta' => $meta, ...$guids ]; - if (true === Config::get('webhook.debug') || null !== ag($request->getQueryParams(), 'debug')) { - saveWebhookPayload($request, "{$this->name}.{$event}", $json + ['entity' => $row]); + $entity = Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); + + if (!$entity->hasGuids()) { + throw new HttpException( + sprintf( + '%s: No supported GUID was given. [%s]', + afterLast(__CLASS__, '\\'), + arrayToString( + [ + 'guids' => !empty($json['Metadata']['Guid']) ? $json['Metadata']['Guid'] : 'None', + 'rGuids' => $entity->hasRelativeGuids() ? $entity->getRelativeGuids() : 'None', + ] + ) + ), 400 + ); } - return Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); + if (false !== $isTainted && (true === Config::get('webhook.debug') || null !== ag( + $request->getQueryParams(), + 'debug' + ))) { + saveWebhookPayload($this->name . '.' . $event, $request, [ + 'entity' => $entity->getAll(), + 'payload' => $json, + ]); + } + + return $entity; } - protected function getParentGUIDs(mixed $id): array + protected function getEpisodeParent(int|string $id): array { if (array_key_exists($id, $this->cacheShow)) { return $this->cacheShow[$id]; @@ -443,7 +450,6 @@ class PlexServer implements ServerInterface $json['Guid'][] = ['id' => $json['guid']]; } - if (!$this->hasSupportedIds($json['Guid'])) { $this->cacheShow[$id] = []; return $this->cacheShow[$id]; @@ -1850,8 +1856,10 @@ class PlexServer implements ServerInterface 'date' => makeDate($item->originallyAvailableAt ?? 'now')->format('Y-m-d'), ]; - if (null !== ($item->grandparentRatingKey ?? null)) { - $meta['parent'] = $this->showInfo[$item->grandparentRatingKey] ?? []; + $parentId = $item->grandparentRatingKey ?? $item->parentRatingKey ?? null; + + if (null !== $parentId) { + $meta['parent'] = $this->showInfo[$parentId] ?? []; } } diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php index 55c2864a..40481b24 100644 --- a/src/Libs/helpers.php +++ b/src/Libs/helpers.php @@ -226,7 +226,7 @@ if (!function_exists('fsize')) { } if (!function_exists('saveWebhookPayload')) { - function saveWebhookPayload(ServerRequestInterface $request, string $name, array $parsed = []): void + function saveWebhookPayload(string $name, ServerRequestInterface $request, array $parsed = []): void { $content = [ 'query' => $request->getQueryParams(),