Эх сурвалжийг харах

CLI protobuf-related updates + new tests

Leszek Wiesner 3 жил өмнө
parent
commit
0977a2b5cc

+ 53 - 0
cli/content-test.sh

@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+cd $SCRIPT_PATH
+
+echo "{}" > ~/tmp/empty.json
+
+export AUTO_CONFIRM=true
+
+yarn workspace api-scripts initialize-content-lead
+# Test create/update/remove category
+yarn joystream-cli content:createVideoCategory -i ./examples/content/CreateCategory.json
+yarn joystream-cli content:createVideoCategory -i ./examples/content/CreateCategory.json
+yarn joystream-cli content:createVideoCategory -i ./examples/content/CreateCategory.json
+yarn joystream-cli content:createChannelCategory -i ./examples/content/CreateCategory.json
+yarn joystream-cli content:createChannelCategory -i ./examples/content/CreateCategory.json
+yarn joystream-cli content:createChannelCategory -i ./examples/content/CreateCategory.json
+yarn joystream-cli content:updateVideoCategory -i ./examples/content/UpdateCategory.json 2
+yarn joystream-cli content:updateChannelCategory -i ./examples/content/UpdateCategory.json 2
+yarn joystream-cli content:deleteChannelCategory 3
+yarn joystream-cli content:deleteVideoCategory 3
+# Group 1 - a valid group
+yarn joystream-cli content:createCuratorGroup
+yarn joystream-cli content:setCuratorGroupStatus 1 1
+yarn joystream-cli content:addCuratorToGroup 1 0
+# Group 2 - test removeCuratorFromGroup
+yarn joystream-cli content:createCuratorGroup
+yarn joystream-cli content:addCuratorToGroup 2 0
+yarn joystream-cli content:removeCuratorFromGroup 2 0
+# Create/update channel
+yarn joystream-cli content:createChannel -i ./examples/content/CreateChannel.json --context Member || true
+yarn joystream-cli content:createChannel -i ./examples/content/CreateChannel.json --context Curator || true
+yarn joystream-cli content:createChannel -i ~/tmp/empty.json --context Member || true
+yarn joystream-cli content:updateChannel -i ./examples/content/UpdateChannel.json 1 || true
+# Create/update video
+yarn joystream-cli content:createVideo -i ./examples/content/CreateVideo.json -c 1 || true
+yarn joystream-cli content:createVideo -i ./examples/content/CreateVideo.json -c 2 || true
+yarn joystream-cli content:createVideo -i ~/tmp/empty.json -c 2 || true
+yarn joystream-cli content:updateVideo -i ./examples/content/UpdateVideo.json 1 || true
+# Set featured videos
+yarn joystream-cli content:setFeaturedVideos 1,2
+yarn joystream-cli content:setFeaturedVideos 2,3
+# Update channel censorship status
+yarn joystream-cli content:updateChannelCensorshipStatus 1 1 --rationale "Test"
+yarn joystream-cli content:updateVideoCensorshipStatus 1 1 --rationale "Test"
+# Display-only commands
+yarn joystream-cli content:videos
+yarn joystream-cli content:video 1
+yarn joystream-cli content:channels
+yarn joystream-cli content:channel 1
+yarn joystream-cli content:curatorGroups
+yarn joystream-cli content:curatorGroup 1

+ 2 - 1
cli/examples/content/UpdateVideo.json

@@ -3,5 +3,6 @@
   "thumbnailPhotoPath": "./avatar-photo-2.png",
   "publishedBeforeJoystream": {
     "isPublished": false
-  }
+  },
+  "license": {}
 }

+ 4 - 0
cli/src/Api.ts

