experimental support for syncing watch progress for played items. #617
This commit is contained in:
21
NEWS.md
21
NEWS.md
@@ -1,5 +1,26 @@
|
||||
# Old Updates
|
||||
|
||||
### 2025-02-02
|
||||
|
||||
We are happy to announce that we have merged in direct support for multi-user in `state:import` and `state:export`
|
||||
commands and tasks. Therefore, `state:sync` command has been removed. Once you generate the sub users configs. it will
|
||||
start working alongside the main user.
|
||||
|
||||
### 2025-02-01
|
||||
|
||||
Breaking changes as of version 20250201~, in earlier versions, if you want to sync multi-user play state, you only had
|
||||
to run `state:sync` command, However, due to us extending support for more operation to support multi-user data, we
|
||||
needed a way to generate per user config instead of relying on `state:sync`, thus we have introduced a new command
|
||||
called `backends:create`, the purpose of this command is to generate the needed config files for each user.
|
||||
|
||||
This change allow us to support more operations in the future.
|
||||
|
||||
We also have minor breaking change in per user db name, before it was named `user_name.db`, now it's named `user.db`
|
||||
this change shouldn't effect you as we have backward compatibility in place to rename the old db to the new name.
|
||||
|
||||
for more information about multi-user, Please read the FAQ entry about it
|
||||
at [this link](FAQ.md#is-there-support-for-multi-user-setup).
|
||||
|
||||
### 2025-01-24
|
||||
|
||||
We are excited to share that multi-user sync is now fully supported! Our first goal was to make sure the feature worked,
|
||||
|
||||
34
README.md
34
README.md
@@ -9,6 +9,19 @@ out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers.
|
||||
|
||||
## Updates
|
||||
|
||||
### 2025-02-19
|
||||
|
||||
We have introduced new experimental feature to allow syncing watch progress for played items. This feature is still in
|
||||
early stages, and might not work as expected. and there are probably still many bugs that we need to fix. Please report
|
||||
any issues you might face.
|
||||
|
||||
The feature is disabled by default, to enable it you need to run add this environment variable `WS_PROGRESS_THRESHOLD`
|
||||
with seconds as value, the minimum value is `180` seconds. `0` seconds means it's disabled. We think reasonable value is
|
||||
`86400` or more this number is about 1day.
|
||||
|
||||
We are still not keen on this feature, and it might be removed in future releases if we aren't able to deal with the
|
||||
issues we are facing.
|
||||
|
||||
### 2025-02-11
|
||||
|
||||
We recently have added support to generate accesstoken for external `Plex` users, i.e. `not home users`. so the
|
||||
@@ -22,27 +35,6 @@ it.
|
||||
We have also added support to webhooks to allow sub users, you simply have to add new hooks using `user@backend`. Please
|
||||
take look at [this FAQ](FAQ.md#how-to-add-webhooks) to learn how to use it for sub users.
|
||||
|
||||
### 2025-02-02
|
||||
|
||||
We are happy to announce that we have merged in direct support for multi-user in `state:import` and `state:export`
|
||||
commands and tasks. Therefore, `state:sync` command has been removed. Once you generate the sub users configs. it will
|
||||
start working alongside the main user.
|
||||
|
||||
### 2025-02-01
|
||||
|
||||
Breaking changes as of version 20250201~, in earlier versions, if you want to sync multi-user play state, you only had
|
||||
to run `state:sync` command, However, due to us extending support for more operation to support multi-user data, we
|
||||
needed a way to generate per user config instead of relying on `state:sync`, thus we have introduced a new command
|
||||
called `backends:create`, the purpose of this command is to generate the needed config files for each user.
|
||||
|
||||
This change allow us to support more operations in the future.
|
||||
|
||||
We also have minor breaking change in per user db name, before it was named `user_name.db`, now it's named `user.db`
|
||||
this change shouldn't effect you as we have backward compatibility in place to rename the old db to the new name.
|
||||
|
||||
for more information about multi-user, Please read the FAQ entry about it
|
||||
at [this link](FAQ.md#is-there-support-for-multi-user-setup).
|
||||
|
||||
---
|
||||
Refer to [NEWS](NEWS.md) for old updates.
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ use Monolog\Level;
|
||||
|
||||
return (function () {
|
||||
$inContainer = inContainer();
|
||||
$progressTimeCheck = fn(int $v, int $d): int => 0 === $v || $v >= 180 ? $v : $d;
|
||||
|
||||
$config = [
|
||||
'name' => 'WatchState',
|
||||
'version' => '$(version_via_ci)',
|
||||
@@ -77,6 +79,10 @@ return (function () {
|
||||
'sync' => [
|
||||
'progress' => (bool)env('WS_SYNC_PROGRESS', true),
|
||||
],
|
||||
'progress' => [
|
||||
// -- Allows to sync watch progress for played items.
|
||||
'threshold' => $progressTimeCheck((int)env('WS_PROGRESS_THRESHOLD', 0), 60 * 10),
|
||||
],
|
||||
];
|
||||
|
||||
$config['guid'] = [
|
||||
|
||||
@@ -181,6 +181,28 @@ return (function () {
|
||||
'description' => 'The path to save the profiler data.',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'key' => 'WS_PROGRESS_THRESHOLD',
|
||||
'description' => 'Allow watch progress sync for played items. Expects seconds. Minimum 180. 0 to disable.',
|
||||
'type' => 'string',
|
||||
'validate' => function (mixed $value): string {
|
||||
if (!is_numeric($value) && empty($value)) {
|
||||
throw new ValidationException('Invalid progress threshold. Empty value.');
|
||||
}
|
||||
|
||||
if (false === is_numeric($value)) {
|
||||
throw new ValidationException('Invalid progress threshold. Must be a number.');
|
||||
}
|
||||
|
||||
$cmp = (int)$value;
|
||||
|
||||
if (0 !== $cmp && $cmp < 181) {
|
||||
throw new ValidationException('Invalid progress threshold. Must be at least 180 seconds.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
$validateCronExpression = function (string $value): string {
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Backends\Common\Context;
|
||||
use App\Backends\Common\GuidInterface as iGuid;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Backends\Emby\EmbyActionTrait;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Enums\Http\Method;
|
||||
@@ -117,7 +118,7 @@ class Progress
|
||||
|
||||
if ($context->backendName === $entity->via) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -125,7 +126,7 @@ class Progress
|
||||
|
||||
if (null === ag($metadata, iState::COLUMN_ID, null)) {
|
||||
$this->logger->warning(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -134,7 +135,7 @@ class Progress
|
||||
$senderDate = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_DATE);
|
||||
if (null === $senderDate) {
|
||||
$this->logger->warning(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -145,7 +146,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(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'compare' => [
|
||||
@@ -161,7 +162,7 @@ class Progress
|
||||
|
||||
if (array_key_exists($logContext['remote']['id'], $sessions)) {
|
||||
$this->logger->notice(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -177,7 +178,7 @@ class Progress
|
||||
|
||||
if (false === $ignoreDate && makeDate($remoteItem->updated)->getTimestamp() > $senderDate) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'compare' => [
|
||||
@@ -190,16 +191,19 @@ class Progress
|
||||
}
|
||||
|
||||
if ($remoteItem->isWatched()) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The backend says the item is marked as watched.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
if (false === ($allowUpdate >= 300 && time() > ($entity->updated + $allowUpdate))) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '#{item.id}: {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}: {user}@{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.id}: {item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'error' => [
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Backends\Common\Context;
|
||||
use App\Backends\Common\GuidInterface as iGuid;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Backends\Jellyfin\JellyfinActionTrait;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Enums\Http\Method;
|
||||
@@ -144,7 +145,7 @@ class Progress
|
||||
|
||||
if ($context->backendName === $entity->via) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -152,7 +153,7 @@ class Progress
|
||||
|
||||
if (null === ag($metadata, iState::COLUMN_ID, null)) {
|
||||
$this->logger->warning(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -161,7 +162,7 @@ class Progress
|
||||
$senderDate = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_DATE);
|
||||
if (null === $senderDate) {
|
||||
$this->logger->warning(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
|
||||
context: $logContext,
|
||||
|
||||
);
|
||||
@@ -173,7 +174,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(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'compare' => ['remote' => makeDate($datetime), 'sender' => makeDate($senderDate),],
|
||||
@@ -186,7 +187,7 @@ class Progress
|
||||
|
||||
if (array_key_exists($logContext['remote']['id'], $sessions)) {
|
||||
$this->logger->notice(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
|
||||
context: $logContext,
|
||||
|
||||
);
|
||||
@@ -199,7 +200,7 @@ class Progress
|
||||
|
||||
if (false === $ignoreDate && makeDate($remoteItem->updated)->getTimestamp() > $senderDate) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'compare' => [
|
||||
@@ -212,16 +213,19 @@ class Progress
|
||||
}
|
||||
|
||||
if ($remoteItem->isWatched()) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The backend says the item is marked as watched.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
if (false === ($allowUpdate >= 300 && time() > ($entity->updated + $allowUpdate))) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '#{item.id}: {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}: {user}@{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.id}: {item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
'error' => [
|
||||
'kind' => $e::class,
|
||||
@@ -255,7 +259,7 @@ class Progress
|
||||
$logContext['remote']['url'] = (string)$url;
|
||||
|
||||
$this->logger->debug(
|
||||
message: "{action}: Updating '{client}: {user}@{backend}' {item.type} '{item.title}' watch progress to '{progress}'.",
|
||||
message: "{action}: Updating '{client}: {user}@{backend}' {item.type} '#{item.id}: {item.title}' watch progress to '{progress}'.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'progress' => $entity->hasPlayProgress() ? formatDuration($entity->getPlayProgress()) : '0:0:0',
|
||||
@@ -287,7 +291,7 @@ class Progress
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
...lw(
|
||||
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}'.",
|
||||
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' change {item.type} '#{item.id}: {item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
'error' => [
|
||||
'kind' => $e::class,
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Backends\Common\Context;
|
||||
use App\Backends\Common\GuidInterface as iGuid;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Backends\Plex\PlexActionTrait;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Enums\Http\Method;
|
||||
@@ -127,7 +128,7 @@ class Progress
|
||||
|
||||
if ($context->backendName === $entity->via) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event originated from this backend.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -135,7 +136,7 @@ class Progress
|
||||
|
||||
if (null === ag($metadata, iState::COLUMN_ID, null)) {
|
||||
$this->logger->warning(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. No metadata was found.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -144,7 +145,7 @@ class Progress
|
||||
$senderDate = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_DATE);
|
||||
if (null === $senderDate) {
|
||||
$this->logger->warning(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. The event originator did not set a date.",
|
||||
context: $logContext,
|
||||
|
||||
);
|
||||
@@ -157,7 +158,7 @@ class Progress
|
||||
|
||||
if (false === $ignoreDate && null !== $datetime && makeDate($datetime)->getTimestamp() > $senderDate) {
|
||||
$this->logger->warning(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event date is older than backend local item date.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'compare' => ['remote' => makeDate($datetime), 'sender' => makeDate($senderDate),],
|
||||
@@ -170,7 +171,7 @@ class Progress
|
||||
|
||||
if (array_key_exists($logContext['remote']['id'], $sessions)) {
|
||||
$this->logger->notice(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. The item is playing right now.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
@@ -187,7 +188,7 @@ class Progress
|
||||
|
||||
if (false === $ignoreDate && makeDate($remoteItem->updated)->getTimestamp() > $senderDate) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
|
||||
message: "{action}: Not processing '#{item.id}: {item.title}' for '{client}: {user}@{backend}'. Event date is older than backend remote item date.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'compare' => [
|
||||
@@ -200,16 +201,19 @@ class Progress
|
||||
}
|
||||
|
||||
if ($remoteItem->isWatched()) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '{item.title}' for '{client}: {user}@{backend}'. The backend says the item is marked as watched.",
|
||||
context: $logContext,
|
||||
);
|
||||
continue;
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
if (false === ($allowUpdate >= 300 && time() > ($entity->updated + $allowUpdate))) {
|
||||
$this->logger->info(
|
||||
message: "{action}: Not processing '#{item.id}: {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}: {user}@{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.id}: {item.title}' status. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
'error' => [
|
||||
'kind' => $e::class,
|
||||
@@ -248,7 +252,7 @@ class Progress
|
||||
$logContext['remote']['url'] = (string)$url;
|
||||
|
||||
$this->logger->debug(
|
||||
message: "{action}: Updating '{client}: {user}@{backend}' {item.type} '{item.title}' watch progress to '{progress}'.",
|
||||
message: "{action}: Updating '{client}: {user}@{backend}' {item.type} '#{item.id}: {item.title}' watch progress to '{progress}'.",
|
||||
context: [
|
||||
...$logContext,
|
||||
'progress' => formatDuration($entity->getPlayProgress()),
|
||||
@@ -270,7 +274,7 @@ class Progress
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
...lw(
|
||||
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}'.",
|
||||
message: "{action}: Exception '{error.kind}' was thrown unhandled during '{client}: {user}@{backend}' change {item.type} '#{item.id}: {item.title}' watch progress. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
'error' => [
|
||||
'kind' => $e::class,
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Libs\Entity;
|
||||
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Guid;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
@@ -566,12 +567,14 @@ final class StateEntity implements iState
|
||||
*/
|
||||
public function hasPlayProgress(): bool
|
||||
{
|
||||
if ($this->isWatched()) {
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
|
||||
if ($this->isWatched() && $allowUpdate < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->getMetadata() as $metadata) {
|
||||
if (0 !== (int)ag($metadata, iState::COLUMN_WATCHED, 0)) {
|
||||
if (0 !== (int)ag($metadata, iState::COLUMN_WATCHED, 0) && $allowUpdate < 1) {
|
||||
continue;
|
||||
}
|
||||
if ((int)ag($metadata, iState::COLUMN_META_DATA_PROGRESS, 0) > 1000) {
|
||||
@@ -587,14 +590,15 @@ final class StateEntity implements iState
|
||||
*/
|
||||
public function getPlayProgress(): int
|
||||
{
|
||||
if ($this->isWatched()) {
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
if ($this->isWatched() && $allowUpdate < 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$compare = [];
|
||||
|
||||
foreach ($this->getMetadata() as $backend => $metadata) {
|
||||
if (0 !== (int)ag($metadata, iState::COLUMN_WATCHED, 0)) {
|
||||
if (0 !== (int)ag($metadata, iState::COLUMN_WATCHED, 0) && $allowUpdate < 1) {
|
||||
continue;
|
||||
}
|
||||
if ((int)ag($metadata, iState::COLUMN_META_DATA_PROGRESS, 0) < 1000) {
|
||||
|
||||
@@ -333,7 +333,12 @@ class DirectMapper implements ImportInterface
|
||||
$this->removePointers($local)->addPointers($local, $local->id);
|
||||
|
||||
$changes = $local->diff(fields: $keys);
|
||||
$progress = !$entity->isWatched() && $playChanged && $entity->hasPlayProgress();
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
|
||||
$progress = $playChanged && $entity->hasPlayProgress();
|
||||
if ($entity->isWatched() && $allowUpdate < 180) {
|
||||
$progress = false;
|
||||
}
|
||||
|
||||
if (count($changes) >= 1) {
|
||||
$_keys = array_merge($keys, [iState::COLUMN_EXTRA]);
|
||||
@@ -356,7 +361,7 @@ class DirectMapper implements ImportInterface
|
||||
|
||||
if (false === $inDryRunMode) {
|
||||
$this->db->update($local);
|
||||
if (true === $entity->hasPlayProgress() && !$entity->isWatched()) {
|
||||
if (true === $progress) {
|
||||
$itemId = r('{type}://{id}:{tainted}@{backend}', [
|
||||
'type' => $entity->type,
|
||||
'backend' => $entity->via,
|
||||
@@ -437,13 +442,16 @@ class DirectMapper implements ImportInterface
|
||||
}
|
||||
|
||||
if ($this->inTraceMode()) {
|
||||
$this->logger->info("{mapper}: [T] Ignoring '{user}@{backend}' - '#{id}: {title}'. No metadata changes detected.", [
|
||||
'user' => $this->userContext?->name ?? 'main',
|
||||
'mapper' => afterLast(self::class, '\\'),
|
||||
'id' => $local->id ?? 'New',
|
||||
'backend' => $entity->via,
|
||||
'title' => $local->getName(),
|
||||
]);
|
||||
$this->logger->info(
|
||||
"{mapper}: [T] Ignoring '{user}@{backend}' - '#{id}: {title}'. No metadata changes detected.",
|
||||
[
|
||||
'user' => $this->userContext?->name ?? 'main',
|
||||
'mapper' => afterLast(self::class, '\\'),
|
||||
'id' => $local->id ?? 'New',
|
||||
'backend' => $entity->via,
|
||||
'title' => $local->getName(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -544,8 +552,13 @@ class DirectMapper implements ImportInterface
|
||||
|
||||
$this->removePointers($cloned)->addPointers($local, $local->id);
|
||||
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
|
||||
$changes = $local->diff(fields: $keys);
|
||||
$progress = !$entity->isWatched() && $playChanged && $entity->hasPlayProgress();
|
||||
$progress = $playChanged && $entity->hasPlayProgress();
|
||||
if ($entity->isWatched() && $allowUpdate < 180) {
|
||||
$progress = false;
|
||||
}
|
||||
|
||||
if (count($changes) >= 1) {
|
||||
$_keys = array_merge($keys, [iState::COLUMN_EXTRA]);
|
||||
@@ -568,8 +581,7 @@ class DirectMapper implements ImportInterface
|
||||
|
||||
if (false === $inDryRunMode) {
|
||||
$this->db->update($local);
|
||||
|
||||
if (true === $entity->hasPlayProgress() && !$entity->isWatched()) {
|
||||
if ($progress) {
|
||||
$itemId = r('{type}://{id}:{tainted}@{backend}', [
|
||||
'type' => $entity->type,
|
||||
'backend' => $entity->via,
|
||||
|
||||
@@ -314,13 +314,16 @@ class MemoryMapper implements ImportInterface
|
||||
}
|
||||
|
||||
if (true === $this->inTraceMode()) {
|
||||
$this->logger->info("{mapper}: [T] Ignoring '{user}@{backend}' - '#{id}: {title}'. No metadata changes detected.", [
|
||||
'user' => $this->userContext?->name ?? 'main',
|
||||
'mapper' => afterLast(self::class, '\\'),
|
||||
'id' => $cloned->id ?? 'New',
|
||||
'backend' => $entity->via,
|
||||
'title' => $cloned->getName(),
|
||||
]);
|
||||
$this->logger->info(
|
||||
"{mapper}: [T] Ignoring '{user}@{backend}' - '#{id}: {title}'. No metadata changes detected.",
|
||||
[
|
||||
'user' => $this->userContext?->name ?? 'main',
|
||||
'mapper' => afterLast(self::class, '\\'),
|
||||
'id' => $cloned->id ?? 'New',
|
||||
'backend' => $entity->via,
|
||||
'title' => $cloned->getName(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -376,8 +379,13 @@ class MemoryMapper implements ImportInterface
|
||||
|
||||
$this->removePointers($cloned)->addPointers($this->objects[$pointer], $pointer);
|
||||
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
|
||||
$changes = $this->objects[$pointer]->diff(fields: $keys);
|
||||
$progress = !$entity->isWatched() && $playChanged && $entity->hasPlayProgress();
|
||||
$progress = $playChanged && $entity->hasPlayProgress();
|
||||
if ($entity->isWatched() && $allowUpdate < 180) {
|
||||
$progress = false;
|
||||
}
|
||||
|
||||
if (count($changes) >= 1) {
|
||||
$_keys = array_merge($keys, [iState::COLUMN_EXTRA]);
|
||||
@@ -400,7 +408,7 @@ class MemoryMapper implements ImportInterface
|
||||
]
|
||||
);
|
||||
|
||||
if (true === $entity->hasPlayProgress() && !$entity->isWatched()) {
|
||||
if (true === $progress) {
|
||||
$itemId = r('{type}://{id}:{tainted}@{backend}', [
|
||||
'type' => $entity->type,
|
||||
'backend' => $entity->via,
|
||||
|
||||
@@ -71,16 +71,24 @@ final readonly class ProcessProgressEvent
|
||||
}
|
||||
|
||||
if ($item->isWatched()) {
|
||||
$writer(Level::Info, "'{user}' item - '{id}: {title}' is marked as watched. Not updating watch process.", [
|
||||
'id' => $item->id,
|
||||
'title' => $item->getName(),
|
||||
'user' => $userContext->name,
|
||||
]);
|
||||
return $e;
|
||||
$allowUpdate = (int)Config::get('progress.threshold', 0);
|
||||
if (false === ($allowUpdate >= 300 && time() > ($item->updated + $allowUpdate))) {
|
||||
$writer(
|
||||
level: Level::Info,
|
||||
message: "'{user}' item - '#{id}: {title}' is marked as watched. Not updating watch progress.",
|
||||
context: [
|
||||
'id' => $item->id,
|
||||
'title' => $item->getName(),
|
||||
'user' => $userContext->name,
|
||||
]
|
||||
);
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === $item->hasPlayProgress()) {
|
||||
$writer(Level::Info, "'{user}' item '{title}' has no watch progress to export.", [
|
||||
$writer(Level::Info, "'{user}' item '#{id}: {title}' has no watch progress to export.", [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'user' => $userContext->name,
|
||||
]);
|
||||
@@ -179,10 +187,12 @@ final readonly class ProcessProgressEvent
|
||||
} catch (Throwable $e) {
|
||||
$writer(
|
||||
Level::Error,
|
||||
"Exception '{error.kind}' was thrown unhandled during '{user}@{backend}' request to sync progress. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
"Exception '{error.kind}' was thrown unhandled during '{user}@{backend}' request to sync '#{id}: {title}' progress. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
[
|
||||
'user' => $userContext->name,
|
||||
'id' => $item->id,
|
||||
'backend' => $name,
|
||||
'title' => $item->getName(),
|
||||
'user' => $userContext->name,
|
||||
'error' => [
|
||||
'kind' => $e::class,
|
||||
'line' => $e->getLine(),
|
||||
@@ -210,10 +220,10 @@ final readonly class ProcessProgressEvent
|
||||
|
||||
$progress = formatDuration($item->getPlayProgress());
|
||||
|
||||
$writer(Level::Notice, "Processing '{user}' - '{id}' - '{via}: {title}' watch progress '{progress}' event.", [
|
||||
'user' => $userContext->name,
|
||||
$writer(Level::Notice, "Processing '{user}@{backend}' - '#{id}: {title}' watch progress '{progress}' event.", [
|
||||
'id' => $item->id,
|
||||
'via' => $item->via,
|
||||
'backend' => $item->via,
|
||||
'user' => $userContext->name,
|
||||
'title' => $item->getName(),
|
||||
'progress' => $progress,
|
||||
]);
|
||||
@@ -223,8 +233,9 @@ final readonly class ProcessProgressEvent
|
||||
$context['user'] = $userContext->name;
|
||||
|
||||
try {
|
||||
if (ag($options, 'trace')) {
|
||||
$writer(Level::Debug, "Processing '{user}@{backend}: {item.title}' response.", [
|
||||
if (true === (bool)ag($options, 'trace')) {
|
||||
$writer(Level::Debug, "Processing '{user}@{backend}' - '#{id}: {item.title}' response.", [
|
||||
'id' => $item->id,
|
||||
'url' => ag($context, 'remote.url', '??'),
|
||||
'status_code' => $response->getStatusCode(),
|
||||
'headers' => $response->getHeaders(false),
|
||||
@@ -233,11 +244,12 @@ final readonly class ProcessProgressEvent
|
||||
]);
|
||||
}
|
||||
|
||||
if (!in_array($response->getStatusCode(), [Status::OK->value, Status::NO_CONTENT->value])) {
|
||||
if (false === in_array(Status::tryFrom($response->getStatusCode()), [Status::OK, Status::NO_CONTENT])) {
|
||||
$writer(
|
||||
Level::Error,
|
||||
"Request to change '{user}@{backend}: {item.title}' watch progress returned with unexpected '{status_code}' status code.",
|
||||
[
|
||||
level: Level::Error,
|
||||
message: "Request to change '{user}@{backend}' - '#{id}: {item.title}' watch progress returned with unexpected '{status_code}' status code.",
|
||||
context: [
|
||||
'id' => $item->id,
|
||||
'status_code' => $response->getStatusCode(),
|
||||
...$context
|
||||
]
|
||||
@@ -245,16 +257,22 @@ final readonly class ProcessProgressEvent
|
||||
continue;
|
||||
}
|
||||
|
||||
$writer(Level::Notice, "Updated '{user}@{backend}: {item.title}' watch progress to '{progress}'.", [
|
||||
...$context,
|
||||
'progress' => $progress,
|
||||
'status_code' => $response->getStatusCode(),
|
||||
]);
|
||||
$writer(
|
||||
level: Level::Notice,
|
||||
message: "Updated '{user}@{backend}' '#{id}: {item.title}' watch progress to '{progress}'.",
|
||||
context: [
|
||||
...$context,
|
||||
'id' => $item->id,
|
||||
'progress' => $progress,
|
||||
'status_code' => $response->getStatusCode(),
|
||||
]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$writer(
|
||||
Level::Error,
|
||||
"Exception '{error.kind}' was thrown unhandled during '{user}@{backend}' request to change watch progress of {item.type} '{item.title}'. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
[
|
||||
level: Level::Error,
|
||||
message: "Exception '{error.kind}' was thrown unhandled during '{user}@{backend}' request to change watch progress of {item.type} '#{id}: {item.title}'. '{error.message}' at '{error.file}:{error.line}'.",
|
||||
context: [
|
||||
'id' => $item->id,
|
||||
'error' => [
|
||||
'kind' => $e::class,
|
||||
'line' => $e->getLine(),
|
||||
|
||||
Reference in New Issue
Block a user