Files
watchstate/src/Backends/Plex/PlexActionTrait.php
2023-12-17 00:08:47 +03:00

321 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Backends\Plex;
use App\Backends\Common\Context;
use App\Backends\Common\GuidInterface as iGuid;
use App\Backends\Common\Response;
use App\Backends\Plex\Action\GetLibrariesList;
use App\Backends\Plex\Action\GetMetaData;
use App\Libs\Container;
use App\Libs\Entity\StateEntity;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Exceptions\Backends\InvalidArgumentException;
use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Guid;
use App\Libs\Options;
trait PlexActionTrait
{
private array $typeMapper = [
PlexClient::TYPE_SHOW => iState::TYPE_SHOW,
PlexClient::TYPE_MOVIE => iState::TYPE_MOVIE,
PlexClient::TYPE_EPISODE => iState::TYPE_EPISODE,
];
/**
* Create {@see StateEntity} Object based on given data.
*
* @param Context $context
* @param iGuid $guid
* @param array $item Plex API item.
* @param array $opts options
*
* @return iState Return object on successful creation.
* @throws InvalidArgumentException if no date was set on object.
* @throws RuntimeException if request failed.
*/
protected function createEntity(Context $context, iGuid $guid, array $item, array $opts = []): iState
{
// -- Handle watched/updated column in a special way to support mark as unplayed.
if (null !== ($opts['override'][iState::COLUMN_WATCHED] ?? null)) {
$isPlayed = (bool)$opts['override'][iState::COLUMN_WATCHED];
$date = $opts['override'][iState::COLUMN_UPDATED] ?? ag($item, 'addedAt');
} else {
$isPlayed = (bool)ag($item, 'viewCount', false);
$date = ag($item, true === $isPlayed ? 'lastViewedAt' : 'addedAt');
}
if (null === $date) {
throw new InvalidArgumentException('No date was set on object.');
}
$year = (int)ag($item, ['grandParentYear', 'parentYear', 'year'], 0);
if (0 === $year && null !== ($airDate = ag($item, 'originallyAvailableAt'))) {
$year = (int)makeDate($airDate)->format('Y');
}
if (null === ag($item, 'Guid')) {
$item['Guid'] = [['id' => ag($item, 'guid')]];
} else {
$item['Guid'][] = ['id' => ag($item, 'guid')];
}
$type = $this->typeMapper[ag($item, 'type')] ?? ag($item, 'type');
$logContext = [
'backend' => $context->backendName,
'item' => [
'id' => ag($item, 'ratingKey'),
'type' => ag($item, 'type'),
'title' => match (ag($item, 'type')) {
PlexClient::TYPE_MOVIE => sprintf(
'%s (%s)',
ag($item, ['title', 'originalTitle'], '??'),
0 === $year ? '0000' : $year,
),
PlexClient::TYPE_EPISODE => sprintf(
'%s - (%sx%s)',
ag($item, ['grandparentTitle', 'originalTitle', 'title'], '??'),
str_pad((string)ag($item, 'parentIndex', 0), 2, '0', STR_PAD_LEFT),
str_pad((string)ag($item, 'index', 0), 3, '0', STR_PAD_LEFT),
),
default => throw new InvalidArgumentException(
r('Unexpected Content type [{type}] was received.', [
'type' => $type
])
),
},
'year' => 0 === $year ? '0000' : $year,
'plex_id' => str_starts_with(ag($item, 'guid', ''), 'plex://') ? ag($item, 'guid') : 'none',
],
];
if (iState::TYPE_EPISODE === $type && true === (bool)ag($opts, Options::DISABLE_GUID, false)) {
$guids = [];
} else {
$guids = $guid->get(guids: ag($item, 'Guid', []), context: $logContext);
}
$builder = [
iState::COLUMN_TYPE => $type,
iState::COLUMN_UPDATED => (int)$date,
iState::COLUMN_WATCHED => (int)$isPlayed,
iState::COLUMN_VIA => $context->backendName,
iState::COLUMN_TITLE => ag($item, ['title', 'originalTitle'], '??'),
iState::COLUMN_GUIDS => $guids,
iState::COLUMN_META_DATA => [
$context->backendName => [
iState::COLUMN_ID => (string)ag($item, 'ratingKey'),
iState::COLUMN_TYPE => $type,
iState::COLUMN_WATCHED => true === $isPlayed ? '1' : '0',
iState::COLUMN_VIA => $context->backendName,
iState::COLUMN_TITLE => ag($item, ['title', 'originalTitle'], '??'),
iState::COLUMN_GUIDS => $guid->parse(
guids: ag($item, 'Guid', []),
context: $logContext
),
iState::COLUMN_META_DATA_ADDED_AT => (string)ag($item, 'addedAt'),
],
],
iState::COLUMN_EXTRA => [],
];
$metadata = &$builder[iState::COLUMN_META_DATA][$context->backendName];
$metadataExtra = &$metadata[iState::COLUMN_META_DATA_EXTRA];
if (null !== ($library = ag($item, 'librarySectionID', $opts['library'] ?? null))) {
$metadata[iState::COLUMN_META_LIBRARY] = (string)$library;
}
if (iState::TYPE_EPISODE === $type) {
$builder[iState::COLUMN_SEASON] = (int)ag($item, 'parentIndex', 0);
$builder[iState::COLUMN_EPISODE] = (int)ag($item, 'index', 0);
$metadata[iState::COLUMN_META_SHOW] = (string)ag($item, ['grandparentRatingKey', 'parentRatingKey'], '??');
$metadata[iState::COLUMN_TITLE] = ag($item, 'grandparentTitle', '??');
$metadata[iState::COLUMN_SEASON] = (string)$builder[iState::COLUMN_SEASON];
$metadata[iState::COLUMN_EPISODE] = (string)$builder[iState::COLUMN_EPISODE];
$metadataExtra[iState::COLUMN_META_DATA_EXTRA_TITLE] = $builder[iState::COLUMN_TITLE];
$builder[iState::COLUMN_TITLE] = $metadata[iState::COLUMN_TITLE];
if (null !== ($parentId = ag($item, ['grandparentRatingKey', 'parentRatingKey']))) {
$builder[iState::COLUMN_PARENT] = $this->getEpisodeParent(
context: $context,
guid: $guid,
id: $parentId
);
$metadata[iState::COLUMN_PARENT] = $builder[iState::COLUMN_PARENT];
}
}
if (0 !== $year) {
$builder[iState::COLUMN_YEAR] = (int)$year;
$metadata[iState::COLUMN_YEAR] = (string)$year;
}
if (null !== ($mediaPath = ag($item, 'Media.0.Part.0.file')) && !empty($mediaPath)) {
$metadata[iState::COLUMN_META_PATH] = (string)$mediaPath;
}
if (null !== ($PremieredAt = ag($item, 'originallyAvailableAt'))) {
$metadataExtra[iState::COLUMN_META_DATA_EXTRA_DATE] = makeDate($PremieredAt)->format('Y-m-d');
}
if (true === $isPlayed) {
$metadata[iState::COLUMN_META_DATA_PLAYED_AT] = (string)$date;
$metadata[iState::COLUMN_META_DATA_PROGRESS] = "0";
}
if (false === $isPlayed && null !== ($progress = ag($item, 'viewOffset', null))) {
// -- Plex reports play progress in milliseconds already no need to convert.
$metadata[iState::COLUMN_META_DATA_PROGRESS] = (string)$progress;
}
unset($metadata, $metadataExtra);
if (null !== ($opts['override'] ?? null)) {
$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);
}
/**
* Get item details.
*
* @param Context $context
* @param string|int $id
* @param array $opts
*
* @return array
* @throws RuntimeException if request failed.
*/
protected function getItemDetails(Context $context, string|int $id, array $opts = []): array
{
$response = $this->getItemInfo(context: $context, id: $id, opts: $opts);
if ($response->isSuccessful()) {
return $response->response;
}
throw new RuntimeException(message: $response->error->format(), previous: $response->error->previous);
}
/**
* Get item details.
*
* @param Context $context
* @param string|int $id
* @param array $opts
* @return Response
*/
protected function getItemInfo(Context $context, string|int $id, array $opts = []): Response
{
return Container::get(GetMetaData::class)(context: $context, id: $id, opts: $opts);
}
/**
* Get episode parent external ids.
*
* @param Context $context
* @param iGuid $guid
* @param int|string $id
* @param array $logContext
*
* @return array
* @throws RuntimeException
*/
protected function getEpisodeParent(Context $context, iGuid $guid, int|string $id, array $logContext = []): array
{
$cacheKey = PlexClient::TYPE_SHOW . '.' . $id;
if (true === $context->cache->has($cacheKey)) {
return $context->cache->get($cacheKey);
}
$json = ag($this->getItemDetails(context: $context, id: $id), 'MediaContainer.Metadata.0', []);
$year = (int)ag($json, ['grandParentYear', 'parentYear', 'year'], 0);
if (0 === $year && null !== ($airDate = ag($json, 'originallyAvailableAt'))) {
$year = (int)makeDate($airDate)->format('Y');
}
$logContext['item'] = [
'id' => ag($json, 'ratingKey'),
'title' => sprintf(
'%s (%s)',
ag($json, ['title', 'originalTitle'], '??'),
0 === $year ? '0000' : $year,
),
'year' => 0 === $year ? '0000' : $year,
'type' => ag($json, 'type', 'unknown'),
];
if (null === ($type = ag($json, 'type')) || PlexClient::TYPE_SHOW !== $type) {
return [];
}
if (null === ($json['Guid'] ?? null)) {
$json['Guid'] = [['id' => $json['guid']]];
} else {
$json['Guid'][] = ['id' => $json['guid']];
}
if (false === $guid->has(guids: $json['Guid'], context: $logContext)) {
$context->cache->set($cacheKey, []);
return [];
}
$gContext = ag_set(
$logContext,
'item.plex_id',
str_starts_with(ag($json, 'guid', ''), 'plex://') ? ag($json, 'guid') : 'none'
);
$context->cache->set(
$cacheKey,
Guid::fromArray(
payload: $guid->get(guids: $json['Guid'], context: [...$gContext]),
context: ['backend' => $context->backendName, ...$logContext]
)->getAll()
);
return $context->cache->get($cacheKey);
}
/**
* Get Backend Libraries details.
* @throws RuntimeException
*/
protected function getBackendLibraries(Context $context, array $opts = []): array
{
$opts = ag_set($opts, Options::RAW_RESPONSE, true);
$response = Container::get(GetLibrariesList::class)(context: $context, opts: $opts);
if (!$response->isSuccessful()) {
throw new RuntimeException(message: $response->error->format(), previous: $response->error->previous);
}
$arr = [];
foreach ($response->response as $item) {
$arr[(int)$item['id']] = $item['raw'];
}
return $arr;
}
}