@@ -61,6 +61,8 @@ export const apiModuleByGroup = {
   [WorkingGroups.Curators]: 'contentDirectoryWorkingGroup',
   [WorkingGroups.Forum]: 'forumWorkingGroup',
   [WorkingGroups.Membership]: 'membershipWorkingGroup',
+  [WorkingGroups.Operations]: 'operationsWorkingGroup',
+  [WorkingGroups.Gateway]: 'gatewayWorkingGroup',
 } as const
 
 export const lockIdByWorkingGroup: { [K in WorkingGroups]: string } = {
@@ -68,6 +70,8 @@ export const lockIdByWorkingGroup: { [K in WorkingGroups]: string } = {
   [WorkingGroups.Curators]: '0x0707070707070707',
   [WorkingGroups.Forum]: '0x0808080808080808',
   [WorkingGroups.Membership]: '0x0909090909090909',
+  [WorkingGroups.Operations]: '0x0d0d0d0d0d0d0d0d',
+  [WorkingGroups.Gateway]: '0x0e0e0e0e0e0e0e0e',
 }
 
 // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future

+ 25 - 19
cli/src/Types.ts

@@ -8,15 +8,15 @@ import { MemberId } from '@joystream/types/common'
 import { Validator } from 'inquirer'
 import { ApiPromise } from '@polkadot/api'
 import { SubmittableModuleExtrinsics, QueryableModuleStorage, QueryableModuleConsts } from '@polkadot/api/types'
-import {
-  VideoMetadata,
-  ChannelMetadata,
-  ChannelCategoryMetadata,
-  VideoCategoryMetadata,
-} from '@joystream/content-metadata-protobuf'
 import { ContentId, ContentParameters } from '@joystream/types/storage'
 
 import { JSONSchema7, JSONSchema7Definition } from 'json-schema'
+import {
+  IChannelMetadata,
+  IVideoMetadata,
+  IVideoCategoryMetadata,
+  IChannelCategoryMetadata,
+} from '@joystream/metadata-protobuf'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -43,6 +43,8 @@ export enum WorkingGroups {
   Curators = 'curators',
   Forum = 'forum',
   Membership = 'membership',
+  Operations = 'operations',
+  Gateway = 'gateway',
 }
 
 // In contrast to Pioneer, currently only StorageProviders group is available in CLI
@@ -51,6 +53,8 @@ export const AvailableGroups: readonly WorkingGroups[] = [
   WorkingGroups.Curators,
   WorkingGroups.Forum,
   WorkingGroups.Membership,
+  WorkingGroups.Operations,
+  WorkingGroups.Gateway,
 ] as const
 
 export type Reward = {
@@ -158,47 +162,49 @@ export type VideoFileMetadata = VideoFFProbeMetadata & {
   mimeType: string
 }
 
-export type VideoInputParameters = Omit<VideoMetadata.AsObject, 'video' | 'thumbnailPhoto'> & {
+export type VideoInputParameters = Omit<IVideoMetadata, 'video' | 'thumbnailPhoto'> & {
   videoPath?: string
   thumbnailPhotoPath?: string
 }
 
-export type ChannelInputParameters = Omit<ChannelMetadata.AsObject, 'coverPhoto' | 'avatarPhoto'> & {
+export type ChannelInputParameters = Omit<IChannelMetadata, 'coverPhoto' | 'avatarPhoto'> & {
   coverPhotoPath?: string
   avatarPhotoPath?: string
   rewardAccount?: string
 }
 
-export type ChannelCategoryInputParameters = ChannelCategoryMetadata.AsObject
+export type ChannelCategoryInputParameters = IChannelCategoryMetadata
 
-export type VideoCategoryInputParameters = VideoCategoryMetadata.AsObject
+export type VideoCategoryInputParameters = IVideoCategoryMetadata
+
+type AnyNonObject = string | number | boolean | any[] | Long
 
 // JSONSchema utility types
 export type JSONTypeName<T> = T extends string
   ? 'string' | ['string', 'null']
   : T extends number
   ? 'number' | ['number', 'null']
-  : T extends any[]
-  ? 'array' | ['array', 'null']
-  : T extends Record<string, unknown>
-  ? 'object' | ['object', 'null']
   : T extends boolean
   ? 'boolean' | ['boolean', 'null']
-  : never
+  : T extends any[]
+  ? 'array' | ['array', 'null']
+  : T extends Long
+  ? 'number' | ['number', 'null']
+  : 'object' | ['object', 'null']
 
 export type PropertySchema<P> = Omit<
   JSONSchema7Definition & {
     type: JSONTypeName<P>
-    properties: P extends Record<string, unknown> ? JsonSchemaProperties<P> : never
+    properties: P extends AnyNonObject ? never : JsonSchemaProperties<P>
   },
-  P extends Record<string, unknown> ? '' : 'properties'
+  P extends AnyNonObject ? 'properties' : ''
 >
 
-export type JsonSchemaProperties<T extends Record<string, unknown>> = {
+export type JsonSchemaProperties<T> = {
   [K in keyof Required<T>]: PropertySchema<Required<T>[K]>
 }
 
-export type JsonSchema<T extends Record<string, unknown>> = JSONSchema7 & {
+export type JsonSchema<T> = JSONSchema7 & {
   type: 'object'
   properties: JsonSchemaProperties<T>
 }

+ 7 - 6
cli/src/base/ApiCommandBase.ts

@@ -3,7 +3,7 @@ import { CLIError } from '@oclif/errors'
 import StateAwareCommandBase from './StateAwareCommandBase'
 import Api from '../Api'
 import { getTypeDef, Option, Tuple } from '@polkadot/types'
-import { Registry, Codec, TypeDef, TypeDefInfo } from '@polkadot/types/types'
+import { Registry, Codec, TypeDef, TypeDefInfo, IEvent } from '@polkadot/types/types'
 import { Vec, Struct, Enum } from '@polkadot/types/codec'
 import { SubmittableResult, WsProvider } from '@polkadot/api'
 import { KeyringPair } from '@polkadot/keyring/types'
@@ -11,10 +11,10 @@ import chalk from 'chalk'
 import { InterfaceTypes } from '@polkadot/types/types/registry'
 import { ApiMethodArg, ApiMethodNamedArgs, ApiParamsOptions, ApiParamOptions } from '../Types'
 import { createParamOptions } from '../helpers/promptOptions'
-import { AugmentedSubmittables, SubmittableExtrinsic, AugmentedEvents } from '@polkadot/api/types'
+import { AugmentedSubmittables, SubmittableExtrinsic, AugmentedEvents, AugmentedEvent } from '@polkadot/api/types'
 import { DistinctQuestion } from 'inquirer'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
-import { DispatchError, Event } from '@polkadot/types/interfaces/system'
+import { DispatchError } from '@polkadot/types/interfaces/system'
 import { formatBalance } from '@polkadot/util'
 import BN from 'bn.js'
 import _ from 'lodash'
@@ -525,9 +525,10 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
   public findEvent<
     S extends keyof AugmentedEvents<'promise'> & string,
-    M extends keyof AugmentedEvents<'promise'>[S] & string
-  >(result: SubmittableResult, section: S, method: M): Event | undefined {
-    return result.findRecord(section, method)?.event
+    M extends keyof AugmentedEvents<'promise'>[S] & string,
+    EventType = AugmentedEvents<'promise'>[S][M] extends AugmentedEvent<'promise', infer T> ? IEvent<T> : never
+  >(result: SubmittableResult, section: S, method: M): EventType | undefined {
+    return result.findRecord(section, method)?.event as EventType | undefined
   }
 
   async buildAndSendExtrinsic<

+ 5 - 0
cli/src/base/UploadCommandBase.ts

@@ -249,6 +249,11 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
     multiBar.stop()
   }
 
+  public assetsIndexes(originalPaths: (string | undefined)[], filteredPaths: string[]): (number | undefined)[] {
+    let lastIndex = -1
+    return originalPaths.map((path) => (filteredPaths.includes(path as string) ? ++lastIndex : undefined))
+  }
+
   private handleRejectedUploads(
     assets: InputAsset[],
     results: boolean[],

+ 8 - 10
cli/src/commands/content/createChannel.ts

@@ -1,6 +1,6 @@
 import { getInputJson } from '../../helpers/InputOutput'
 import { ChannelInputParameters } from '../../Types'
-import { metadataToBytes, channelMetadataFromInput } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { ChannelCreationParameters } from '@joystream/types/content'
@@ -8,6 +8,7 @@ import { ChannelInputSchema } from '../../json-schemas/ContentDirectory'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import UploadCommandBase from '../../base/UploadCommandBase'
 import chalk from 'chalk'
+import { ChannelMetadata } from '@joystream/metadata-protobuf'
 
 export default class CreateChannelCommand extends UploadCommandBase {
   static description = 'Create channel inside content directory.'
@@ -30,27 +31,24 @@ export default class CreateChannelCommand extends UploadCommandBase {
     const [actor, address] = await this.getContentActor(context)
 
     const channelInput = await getInputJson<ChannelInputParameters>(input, ChannelInputSchema)
+    const meta = asValidatedMetadata(ChannelMetadata, channelInput)
 
-    const meta = channelMetadataFromInput(channelInput)
     const { coverPhotoPath, avatarPhotoPath } = channelInput
     const assetsPaths = [coverPhotoPath, avatarPhotoPath].filter((v) => v !== undefined) as string[]
     const inputAssets = await this.prepareInputAssets(assetsPaths, input)
     const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
     // Set assets indexes in the metadata
-    if (coverPhotoPath) {
-      meta.setCoverPhoto(0)
-    }
-    if (avatarPhotoPath) {
-      meta.setAvatarPhoto(coverPhotoPath ? 1 : 0)
-    }
+    const [coverPhotoIndex, avatarPhotoIndex] = this.assetsIndexes([coverPhotoPath, avatarPhotoPath], assetsPaths)
+    meta.coverPhoto = coverPhotoIndex
+    meta.avatarPhoto = avatarPhotoIndex
 
     const channelCreationParameters: CreateInterface<ChannelCreationParameters> = {
       assets,
-      meta: metadataToBytes(meta),
+      meta: metadataToBytes(ChannelMetadata, meta),
       reward_account: channelInput.rewardAccount,
     }
 
-    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta.toObject() }))
+    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta }))
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 

+ 4 - 4
cli/src/commands/content/createChannelCategory.ts

@@ -1,12 +1,13 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { getInputJson } from '../../helpers/InputOutput'
 import { ChannelCategoryInputParameters } from '../../Types'
-import { channelCategoryMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { ChannelCategoryCreationParameters } from '@joystream/types/content'
 import { ChannelCategoryInputSchema } from '../../json-schemas/ContentDirectory'
 import chalk from 'chalk'
+import { ChannelCategoryMetadata } from '@joystream/metadata-protobuf'
 
 export default class CreateChannelCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create channel category inside content directory.'
@@ -25,11 +26,10 @@ export default class CreateChannelCategoryCommand extends ContentDirectoryComman
     const [actor, address] = context ? await this.getContentActor(context) : await this.getCategoryManagementActor()
 
     const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input, ChannelCategoryInputSchema)
-
-    const meta = channelCategoryMetadataFromInput(channelCategoryInput)
+    const meta = asValidatedMetadata(ChannelCategoryMetadata, channelCategoryInput)
 
     const channelCategoryCreationParameters: CreateInterface<ChannelCategoryCreationParameters> = {
-      meta: metadataToBytes(meta),
+      meta: metadataToBytes(ChannelCategoryMetadata, meta),
     }
 
     this.jsonPrettyPrint(JSON.stringify(channelCategoryInput))

+ 30 - 29
cli/src/commands/content/createVideo.ts

@@ -1,12 +1,12 @@
 import UploadCommandBase from '../../base/UploadCommandBase'
 import { getInputJson } from '../../helpers/InputOutput'
-import { videoMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { VideoInputParameters, VideoFileMetadata } from '../../Types'
 import { CreateInterface } from '@joystream/types'
 import { flags } from '@oclif/command'
 import { VideoCreationParameters } from '@joystream/types/content'
-import { MediaType, VideoMetadata } from '@joystream/content-metadata-protobuf'
-import { VideoInputSchema } from '../../json-schemas/ContentDirectory'
+import { IVideoMetadata, VideoMetadata } from '@joystream/metadata-protobuf'
+import { integrateMeta } from '@joystream/metadata-protobuf/utils'
 import chalk from 'chalk'
 
 export default class CreateVideoCommand extends UploadCommandBase {
@@ -24,17 +24,19 @@ export default class CreateVideoCommand extends UploadCommandBase {
     }),
   }
 
-  setVideoMetadataDefaults(metadata: VideoMetadata, videoFileMetadata: VideoFileMetadata) {
-    const metaObj = metadata.toObject()
-    metadata.setDuration((metaObj.duration || videoFileMetadata.duration) as number)
-    metadata.setMediaPixelWidth((metaObj.mediaPixelWidth || videoFileMetadata.width) as number)
-    metadata.setMediaPixelHeight((metaObj.mediaPixelHeight || videoFileMetadata.height) as number)
-
-    const fileMediaType = new MediaType()
-    fileMediaType.setCodecName(videoFileMetadata.codecName as string)
-    fileMediaType.setContainer(videoFileMetadata.container)
-    fileMediaType.setMimeMediaType(videoFileMetadata.mimeType)
-    metadata.setMediaType(metadata.getMediaType() || fileMediaType)
+  setVideoMetadataDefaults(metadata: IVideoMetadata, videoFileMetadata: VideoFileMetadata): void {
+    const videoMetaToIntegrate = {
+      duration: videoFileMetadata.duration,
+      mediaPixelWidth: videoFileMetadata.width,
+      mediaPixelHeight: videoFileMetadata.height,
+    }
+    const mediaTypeMetaToIntegrate = {
+      codecName: videoFileMetadata.codecName,
+      container: videoFileMetadata.container,
+      mimeMediaType: videoFileMetadata.mimeType,
+    }
+    integrateMeta(metadata, videoMetaToIntegrate, ['duration', 'mediaPixelWidth', 'mediaPixelHeight'])
+    integrateMeta(metadata.mediaType || {}, mediaTypeMetaToIntegrate, ['codecName', 'container', 'mimeMediaType'])
   }
 
   async run() {
@@ -45,9 +47,8 @@ export default class CreateVideoCommand extends UploadCommandBase {
     const [actor, address] = await this.getChannelOwnerActor(channel)
 
     // Get input from file
-    const videoCreationParametersInput = await getInputJson<VideoInputParameters>(input, VideoInputSchema)
-
-    const meta = videoMetadataFromInput(videoCreationParametersInput)
+    const videoCreationParametersInput = await getInputJson<VideoInputParameters>(input)
+    const meta = asValidatedMetadata(VideoMetadata, videoCreationParametersInput)
 
     // Assets
     const { videoPath, thumbnailPhotoPath } = videoCreationParametersInput
@@ -55,25 +56,24 @@ export default class CreateVideoCommand extends UploadCommandBase {
     const inputAssets = await this.prepareInputAssets(assetsPaths, input)
     const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
     // Set assets indexes in the metadata
-    if (videoPath) {
-      meta.setVideo(0)
-    }
-    if (thumbnailPhotoPath) {
-      meta.setThumbnailPhoto(videoPath ? 1 : 0)
-    }
+    const [videoIndex, thumbnailPhotoIndex] = this.assetsIndexes([videoPath, thumbnailPhotoPath], assetsPaths)
+    meta.video = videoIndex
+    meta.thumbnailPhoto = thumbnailPhotoIndex
 
     // Try to get video file metadata
-    const videoFileMetadata = await this.getVideoFileMetadata(inputAssets[0].path)
-    this.log('Video media file parameters established:', videoFileMetadata)
-    this.setVideoMetadataDefaults(meta, videoFileMetadata)
+    if (videoIndex !== undefined) {
+      const videoFileMetadata = await this.getVideoFileMetadata(inputAssets[videoIndex].path)
+      this.log('Video media file parameters established:', videoFileMetadata)
+      this.setVideoMetadataDefaults(meta, videoFileMetadata)
+    }
 
     // Create final extrinsic params and send the extrinsic
     const videoCreationParameters: CreateInterface<VideoCreationParameters> = {
       assets,
-      meta: metadataToBytes(meta),
+      meta: metadataToBytes(VideoMetadata, meta),
     }
 
-    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta.toObject() }))
+    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta }))
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 
@@ -84,7 +84,8 @@ export default class CreateVideoCommand extends UploadCommandBase {
     ])
     if (result) {
       const event = this.findEvent(result, 'content', 'VideoCreated')
-      this.log(chalk.green(`Video with id ${chalk.cyanBright(event?.data[2].toString())} successfully created!`))
+      const videoId = event?.data[2]
+      this.log(chalk.green(`Video with id ${chalk.cyanBright(videoId?.toString())} successfully created!`))
     }
 
     // Upload assets

