Skip to content

Commit 4514e77

Browse files
authored
[Assets] Add video thumbnail status endpoint for Studio polling (#1810)
1 parent 9789825 commit 4514e77

6 files changed

Lines changed: 301 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* This source file is available under the terms of the
6+
* Pimcore Open Core License (POCL)
7+
* Full copyright and license information is available in
8+
* LICENSE.md which is distributed with this source code.
9+
*
10+
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
11+
* @license Pimcore Open Core License (POCL)
12+
*/
13+
14+
namespace Pimcore\Bundle\StudioBackendBundle\Asset\Controller\Video;
15+
16+
use OpenApi\Attributes\Get;
17+
use OpenApi\Attributes\JsonContent;
18+
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Path\ThumbnailNameParameter;
19+
use Pimcore\Bundle\StudioBackendBundle\Asset\Schema\Type\Video\VideoThumbnailStatus;
20+
use Pimcore\Bundle\StudioBackendBundle\Asset\Service\AssetServiceInterface;
21+
use Pimcore\Bundle\StudioBackendBundle\Asset\Service\VideoServiceInterface;
22+
use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController;
23+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException;
24+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidElementTypeException;
25+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidThumbnailException;
26+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException;
27+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\UserNotFoundException;
28+
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter;
29+
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses;
30+
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\SuccessResponse;
31+
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags;
32+
use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface;
33+
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes;
34+
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions;
35+
use Symfony\Component\HttpFoundation\JsonResponse;
36+
use Symfony\Component\Routing\Attribute\Route;
37+
use Symfony\Component\Security\Http\Attribute\IsGranted;
38+
use Symfony\Component\Serializer\SerializerInterface;
39+
40+
/**
41+
* @internal
42+
*/
43+
final class ThumbnailStatusController extends AbstractApiController
44+
{
45+
public function __construct(
46+
SerializerInterface $serializer,
47+
private readonly AssetServiceInterface $assetService,
48+
private readonly SecurityServiceInterface $securityService,
49+
private readonly VideoServiceInterface $videoService,
50+
) {
51+
parent::__construct($serializer);
52+
}
53+
54+
/**
55+
* @throws ForbiddenException
56+
* @throws InvalidElementTypeException
57+
* @throws InvalidThumbnailException
58+
* @throws NotFoundException
59+
* @throws UserNotFoundException
60+
*/
61+
#[Route(
62+
'/assets/{id}/video/thumbnail/{thumbnailName}/status',
63+
name: 'pimcore_studio_api_video_thumbnail_status',
64+
methods: ['GET']
65+
)]
66+
#[IsGranted(UserPermissions::ASSETS->value)]
67+
#[Get(
68+
path: self::PREFIX . '/assets/{id}/video/thumbnail/{thumbnailName}/status',
69+
operationId: 'asset_video_thumbnail_status',
70+
description: 'asset_video_thumbnail_status_description',
71+
summary: 'asset_video_thumbnail_status_summary',
72+
tags: [Tags::Assets->name]
73+
)]
74+
#[IdParameter(type: 'video')]
75+
#[ThumbnailNameParameter]
76+
#[SuccessResponse(
77+
description: 'asset_video_thumbnail_status_success_response',
78+
content: new JsonContent(ref: VideoThumbnailStatus::class)
79+
)]
80+
#[DefaultResponses([
81+
HttpResponseCodes::BAD_REQUEST,
82+
HttpResponseCodes::FORBIDDEN,
83+
HttpResponseCodes::UNAUTHORIZED,
84+
HttpResponseCodes::NOT_FOUND,
85+
])]
86+
public function getVideoThumbnailStatus(int $id, string $thumbnailName): JsonResponse
87+
{
88+
$asset = $this->assetService->getAssetElement(
89+
$this->securityService->getCurrentUser(),
90+
$id
91+
);
92+
93+
return $this->jsonResponse(
94+
$this->videoService->getThumbnailStatus($asset, $thumbnailName)
95+
);
96+
}
97+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* This source file is available under the terms of the
6+
* Pimcore Open Core License (POCL)
7+
* Full copyright and license information is available in
8+
* LICENSE.md which is distributed with this source code.
9+
*
10+
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
11+
* @license Pimcore Open Core License (POCL)
12+
*/
13+
14+
namespace Pimcore\Bundle\StudioBackendBundle\Asset\Schema\Type\Video;
15+
16+
use OpenApi\Attributes\Property;
17+
use OpenApi\Attributes\Schema;
18+
use Pimcore\Bundle\StudioBackendBundle\Util\Schema\AdditionalAttributesInterface;
19+
use Pimcore\Bundle\StudioBackendBundle\Util\Trait\AdditionalAttributesTrait;
20+
21+
#[Schema(
22+
schema: 'VideoThumbnailStatus',
23+
title: 'Video Thumbnail Status',
24+
required: ['status'],
25+
type: 'object'
26+
)]
27+
final class VideoThumbnailStatus implements AdditionalAttributesInterface
28+
{
29+
use AdditionalAttributesTrait;
30+
31+
public const string STATUS_FINISHED = 'finished';
32+
33+
public const string STATUS_INPROGRESS = 'inprogress';
34+
35+
public const string STATUS_ERROR = 'error';
36+
37+
public const string STATUS_NOT_STARTED = 'not_started';
38+
39+
public function __construct(
40+
#[Property(
41+
description: 'Conversion status of the requested video thumbnail.',
42+
type: 'string',
43+
enum: [
44+
self::STATUS_FINISHED,
45+
self::STATUS_INPROGRESS,
46+
self::STATUS_ERROR,
47+
self::STATUS_NOT_STARTED,
48+
],
49+
example: self::STATUS_INPROGRESS,
50+
)]
51+
private readonly string $status,
52+
) {
53+
}
54+
55+
public function getStatus(): string
56+
{
57+
return $this->status;
58+
}
59+
}

