Migrated Jellyfin/emby import/export to separate actions.

This commit is contained in:
Abdulmhsen B. A. A
2022-06-19 17:00:13 +03:00
parent 750f19014d
commit 4ecc664238
8 changed files with 931 additions and 968 deletions

View File

@@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
namespace App\Backends\Jellyfin\Action;
use App\Backends\Common\Context;
use App\Backends\Common\GuidInterface as iGuid;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Data;
use App\Libs\Entity\StateInterface as iFace;
use App\Libs\Mappers\ImportInterface;
use App\Libs\Options;
use App\Libs\QueueRequests;
use DateTimeInterface;
use Symfony\Contracts\HttpClient\ResponseInterface as iResponse;
use Throwable;
class Export extends Import
{
/**
* @param Context $context
* @param iGuid $guid
* @param ImportInterface $mapper
* @param DateTimeInterface|null $after
* @param array $opts
*
* @return Response
*/
public function __invoke(
Context $context,
iGuid $guid,
ImportInterface $mapper,
DateTimeInterface|null $after = null,
array $opts = []
): Response {
return $this->tryResponse($context, fn() => $this->getLibraries(
context: $context,
handle: fn(array $logContext = []) => fn(iResponse $response) => $this->handle(
context: $context,
response: $response,
callback: fn(array $item, array $logContext = []) => $this->export(
context: $context,
guid: $guid,
queue: $opts['queue'],
mapper: $mapper,
item: $item,
logContext: $logContext,
opts: ['after' => $after],
),
logContext: $logContext
),
error: fn(array $logContext = []) => fn(Throwable $e) => $this->logger->error(
'Unhandled Exception was thrown during [%(backend)] library [%(library.title)] request.',
[
'backend' => $context->backendName,
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]
),
));
}
private function export(
Context $context,
iGuid $guid,
QueueRequests $queue,
ImportInterface $mapper,
array $item,
array $logContext = [],
array $opts = [],
): void {
if (JFC::TYPE_SHOW === ($type = ag($item, 'Type'))) {
$this->processShow(context: $context, guid: $guid, item: $item, logContext: $logContext);
return;
}
try {
$after = ag($opts, 'after');
$type = JFC::TYPE_MAPPER[$type];
Data::increment($context->backendName, $type . '_total');
$logContext['item'] = [
'id' => ag($item, 'Id'),
'title' => match ($type) {
iFace::TYPE_MOVIE => sprintf(
'%s (%d)',
ag($item, ['Name', 'OriginalTitle'], '??'),
ag($item, 'ProductionYear', 0000)
),
iFace::TYPE_EPISODE => trim(
sprintf(
'%s - (%sx%s)',
ag($item, 'SeriesName', '??'),
str_pad((string)ag($item, 'ParentIndexNumber', 0), 2, '0', STR_PAD_LEFT),
str_pad((string)ag($item, 'IndexNumber', 0), 3, '0', STR_PAD_LEFT),
)
),
},
'type' => $type,
];
if ($context->trace) {
$this->logger->debug('Processing [%(backend)] %(item.type) [%(item.title)] payload.', [
'backend' => $context->backendName,
...$logContext,
'response' => [
'body' => $item
],
]);
}
$isPlayed = true === (bool)ag($item, 'UserData.Played');
$dateKey = true === $isPlayed ? 'UserData.LastPlayedDate' : 'DateCreated';
if (null === ag($item, $dateKey)) {
$this->logger->debug('Ignoring [%(backend)] %(item.type) [%(item.title)]. No Date is set on object.', [
'backend' => $context->backendName,
'date_key' => $dateKey,
...$logContext,
'response' => [
'body' => $item,
],
]);
Data::increment($context->backendName, $type . '_ignored_no_date_is_set');
return;
}
$rItem = $this->createEntity(
context: $context,
guid: $guid,
item: $item,
opts: $opts + ['library' => ag($logContext, 'library.id')]
);
if (!$rItem->hasGuids() && !$rItem->hasRelativeGuid()) {
$providerIds = (array)ag($item, 'ProviderIds', []);
$message = 'Ignoring [%(backend)] [%(item.title)]. No valid/supported external ids.';
if (empty($providerIds)) {
$message .= ' Most likely unmatched %(item.type).';
}
$this->logger->info($message, [
'backend' => $context->backendName,
...$logContext,
'context' => [
'guids' => !empty($providerIds) ? $providerIds : 'None'
],
]);
Data::increment($context->backendName, $type . '_ignored_no_supported_guid');
return;
}
if (false === ag($context->options, Options::IGNORE_DATE, false)) {
if (true === ($after instanceof DateTimeInterface) && $rItem->updated >= $after->getTimestamp()) {
$this->logger->debug(
'Ignoring [%(backend)] [%(item.title)]. Backend date is equal or newer than last sync date.',
[
'backend' => $context->backendName,
...$logContext,
'comparison' => [
'lastSync' => makeDate($after),
'backend' => makeDate($rItem->updated),
],
]
);
Data::increment($context->backendName, $type . '_ignored_date_is_equal_or_higher');
return;
}
}
if (null === ($entity = $mapper->get($rItem))) {
$this->logger->warning('Ignoring [%(backend)] [%(item.title)]. %(item.type) Is not imported yet.', [
'backend' => $context->backendName,
...$logContext,
]);
Data::increment($context->backendName, $type . '_ignored_not_found_in_db');
return;
}
if ($rItem->watched === $entity->watched) {
if (true === (bool)ag($context->options, Options::DEBUG_TRACE)) {
$this->logger->debug(
'Ignoring [%(backend)] [%(item.title)]. %(item.type) play state is identical.',
[
'backend' => $context->backendName,
...$logContext,
'comparison' => [
'backend' => $entity->isWatched() ? 'Played' : 'Unplayed',
'remote' => $rItem->isWatched() ? 'Played' : 'Unplayed',
],
]
);
}
Data::increment($context->backendName, $type . '_ignored_state_unchanged');
return;
}
if ($rItem->updated >= $entity->updated && false === ag($context->options, Options::IGNORE_DATE, false)) {
$this->logger->debug(
'Ignoring [%(backend)] [%(item.title)]. Backend date is equal or newer than storage date.',
[
'backend' => $context->backendName,
...$logContext,
'comparison' => [
'storage' => makeDate($entity->updated),
'backend' => makeDate($rItem->updated),
],
]
);
Data::increment($context->backendName, $type . '_ignored_date_is_newer');
return;
}
$url = $context->backendUrl->withPath(
sprintf('/Users/%s/PlayedItems/%s', $context->backendUser, ag($item, 'Id'))
);
$logContext['item']['url'] = $url;
$this->logger->debug(
'Queuing Request to change [%(backend)] [%(item.title)] play state to [%(play_state)].',
[
'backend' => $context->backendName,
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
...$logContext,
]
);
if (false === (bool)ag($context->options, Options::DRY_RUN, false)) {
$queue->add(
$this->http->request(
$entity->isWatched() ? 'POST' : 'DELETE',
(string)$url,
$context->backendHeaders + [
'user_data' => [
'context' => $logContext + [
'backend' => $context->backendName,
'play_state' => $entity->isWatched() ? 'Played' : 'Unplayed',
],
],
]
)
);
}
} catch (Throwable $e) {
$this->logger->error(
'Unhandled exception was thrown during handling of [%(backend)] [%(library.title)] [%(item.title)] export.',
[
'backend' => $context->backendName,
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]
);
}
}
}

