Made it possible to request show/movie fanart/poster via API
This commit is contained in:
@@ -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(),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
40
src/Backends/Emby/Action/getImagesUrl.php
Normal file
40
src/Backends/Emby/Action/getImagesUrl.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
40
src/Backends/Jellyfin/Action/getImagesUrl.php
Normal file
40
src/Backends/Jellyfin/Action/getImagesUrl.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
55
src/Backends/Plex/Action/getImagesUrl.php
Normal file
55
src/Backends/Plex/Action/getImagesUrl.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user