Added an option to disable external id parsing for episodes and rely on relative ids.
This commit is contained in:
39
FAQ.md
39
FAQ.md
@@ -308,25 +308,26 @@ it's slower than `MemoryMapper`.
|
||||
|
||||
### Q: What environment variables supported?
|
||||
|
||||
| Key | Type | Description | Default |
|
||||
|-----------------------|--------|---------------------------------------------------------------------------------|-------------------------------|
|
||||
| WS_DATA_PATH | string | Where to store main data. (config, db). | `${BASE_PATH}/var` |
|
||||
| WS_TMP_DIR | string | Where to store temp data. (logs, cache) | `${WS_DATA_PATH}` |
|
||||
| WS_TZ | string | Set timezone. | `UTC` |
|
||||
| WS_CRON_IMPORT | bool | Enable import scheduled task. Value casted to bool. | `false` |
|
||||
| WS_CRON_IMPORT_AT | string | When to run import scheduled task. Valid Cron Expression Expected. | `0 */1 * * *` (Every 1h) |
|
||||
| WS_CRON_IMPORT_ARGS | string | Flags to pass to the import command. | `-v` |
|
||||
| WS_CRON_EXPORT | bool | Enable export scheduled task. Value casted to bool. | `false` |
|
||||
| WS_CRON_EXPORT_AT | string | When to run export scheduled task. Valid Cron Expression Expected. | `30 */1 * * *` (Every 1h 30m) |
|
||||
| WS_CRON_EXPORT_ARGS | string | Flags to pass to the export command. | `-v` |
|
||||
| WS_CRON_PUSH | bool | Enable push scheduled task. Value casted to bool. | `false` |
|
||||
| WS_CRON_PUSH_AT | string | When to run push scheduled task. Valid Cron Expression Expected. | `*/10 * * * *` (Every 10m) |
|
||||
| WS_CRON_PUSH_ARGS | string | Flags to pass to the push command. | `-v` |
|
||||
| WS_LOGS_PRUNE_AFTER | string | Delete logs older than specified time. Set to `disable` to disable the pruning. | `-3 DAYS` |
|
||||
| WS_LOGS_CONTEXT | bool | Add context to console output messages. | `false` |
|
||||
| WS_LOGGER_FILE_ENABLE | bool | Save logs to file. | `true` |
|
||||
| WS_LOGGER_FILE_LEVEL | string | File Logger Level. | `ERROR` |
|
||||
| WS_WEBHOOK_DEBUG | bool | If enabled, allow dumping request/webhook using `rdump` & `wdump` parameters. | `false` |
|
||||
| Key | Type | Description | Default |
|
||||
|--------------------------|--------|---------------------------------------------------------------------------------|-------------------------------|
|
||||
| WS_DATA_PATH | string | Where to store main data. (config, db). | `${BASE_PATH}/var` |
|
||||
| WS_TMP_DIR | string | Where to store temp data. (logs, cache) | `${WS_DATA_PATH}` |
|
||||
| WS_TZ | string | Set timezone. | `UTC` |
|
||||
| WS_CRON_IMPORT | bool | Enable import scheduled task. Value casted to bool. | `false` |
|
||||
| WS_CRON_IMPORT_AT | string | When to run import scheduled task. Valid Cron Expression Expected. | `0 */1 * * *` (Every 1h) |
|
||||
| WS_CRON_IMPORT_ARGS | string | Flags to pass to the import command. | `-v` |
|
||||
| WS_CRON_EXPORT | bool | Enable export scheduled task. Value casted to bool. | `false` |
|
||||
| WS_CRON_EXPORT_AT | string | When to run export scheduled task. Valid Cron Expression Expected. | `30 */1 * * *` (Every 1h 30m) |
|
||||
| WS_CRON_EXPORT_ARGS | string | Flags to pass to the export command. | `-v` |
|
||||
| WS_CRON_PUSH | bool | Enable push scheduled task. Value casted to bool. | `false` |
|
||||
| WS_CRON_PUSH_AT | string | When to run push scheduled task. Valid Cron Expression Expected. | `*/10 * * * *` (Every 10m) |
|
||||
| WS_CRON_PUSH_ARGS | string | Flags to pass to the push command. | `-v` |
|
||||
| WS_LOGS_PRUNE_AFTER | string | Delete logs older than specified time. Set to `disable` to disable the pruning. | `-3 DAYS` |
|
||||
| WS_LOGS_CONTEXT | bool | Add context to console output messages. | `false` |
|
||||
| WS_LOGGER_FILE_ENABLE | bool | Save logs to file. | `true` |
|
||||
| WS_LOGGER_FILE_LEVEL | string | File Logger Level. | `ERROR` |
|
||||
| WS_WEBHOOK_DEBUG | bool | If enabled, allow dumping request/webhook using `rdump` & `wdump` parameters. | `false` |
|
||||
| WS_EPISODES_DISABLE_GUID | bool | Disable external id parsing for episodes and rely on relative ids. | `false` |
|
||||
|
||||
#### Container specific environment variables.
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ return (function () {
|
||||
// -- Trigger full export mode if changes exceed X number.
|
||||
'threshold' => env('WS_EXPORT_THRESHOLD', 1000),
|
||||
],
|
||||
'episodes' => [
|
||||
'disable' => [
|
||||
'guid' => (bool)env('WS_EPISODES_DISABLE_GUID', false),
|
||||
]
|
||||
],
|
||||
'ignore' => [],
|
||||
];
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ use App\Backends\Common\Response;
|
||||
use App\Backends\Emby\EmbyActionTrait;
|
||||
use App\Backends\Emby\EmbyClient;
|
||||
use App\Backends\Jellyfin\JellyfinActionTrait;
|
||||
use App\Libs\Entity\StateInterface as iFace;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\Options;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Throwable;
|
||||
|
||||
@@ -92,6 +94,8 @@ final class ParseWebhook
|
||||
}
|
||||
|
||||
try {
|
||||
$obj = $this->getItemDetails(context: $context, id: $id);
|
||||
|
||||
if ('item.markplayed' === $event || 'playback.scrobble' === $event) {
|
||||
$isPlayed = 1;
|
||||
$lastPlayedAt = time();
|
||||
@@ -112,31 +116,7 @@ final class ParseWebhook
|
||||
$lastPlayedAt = (0 === $isPlayed) ? makeDate(ag($json, 'Item.DateCreated'))->getTimestamp() : time();
|
||||
}
|
||||
|
||||
$fields = [
|
||||
iFace::COLUMN_EXTRA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_EXTRA_EVENT => $event,
|
||||
iFace::COLUMN_EXTRA_DATE => makeDate('now'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (false === in_array($event, self::WEBHOOK_TAINTED_EVENTS)) {
|
||||
$fields += [
|
||||
iFace::COLUMN_WATCHED => $isPlayed,
|
||||
iFace::COLUMN_UPDATED => $lastPlayedAt,
|
||||
iFace::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_WATCHED => (string)$isPlayed,
|
||||
iFace::COLUMN_META_DATA_PLAYED_AT => (string)$lastPlayedAt,
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$obj = $this->getItemDetails(context: $context, id: $id);
|
||||
|
||||
$guids = $guid->get(guids: ag($json, 'Item.ProviderIds', []), context: [
|
||||
$logContext = [
|
||||
'item' => [
|
||||
'id' => ag($obj, 'Id'),
|
||||
'type' => ag($obj, 'Type'),
|
||||
@@ -157,19 +137,54 @@ final class ParseWebhook
|
||||
},
|
||||
'year' => ag($obj, 'ProductionYear'),
|
||||
],
|
||||
]);
|
||||
];
|
||||
|
||||
if (count($guids) >= 1) {
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)$id);
|
||||
$fields[iFace::COLUMN_GUIDS] = $guids;
|
||||
$fields[iFace::COLUMN_META_DATA][$context->backendName][iFace::COLUMN_GUIDS] = $fields[iFace::COLUMN_GUIDS];
|
||||
$disableGuid = (bool)Config::get('episodes.disable.guid');
|
||||
|
||||
if (EmbyClient::TYPE_EPISODE === $type && true === $disableGuid) {
|
||||
$guids = [];
|
||||
} else {
|
||||
$guids = $guid->get(guids: ag($json, 'Item.ProviderIds', []), context: $logContext);
|
||||
}
|
||||
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)$id);
|
||||
|
||||
$fields = [
|
||||
iState::COLUMN_GUIDS => $guids,
|
||||
iState::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_GUIDS => $guid->parse(
|
||||
guids: ag($json, 'Item.ProviderIds', []),
|
||||
context: $logContext
|
||||
),
|
||||
]
|
||||
],
|
||||
iState::COLUMN_EXTRA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_EXTRA_EVENT => $event,
|
||||
iState::COLUMN_EXTRA_DATE => makeDate('now'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (false === in_array($event, self::WEBHOOK_TAINTED_EVENTS)) {
|
||||
$fields = array_replace_recursive($fields, [
|
||||
iState::COLUMN_WATCHED => $isPlayed,
|
||||
iState::COLUMN_UPDATED => $lastPlayedAt,
|
||||
iState::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_WATCHED => (string)$isPlayed,
|
||||
iState::COLUMN_META_DATA_PLAYED_AT => (string)$lastPlayedAt,
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$entity = $this->createEntity(
|
||||
context: $context,
|
||||
guid: $guid,
|
||||
item: $obj,
|
||||
opts: ['override' => $fields],
|
||||
opts: ['override' => $fields, Options::DISABLE_GUID => $disableGuid],
|
||||
)->setIsTainted(isTainted: true === in_array($event, self::WEBHOOK_TAINTED_EVENTS));
|
||||
|
||||
if (false === $entity->hasGuids() && false === $entity->hasRelativeGuid()) {
|
||||
|
||||
@@ -193,7 +193,7 @@ class Export extends Import
|
||||
if (true === (bool)ag($context->options, Options::DRY_RUN, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$queue->add(
|
||||
$this->http->request(
|
||||
$entity->isWatched() ? 'POST' : 'DELETE',
|
||||
|
||||
@@ -11,9 +11,11 @@ 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\Libs\Entity\StateInterface as iFace;
|
||||
use App\Backends\Jellyfin\JellyfinClient as JFC;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\Options;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Throwable;
|
||||
|
||||
@@ -22,8 +24,8 @@ final class ParseWebhook
|
||||
use CommonTrait, JellyfinActionTrait;
|
||||
|
||||
protected const WEBHOOK_ALLOWED_TYPES = [
|
||||
JellyfinClient::TYPE_MOVIE,
|
||||
JellyfinClient::TYPE_EPISODE,
|
||||
JFC::TYPE_MOVIE,
|
||||
JFC::TYPE_EPISODE,
|
||||
];
|
||||
|
||||
protected const WEBHOOK_ALLOWED_EVENTS = [
|
||||
@@ -87,58 +89,22 @@ final class ParseWebhook
|
||||
}
|
||||
|
||||
try {
|
||||
$obj = $this->getItemDetails(context: $context, id: $id);
|
||||
|
||||
$isPlayed = (bool)ag($json, 'Played');
|
||||
$lastPlayedAt = true === $isPlayed ? ag($json, 'LastPlayedDate') : null;
|
||||
|
||||
$fields = [
|
||||
iFace::COLUMN_WATCHED => (int)$isPlayed,
|
||||
iFace::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_WATCHED => true === $isPlayed ? '1' : '0',
|
||||
]
|
||||
],
|
||||
iFace::COLUMN_EXTRA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_EXTRA_EVENT => $event,
|
||||
iFace::COLUMN_EXTRA_DATE => makeDate('now'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (true === $isPlayed && null !== $lastPlayedAt) {
|
||||
$lastPlayedAt = makeDate($lastPlayedAt)->getTimestamp();
|
||||
$fields = array_replace_recursive($fields, [
|
||||
iFace::COLUMN_UPDATED => $lastPlayedAt,
|
||||
iFace::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_META_DATA_PLAYED_AT => (string)$lastPlayedAt,
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$obj = $this->getItemDetails(context: $context, id: $id);
|
||||
|
||||
$providersId = [];
|
||||
|
||||
foreach (array_change_key_case($json, CASE_LOWER) as $key => $val) {
|
||||
if (false === str_starts_with($key, 'provider_')) {
|
||||
continue;
|
||||
}
|
||||
$providersId[after($key, 'provider_')] = $val;
|
||||
}
|
||||
|
||||
$guids = $guid->get(guids: $providersId, context: [
|
||||
$logContext = [
|
||||
'item' => [
|
||||
'id' => ag($obj, 'Id'),
|
||||
'type' => ag($obj, 'Type'),
|
||||
'title' => match (ag($obj, 'Type')) {
|
||||
JellyfinClient::TYPE_MOVIE => sprintf(
|
||||
JFC::TYPE_MOVIE => sprintf(
|
||||
'%s (%s)',
|
||||
ag($obj, ['Name', 'OriginalTitle'], '??'),
|
||||
ag($obj, 'ProductionYear', '0000')
|
||||
),
|
||||
JellyfinClient::TYPE_EPISODE => trim(
|
||||
JFC::TYPE_EPISODE => trim(
|
||||
sprintf(
|
||||
'%s - (%sx%s)',
|
||||
ag($obj, 'SeriesName', '??'),
|
||||
@@ -149,19 +115,64 @@ final class ParseWebhook
|
||||
},
|
||||
'year' => ag($obj, 'ProductionYear'),
|
||||
],
|
||||
]);
|
||||
];
|
||||
|
||||
if (count($guids) >= 1) {
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)$id);
|
||||
$fields[iFace::COLUMN_GUIDS] = $guids;
|
||||
$fields[iFace::COLUMN_META_DATA][$context->backendName][iFace::COLUMN_GUIDS] = $fields[iFace::COLUMN_GUIDS];
|
||||
$disableGuid = (bool)Config::get('episodes.disable.guid');
|
||||
|
||||
$providersId = [];
|
||||
|
||||
foreach (array_change_key_case($json, CASE_LOWER) as $key => $val) {
|
||||
if (false === str_starts_with($key, 'provider_')) {
|
||||
continue;
|
||||
}
|
||||
$providersId[after($key, 'provider_')] = $val;
|
||||
}
|
||||
|
||||
if (JFC::TYPE_EPISODE === $type && true === $disableGuid) {
|
||||
$guids = [];
|
||||
} else {
|
||||
$guids = $guid->get(guids: $providersId, context: $logContext);
|
||||
}
|
||||
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)$id);
|
||||
|
||||
$fields = [
|
||||
iState::COLUMN_WATCHED => (int)$isPlayed,
|
||||
iState::COLUMN_GUIDS => $guids,
|
||||
iState::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_WATCHED => true === $isPlayed ? '1' : '0',
|
||||
iState::COLUMN_GUIDS => $guid->parse(
|
||||
guids: $providersId,
|
||||
context: $logContext
|
||||
),
|
||||
]
|
||||
],
|
||||
iState::COLUMN_EXTRA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_EXTRA_EVENT => $event,
|
||||
iState::COLUMN_EXTRA_DATE => makeDate('now'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (true === $isPlayed && null !== $lastPlayedAt) {
|
||||
$lastPlayedAt = makeDate($lastPlayedAt)->getTimestamp();
|
||||
$fields = array_replace_recursive($fields, [
|
||||
iState::COLUMN_UPDATED => $lastPlayedAt,
|
||||
iState::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_META_DATA_PLAYED_AT => (string)$lastPlayedAt,
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$entity = $this->createEntity(
|
||||
context: $context,
|
||||
guid: $guid,
|
||||
item: $obj,
|
||||
opts: ['override' => $fields],
|
||||
opts: ['override' => $fields, Options::DISABLE_GUID => $disableGuid],
|
||||
)->setIsTainted(isTainted: true === in_array($event, self::WEBHOOK_TAINTED_EVENTS));
|
||||
|
||||
if (false === $entity->hasGuids() && false === $entity->hasRelativeGuid()) {
|
||||
|
||||
@@ -44,6 +44,7 @@ trait JellyfinActionTrait
|
||||
$type = JellyfinClient::TYPE_MAPPER[ag($item, 'Type')] ?? ag($item, 'Type');
|
||||
|
||||
$logContext = [
|
||||
'backend' => $context->backendName,
|
||||
'item' => [
|
||||
'id' => (string)ag($item, 'Id'),
|
||||
'type' => ag($item, 'Type'),
|
||||
@@ -64,7 +65,12 @@ trait JellyfinActionTrait
|
||||
],
|
||||
];
|
||||
|
||||
$guids = $guid->get(guids: ag($item, 'ProviderIds', []), context: $logContext);
|
||||
if (iState::TYPE_EPISODE === $type && true === (bool)ag($opts, Options::DISABLE_GUID, false)) {
|
||||
$guids = [];
|
||||
} else {
|
||||
$guids = $guid->get(guids: ag($item, 'ProviderIds', []), context: $logContext);
|
||||
}
|
||||
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)ag($item, 'Id'));
|
||||
|
||||
$builder = [
|
||||
@@ -147,6 +153,13 @@ trait JellyfinActionTrait
|
||||
$builder = array_replace_recursive($builder, $opts['override'] ?? []);
|
||||
}
|
||||
|
||||
if (true === is_array($builder[iState::COLUMN_GUIDS] ?? false)) {
|
||||
$builder[iState::COLUMN_GUIDS] = Guid::fromArray(
|
||||
payload: $builder[iState::COLUMN_GUIDS],
|
||||
context: $logContext,
|
||||
)->getAll();
|
||||
}
|
||||
|
||||
return Container::get(iState::class)::fromArray($builder);
|
||||
}
|
||||
|
||||
|
||||
@@ -124,21 +124,23 @@ class JellyfinGuid implements iGuid
|
||||
|
||||
$guid[self::GUID_MAPPER[$key]] = $value;
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown in parsing of [%(backend)] [%(agent)] identifier.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'agent' => $value,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
'trace' => $this->context->trace ? $e->getTrace() : [],
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
if (true === $log) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown in parsing of [%(backend)] [%(agent)] identifier.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'agent' => $value,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
'trace' => $this->context->trace ? $e->getTrace() : [],
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ use App\Backends\Common\Levels;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Backends\Plex\PlexActionTrait;
|
||||
use App\Backends\Plex\PlexClient;
|
||||
use App\Libs\Entity\StateInterface as iFace;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Guid;
|
||||
use App\Libs\Options;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Throwable;
|
||||
|
||||
@@ -104,53 +106,27 @@ final class ParseWebhook
|
||||
}
|
||||
|
||||
try {
|
||||
$obj = ag($this->getItemDetails(context: $context, id: $id), 'MediaContainer.Metadata.0', []);
|
||||
|
||||
$isPlayed = (bool)ag($item, 'viewCount', false);
|
||||
$lastPlayedAt = true === $isPlayed ? ag($item, 'lastViewedAt') : null;
|
||||
|
||||
$fields = [
|
||||
iFace::COLUMN_WATCHED => (int)$isPlayed,
|
||||
iFace::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_WATCHED => true === $isPlayed ? '1' : '0',
|
||||
]
|
||||
],
|
||||
iFace::COLUMN_EXTRA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_EXTRA_EVENT => $event,
|
||||
iFace::COLUMN_EXTRA_DATE => makeDate('now'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (true === $isPlayed && null !== $lastPlayedAt) {
|
||||
$fields = array_replace_recursive($fields, [
|
||||
iFace::COLUMN_UPDATED => (int)$lastPlayedAt,
|
||||
iFace::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iFace::COLUMN_META_DATA_PLAYED_AT => (string)$lastPlayedAt,
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$obj = ag($this->getItemDetails(context: $context, id: $id), 'MediaContainer.Metadata.0', []);
|
||||
|
||||
$year = (int)ag($obj, ['grandParentYear', 'parentYear', 'year'], 0);
|
||||
if (0 === $year && null !== ($airDate = ag($obj, 'originallyAvailableAt'))) {
|
||||
$year = (int)makeDate($airDate)->format('Y');
|
||||
}
|
||||
|
||||
$guids = $guid->get(guids: ag($item, 'Guid', []), context: [
|
||||
$logContext = [
|
||||
'item' => [
|
||||
'id' => ag($item, 'ratingKey'),
|
||||
'type' => ag($item, 'type'),
|
||||
'title' => match ($type) {
|
||||
iFace::TYPE_MOVIE => sprintf(
|
||||
iState::TYPE_MOVIE => sprintf(
|
||||
'%s (%s)',
|
||||
ag($item, ['title', 'originalTitle'], '??'),
|
||||
0 === $year ? '0000' : $year,
|
||||
),
|
||||
iFace::TYPE_EPISODE => sprintf(
|
||||
iState::TYPE_EPISODE => sprintf(
|
||||
'%s - (%sx%s)',
|
||||
ag($item, ['grandparentTitle', 'originalTitle', 'title'], '??'),
|
||||
str_pad((string)ag($item, 'parentIndex', 0), 2, '0', STR_PAD_LEFT),
|
||||
@@ -160,19 +136,54 @@ final class ParseWebhook
|
||||
'year' => 0 === $year ? '0000' : $year,
|
||||
'plex_id' => str_starts_with(ag($item, 'guid', ''), 'plex://') ? ag($item, 'guid') : 'none',
|
||||
],
|
||||
]);
|
||||
];
|
||||
|
||||
if (count($guids) >= 1) {
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)$id);
|
||||
$fields[iFace::COLUMN_GUIDS] = $guids;
|
||||
$fields[iFace::COLUMN_META_DATA][$context->backendName][iFace::COLUMN_GUIDS] = $fields[iFace::COLUMN_GUIDS];
|
||||
$disableGuid = (bool)Config::get('episodes.disable.guid');
|
||||
|
||||
if (PlexClient::TYPE_EPISODE === $type && true === $disableGuid) {
|
||||
$guids = [];
|
||||
} else {
|
||||
$guids = $guid->get(guids: ag($item, 'Guid', []), context: $logContext);
|
||||
}
|
||||
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)$id);
|
||||
|
||||
$fields = [
|
||||
iState::COLUMN_WATCHED => (int)$isPlayed,
|
||||
iState::COLUMN_GUIDS => $guids,
|
||||
iState::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_WATCHED => true === $isPlayed ? '1' : '0',
|
||||
iState::COLUMN_GUIDS => $guid->parse(
|
||||
guids: ag($item, 'Guid', []),
|
||||
context: $logContext
|
||||
),
|
||||
]
|
||||
],
|
||||
iState::COLUMN_EXTRA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_EXTRA_EVENT => $event,
|
||||
iState::COLUMN_EXTRA_DATE => makeDate('now'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (true === $isPlayed && null !== $lastPlayedAt) {
|
||||
$fields = array_replace_recursive($fields, [
|
||||
iState::COLUMN_UPDATED => (int)$lastPlayedAt,
|
||||
iState::COLUMN_META_DATA => [
|
||||
$context->backendName => [
|
||||
iState::COLUMN_META_DATA_PLAYED_AT => (string)$lastPlayedAt,
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$entity = $this->createEntity(
|
||||
context: $context,
|
||||
guid: $guid,
|
||||
item: $obj,
|
||||
opts: ['override' => $fields],
|
||||
opts: ['override' => $fields, Options::DISABLE_GUID => $disableGuid],
|
||||
)->setIsTainted(isTainted: true === in_array($event, self::WEBHOOK_TAINTED_EVENTS));
|
||||
|
||||
if (false === $entity->hasGuids() && false === $entity->hasRelativeGuid()) {
|
||||
|
||||
@@ -62,6 +62,7 @@ trait PlexActionTrait
|
||||
$type = $this->typeMapper[ag($item, 'type')] ?? ag($item, 'type');
|
||||
|
||||
$logContext = [
|
||||
'backend' => $context->backendName,
|
||||
'item' => [
|
||||
'id' => ag($item, 'ratingKey'),
|
||||
'type' => ag($item, 'type'),
|
||||
@@ -83,7 +84,12 @@ trait PlexActionTrait
|
||||
],
|
||||
];
|
||||
|
||||
$guids = $guid->get(guids: ag($item, 'Guid', []), context: $logContext);
|
||||
if (iState::TYPE_EPISODE === $type && true === (bool)ag($opts, Options::DISABLE_GUID, false)) {
|
||||
$guids = [];
|
||||
} else {
|
||||
$guids = $guid->get(guids: ag($item, 'Guid', []), context: $logContext);
|
||||
}
|
||||
|
||||
$guids += Guid::makeVirtualGuid($context->backendName, (string)ag($item, 'ratingKey'));
|
||||
|
||||
$builder = [
|
||||
@@ -163,6 +169,13 @@ trait PlexActionTrait
|
||||
$builder = array_replace_recursive($builder, $opts['override'] ?? []);
|
||||
}
|
||||
|
||||
if (true === is_array($builder[iState::COLUMN_GUIDS] ?? false)) {
|
||||
$builder[iState::COLUMN_GUIDS] = Guid::fromArray(
|
||||
payload: $builder[iState::COLUMN_GUIDS],
|
||||
context: $logContext,
|
||||
)->getAll();
|
||||
}
|
||||
|
||||
return Container::get(iState::class)::fromArray($builder);
|
||||
}
|
||||
|
||||
|
||||
@@ -186,21 +186,23 @@ final class PlexGuid implements GuidInterface
|
||||
|
||||
$guid[self::GUID_MAPPER[$key]] = $value;
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown in parsing of [%(backend)] [%(agent)] identifier.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'agent' => $val ?? null,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
'trace' => $this->context->trace ? $e->getTrace() : [],
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
if (true === $log) {
|
||||
$this->logger->error(
|
||||
'Unhandled exception was thrown in parsing of [%(backend)] [%(agent)] identifier.',
|
||||
[
|
||||
'backend' => $this->context->backendName,
|
||||
'agent' => $val ?? null,
|
||||
'exception' => [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'kind' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
'trace' => $this->context->trace ? $e->getTrace() : [],
|
||||
...$context,
|
||||
]
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ class ExportCommand extends Command
|
||||
$opts[Options::IGNORE_DATE] = true;
|
||||
}
|
||||
|
||||
if ($input->getOption('trace')) {
|
||||
$opts[Options::DEBUG_TRACE] = true;
|
||||
}
|
||||
|
||||
if ($input->getOption('dry-run')) {
|
||||
$opts[Options::DRY_RUN] = true;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ declare(strict_types=1);
|
||||
namespace App\Libs;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Stringable;
|
||||
|
||||
final class Guid
|
||||
final class Guid implements JsonSerializable, Stringable
|
||||
{
|
||||
public const GUID_IMDB = 'guid_imdb';
|
||||
public const GUID_TVDB = 'guid_tvdb';
|
||||
@@ -268,4 +270,14 @@ final class Guid
|
||||
|
||||
return self::$logger;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->getAll();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return json_encode($this->getAll());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ final class Options
|
||||
public const MAPPER_DISABLE_AUTOCOMMIT = 'DISABLE_AUTOCOMMIT';
|
||||
public const IMPORT_METADATA_ONLY = 'IMPORT_METADATA_ONLY';
|
||||
public const MISMATCH_DEEP_SCAN = 'MISMATCH_DEEP_SCAN';
|
||||
public const DISABLE_GUID = 'DISABLE_GUID';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Backends\Jellyfin\Action\SearchId;
|
||||
use App\Backends\Jellyfin\Action\SearchQuery;
|
||||
use App\Backends\Jellyfin\JellyfinActionTrait;
|
||||
use App\Backends\Jellyfin\JellyfinGuid;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Entity\StateInterface as iFace;
|
||||
use App\Libs\HttpException;
|
||||
@@ -260,7 +261,10 @@ class JellyfinServer implements ServerInterface
|
||||
context: $this->context,
|
||||
guid: $this->guid,
|
||||
mapper: $mapper,
|
||||
after: $after
|
||||
after: $after,
|
||||
opts: [
|
||||
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->hasError()) {
|
||||
@@ -281,7 +285,10 @@ class JellyfinServer implements ServerInterface
|
||||
guid: $this->guid,
|
||||
mapper: $mapper,
|
||||
after: $after,
|
||||
opts: ['queue' => $queue]
|
||||
opts: [
|
||||
'queue' => $queue,
|
||||
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->hasError()) {
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Backends\Plex\Action\SearchId;
|
||||
use App\Backends\Plex\Action\SearchQuery;
|
||||
use App\Backends\Plex\PlexActionTrait;
|
||||
use App\Backends\Plex\PlexGuid;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Entity\StateInterface as iFace;
|
||||
use App\Libs\HttpException;
|
||||
@@ -252,7 +253,10 @@ class PlexServer implements ServerInterface
|
||||
context: $this->context,
|
||||
guid: $this->guid,
|
||||
mapper: $mapper,
|
||||
after: $after
|
||||
after: $after,
|
||||
opts: [
|
||||
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->hasError()) {
|
||||
@@ -273,7 +277,10 @@ class PlexServer implements ServerInterface
|
||||
guid: $this->guid,
|
||||
mapper: $mapper,
|
||||
after: $after,
|
||||
opts: ['queue' => $queue],
|
||||
opts: [
|
||||
'queue' => $queue,
|
||||
Options::DISABLE_GUID => (bool)Config::get('episodes.disable.guid'),
|
||||
],
|
||||
);
|
||||
|
||||
if ($response->hasError()) {
|
||||
|
||||
Reference in New Issue
Block a user