View File

@@ -0,0 +1,597 @@
<?php
declare(strict_types=1);
namespace App\Backends\Jellyfin\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\GuidInterface as iGuid;
use App\Backends\Common\Response;
use App\Backends\Jellyfin\JellyfinActionTrait;
use App\Backends\Jellyfin\JellyfinClient as JFC;
use App\Libs\Config;
use App\Libs\Data;
use App\Libs\Entity\StateInterface as iFace;
use App\Libs\Guid;
use App\Libs\Mappers\ImportInterface;
use App\Libs\Options;
use Closure;
use DateTimeInterface;
use JsonException;
use JsonMachine\Items;
use JsonMachine\JsonDecoder\DecodingError;
use JsonMachine\JsonDecoder\ErrorWrappingDecoder;
use JsonMachine\JsonDecoder\ExtJsonDecoder;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface as iResponse;
use Throwable;
class Import
{
use CommonTrait, JellyfinActionTrait;
public function __construct(protected HttpClientInterface $http, protected LoggerInterface $logger)
{
}
/**
* @param Context $context
* @param iGuid $guid
* @param ImportInterface $mapper
* @param DateTimeInterface|null $after
* @param array $opts
*
* @return Response
*/
public function __invoke(
Context $context,
iGuid $guid,
ImportInterface $mapper,
DateTimeInterface|null $after = null,
array $opts = []
): Response {
return $this->tryResponse($context, fn() => $this->getLibraries(
context: $context,
handle: fn(array $logContext = []) => fn(iResponse $response) => $this->handle(
context: $context,
response: $response,
callback: fn(array $item, array $logContext = []) => $this->import(
context: $context,
guid: $guid,
mapper: $mapper,
item: $item,
logContext: $logContext,
opts: ['after' => $after],
),
logContext: $logContext
),
error: fn(array $logContext = []) => fn(Throwable $e) => $this->logger->error(
'Unhandled Exception was thrown during [%(backend)] library [%(library.title)] request.',
[
'backend' => $context->backendName,
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]
),
));
}
protected function getLibraries(Context $context, Closure $handle, Closure $error): array
{
try {
$url = $context->backendUrl->withPath(sprintf('/Users/%s/items/', $context->backendUser));
$this->logger->debug('Requesting [%(backend)] libraries.', [
'backend' => $context->backendName,
'url' => $url
]);
$response = $this->http->request('GET', (string)$url, $context->backendHeaders);
if (200 !== $response->getStatusCode()) {
$this->logger->error(
'Request for [%(backend)] libraries returned with unexpected [%(status_code)] status code.',
[
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
]
);
Data::add($context->backendName, 'no_import_update', true);
return [];
}
$json = json_decode(
json: $response->getContent(),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
$listDirs = ag($json, 'Items', []);
if (empty($listDirs)) {
$this->logger->warning('Request for [%(backend)] libraries returned with empty list.', [
'backend' => $context->backendName,
'context' => [
'body' => $json,
]
]);
Data::add($context->backendName, 'no_import_update', true);
return [];
}
} catch (ExceptionInterface $e) {
$this->logger->error('Request for [%(backend)] libraries has failed.', [
'backend' => $context->backendName,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]);
Data::add($context->backendName, 'no_import_update', true);
return [];
} catch (JsonException $e) {
$this->logger->error('Request for [%(backend)] libraries returned with invalid body.', [
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
],
]);
Data::add($context->backendName, 'no_import_update', true);
return [];
}
if (null !== ($ignoreIds = ag($context->options, 'ignore', null))) {
$ignoreIds = array_map(fn($v) => trim($v), explode(',', (string)$ignoreIds));
}
$requests = [];
$ignored = $unsupported = 0;
// -- Episodes Parent external ids.
foreach ($listDirs as $section) {
$logContext = [
'library' => [
'id' => (string)ag($section, 'Id'),
'title' => ag($section, 'Name', '??'),
'type' => ag($section, 'CollectionType', 'unknown'),
],
];
if (JFC::COLLECTION_TYPE_SHOWS !== ag($logContext, 'library.type')) {
continue;
}
if (true === in_array(ag($logContext, 'library.id'), $ignoreIds ?? [])) {
continue;
}
$url = $context->backendUrl->withPath(sprintf('/Users/%s/items/', $context->backendUser))->withQuery(
http_build_query(
[
'parentId' => ag($logContext, 'library.id'),
'recursive' => 'false',
'enableUserData' => 'false',
'enableImages' => 'false',
'fields' => implode(',', JFC::EXTRA_FIELDS),
'excludeLocationTypes' => 'Virtual',
]
)
);
$logContext['library']['url'] = (string)$url;
$this->logger->debug('Requesting [%(backend)] [%(library.title)] series external ids.', [
'backend' => $context->backendName,
...$logContext,
]);
try {
$requests[] = $this->http->request(
'GET',
(string)$url,
array_replace_recursive($context->backendHeaders, [
'user_data' => [
'ok' => $handle($logContext),
'error' => $error($logContext),
]
])
);
} catch (ExceptionInterface $e) {
$this->logger->error(
'Request for [%(backend)] [%(library.title)] series external ids has failed.',
[
'backend' => $context->backendName,
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]
);
continue;
}
}
foreach ($listDirs as $section) {
$logContext = [
'library' => [
'id' => (string)ag($section, 'Id'),
'title' => ag($section, 'Name', '??'),
'type' => ag($section, 'CollectionType', 'unknown'),
],
];
if (true === in_array(ag($logContext, 'library.id'), $ignoreIds ?? [])) {
$ignored++;
$this->logger->info('Ignoring [%(backend)] [%(library.title)]. Requested by user config.', [
'backend' => $context->backendName,
...$logContext,
]);
continue;
}
if (!in_array(ag($logContext, 'library.type'), [JFC::COLLECTION_TYPE_SHOWS, JFC::COLLECTION_TYPE_MOVIES])) {
$unsupported++;
$this->logger->info(
'Ignoring [%(backend)] [%(library.title)]. Library type [%(library.type)] is not supported.',
[
'backend' => $context->backendName,
...$logContext,
]
);
continue;
}
$url = $context->backendUrl->withPath(sprintf('/Users/%s/items/', $context->backendUser))->withQuery(
http_build_query(
[
'parentId' => ag($logContext, 'library.id'),
'recursive' => 'true',
'enableUserData' => 'true',
'enableImages' => 'false',
'excludeLocationTypes' => 'Virtual',
'fields' => implode(',', JFC::EXTRA_FIELDS),
'includeItemTypes' => implode(',', [JFC::TYPE_MOVIE, JFC::TYPE_EPISODE]),
]
)
);
$logContext['library']['url'] = (string)$url;
$this->logger->debug('Requesting [%(backend)] [%(library.title)] content list.', [
'backend' => $context->backendName,
...$logContext,
]);
try {
$requests[] = $this->http->request(
'GET',
(string)$url,
$context->backendHeaders + [
'user_data' => [
'ok' => $handle($logContext),
'error' => $error($logContext),
]
]
);
} catch (ExceptionInterface $e) {
$this->logger->error('Requesting for [%(backend)] [%(library.title)] content list has failed.', [
'backend' => $context->backendName,
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]);
continue;
}
}
if (0 === count($requests)) {
$this->logger->warning('No requests for [%(backend)] libraries were queued.', [
'backend' => $context->backendName,
'context' => [
'total' => count($listDirs),
'ignored' => $ignored,
'unsupported' => $unsupported,
],
]);
Data::add($context->backendName, 'no_import_update', true);
return [];
}
return $requests;
}
/**
* @throws TransportExceptionInterface
*/
protected function handle(Context $context, iResponse $response, Closure $callback, array $logContext = []): void
{
if (200 !== $response->getStatusCode()) {
$this->logger->error(
'Request for [%(backend)] [%(library.title)] content returned with unexpected [%(status_code)] status code.',
[
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
...$logContext,
]
);
return;
}
$start = makeDate();
$this->logger->info('Parsing [%(backend)] library [%(library.title)] response.', [
'backend' => $context->backendName,
...$logContext,
'time' => [
'start' => $start,
],
]);
try {
$it = Items::fromIterable(
iterable: httpClientChunks($this->http->stream($response)),
options: [
'pointer' => '/Items',
'decoder' => new ErrorWrappingDecoder(
innerDecoder: new ExtJsonDecoder(
assoc: true,
options: JSON_INVALID_UTF8_IGNORE
)
)
]
);
foreach ($it as $entity) {
if ($entity instanceof DecodingError) {
$this->logger->warning(
'Failed to decode one item of [%(backend)] [%(library.title)] content.',
[
'backend' => $context->backendName,
...$logContext,
'error' => [
'message' => $entity->getErrorMessage(),
'body' => $entity->getMalformedJson(),
],
]
);
continue;
}
$callback(item: $entity, logContext: $logContext);
}
} catch (Throwable $e) {
$this->logger->error(
'Unhandled exception was thrown during parsing of [%(backend)] library [%(library.title)] response.',
[
'backend' => $context->backendName,
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]
);
}
$end = makeDate();
$this->logger->info('Parsing [%(backend)] library [%(library.title)] response is complete.', [
'backend' => $context->backendName,
...$logContext,
'time' => [
'start' => $start,
'end' => $end,
'duration' => number_format($end->getTimestamp() - $start->getTimestamp()),
],
]);
}
protected function processShow(Context $context, iGuid $guid, array $item, array $logContext = []): void
{
$logContext['item'] = [
'id' => ag($item, 'Id'),
'title' => sprintf(
'%s (%s)',
ag($item, ['Name', 'OriginalTitle'], '??'),
ag($item, 'ProductionYear', '0000')
),
'year' => ag($item, 'ProductionYear', null),
'type' => ag($item, 'Type'),
];
if ($context->trace) {
$this->logger->debug('Processing [%(backend)] %(item.type) [%(item.title) (%(item.year))] payload.', [
'backend' => $context->backendName,
...$logContext,
'trace' => $item,
]);
}
$providersId = (array)ag($item, 'ProviderIds', []);
if (false === $guid->has($providersId)) {
$message = 'Ignoring [%(backend)] [%(item.title)]. %(item.type) has no valid/supported external ids.';
if (empty($providersId)) {
$message .= ' Most likely unmatched %(item.type).';
}
$this->logger->info($message, [
'backend' => $context->backendName,
...$logContext,
'data' => [
'guids' => !empty($providersId) ? $providersId : 'None'
],
]);
return;
}
$context->cache->set(
JFC::TYPE_SHOW . '.' . ag($logContext, 'item.id'),
Guid::fromArray($guid->get($providersId), context: [
'backend' => $context->backendName,
...$logContext,
])->getAll()
);
}
private function import(
Context $context,
iGuid $guid,
ImportInterface $mapper,
array $item,
array $logContext = [],
array $opts = []
): void {
if (JFC::TYPE_SHOW === ($type = ag($item, 'Type'))) {
$this->processShow(context: $context, guid: $guid, item: $item, logContext: $logContext);
return;
}
try {
Data::increment($context->backendName, $type . '_total');
$logContext['item'] = [
'id' => ag($item, 'Id'),
'title' => match ($type) {
JFC::TYPE_MOVIE => sprintf(
'%s (%d)',
ag($item, ['Name', 'OriginalTitle'], '??'),
ag($item, 'ProductionYear', 0000)
),
JFC::TYPE_EPISODE => trim(
sprintf(
'%s - (%sx%s)',
ag($item, 'SeriesName', '??'),
str_pad((string)ag($item, 'ParentIndexNumber', 0), 2, '0', STR_PAD_LEFT),
str_pad((string)ag($item, 'IndexNumber', 0), 3, '0', STR_PAD_LEFT),
)
),
default => throw new \InvalidArgumentException('Invalid Content type was given.')
},
'type' => ag($item, 'Type'),
];
if ($context->trace) {
$this->logger->debug('Processing [%(backend)] %(item.type) [%(item.title)] payload.', [
'backend' => $context->backendName,
...$logContext,
'response' => [
'body' => $item
],
]);
}
$isPlayed = true === (bool)ag($item, 'UserData.Played');
$dateKey = true === $isPlayed ? 'UserData.LastPlayedDate' : 'DateCreated';
if (null === ag($item, $dateKey)) {
$this->logger->debug('Ignoring [%(backend)] %(item.type) [%(item.title)]. No Date is set on object.', [
'backend' => $context->backendName,
'date_key' => $dateKey,
...$logContext,
'response' => [
'body' => $item,
],
]);
Data::increment($context->backendName, $type . '_ignored_no_date_is_set');
return;
}
$entity = $this->createEntity(
context: $context,
guid: $guid,
item: $item,
opts: $opts + [
'library' => ag($logContext, 'library.id'),
'override' => [
iFace::COLUMN_EXTRA => [
$context->backendName => [
iFace::COLUMN_EXTRA_EVENT => 'task.import',
iFace::COLUMN_EXTRA_DATE => makeDate('now'),
],
],
]
],
);
if (false === $entity->hasGuids() && false === $entity->hasRelativeGuid()) {
if (true === (bool)Config::get('debug.import')) {
$name = sprintf(
Config::get('tmpDir') . '/debug/%s.%s.json',
$context->backendName,
ag($item, 'Id')
);
if (!file_exists($name)) {
file_put_contents(
$name,
json_encode(
$item,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
)
);
}
}
$providerIds = (array)ag($item, 'ProviderIds', []);
$message = 'Ignoring [%(backend)] [%(item.title)]. No valid/supported external ids.';
if (empty($providerIds)) {
$message .= ' Most likely unmatched %(item.type).';
}
$this->logger->info($message, [
'backend' => $context->backendName,
...$logContext,
'context' => [
'guids' => !empty($providerIds) ? $providerIds : 'None'
],
]);
Data::increment($context->backendName, $type . '_ignored_no_supported_guid');
return;
}
$mapper->add(entity: $entity, opts: [
'after' => ag($opts, 'after'),
Options::IMPORT_METADATA_ONLY => true === (bool)ag($context->options, Options::IMPORT_METADATA_ONLY),
]);
} catch (Throwable $e) {
$this->logger->error(
'Unhandled exception was thrown during handling of [%(backend)] [%(library.title)] [%(item.title)] import.',
[
'backend' => $context->backendName,
...$logContext,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'kind' => get_class($e),
'message' => $e->getMessage(),
],
]
);
}
}
}

