From 817793a5871ed199f4665ff305913a3fdf9a7a5b Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 14 Jul 2022 17:54:43 +0300 Subject: [PATCH] subtitles crud --- app/controllers/api/videos.php | 57 ++++++++---- src/Appwrite/Extend/Exception.php | 5 +- src/Appwrite/Utopia/Response.php | 14 +-- .../Utopia/Response/Model/VideoRendition.php | 2 +- .../Utopia/Response/Model/VideoSubtitle.php | 89 +++++++++++++++++++ .../Services/Videos/VideoCustomServerTest.php | 64 ++++++++++++- 6 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 src/Appwrite/Utopia/Response/Model/VideoSubtitle.php diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index 00d745247b..79c8411592 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -195,7 +195,7 @@ App::get('/v1/videos/profiles/:profileId') $profile = Authorization::skip(fn() => $dbForProject->getDocument('videos_profiles', $profileId)); if ($profile->isEmpty()) { - throw new Exception('Video profile not found', 404, Exception::PROFILES_NOT_FOUND); + throw new Exception('Video profile not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); } $response->dynamic($profile, Response::MODEL_VIDEO_PROFILE); @@ -219,7 +219,7 @@ App::get('/v1/videos/profiles') $profiles = Authorization::skip(fn () => $dbForProject->find('videos_profiles', [], 12, 0, [], ['ASC'])); if (empty($profiles)) { - throw new Exception('Video profiles where not found', 404, Exception::PROFILES_NOT_FOUND); + throw new Exception('Video profiles where not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); } $response->dynamic(new Document([ @@ -246,7 +246,7 @@ App::delete('/v1/videos/profiles/:profileId') $profile = Authorization::skip(fn() => $dbForProject->getDocument('videos_profiles', $profileId)); if ($profile->isEmpty()) { - throw new Exception('Video profile not found', 404, Exception::PROFILES_NOT_FOUND); + throw new Exception('Video profile not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); } $deleted = $dbForProject->deleteDocument('videos_profiles', $profileId); @@ -260,8 +260,7 @@ App::delete('/v1/videos/profiles/:profileId') App::post('/v1/videos/:videoId/subtitles') - ->alias('/v1/videos/:videoId/subtitles', []) - ->desc('Attach a subtitle file to a video') + ->desc('Add subtitle to video') ->groups(['api', 'video']) ->label('scope', 'files.write') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) @@ -299,7 +298,7 @@ App::post('/v1/videos/:videoId/subtitles') validateFilePermissions($dbForProject, $bucketId, $fileId, $mode, $user); try { - Authorization::skip(function () use ($dbForProject, $videoId, $bucketId, $fileId, $name, $code, $default) { + $subtitle = Authorization::skip(function () use ($dbForProject, $videoId, $bucketId, $fileId, $name, $code, $default) { return $dbForProject->createDocument('videos_subtitles', new Document([ 'videoId' => $videoId, 'bucketId' => $bucketId, @@ -313,9 +312,37 @@ App::post('/v1/videos/:videoId/subtitles') throw new Exception($exception->getMessage(), 400, Exception::DOCUMENT_INVALID_STRUCTURE); } - $response->json(['result' => 'ok']); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($subtitle, Response::MODEL_VIDEO_SUBTITLE); }); +App::get('/v1/videos/:videoId/subtitles') + ->desc('Get all video subtitles') + ->groups(['api', 'video']) + ->label('scope', 'files.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'video') + ->label('sdk.method', 'getSubtitles') + ->label('sdk.description', '/docs/references/videos/get-subtitles.md') // TODO: Create markdown + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_VIDEO_SUBTITLE_LIST) + ->param('videoId', null, new UID(), 'Video unique ID.') + ->inject('response') + ->inject('dbForProject') + ->action(function ($videoId, Response $response, Database $dbForProject) { + + $subtitles = Authorization::skip(fn () => $dbForProject->find('videos_subtitles', [new Query('videoId', Query::TYPE_EQUAL, [$videoId])], 12, 0, [], ['ASC'])); + + if (empty($subtitles)) { + throw new Exception('Video subtitles not found', 404, Exception::VIDEO_SUBTITLE_NOT_FOUND); + } + + $response->dynamic(new Document([ + 'total' => $dbForProject->count('videos_subtitles', [], APP_LIMIT_COUNT), + 'subtitles' => $subtitles, + ]), Response::MODEL_VIDEO_SUBTITLE_LIST); + }); App::post('/v1/video') ->desc('Create Video') @@ -344,19 +371,15 @@ App::post('/v1/video') /** @var Utopia\Database\Document $project */ $file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode, $user); - - try { - $video = Authorization::skip(function () use ($dbForProject, $bucketId, $file) { + $video = Authorization::skip(function () use ($dbForProject, $bucketId, $file) { return $dbForProject->createDocument('videos', new Document([ 'bucketId' => $bucketId, 'fileId' => $file->getId(), 'size' => $file->getAttribute('sizeOriginal'), ])); - }); - } catch (StructureException $exception) { - throw new Exception($exception->getMessage(), 400, Exception::DOCUMENT_INVALID_STRUCTURE); - } + }); + $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic(new Document([ '$id' => $video->getId(), 'fileId' => $video['fileId'], @@ -404,7 +427,7 @@ App::post('/v1/videos/:videoId/rendition') $profile = Authorization::skip(fn() => $dbForProject->findOne('videos_profiles', [new Query('_uid', Query::TYPE_EQUAL, [$profileId])])); if (!$profile) { - throw new Exception('Video profile not found', 400, Exception::PROFILES_NOT_FOUND); + throw new Exception('Video profile not found', 400, Exception::VIDEO_PROFILE_NOT_FOUND); } $transcoder = new Transcoding(); @@ -430,7 +453,7 @@ App::get('/v1/videos/:videoId/:stream/renditions') ->label('sdk.description', '/docs/references/videos/get-renditions.md') // TODO: Create markdown ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_VIDEO_RENDITIONS_LIST) + ->label('sdk.response.model', Response::MODEL_VIDEO_RENDITION_LIST) ->param('videoId', null, new UID(), 'Video unique ID.') ->param('stream', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name') ->inject('response') @@ -460,7 +483,7 @@ App::get('/v1/videos/:videoId/:stream/renditions') $response->dynamic(new Document([ 'total' => $dbForProject->count('videos_renditions', $queries, APP_LIMIT_COUNT), 'renditions' => $renditions, - ]), Response::MODEL_VIDEO_RENDITIONS_LIST); + ]), Response::MODEL_VIDEO_RENDITION_LIST); }); diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index f0f8074b76..93527f9c81 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -171,8 +171,9 @@ class Exception extends \Exception public const DOMAIN_VERIFICATION_FAILED = 'domain_verification_failed'; /** Video */ - public const PROFILES_NOT_FOUND = 'profiles_not_found'; - public const VIDEO_NOT_FOUND = 'video_not_found'; + public const VIDEO_PROFILE_NOT_FOUND = 'profile_not_found'; + public const VIDEO_NOT_FOUND = 'video_not_found'; + public const VIDEO_SUBTITLE_NOT_FOUND = 'subtitle_not_found'; private $type = ''; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 69f6d3de8b..01ea7f9432 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -70,9 +70,10 @@ use Appwrite\Utopia\Response\Model\UsageFunctions; use Appwrite\Utopia\Response\Model\UsageProject; use Appwrite\Utopia\Response\Model\UsageStorage; use Appwrite\Utopia\Response\Model\UsageUsers; -use Appwrite\Utopia\Response\Model\VideoRendition; use Appwrite\Utopia\Response\Model\Video; use Appwrite\Utopia\Response\Model\VideoProfile; +use Appwrite\Utopia\Response\Model\VideoRendition; +use Appwrite\Utopia\Response\Model\VideoSubtitle; /** * @method Response setStatusCode(int $code = 200) @@ -140,7 +141,9 @@ class Response extends SwooleResponse public const MODEL_VIDEO_PROFILE = 'videoProfile'; public const MODEL_VIDEO_PROFILE_LIST = 'videoProfileList'; public const MODEL_VIDEO_RENDITION = 'videoRendition'; - public const MODEL_VIDEO_RENDITIONS_LIST = 'videoRenditionsList'; + public const MODEL_VIDEO_RENDITION_LIST = 'videoRenditionList'; + public const MODEL_VIDEO_SUBTITLE = 'videoSubtitle'; + public const MODEL_VIDEO_SUBTITLE_LIST = 'videoSubtitleList'; // Locale public const MODEL_LOCALE = 'locale'; @@ -252,9 +255,9 @@ class Response extends SwooleResponse ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) - ->setModel(new BaseList('Video Renditions List', self::MODEL_VIDEO_RENDITIONS_LIST, 'renditions', self::MODEL_VIDEO_RENDITION)) ->setModel(new BaseList('video profile List', self::MODEL_VIDEO_PROFILE_LIST, 'profiles', self::MODEL_VIDEO_PROFILE)) - + ->setModel(new BaseList('Video Rendition List', self::MODEL_VIDEO_RENDITION_LIST, 'renditions', self::MODEL_VIDEO_RENDITION)) + ->setModel(new BaseList('Video Subtitle List', self::MODEL_VIDEO_SUBTITLE_LIST, 'subtitles', self::MODEL_VIDEO_SUBTITLE)) // Entities ->setModel(new Database()) ->setModel(new Collection()) @@ -310,9 +313,10 @@ class Response extends SwooleResponse ->setModel(new UsageBuckets()) ->setModel(new UsageFunctions()) ->setModel(new UsageProject()) - ->setModel(new VideoRendition()) ->setModel(new Video()) ->setModel(new VideoProfile()) + ->setModel(new VideoRendition()) + ->setModel(new VideoSubtitle()) // Verification // Recovery diff --git a/src/Appwrite/Utopia/Response/Model/VideoRendition.php b/src/Appwrite/Utopia/Response/Model/VideoRendition.php index 2768c87064..23bbb4acaf 100644 --- a/src/Appwrite/Utopia/Response/Model/VideoRendition.php +++ b/src/Appwrite/Utopia/Response/Model/VideoRendition.php @@ -74,7 +74,7 @@ class VideoRendition extends Model */ public function getName(): string { - return 'File rendition'; + return 'Video rendition'; } /** diff --git a/src/Appwrite/Utopia/Response/Model/VideoSubtitle.php b/src/Appwrite/Utopia/Response/Model/VideoSubtitle.php new file mode 100644 index 0000000000..e72d311d7d --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/VideoSubtitle.php @@ -0,0 +1,89 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('videoId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Video ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('bucketId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Bucket ID.', + 'default' => '', + 'example' => 'd5fg5ehg1c168g7c', + ]) + ->addRule('fileId', [ + 'type' => self::TYPE_STRING, + 'description' => 'file ID.', + 'default' => '', + 'example' => 'c5fg5emg1c168grr', + ]) + ->addRule('path', [ + 'type' => self::TYPE_STRING, + 'description' => 'Subtitle path.', + 'default' => '', + 'example' => '640x360@500', + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Subtitle name.', + 'default' => '', + 'example' => 'English', + ]) + ->addRule('code', [ + 'type' => self::TYPE_STRING, + 'description' => 'Subtitle code.', + 'default' => '', + 'example' => 'Eng', + ]) + ->addRule('default', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Subtitle default', + 'default' => '', + 'example' => false, + ]) + ->addRule('status', [ + 'type' => self::TYPE_STRING, + 'description' => 'Subtitle packaging status', + 'default' => '', + 'example' => 'ready', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Video subtitle'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_VIDEO_SUBTITLE; + } +} diff --git a/tests/e2e/Services/Videos/VideoCustomServerTest.php b/tests/e2e/Services/Videos/VideoCustomServerTest.php index 096ed33cc9..f2f9ad33a5 100644 --- a/tests/e2e/Services/Videos/VideoCustomServerTest.php +++ b/tests/e2e/Services/Videos/VideoCustomServerTest.php @@ -14,7 +14,6 @@ class VideoCustomServerTest extends Scope use VideoCustom; use SideServer; - public function testCreateVideoProfile() { @@ -95,9 +94,70 @@ class VideoCustomServerTest extends Scope } + public function testCreateVideo(): string + { + + $response = $this->client->call(Client::METHOD_POST, '/videos', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => $this->getBucket()['$id'], + 'fileId' => $this->getVideo()['$id'] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + + return $response['body']['$id']; + } + + /** + * @depends testCreateVideo + */ + public function testCreateVideoSubtitle($videoId) + { + + $response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/subtitles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => $this->getBucket()['$id'], + 'fileId' => $this->getSubtitle()['$id'], + 'name' => 'English', + 'code' => 'Eng', + 'default' => true, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + + $response = $this->client->call(Client::METHOD_GET, '/videos/'. $videoId . '/subtitles' , [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals(1, $response['body']['total']); + $this->assertNotEmpty($response['body']['subtitles']); + $this->assertNotEmpty($response['body']['subtitles'][0]['$id']); + $this->assertEquals('Eng', $response['body']['subtitles'][0]['code']); + +// +//// $this->assertEquals(404, $response['headers']['status-code']); +//// $this->assertNotEmpty($response['body']); +//// $this->assertEquals('Video profile not found', $response['body']['message']); + } - + /** + * @depends testCreateVideo + */ public function testTranscodeWithSubs(): array {