+ 4 - 4
cli/src/commands/content/createVideoCategory.ts

@@ -1,12 +1,13 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { getInputJson } from '../../helpers/InputOutput'
 import { VideoCategoryInputParameters } from '../../Types'
-import { metadataToBytes, videoCategoryMetadataFromInput } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { VideoCategoryCreationParameters } from '@joystream/types/content'
 import { VideoCategoryInputSchema } from '../../json-schemas/ContentDirectory'
 import chalk from 'chalk'
+import { VideoCategoryMetadata } from '@joystream/metadata-protobuf'
 
 export default class CreateVideoCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create video category inside content directory.'
@@ -25,11 +26,10 @@ export default class CreateVideoCategoryCommand extends ContentDirectoryCommandB
     const [actor, address] = context ? await this.getContentActor(context) : await this.getCategoryManagementActor()
 
     const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input, VideoCategoryInputSchema)
-
-    const meta = videoCategoryMetadataFromInput(videoCategoryInput)
+    const meta = asValidatedMetadata(VideoCategoryMetadata, videoCategoryInput)
 
     const videoCategoryCreationParameters: CreateInterface<VideoCategoryCreationParameters> = {
-      meta: metadataToBytes(meta),
+      meta: metadataToBytes(VideoCategoryMetadata, meta),
     }
 
     this.jsonPrettyPrint(JSON.stringify(videoCategoryInput))