View File

@@ -9,7 +9,6 @@ use App\Backends\Common\GuidInterface as iGuid;
use App\Backends\Jellyfin\Action\GetLibrariesList;
use App\Backends\Jellyfin\Action\GetMetaData;
use App\Libs\Container;
use App\Libs\Entity\StateEntity;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Guid;
use App\Libs\Options;
@@ -17,14 +16,8 @@ use RuntimeException;
trait JellyfinActionTrait
{
private array $typeMapper = [
JellyfinClient::TYPE_SHOW => iState::TYPE_SHOW,
JellyfinClient::TYPE_MOVIE => iState::TYPE_MOVIE,
JellyfinClient::TYPE_EPISODE => iState::TYPE_EPISODE,
];
/**
* Create {@see StateEntity} Object based on given data.
* Create {@see iState} Object based on given data.
*
* @param Context $context
* @param iGuid $guid
@@ -48,7 +41,7 @@ trait JellyfinActionTrait
throw new RuntimeException('No date was set on object.');
}
$type = $this->typeMapper[ag($item, 'Type')] ?? ag($item, 'Type');
$type = JellyfinClient::TYPE_MAPPER[ag($item, 'Type')] ?? ag($item, 'Type');
$guids = $guid->get(ag($item, 'ProviderIds', []), context: [
'item' => [

View File

@@ -8,6 +8,7 @@ use App\Backends\Common\Context;
use App\Backends\Jellyfin\Action\GetIdentifier;
use App\Backends\Jellyfin\Action\GetMetaData;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use RuntimeException;
@@ -33,6 +34,12 @@ class JellyfinClient
'Path',
];
public const TYPE_MAPPER = [
JellyfinClient::TYPE_SHOW => iState::TYPE_SHOW,
JellyfinClient::TYPE_MOVIE => iState::TYPE_MOVIE,
JellyfinClient::TYPE_EPISODE => iState::TYPE_EPISODE,
];
private Context|null $context = null;
public function __construct(

View File

@@ -13,7 +13,7 @@ use App\Libs\Mappers\ImportInterface;
use App\Libs\Options;
use App\Libs\QueueRequests;
use DateTimeInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseInterface as iResponse;
use Throwable;
final class Export extends Import
@@ -34,17 +34,15 @@ final class Export extends Import
DateTimeInterface|null $after = null,
array $opts = []
): Response {
$outerOpts = $opts;
return $this->tryResponse($context, fn() => $this->getLibraries(
context: $context,
handle: fn(array $logContext = []) => fn(ResponseInterface $response) => $this->handle(
handle: fn(array $logContext = []) => fn(iResponse $response) => $this->handle(
context: $context,
response: $response,
callback: fn(array $item, array $logContext = []) => $this->export(
context: $context,
guid: $guid,
queue: $outerOpts['queue'],
queue: $opts['queue'],
mapper: $mapper,
item: $item,
logContext: $logContext,

View File

@@ -10,26 +10,11 @@ use App\Libs\Container;
use App\Libs\Entity\StateInterface as iFace;
use App\Libs\HttpException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
class EmbyServer extends JellyfinServer
{
public const NAME = 'EmbyBackend';
public function setUp(
string $name,
UriInterface $url,
string|int|null $token = null,
string|int|null $userId = null,
string|int|null $uuid = null,
array $persist = [],
array $options = []
): ServerInterface {
$options['emby'] = true;
return parent::setUp($name, $url, $token, $userId, $uuid, $persist, $options);
}
public function parseWebhook(ServerRequestInterface $request): iFace
{
$response = Container::get(ParseWebhook::class)(context: $this->context, guid: $this->guid, request: $request);

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,6 @@ class PlexServer implements ServerInterface
array $options = []
): ServerInterface {
$cloned = clone $this;
$cloned->persist = $persist;
$cloned->context = new Context(
clientName: static::NAME,
backendName: $name,
@@ -287,9 +286,7 @@ class PlexServer implements ServerInterface
guid: $this->guid,
mapper: $mapper,
after: $after,
opts: [
'queue' => $queue
],
opts: ['queue' => $queue],
);
if ($response->hasError()) {