src/Asset/Service/VideoService.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
namespace Pimcore\Bundle\StudioBackendBundle\Asset\Service;
1515

1616
use Pimcore\Bundle\StudioBackendBundle\Asset\Event\PreResponse\VideoTypeEvent;
17+
use Pimcore\Bundle\StudioBackendBundle\Asset\Schema\Type\Video\VideoThumbnailStatus;
1718
use Pimcore\Bundle\StudioBackendBundle\Asset\Schema\Type\Video\VideoType;
19+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidElementTypeException;
20+
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\ElementTypes;
21+
use Pimcore\Model\Asset;
22+
use Pimcore\Model\Asset\Video as VideoAsset;
1823
use Pimcore\Model\DataObject\ClassDefinition\Data\Video;
1924
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2025

@@ -25,6 +30,7 @@
2530
{
2631
public function __construct(
2732
private EventDispatcherInterface $eventDispatcher,
33+
private ThumbnailServiceInterface $thumbnailService,
2834
) {
2935
}
3036

@@ -40,4 +46,23 @@ public function getVideoTypes(): array
4046

4147
return $types;
4248
}
49+
50+
public function getThumbnailStatus(Asset $video, string $thumbnailName): VideoThumbnailStatus
51+
{
52+
if (!$video instanceof VideoAsset) {
53+
throw new InvalidElementTypeException($video->getType(), ElementTypes::TYPE_ASSET);
54+
}
55+
56+
$configuration = $this->thumbnailService->getVideoThumbnailConfig($thumbnailName);
57+
58+
// Read the status from the custom setting directly: Asset\Video::getThumbnail() would
59+
// start a conversion as a side effect and returns null for errored conversions,
60+
// which would make the error status unreachable for polling clients.
61+
$customSetting = $video->getCustomSetting('thumbnails');
62+
$status = is_array($customSetting)
63+
? ($customSetting[$configuration->getName()]['status'] ?? VideoThumbnailStatus::STATUS_NOT_STARTED)
64+
: VideoThumbnailStatus::STATUS_NOT_STARTED;
65+
66+
return new VideoThumbnailStatus((string) $status);
67+
}
4368
}

src/Asset/Service/VideoServiceInterface.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313

1414
namespace Pimcore\Bundle\StudioBackendBundle\Asset\Service;
1515

16+
use Pimcore\Bundle\StudioBackendBundle\Asset\Schema\Type\Video\VideoThumbnailStatus;
1617
use Pimcore\Bundle\StudioBackendBundle\Asset\Schema\Type\Video\VideoType;
18+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidElementTypeException;
19+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidThumbnailException;
20+
use Pimcore\Model\Asset;
1721

