Made it possible to request show/movie fanart/poster via API

This commit is contained in:
ArabCoders
2025-02-24 19:38:04 +03:00
parent ff07c73f2e
commit f8a3b5cdc7
9 changed files with 318 additions and 20 deletions

View File

@@ -168,12 +168,12 @@ $exitCode = $profiler->process(function () use ($request) {
$out(
r(
text: "HTTP: Exception '{kind}' was thrown unhandled during HTTP boot context. Error '{message} @ {file}:{line}'.",
text: "HTTP: Exception '{kind}' was thrown unhandled during HTTP boot context. {message} at {file}:{line}.",
context: [
'kind' => $e::class,
'line' => $e->getLine(),
'message' => $e->getMessage(),
'file' => after($e->getFile(), ROOT_PATH),
'file' => $e->getFile(),
]
)
);

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\API\History;
use App\API\Player\Subtitle;
use App\Libs\APIResponse;
use App\Libs\Attributes\DI\Inject;
use App\Libs\Attributes\Route\Delete;
use App\Libs\Attributes\Route\Get;
@@ -12,6 +13,7 @@ use App\Libs\Attributes\Route\Route;
use App\Libs\Container;
use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Guid;
@@ -622,4 +624,62 @@ final class Index
return $this->read($request, $id);
}
#[Get(self::URL . '/{id:\d+}/images/{type:poster|background}[/]', name: 'history.item.images')]
public function fanart(iRequest $request, string $id, string $type): iResponse
{
if ($request->hasHeader('if-modified-since')) {
return api_response(Status::NOT_MODIFIED, headers: ['Cache-Control' => 'public, max-age=25920000']);
}
try {
$userContext = $this->getUserContext(request: $request, mapper: $this->mapper, logger: $this->logger);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}
$entity = Container::get(iState::class)::fromArray([iState::COLUMN_ID => $id]);
if (null === ($item = $userContext->db->get($entity))) {
return api_error('Not found.', Status::NOT_FOUND);
}
if (null === ($rId = ag($item->getMetadata($item->via), $item->isMovie() ? 'id' : 'show', null))) {
return api_error('Remote item id not found.', Status::NOT_FOUND);
}
try {
$client = $this->getClient(name: $item->via, userContext: $userContext);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}
$images = $client->getImagesUrl($rId);
if (false === array_key_exists($type, $images)) {
return api_error('Invalid image type.', Status::BAD_REQUEST);
}
$apiRequest = $client->proxy(Method::GET, $images[$type]);
if (false === $apiRequest->isSuccessful()) {
$this->logger->log($apiRequest->error->level(), $apiRequest->error->message, $apiRequest->error->context);
return api_error('Failed to fetch image.', Status::BAD_REQUEST);
}
$response = $apiRequest->response;
assert($response instanceof APIResponse);
if (Status::OK !== $response->status) {
return api_error(r("Failed to fetch image."), $response->status);
}
return api_response($response->status, body: $response->stream, headers: [
'Pragma' => 'public',
'Cache-Control' => sprintf('public, max-age=%s', time() + 31536000),
'Last-Modified' => sprintf('%s GMT', gmdate('D, d M Y H:i:s', time())),
'Expires' => sprintf('%s GMT', gmdate('D, d M Y H:i:s', time() + 31536000)),
'Content-Type' => ag($response->headers, 'content-type', 'image/jpeg'),
'X-Via' => r('{user}@{backend}', ['user' => $userContext->name, 'backend' => $item->via]),
]);
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Backends\Common;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Exceptions\Backends\InvalidContextException;
use App\Libs\Exceptions\Backends\NotImplementedException;
use App\Libs\Exceptions\Backends\UnexpectedVersionException;
@@ -173,6 +174,28 @@ interface ClientInterface
*/
public function getMetadata(string|int $id, array $opts = []): array;
/**
* Get backend item specific images url.
*
* @param string|int $id item id.
* @param array $opts options.
*
* @return array<array-key,iUri> empty array if not found.
*/
public function getImagesUrl(string|int $id, array $opts = []): array;
/**
* Proxy request to backend.
*
* @param Method $method request method.
* @param iUri $uri request uri.
* @param array|iStream $body request body. Optional.
* @param array $opts options.
*
* @return Response
*/
public function proxy(Method $method, iUri $uri, array|iStream $body = [], array $opts = []): Response;
/**
* Get entire library content.
*

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Backends\Emby\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Response;
class getImagesUrl
{
use CommonTrait;
protected string $action = 'emby.getImagesUrl';
/**
* Get Backend images url.
*
* @param Context $context backend context.
* @param string|int $id item id.
* @param array $opts optional options.
*
* @return Response
*/
public function __invoke(Context $context, string|int $id, array $opts = []): Response
{
return $this->tryResponse(
context: $context,
fn: fn() => new Response(
status: true,
response: [
'poster' => $context->backendUrl->withPath("/emby/Items/{$id}/Images/Primary/"),
'background' => $context->backendUrl->withPath("/emby/Items/{$id}/Images/Backdrop/"),
]
),
action: $this->action
);
}
}

View File

@@ -13,6 +13,7 @@ use App\Backends\Emby\Action\Backup;
use App\Backends\Emby\Action\Export;
use App\Backends\Emby\Action\GenerateAccessToken;
use App\Backends\Emby\Action\GetIdentifier;
use App\Backends\Emby\Action\getImagesUrl;
use App\Backends\Emby\Action\GetInfo;
use App\Backends\Emby\Action\GetLibrariesList;
use App\Backends\Emby\Action\GetLibrary;
@@ -25,6 +26,7 @@ use App\Backends\Emby\Action\Import;
use App\Backends\Emby\Action\InspectRequest;
use App\Backends\Emby\Action\ParseWebhook;
use App\Backends\Emby\Action\Progress;
use App\Backends\Emby\Action\Proxy;
use App\Backends\Emby\Action\Push;
use App\Backends\Emby\Action\SearchId;
use App\Backends\Emby\Action\SearchQuery;
@@ -34,6 +36,7 @@ use App\Backends\Jellyfin\JellyfinClient;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Exceptions\HttpException;
use App\Libs\Mappers\Import\ReadOnlyMapper;
@@ -44,8 +47,8 @@ use App\Libs\Uri;
use App\Libs\UserContext;
use DateTimeInterface as iDate;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\StreamInterface as iStream;
use Psr\Http\Message\UriInterface as iUri;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
@@ -252,7 +255,7 @@ class EmbyClient implements iClient
/**
* @inheritdoc
*/
public function backup(iImport $mapper, StreamInterface|null $writer = null, array $opts = []): array
public function backup(iImport $mapper, iStream|null $writer = null, array $opts = []): array
{
$response = Container::get(Backup::class)(
context: $this->context,
@@ -395,12 +398,7 @@ class EmbyClient implements iClient
*/
public function getMetadata(string|int $id, array $opts = []): array
{
$response = Container::get(GetMetaData::class)(
context: $this->context,
id: $id,
opts: $opts
);
$response = Container::get(GetMetaData::class)(context: $this->context, id: $id, opts: $opts);
if (false === $response->isSuccessful()) {
$this->throwError($response);
}
@@ -408,6 +406,33 @@ class EmbyClient implements iClient
return $response->response;
}
/**
* @inheritdoc
*/
public function getImagesUrl(string|int $id, array $opts = []): array
{
$response = Container::get(getImagesUrl::class)(context: $this->context, id: $id, opts: $opts);
if (false === $response->isSuccessful()) {
$this->throwError($response);
}
return $response->response;
}
/**
* @inheritdoc
*/
public function proxy(Method $method, iUri $uri, array|iStream $body = [], array $opts = []): Response
{
return Container::get(Proxy::class)(
context: $this->context,
method: $method,
uri: $uri,
body: $body,
opts: $opts
);
}
/**
* @inheritdoc
*/
@@ -540,7 +565,7 @@ class EmbyClient implements iClient
/**
* @inheritdoc
*/
public function getWebUrl(string $type, int|string $id): UriInterface
public function getWebUrl(string $type, int|string $id): iUri
{
$response = Container::get(GetWebUrl::class)($this->context, $type, $id);

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Backends\Jellyfin\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Response;
class getImagesUrl
{
use CommonTrait;
protected string $action = 'jellyfin.getImagesUrl';
/**
* Get Backend images url.
*
* @param Context $context backend context.
* @param string|int $id item id.
* @param array $opts optional options.
*
* @return Response
*/
public function __invoke(Context $context, string|int $id, array $opts = []): Response
{
return $this->tryResponse(
context: $context,
fn: fn() => new Response(
status: true,
response: [
'poster' => $context->backendUrl->withPath("/Items/{$id}/Images/Primary/"),
'background' => $context->backendUrl->withPath("/Items/{$id}/Images/Backdrop/"),
]
),
action: $this->action
);
}
}

View File

@@ -15,6 +15,7 @@ use App\Backends\Jellyfin\Action\Backup;
use App\Backends\Jellyfin\Action\Export;
use App\Backends\Jellyfin\Action\GenerateAccessToken;
use App\Backends\Jellyfin\Action\GetIdentifier;
use App\Backends\Jellyfin\Action\getImagesUrl;
use App\Backends\Jellyfin\Action\GetInfo;
use App\Backends\Jellyfin\Action\GetLibrariesList;
use App\Backends\Jellyfin\Action\GetLibrary;
@@ -27,6 +28,7 @@ use App\Backends\Jellyfin\Action\Import;
use App\Backends\Jellyfin\Action\InspectRequest;
use App\Backends\Jellyfin\Action\ParseWebhook;
use App\Backends\Jellyfin\Action\Progress;
use App\Backends\Jellyfin\Action\Proxy;
use App\Backends\Jellyfin\Action\Push;
use App\Backends\Jellyfin\Action\SearchId;
use App\Backends\Jellyfin\Action\SearchQuery;
@@ -35,6 +37,7 @@ use App\Backends\Jellyfin\Action\UpdateState;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method;
use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Exceptions\Backends\UnexpectedVersionException;
use App\Libs\Exceptions\HttpException;
@@ -47,7 +50,7 @@ use App\Libs\UserContext;
use DateTimeInterface as iDate;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Http\Message\StreamInterface as iStream;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\UriInterface as iUri;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
@@ -439,12 +442,7 @@ class JellyfinClient implements iClient
*/
public function getMetadata(string|int $id, array $opts = []): array
{
$response = Container::get(GetMetaData::class)(
context: $this->context,
id: $id,
opts: $opts
);
$response = Container::get(GetMetaData::class)(context: $this->context, id: $id, opts: $opts);
if (false === $response->isSuccessful()) {
$this->throwError($response);
}
@@ -452,6 +450,33 @@ class JellyfinClient implements iClient
return $response->response;
}
/**
* @inheritdoc
*/
public function getImagesUrl(string|int $id, array $opts = []): array
{
$response = Container::get(getImagesUrl::class)(context: $this->context, id: $id, opts: $opts);
if (false === $response->isSuccessful()) {
$this->throwError($response);
}
return $response->response;
}
/**
* @inheritdoc
*/
public function proxy(Method $method, iUri $uri, array|iStream $body = [], array $opts = []): Response
{
return Container::get(Proxy::class)(
context: $this->context,
method: $method,
uri: $uri,
body: $body,
opts: $opts
);
}
/**
* @inheritdoc
*/
@@ -584,7 +609,7 @@ class JellyfinClient implements iClient
/**
* @inheritdoc
*/
public function getWebUrl(string $type, int|string $id): UriInterface
public function getWebUrl(string $type, int|string $id): iUri
{
$response = Container::get(GetWebUrl::class)($this->context, $type, $id);

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Backends\Plex\Action;
use App\Backends\Common\CommonTrait;
use App\Backends\Common\Context;
use App\Backends\Common\Response;
use App\Backends\Plex\PlexActionTrait;
use App\Libs\Options;
use DateInterval;
class getImagesUrl
{
use CommonTrait;
use PlexActionTrait;
protected string $action = 'jellyfin.getImagesUrl';
/**
* Get Backend images url.
*
* @param Context $context backend context.
* @param string|int $id item id.
* @param array $opts optional options.
*
* @return Response
*/
public function __invoke(Context $context, string|int $id, array $opts = []): Response
{
// -- Plex love's to play hard to get, so special care needed and API request is needed to fetch
// -- the images url.
$response = $this->getItemInfo($context, $id, opts: [Options::CACHE_TTL => new DateInterval('PT60M')]);
if (false === $response->isSuccessful()) {
return $response;
}
$data = ag($response->response, 'MediaContainer.Metadata.0', []);
$poster = ag($data, 'thumb', null);
$background = ag($data, 'art', null);
return $this->tryResponse(
context: $context,
fn: fn() => new Response(
status: true,
response: [
'poster' => $poster ? $context->backendUrl->withPath($poster) : null,
'background' => $background ? $context->backendUrl->withPath($background) : null,
]
),
action: $this->action
);
}
}

View File

@@ -12,6 +12,7 @@ use App\Backends\Common\Response;
use App\Backends\Plex\Action\Backup;
use App\Backends\Plex\Action\Export;
use App\Backends\Plex\Action\GetIdentifier;
use App\Backends\Plex\Action\getImagesUrl;
use App\Backends\Plex\Action\GetInfo;
use App\Backends\Plex\Action\GetLibrariesList;
use App\Backends\Plex\Action\GetLibrary;
@@ -25,6 +26,7 @@ use App\Backends\Plex\Action\Import;
use App\Backends\Plex\Action\InspectRequest;
use App\Backends\Plex\Action\ParseWebhook;
use App\Backends\Plex\Action\Progress;
use App\Backends\Plex\Action\Proxy;
use App\Backends\Plex\Action\Push;
use App\Backends\Plex\Action\SearchId;
use App\Backends\Plex\Action\SearchQuery;
@@ -421,6 +423,34 @@ class PlexClient implements iClient
return $response->response;
}
/**
* @inheritdoc
*/
public function getImagesUrl(string|int $id, array $opts = []): array
{
$response = Container::get(getImagesUrl::class)(context: $this->context, id: $id, opts: $opts);
if (false === $response->isSuccessful()) {
$this->throwError($response);
}
return $response->response;
}
/**
* @inheritdoc
*/
public function proxy(Method $method, iUri $uri, array|iStream $body = [], array $opts = []): Response
{
return Container::get(Proxy::class)(
context: $this->context,
method: $method,
uri: $uri,
body: $body,
opts: $opts
);
}
/**
* @inheritdoc
*/