From 89730d9f14aa265de328fc6a19f2e0aae877c219 Mon Sep 17 00:00:00 2001 From: "Abdulmhsen B. A. A" Date: Wed, 20 Dec 2023 16:54:07 +0300 Subject: [PATCH] jellyfin added version constraint check for play progress sync. --- src/Backends/Jellyfin/Action/Progress.php | 58 ++++++++------ src/Backends/Jellyfin/JellyfinClient.php | 24 ++++++ src/Commands/Database/ListCommand.php | 1 + src/Commands/State/ProgressCommand.php | 75 +++++++++++++++---- .../Backends/UnexpectedVersionException.php | 14 ++++ 5 files changed, 134 insertions(+), 38 deletions(-) create mode 100644 src/Libs/Exceptions/Backends/UnexpectedVersionException.php diff --git a/src/Backends/Jellyfin/Action/Progress.php b/src/Backends/Jellyfin/Action/Progress.php index 3c177ba7..bb7d030e 100644 --- a/src/Backends/Jellyfin/Action/Progress.php +++ b/src/Backends/Jellyfin/Action/Progress.php @@ -114,16 +114,19 @@ class Progress ]; if ($context->backendName === $entity->via) { - $this->logger->info('Ignoring [{item.title}] for [{backend}]. Event originated from this backend.', [ - 'backend' => $context->backendName, - ...$logContext, - ]); + $this->logger->info( + 'Jellyfin.Progress: Ignoring [{item.title}] for [{backend}]. Event originated from this backend.', + [ + 'backend' => $context->backendName, + ...$logContext, + ] + ); continue; } if (null === ag($metadata, iState::COLUMN_ID, null)) { $this->logger->warning( - 'Ignoring [{item.title}] for [{backend}]. No metadata was found.', + 'Jellyfin.Progress: Ignoring [{item.title}] for [{backend}]. No metadata was found.', [ 'backend' => $context->backendName, ...$logContext, @@ -134,10 +137,13 @@ class Progress $senderDate = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_DATE); if (null === $senderDate) { - $this->logger->warning('Ignoring [{item.title}] for [{backend}]. Sender did not set a date.', [ - 'backend' => $context->backendName, - ...$logContext, - ]); + $this->logger->warning( + 'Jellyfin.Progress: Ignoring [{item.title}] for [{backend}]. Sender did not set a date.', + [ + 'backend' => $context->backendName, + ...$logContext, + ] + ); continue; } $senderDate = makeDate($senderDate)->getTimestamp(); @@ -145,7 +151,7 @@ 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( - 'Ignoring [{item.title}] for [{backend}]. Sender date is older than backend date.', + 'Jellyfin.Progress: Ignoring [{item.title}] for [{backend}]. Sender date is older than backend date.', [ 'backend' => $context->backendName, ...$logContext, @@ -167,7 +173,7 @@ class Progress if (false === $ignoreDate && makeDate($remoteItem->updated)->getTimestamp() > $senderDate) { $this->logger->info( - 'Ignoring [{item.title}] for [{backend}]. Sender date is older than backend item date.', + 'Jellyfin.Progress: Ignoring [{item.title}] for [{backend}]. Sender date is older than backend item date.', [ 'backend' => $context->backendName, ...$logContext, @@ -178,7 +184,7 @@ class Progress if ($remoteItem->isWatched()) { $this->logger->info( - 'Ignoring [{item.title}] for [{backend}]. The backend reported the item as watched.', + 'Jellyfin.Progress: Ignoring [{item.title}] for [{backend}]. The backend reported the item as watched.', [ 'backend' => $context->backendName, ...$logContext, @@ -213,29 +219,37 @@ class Progress try { $url = $context->backendUrl->withPath( - r('/Users/{user_id}/PlayingItems/{item_id}', [ + r('/Users/{user_id}/Items/{item_id}/UserData', [ 'user_id' => $context->backendUser, 'item_id' => $logContext['remote']['id'], ]) - )->withQuery( - http_build_query([ - 'positionTicks' => (string)floor($entity->getPlayProgress() * 1_00_00), - ]) ); $logContext['remote']['url'] = (string)$url; - $this->logger->debug('Updating [{backend}] {item.type} [{item.title}] watch progress.', [ - 'backend' => $context->backendName, - ...$logContext, - ]); + $this->logger->debug( + 'Jellyfin.Progress: Updating [{client}: {backend}] {item.type} [{item.title}] watch progress.', + [ + // -- convert time to ticks for emby to understand it. + 'time' => floor($entity->getPlayProgress() * 1_00_00), + 'client' => $context->clientName, + 'backend' => $context->backendName, + ...$logContext, + ] + ); if (false === (bool)ag($context->options, Options::DRY_RUN, false)) { $queue->add( $this->http->request( - 'DELETE', + '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 + [ diff --git a/src/Backends/Jellyfin/JellyfinClient.php b/src/Backends/Jellyfin/JellyfinClient.php index d28cf702..b50320d8 100644 --- a/src/Backends/Jellyfin/JellyfinClient.php +++ b/src/Backends/Jellyfin/JellyfinClient.php @@ -7,7 +7,9 @@ namespace App\Backends\Jellyfin; use App\Backends\Common\Cache; use App\Backends\Common\ClientInterface as iClient; use App\Backends\Common\Context; +use App\Backends\Common\Error; use App\Backends\Common\GuidInterface as iGuid; +use App\Backends\Common\Levels; use App\Backends\Common\Response; use App\Backends\Jellyfin\Action\Backup; use App\Backends\Jellyfin\Action\Export; @@ -29,6 +31,7 @@ use App\Libs\Config; use App\Libs\Container; use App\Libs\Entity\StateInterface as iState; use App\Libs\Exceptions\Backends\RuntimeException; +use App\Libs\Exceptions\Backends\UnexpectedVersionException; use App\Libs\Exceptions\HttpException; use App\Libs\Mappers\ImportInterface as iImport; use App\Libs\Options; @@ -332,6 +335,27 @@ class JellyfinClient implements iClient */ public function progress(array $entities, QueueRequests $queue, iDate|null $after = null): array { + $version = $this->getVersion(); + + if (false === version_compare($version, '10.8.14', '>=')) { + $this->throwError( + 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}.', + context: [ + 'version' => [ + 'current' => $version, + 'required' => '10.8.14', + ], + ], + level: Levels::ERROR, + ) + ), + className: UnexpectedVersionException::class + ); + } + $response = Container::get(Progress::class)( context: $this->context, guid: $this->guid, diff --git a/src/Commands/Database/ListCommand.php b/src/Commands/Database/ListCommand.php index 295f7959..a35ce7bc 100644 --- a/src/Commands/Database/ListCommand.php +++ b/src/Commands/Database/ListCommand.php @@ -476,6 +476,7 @@ final class ListCommand extends Command 'via' => $entity->via ?? '??', 'date' => makeDate($entity->updated)->format('Y-m-d H:i:s T'), 'played' => $entity->isWatched() ? 'Yes' : 'No', + 'progress' => $entity->hasPlayProgress() ? $entity->getPlayProgress() : 'None', 'event' => ag($entity->extra[$entity->via] ?? [], iState::COLUMN_EXTRA_EVENT, '-'), ]; } diff --git a/src/Commands/State/ProgressCommand.php b/src/Commands/State/ProgressCommand.php index e536873d..53b9441b 100644 --- a/src/Commands/State/ProgressCommand.php +++ b/src/Commands/State/ProgressCommand.php @@ -8,6 +8,7 @@ use App\Command; use App\Libs\Config; use App\Libs\Database\DatabaseInterface as iDB; use App\Libs\Entity\StateInterface as iState; +use App\Libs\Exceptions\Backends\UnexpectedVersionException; use App\Libs\Options; use App\Libs\QueueRequests; use App\Libs\Routable; @@ -195,24 +196,66 @@ class ProgressCommand extends Command } foreach ($list as $name => &$backend) { - $opts = ag($backend, 'options', []); + try { + $opts = ag($backend, 'options', []); - if ($input->getOption('ignore-date')) { - $opts[Options::IGNORE_DATE] = true; + if ($input->getOption('ignore-date')) { + $opts[Options::IGNORE_DATE] = true; + } + + if ($input->getOption('dry-run')) { + $opts[Options::DRY_RUN] = true; + } + + if ($input->getOption('trace')) { + $opts[Options::DEBUG_TRACE] = true; + } + + $backend['options'] = $opts; + $backend['class'] = $this->getBackend(name: $name, config: $backend); + + $backend['class']->progress(entities: $entities, queue: $this->queue); + } /** @noinspection PhpRedundantCatchClauseInspection */ + catch (UnexpectedVersionException $e) { + $this->logger->notice( + 'SYSTEM: Sync play progress is not supported for [{backend}]. Error [{error.message} @ {error.file}:{error.line}].', + [ + 'backend' => $name, + 'error' => [ + 'kind' => $e::class, + 'line' => $e->getLine(), + 'message' => $e->getMessage(), + 'file' => after($e->getFile(), ROOT_PATH), + ], + 'exception' => [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'kind' => get_class($e), + 'message' => $e->getMessage(), + 'trace' => $e->getTrace(), + ], + ] + ); + } catch (Throwable $e) { + $this->logger->error( + message: 'SYSTEM: Exception [{error.kind}] was thrown unhandled during [{backend}] request to sync progress. Error [{error.message} @ {error.file}:{error.line}].', + context: [ + 'error' => [ + 'kind' => $e::class, + 'line' => $e->getLine(), + 'message' => $e->getMessage(), + 'file' => after($e->getFile(), ROOT_PATH), + ], + 'exception' => [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'kind' => get_class($e), + 'message' => $e->getMessage(), + 'trace' => $e->getTrace(), + ], + ] + ); } - - if ($input->getOption('dry-run')) { - $opts[Options::DRY_RUN] = true; - } - - if ($input->getOption('trace')) { - $opts[Options::DEBUG_TRACE] = true; - } - - $backend['options'] = $opts; - $backend['class'] = $this->getBackend(name: $name, config: $backend); - - $backend['class']->progress(entities: $entities, queue: $this->queue); } unset($backend); diff --git a/src/Libs/Exceptions/Backends/UnexpectedVersionException.php b/src/Libs/Exceptions/Backends/UnexpectedVersionException.php new file mode 100644 index 00000000..97767544 --- /dev/null +++ b/src/Libs/Exceptions/Backends/UnexpectedVersionException.php @@ -0,0 +1,14 @@ +