1822
/**
1923
* @internal
@@ -24,4 +28,10 @@ interface VideoServiceInterface
2428
* @return VideoType[]
2529
*/
2630
public function getVideoTypes(): array;
31+
32+
/**
33+
* @throws InvalidElementTypeException
34+
* @throws InvalidThumbnailException
35+
*/
36+
public function getThumbnailStatus(Asset $video, string $thumbnailName): VideoThumbnailStatus;
2737
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* This source file is available under the terms of the
6+
* Pimcore Open Core License (POCL)
7+
* Full copyright and license information is available in
8+
* LICENSE.md which is distributed with this source code.
9+
*
10+
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
11+
* @license Pimcore Open Core License (POCL)
12+
*/
13+
14+
namespace Pimcore\Bundle\StudioBackendBundle\Tests\Unit\Asset\Service;
15+
16+
use Codeception\Test\Unit;
17+
use Exception;
18+
use Pimcore\Bundle\StudioBackendBundle\Asset\Schema\Type\Video\VideoThumbnailStatus;
19+
use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ThumbnailServiceInterface;
20+
use Pimcore\Bundle\StudioBackendBundle\Asset\Service\VideoService;
21+
use Pimcore\Bundle\StudioBackendBundle\Asset\Service\VideoServiceInterface;
22+
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidElementTypeException;
23+
use Pimcore\Model\Asset\Document;
24+
use Pimcore\Model\Asset\Video;
25+
use Pimcore\Model\Asset\Video\Thumbnail\Config;
26+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
27+
28+
/**
29+
* @internal
30+
*/
31+
final class VideoServiceTest extends Unit
32+
{
33+
private const string THUMBNAIL_NAME = 'content';
34+
35+
public function testGetThumbnailStatusWithWrongElementType(): void
36+
{
37+
$this->expectException(InvalidElementTypeException::class);
38+
39+
$this->getVideoService()->getThumbnailStatus(new Document(), self::THUMBNAIL_NAME);
40+
}
41+
42+
/**
43+
* @throws Exception
44+
*/
45+
public function testGetThumbnailStatusReturnsStatusFromCustomSetting(): void
46+
{
47+
$video = $this->makeEmpty(Video::class, [
48+
'getCustomSetting' => [
49+
self::THUMBNAIL_NAME => [
50+
'status' => VideoThumbnailStatus::STATUS_ERROR,
51+
],
52+
],
53+
]);
54+
55+
$status = $this->getVideoService()->getThumbnailStatus($video, self::THUMBNAIL_NAME);
56+
57+
$this->assertSame(VideoThumbnailStatus::STATUS_ERROR, $status->getStatus());
58+
}
59+
60+
/**
61+
* @throws Exception
62+
*/
63+
public function testGetThumbnailStatusReturnsNotStartedWithoutCustomSetting(): void
64+
{
65+
$video = $this->makeEmpty(Video::class, [
66+
'getCustomSetting' => null,
67+
]);
68+
69+
$status = $this->getVideoService()->getThumbnailStatus($video, self::THUMBNAIL_NAME);
70+
71+
$this->assertSame(VideoThumbnailStatus::STATUS_NOT_STARTED, $status->getStatus());
72+
}
73+
74+
/**
75+
* @throws Exception
76+
*/
77+
public function testGetThumbnailStatusReturnsNotStartedForUnknownThumbnail(): void
78+
{
79+
$video = $this->makeEmpty(Video::class, [
80+
'getCustomSetting' => [
81+
'other-thumbnail' => [
82+
'status' => VideoThumbnailStatus::STATUS_FINISHED,
83+
],
84+
],
85+
]);
86+
87+
$status = $this->getVideoService()->getThumbnailStatus($video, self::THUMBNAIL_NAME);
88+
89+
$this->assertSame(VideoThumbnailStatus::STATUS_NOT_STARTED, $status->getStatus());
90+
}
91+
92+
private function getVideoService(): VideoServiceInterface
93+
{
94+
$config = new Config();
95+
$config->setName(self::THUMBNAIL_NAME);
96+
97+
return new VideoService(
98+
$this->makeEmpty(EventDispatcherInterface::class),
99+
$this->makeEmpty(ThumbnailServiceInterface::class, [
100+
'getVideoThumbnailConfig' => $config,
101+
]),
102+
);
103+
}
104+
}

translations/studio_api_docs.en.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ asset_video_stream_by_thumbnail_description: |
212212
List of <b>thumbnail names</b> can be obtained via the thumbnail video collection endpoint
213213
asset_video_stream_by_thumbnail_success_response: Video stream based on thumbnail name
214214
asset_video_stream_by_thumbnail_summary: Stream video asset by ID and thumbnail name
215+
asset_video_thumbnail_status_description: |
216+
Get the conversion status of a video thumbnail based on the provided <strong>{id}</strong> and <strong>{thumbnailName}</strong>. <br>
217+
The <strong>{id}</strong> must be an ID of existing asset video <br>
218+
List of <b>thumbnail names</b> can be obtained via the thumbnail video collection endpoint
219+
asset_video_thumbnail_status_success_response: Conversion status of the video thumbnail
220+
asset_video_thumbnail_status_summary: Get video thumbnail conversion status by asset ID and thumbnail name
215221
asset_get_search_configuration: Get asset search configuration
216222
asset_get_search_configuration_description: Get asset configuration
217223
asset_get_search_configuration_summary: Get asset search configuration

0 commit comments

Comments
 (0)