+ 8 - 11
cli/src/commands/content/updateChannel.ts

@@ -1,11 +1,12 @@
 import { getInputJson } from '../../helpers/InputOutput'
-import { channelMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { ChannelInputParameters } from '../../Types'
 import { flags } from '@oclif/command'
 import UploadCommandBase from '../../base/UploadCommandBase'
 import { CreateInterface } from '@joystream/types'
 import { ChannelUpdateParameters } from '@joystream/types/content'
 import { ChannelInputSchema } from '../../json-schemas/ContentDirectory'
+import { ChannelMetadata } from '@joystream/metadata-protobuf'
 
 export default class UpdateChannelCommand extends UploadCommandBase {
   static description = 'Update existing content directory channel.'
@@ -49,28 +50,24 @@ export default class UpdateChannelCommand extends UploadCommandBase {
     const [actor, address] = await this.getChannelOwnerActor(channel)
 
     const channelInput = await getInputJson<ChannelInputParameters>(input, ChannelInputSchema)
-
-    const meta = channelMetadataFromInput(channelInput)
+    const meta = asValidatedMetadata(ChannelMetadata, channelInput)
 
     const { coverPhotoPath, avatarPhotoPath, rewardAccount } = channelInput
     const inputPaths = [coverPhotoPath, avatarPhotoPath].filter((p) => p !== undefined) as string[]
     const inputAssets = await this.prepareInputAssets(inputPaths, input)
     const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
     // Set assets indexes in the metadata
-    if (coverPhotoPath) {
-      meta.setCoverPhoto(0)
-    }
-    if (avatarPhotoPath) {
-      meta.setAvatarPhoto(coverPhotoPath ? 1 : 0)
-    }
+    const [coverPhotoIndex, avatarPhotoIndex] = this.assetsIndexes([coverPhotoPath, avatarPhotoPath], inputPaths)
+    meta.coverPhoto = coverPhotoIndex
+    meta.avatarPhoto = avatarPhotoIndex
 
     const channelUpdateParameters: CreateInterface<ChannelUpdateParameters> = {
       assets,
-      new_meta: metadataToBytes(meta),
+      new_meta: metadataToBytes(ChannelMetadata, meta),
       reward_account: this.parseRewardAccountInput(rewardAccount),
     }
 
-    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta.toObject(), rewardAccount }))
+    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta, rewardAccount }))
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 

+ 4 - 4
cli/src/commands/content/updateChannelCategory.ts

@@ -1,11 +1,12 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { getInputJson } from '../../helpers/InputOutput'
 import { ChannelCategoryInputParameters } from '../../Types'
-import { channelCategoryMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { CreateInterface } from '@joystream/types'
 import { ChannelCategoryUpdateParameters } from '@joystream/types/content'
 import { flags } from '@oclif/command'
 import { ChannelCategoryInputSchema } from '../../json-schemas/ContentDirectory'
+import { ChannelCategoryMetadata } from '@joystream/metadata-protobuf'
 export default class UpdateChannelCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Update channel category inside content directory.'
   static flags = {
@@ -33,11 +34,10 @@ export default class UpdateChannelCategoryCommand extends ContentDirectoryComman
     const [actor, address] = context ? await this.getContentActor(context) : await this.getCategoryManagementActor()
 
     const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input, ChannelCategoryInputSchema)
-
-    const meta = channelCategoryMetadataFromInput(channelCategoryInput)
+    const meta = asValidatedMetadata(ChannelCategoryMetadata, channelCategoryInput)
 
     const channelCategoryUpdateParameters: CreateInterface<ChannelCategoryUpdateParameters> = {
-      new_meta: metadataToBytes(meta),
+      new_meta: metadataToBytes(ChannelCategoryMetadata, meta),
     }
 
     this.jsonPrettyPrint(JSON.stringify(channelCategoryInput))

+ 3 - 1
cli/src/commands/content/updateChannelCensorshipStatus.ts

@@ -54,7 +54,9 @@ export default class UpdateChannelCensorshipStatusCommand extends ContentDirecto
     }
 
     if (rationale === undefined) {
-      rationale = await this.simplePrompt({ message: 'Please provide the rationale for updating the status' })
+      rationale = (await this.simplePrompt({
+        message: 'Please provide the rationale for updating the status',
+      })) as string
     }
 
     await this.sendAndFollowNamedTx(await this.getDecodedPair(address), 'content', 'updateChannelCensorshipStatus', [

+ 8 - 10
cli/src/commands/content/updateVideo.ts

@@ -1,11 +1,12 @@
 import { getInputJson } from '../../helpers/InputOutput'
 import { VideoInputParameters } from '../../Types'
-import { metadataToBytes, videoMetadataFromInput } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import UploadCommandBase from '../../base/UploadCommandBase'
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { VideoUpdateParameters } from '@joystream/types/content'
 import { VideoInputSchema } from '../../json-schemas/ContentDirectory'
+import { VideoMetadata } from '@joystream/metadata-protobuf'
 
 export default class UpdateVideoCommand extends UploadCommandBase {
   static description = 'Update video under specific id.'
@@ -37,26 +38,23 @@ export default class UpdateVideoCommand extends UploadCommandBase {
     const [actor, address] = await this.getChannelOwnerActor(channel)
 
     const videoInput = await getInputJson<VideoInputParameters>(input, VideoInputSchema)
+    const meta = asValidatedMetadata(VideoMetadata, videoInput)
 
-    const meta = videoMetadataFromInput(videoInput)
     const { videoPath, thumbnailPhotoPath } = videoInput
     const inputPaths = [videoPath, thumbnailPhotoPath].filter((p) => p !== undefined) as string[]
     const inputAssets = await this.prepareInputAssets(inputPaths, input)
     const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
     // Set assets indexes in the metadata
-    if (videoPath) {
-      meta.setVideo(0)
-    }
-    if (thumbnailPhotoPath) {
-      meta.setThumbnailPhoto(videoPath ? 1 : 0)
-    }
+    const [videoIndex, thumbnailPhotoIndex] = this.assetsIndexes([videoPath, thumbnailPhotoPath], inputPaths)
+    meta.video = videoIndex
+    meta.thumbnailPhoto = thumbnailPhotoIndex
 
     const videoUpdateParameters: CreateInterface<VideoUpdateParameters> = {
       assets,
-      new_meta: metadataToBytes(meta),
+      new_meta: metadataToBytes(VideoMetadata, meta),
     }
 
-    this.jsonPrettyPrint(JSON.stringify({ assets, newMetadata: meta.toObject() }))
+    this.jsonPrettyPrint(JSON.stringify({ assets, newMetadata: meta }))
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 

+ 4 - 4
cli/src/commands/content/updateVideoCategory.ts

@@ -1,11 +1,12 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { getInputJson } from '../../helpers/InputOutput'
 import { VideoCategoryInputParameters } from '../../Types'
-import { metadataToBytes, videoCategoryMetadataFromInput } from '../../helpers/serialization'
+import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { VideoCategoryUpdateParameters } from '@joystream/types/content'
 import { VideoCategoryInputSchema } from '../../json-schemas/ContentDirectory'
+import { VideoCategoryMetadata } from '@joystream/metadata-protobuf'
 
 export default class UpdateVideoCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Update video category inside content directory.'
@@ -34,11 +35,10 @@ export default class UpdateVideoCategoryCommand extends ContentDirectoryCommandB
     const [actor, address] = context ? await this.getContentActor(context) : await this.getCategoryManagementActor()
 
     const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input, VideoCategoryInputSchema)
-
-    const meta = videoCategoryMetadataFromInput(videoCategoryInput)
+    const meta = asValidatedMetadata(VideoCategoryMetadata, videoCategoryInput)
 
     const videoCategoryUpdateParameters: CreateInterface<VideoCategoryUpdateParameters> = {
-      new_meta: metadataToBytes(meta),
+      new_meta: metadataToBytes(VideoCategoryMetadata, meta),
     }
 
     this.jsonPrettyPrint(JSON.stringify(videoCategoryInput))

+ 3 - 1
cli/src/commands/content/updateVideoCensorshipStatus.ts

@@ -55,7 +55,9 @@ export default class UpdateVideoCensorshipStatusCommand extends ContentDirectory
     }
 
     if (rationale === undefined) {
-      rationale = await this.simplePrompt({ message: 'Please provide the rationale for updating the status' })
+      rationale = (await this.simplePrompt({
+        message: 'Please provide the rationale for updating the status',
+      })) as string
     }
 
     await this.sendAndFollowNamedTx(await this.getDecodedPair(address), 'content', 'updateVideoCensorshipStatus', [

+ 13 - 89
cli/src/helpers/serialization.ts

@@ -1,98 +1,22 @@
-import {
-  VideoMetadata,
-  PublishedBeforeJoystream,
-  License,
-  MediaType,
-  ChannelMetadata,
-  ChannelCategoryMetadata,
-  VideoCategoryMetadata,
-} from '@joystream/content-metadata-protobuf'
-import {
-  ChannelCategoryInputParameters,
-  ChannelInputParameters,
-  VideoCategoryInputParameters,
-  VideoInputParameters,
-} from '../Types'
+import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
 import { Bytes } from '@polkadot/types/primitive'
 import { createType } from '@joystream/types'
+import { CLIError } from '@oclif/errors'
+import ExitCodes from '../ExitCodes'
+import { metaToObject } from '@joystream/metadata-protobuf/utils'
 
-type AnyMetadata = {
-  serializeBinary(): Uint8Array
+export function metadataToBytes<T>(metaClass: AnyMetadataClass<T>, obj: T): Bytes {
+  return createType('Bytes', '0x' + Buffer.from(metaClass.encode(obj).finish()).toString('hex'))
 }
 
-export function metadataToBytes(metadata: AnyMetadata): Bytes {
-  const bytes = createType('Bytes', '0x' + Buffer.from(metadata.serializeBinary()).toString('hex'))
-  console.log('Metadata as Bytes:', bytes.toString())
-  return bytes
+export function metadataFromBytes<T>(metaClass: AnyMetadataClass<T>, bytes: Bytes): DecodedMetadataObject<T> {
+  return metaToObject(metaClass, metaClass.decode(bytes.toU8a(true)))
 }
 
-// TODO: If "fromObject()" was generated for the protobuffs we could avoid having to create separate converters for each metadata
-
-export function videoMetadataFromInput(videoParametersInput: VideoInputParameters): VideoMetadata {
-  const videoMetadata = new VideoMetadata()
-  videoMetadata.setTitle(videoParametersInput.title as string)
-  videoMetadata.setDescription(videoParametersInput.description as string)
-  videoMetadata.setDuration(videoParametersInput.duration as number)
-  videoMetadata.setMediaPixelHeight(videoParametersInput.mediaPixelHeight as number)
-  videoMetadata.setMediaPixelWidth(videoParametersInput.mediaPixelWidth as number)
-  videoMetadata.setLanguage(videoParametersInput.language as string)
-  videoMetadata.setHasMarketing(videoParametersInput.hasMarketing as boolean)
-  videoMetadata.setIsPublic(videoParametersInput.isPublic as boolean)
-  videoMetadata.setIsExplicit(videoParametersInput.isExplicit as boolean)
-  videoMetadata.setPersonsList(videoParametersInput.personsList as number[])
-  videoMetadata.setCategory(videoParametersInput.category as number)
-
-  if (videoParametersInput.mediaType) {
-    const mediaType = new MediaType()
-    mediaType.setCodecName(videoParametersInput.mediaType.codecName as string)
-    mediaType.setContainer(videoParametersInput.mediaType.container as string)
-    mediaType.setMimeMediaType(videoParametersInput.mediaType.mimeMediaType as string)
-    videoMetadata.setMediaType(mediaType)
-  }
-
-  if (videoParametersInput.publishedBeforeJoystream) {
-    const publishedBeforeJoystream = new PublishedBeforeJoystream()
-    publishedBeforeJoystream.setIsPublished(videoParametersInput.publishedBeforeJoystream.isPublished as boolean)
-    publishedBeforeJoystream.setDate(videoParametersInput.publishedBeforeJoystream.date as string)
-    videoMetadata.setPublishedBeforeJoystream(publishedBeforeJoystream)
+export function asValidatedMetadata<T>(metaClass: AnyMetadataClass<T>, anyObject: any): T {
+  const error = metaClass.verify(anyObject)
+  if (error) {
+    throw new CLIError(`Invalid metadata: ${error}`, { exit: ExitCodes.InvalidInput })
   }
-
-  if (videoParametersInput.license) {
-    const license = new License()
-    license.setCode(videoParametersInput.license.code as number)
-    license.setAttribution(videoParametersInput.license.attribution as string)
-    license.setCustomText(videoParametersInput.license.customText as string)
-    videoMetadata.setLicense(license)
-  }
-
-  return videoMetadata
-}
-
-export function channelMetadataFromInput(channelParametersInput: ChannelInputParameters): ChannelMetadata {
-  const channelMetadata = new ChannelMetadata()
-  channelMetadata.setTitle(channelParametersInput.title as string)
-  channelMetadata.setDescription(channelParametersInput.description as string)
-  channelMetadata.setIsPublic(channelParametersInput.isPublic as boolean)
-  channelMetadata.setLanguage(channelParametersInput.language as string)
-  channelMetadata.setCategory(channelParametersInput.category as number)
-
-  return channelMetadata
-}
-
-export function channelCategoryMetadataFromInput(
-  channelCategoryParametersInput: ChannelCategoryInputParameters
-): ChannelCategoryMetadata {
-  const channelCategoryMetadata = new ChannelCategoryMetadata()
-  channelCategoryMetadata.setName(channelCategoryParametersInput.name as string)
-
-  return channelCategoryMetadata
-}
-
-export function videoCategoryMetadataFromInput(
-  videoCategoryParametersInput: VideoCategoryInputParameters
-): VideoCategoryMetadata {
-  const videoCategoryMetadata = new VideoCategoryMetadata()
-  videoCategoryMetadata.setName(videoCategoryParametersInput.name as string)
-
-  return videoCategoryMetadata
+  return { ...anyObject } as T
 }

+ 1 - 1
cli/src/json-schemas/ContentDirectory.ts

@@ -74,7 +74,7 @@ export const VideoInputSchema: JsonSchema<VideoInputParameters> = {
         },
       },
     },
-    personsList: { type: 'array' },
+    persons: { type: 'array' },
     publishedBeforeJoystream: {
       type: 'object',
       properties: {