|
@@ -6,26 +6,29 @@
|
|
|
// every time query node codegen is run (that will overwrite said manual changes)
|
|
|
// - verify in integration tests that the records are trully created/updated/removed as expected
|
|
|
|
|
|
-import { DatabaseManager } from '@dzlzv/hydra-common'
|
|
|
-import { Bytes } from '@polkadot/types'
|
|
|
+import { DatabaseManager, EventContext, StoreContext } from '@dzlzv/hydra-common'
|
|
|
import ISO6391 from 'iso-639-1'
|
|
|
import { FindConditions } from 'typeorm'
|
|
|
-import * as jspb from 'google-protobuf'
|
|
|
import {
|
|
|
- ChannelMetadata,
|
|
|
- ChannelCategoryMetadata,
|
|
|
- PublishedBeforeJoystream as PublishedBeforeJoystreamMetadata,
|
|
|
- License as LicenseMetadata,
|
|
|
- MediaType as MediaTypeMetadata,
|
|
|
- VideoMetadata,
|
|
|
- VideoCategoryMetadata,
|
|
|
-} from '@joystream/content-metadata-protobuf'
|
|
|
-import { invalidMetadata, inconsistentState, logger, prepareDataObject } from '../common'
|
|
|
+ IVideoMetadata,
|
|
|
+ IPublishedBeforeJoystream,
|
|
|
+ ILicense,
|
|
|
+ IMediaType,
|
|
|
+ IChannelMetadata,
|
|
|
+} from '@joystream/metadata-protobuf'
|
|
|
+import {
|
|
|
+ invalidMetadata,
|
|
|
+ inconsistentState,
|
|
|
+ logger,
|
|
|
+ isSet,
|
|
|
+ integrateMeta,
|
|
|
+ unexpectedData,
|
|
|
+ createDataObject,
|
|
|
+} from '../common'
|
|
|
import {
|
|
|
// primary entities
|
|
|
CuratorGroup,
|
|
|
Channel,
|
|
|
- ChannelCategory,
|
|
|
Video,
|
|
|
VideoCategory,
|
|
|
// secondary entities
|
|
@@ -33,298 +36,186 @@ import {
|
|
|
License,
|
|
|
VideoMediaMetadata,
|
|
|
// asset
|
|
|
+ Asset,
|
|
|
DataObjectOwner,
|
|
|
DataObjectOwnerChannel,
|
|
|
- DataObject,
|
|
|
- LiaisonJudgement,
|
|
|
- AssetAvailability,
|
|
|
Membership,
|
|
|
+ VideoMediaEncoding,
|
|
|
+ AssetExternal,
|
|
|
+ AssetJoystreamStorage,
|
|
|
+ ChannelCategory,
|
|
|
} from 'query-node/dist/model'
|
|
|
// Joystream types
|
|
|
import { ContentParameters, NewAsset, ContentActor } from '@joystream/types/augment'
|
|
|
import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
|
|
|
import { registry } from '@joystream/types'
|
|
|
|
|
|
-/*
|
|
|
- Asset either stored in storage or describing list of URLs.
|
|
|
-*/
|
|
|
-type AssetStorageOrUrls = DataObject | string[]
|
|
|
-
|
|
|
-/*
|
|
|
- Type guard differentiating asset stored in storage from asset describing a list of URLs.
|
|
|
-*/
|
|
|
-function isAssetInStorage(dataObject: AssetStorageOrUrls): dataObject is DataObject {
|
|
|
- if (Array.isArray(dataObject)) {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- return true
|
|
|
-}
|
|
|
-
|
|
|
-export interface IReadProtobufArguments {
|
|
|
- metadata: Bytes
|
|
|
- store: DatabaseManager
|
|
|
- blockNumber: number
|
|
|
-}
|
|
|
-
|
|
|
-export interface IReadProtobufArgumentsWithAssets extends IReadProtobufArguments {
|
|
|
- assets: NewAsset[] // assets provided in event
|
|
|
- contentOwner: typeof DataObjectOwner
|
|
|
-}
|
|
|
+export async function processChannelMetadata(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ channel: Channel,
|
|
|
+ meta: IChannelMetadata,
|
|
|
+ assets: NewAsset[]
|
|
|
+): Promise<Channel> {
|
|
|
+ const assetsOwner = new DataObjectOwnerChannel()
|
|
|
+ assetsOwner.channelId = channel.id
|
|
|
|
|
|
-/*
|
|
|
- This class represents one of 3 possible states when changing property read from metadata.
|
|
|
- NoChange - don't change anything (used when invalid metadata are encountered)
|
|
|
- Unset - unset the value (used when the unset is requested in runtime)
|
|
|
- Change - set the new value
|
|
|
-*/
|
|
|
-export class PropertyChange<T> {
|
|
|
- static newUnset<T>(): PropertyChange<T> {
|
|
|
- return new PropertyChange<T>('unset')
|
|
|
- }
|
|
|
+ const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
|
|
|
|
|
|
- static newNoChange<T>(): PropertyChange<T> {
|
|
|
- return new PropertyChange<T>('nochange')
|
|
|
- }
|
|
|
+ integrateMeta(channel, meta, ['title', 'description', 'isPublic'])
|
|
|
|
|
|
- static newChange<T>(value: T): PropertyChange<T> {
|
|
|
- return new PropertyChange<T>('change', value)
|
|
|
+ // prepare channel category if needed
|
|
|
+ if (isSet(meta.category)) {
|
|
|
+ channel.category = await processChannelCategory(ctx, channel.category, meta.category.toNumber())
|
|
|
}
|
|
|
|
|
|
- /*
|
|
|
- Determines property change from the given object property.
|
|
|
- */
|
|
|
- static fromObjectProperty<T, Key extends string, ChangedObject extends { [key in Key]?: T }>(
|
|
|
- object: ChangedObject,
|
|
|
- key: Key
|
|
|
- ): PropertyChange<T> {
|
|
|
- if (!(key in object)) {
|
|
|
- return PropertyChange.newNoChange<T>()
|
|
|
+ // prepare cover photo asset if needed
|
|
|
+ if (isSet(meta.coverPhoto)) {
|
|
|
+ const asset = findAssetByIndex(processedAssets, meta.coverPhoto, 'channel cover photo')
|
|
|
+ if (asset) {
|
|
|
+ channel.coverPhoto = asset
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (object[key] === undefined) {
|
|
|
- return PropertyChange.newUnset<T>()
|
|
|
+ // prepare avatar photo asset if needed
|
|
|
+ if (isSet(meta.avatarPhoto)) {
|
|
|
+ const asset = findAssetByIndex(processedAssets, meta.avatarPhoto, 'channel avatar photo')
|
|
|
+ if (asset) {
|
|
|
+ channel.avatarPhoto = asset
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return PropertyChange.newChange<T>(object[key] as T)
|
|
|
+ // prepare language if needed
|
|
|
+ if (isSet(meta.language)) {
|
|
|
+ channel.language = await processLanguage(ctx, channel.language, meta.language)
|
|
|
}
|
|
|
|
|
|
- private type: string
|
|
|
- private value?: T
|
|
|
+ return channel
|
|
|
+}
|
|
|
|
|
|
- private constructor(type: 'change' | 'nochange' | 'unset', value?: T) {
|
|
|
- this.type = type
|
|
|
- this.value = value
|
|
|
- }
|
|
|
+export async function processVideoMetadata(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ channel: Channel,
|
|
|
+ video: Video,
|
|
|
+ meta: IVideoMetadata,
|
|
|
+ assets: NewAsset[]
|
|
|
+): Promise<Video> {
|
|
|
+ const assetsOwner = new DataObjectOwnerChannel()
|
|
|
+ assetsOwner.channelId = channel.id
|
|
|
|
|
|
- public isUnset(): boolean {
|
|
|
- return this.type === 'unset'
|
|
|
- }
|
|
|
+ const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
|
|
|
|
|
|
- public isNoChange(): boolean {
|
|
|
- return this.type === 'nochange'
|
|
|
+ integrateMeta(video, meta, ['title', 'description', 'duration', 'hasMarketing', 'isExplicit', 'isPublic'])
|
|
|
+
|
|
|
+ // prepare video category if needed
|
|
|
+ if (meta.category) {
|
|
|
+ video.category = await processVideoCategory(ctx, video.category, meta.category.toNumber())
|
|
|
}
|
|
|
|
|
|
- public isValue(): boolean {
|
|
|
- return this.type === 'change'
|
|
|
+ // prepare media meta information if needed
|
|
|
+ if (isSet(meta.mediaType) || isSet(meta.mediaPixelWidth) || isSet(meta.mediaPixelHeight)) {
|
|
|
+ // prepare video file size if poosible
|
|
|
+ const videoSize = extractVideoSize(assets, meta.video)
|
|
|
+ video.mediaMetadata = await processVideoMediaMetadata(ctx, video.mediaMetadata, meta, videoSize)
|
|
|
}
|
|
|
|
|
|
- public getValue(): T | undefined {
|
|
|
- return this.type === 'change' ? this.value : undefined
|
|
|
+ // prepare license if needed
|
|
|
+ if (isSet(meta.license)) {
|
|
|
+ video.license = await processLicense(ctx, video.license, meta.license)
|
|
|
}
|
|
|
|
|
|
- /*
|
|
|
- Integrates the value into the given dictionary.
|
|
|
- */
|
|
|
- public integrateInto(object: Record<string, unknown>, key: string): void {
|
|
|
- if (this.isNoChange()) {
|
|
|
- return
|
|
|
+ // prepare thumbnail photo asset if needed
|
|
|
+ if (isSet(meta.thumbnailPhoto)) {
|
|
|
+ const asset = findAssetByIndex(processedAssets, meta.thumbnailPhoto, 'thumbnail photo')
|
|
|
+ if (asset) {
|
|
|
+ video.thumbnailPhoto = asset
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (this.isUnset()) {
|
|
|
- delete object[key]
|
|
|
- return
|
|
|
+ // prepare video asset if needed
|
|
|
+ if (isSet(meta.video)) {
|
|
|
+ const asset = findAssetByIndex(processedAssets, meta.video, 'video')
|
|
|
+ if (asset) {
|
|
|
+ video.media = asset
|
|
|
}
|
|
|
-
|
|
|
- object[key] = this.value
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-export interface RawVideoMetadata {
|
|
|
- encoding: {
|
|
|
- codecName: PropertyChange<string>
|
|
|
- container: PropertyChange<string>
|
|
|
- mimeMediaType: PropertyChange<string>
|
|
|
+ // prepare language if needed
|
|
|
+ if (isSet(meta.language)) {
|
|
|
+ video.language = await processLanguage(ctx, video.language, meta.language)
|
|
|
}
|
|
|
- pixelWidth: PropertyChange<number>
|
|
|
- pixelHeight: PropertyChange<number>
|
|
|
- size: PropertyChange<number>
|
|
|
-}
|
|
|
|
|
|
-/*
|
|
|
- Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db.
|
|
|
-*/
|
|
|
-export async function readProtobuf<T extends ChannelCategory | VideoCategory>(
|
|
|
- type: T,
|
|
|
- parameters: IReadProtobufArguments
|
|
|
-): Promise<Partial<T>> {
|
|
|
- // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
|
|
|
- const metaU8a = parameters.metadata.toU8a(true)
|
|
|
-
|
|
|
- // process channel category
|
|
|
- if (type instanceof ChannelCategory) {
|
|
|
- const meta = ChannelCategoryMetadata.deserializeBinary(metaU8a)
|
|
|
- const result = convertMetadataToObject<ChannelCategoryMetadata.AsObject>(meta) as Partial<T>
|
|
|
-
|
|
|
- return result
|
|
|
+ if (isSet(meta.publishedBeforeJoystream)) {
|
|
|
+ video.publishedBeforeJoystream = processPublishedBeforeJoystream(
|
|
|
+ ctx,
|
|
|
+ video.publishedBeforeJoystream,
|
|
|
+ meta.publishedBeforeJoystream
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- // process video category
|
|
|
- if (type instanceof VideoCategory) {
|
|
|
- const meta = VideoCategoryMetadata.deserializeBinary(metaU8a)
|
|
|
- const result = convertMetadataToObject<VideoCategoryMetadata.AsObject>(meta) as Partial<T>
|
|
|
-
|
|
|
- return result
|
|
|
- }
|
|
|
-
|
|
|
- // this should never happen
|
|
|
- logger.error('Not implemented metadata type', { type })
|
|
|
- throw new Error(`Not implemented metadata type`)
|
|
|
+ return video
|
|
|
}
|
|
|
|
|
|
-/*
|
|
|
- Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db.
|
|
|
- In addition it handles any assets associated with the metadata.
|
|
|
-*/
|
|
|
-
|
|
|
-export async function readProtobufWithAssets<T extends Channel | Video>(
|
|
|
- type: T,
|
|
|
- parameters: IReadProtobufArgumentsWithAssets
|
|
|
-): Promise<Partial<T>> {
|
|
|
- // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
|
|
|
- const metaU8a = parameters.metadata.toU8a(true)
|
|
|
-
|
|
|
- // process channel
|
|
|
- if (type instanceof Channel) {
|
|
|
- const meta = ChannelMetadata.deserializeBinary(metaU8a)
|
|
|
- const metaAsObject = convertMetadataToObject<ChannelMetadata.AsObject>(meta)
|
|
|
- const result = (metaAsObject as any) as Partial<Channel>
|
|
|
-
|
|
|
- // prepare cover photo asset if needed
|
|
|
- if ('coverPhoto' in metaAsObject) {
|
|
|
- const asset = await extractAsset({
|
|
|
- // assetIndex: metaAsObject.coverPhoto,
|
|
|
- assetIndex: metaAsObject.coverPhoto,
|
|
|
- assets: parameters.assets,
|
|
|
- store: parameters.store,
|
|
|
- blockNumber: parameters.blockNumber,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
- integrateAsset('coverPhoto', result, asset) // changes `result` inline!
|
|
|
- delete metaAsObject.coverPhoto
|
|
|
- }
|
|
|
-
|
|
|
- // prepare avatar photo asset if needed
|
|
|
- if ('avatarPhoto' in metaAsObject) {
|
|
|
- const asset = await extractAsset({
|
|
|
- assetIndex: metaAsObject.avatarPhoto,
|
|
|
- assets: parameters.assets,
|
|
|
- store: parameters.store,
|
|
|
- blockNumber: parameters.blockNumber,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
- integrateAsset('avatarPhoto', result, asset) // changes `result` inline!
|
|
|
- delete metaAsObject.avatarPhoto
|
|
|
- }
|
|
|
-
|
|
|
- // prepare language if needed
|
|
|
- if ('language' in metaAsObject) {
|
|
|
- const language = await prepareLanguage(metaAsObject.language, parameters.store, parameters.blockNumber)
|
|
|
- delete metaAsObject.language // make sure temporary value will not interfere
|
|
|
- language.integrateInto(result, 'language')
|
|
|
- }
|
|
|
+function findAssetByIndex(assets: typeof Asset[], index: number, name?: string): typeof Asset | null {
|
|
|
+ if (assets[index]) {
|
|
|
+ return assets[index]
|
|
|
+ } else {
|
|
|
+ invalidMetadata(`Invalid${name ? ' ' + name : ''} asset index`, {
|
|
|
+ numberOfAssets: assets.length,
|
|
|
+ requestedAssetIndex: index,
|
|
|
+ })
|
|
|
|
|
|
- return result as Partial<T>
|
|
|
+ return null
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- // process video
|
|
|
- if (type instanceof Video) {
|
|
|
- const meta = VideoMetadata.deserializeBinary(metaU8a)
|
|
|
- const metaAsObject = convertMetadataToObject<VideoMetadata.AsObject>(meta)
|
|
|
- const result = (metaAsObject as any) as Partial<Video>
|
|
|
-
|
|
|
- // prepare video category if needed
|
|
|
- if ('category' in metaAsObject) {
|
|
|
- const category = await prepareVideoCategory(metaAsObject.category, parameters.store)
|
|
|
- delete metaAsObject.category // make sure temporary value will not interfere
|
|
|
- category.integrateInto(result, 'category')
|
|
|
- }
|
|
|
-
|
|
|
- // prepare media meta information if needed
|
|
|
- if ('mediaType' in metaAsObject || 'mediaPixelWidth' in metaAsObject || 'mediaPixelHeight' in metaAsObject) {
|
|
|
- // prepare video file size if poosible
|
|
|
- const videoSize = extractVideoSize(parameters.assets, metaAsObject.video)
|
|
|
-
|
|
|
- // NOTE: type hack - `RawVideoMetadata` is inserted instead of VideoMediaMetadata - it should be edited in `video.ts`
|
|
|
- // see `integrateVideoMetadata()` in `video.ts` for more info
|
|
|
- result.mediaMetadata = (prepareVideoMetadata(metaAsObject, videoSize) as unknown) as VideoMediaMetadata
|
|
|
-
|
|
|
- // remove extra values
|
|
|
- delete metaAsObject.mediaType
|
|
|
- delete metaAsObject.mediaPixelWidth
|
|
|
- delete metaAsObject.mediaPixelHeight
|
|
|
- }
|
|
|
-
|
|
|
- // prepare license if needed
|
|
|
- if ('license' in metaAsObject) {
|
|
|
- result.license = await prepareLicense(metaAsObject.license)
|
|
|
- }
|
|
|
-
|
|
|
- // prepare thumbnail photo asset if needed
|
|
|
- if ('thumbnailPhoto' in metaAsObject) {
|
|
|
- const asset = await extractAsset({
|
|
|
- assetIndex: metaAsObject.thumbnailPhoto,
|
|
|
- assets: parameters.assets,
|
|
|
- store: parameters.store,
|
|
|
- blockNumber: parameters.blockNumber,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
- integrateAsset('thumbnailPhoto', result, asset) // changes `result` inline!
|
|
|
- delete metaAsObject.thumbnailPhoto
|
|
|
- }
|
|
|
-
|
|
|
- // prepare video asset if needed
|
|
|
- if ('video' in metaAsObject) {
|
|
|
- const asset = await extractAsset({
|
|
|
- assetIndex: metaAsObject.video,
|
|
|
- assets: parameters.assets,
|
|
|
- store: parameters.store,
|
|
|
- blockNumber: parameters.blockNumber,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
- integrateAsset('media', result, asset) // changes `result` inline!
|
|
|
- delete metaAsObject.video
|
|
|
- }
|
|
|
+async function processVideoMediaEncoding(
|
|
|
+ { store, event }: StoreContext & EventContext,
|
|
|
+ existingVideoMediaEncoding: VideoMediaEncoding | undefined,
|
|
|
+ metadata: IMediaType
|
|
|
+): Promise<VideoMediaEncoding> {
|
|
|
+ const encoding =
|
|
|
+ existingVideoMediaEncoding ||
|
|
|
+ new VideoMediaEncoding({
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ createdById: '1',
|
|
|
+ updatedById: '1',
|
|
|
+ })
|
|
|
+ // integrate media encoding-related data
|
|
|
+ integrateMeta(encoding, metadata, ['codecName', 'container', 'mimeMediaType'])
|
|
|
+ encoding.updatedAt = new Date(event.blockTimestamp)
|
|
|
+ await store.save<VideoMediaEncoding>(encoding)
|
|
|
|
|
|
- // prepare language if needed
|
|
|
- if ('language' in metaAsObject) {
|
|
|
- const language = await prepareLanguage(metaAsObject.language, parameters.store, parameters.blockNumber)
|
|
|
- delete metaAsObject.language // make sure temporary value will not interfere
|
|
|
- language.integrateInto(result, 'language')
|
|
|
- }
|
|
|
+ return encoding
|
|
|
+}
|
|
|
|
|
|
- if (metaAsObject.publishedBeforeJoystream) {
|
|
|
- const publishedBeforeJoystream = handlePublishedBeforeJoystream(result, metaAsObject.publishedBeforeJoystream)
|
|
|
- delete metaAsObject.publishedBeforeJoystream // make sure temporary value will not interfere
|
|
|
- publishedBeforeJoystream.integrateInto(result, 'publishedBeforeJoystream')
|
|
|
- }
|
|
|
+async function processVideoMediaMetadata(
|
|
|
+ ctx: StoreContext & EventContext,
|
|
|
+ existingVideoMedia: VideoMediaMetadata | undefined,
|
|
|
+ metadata: IVideoMetadata,
|
|
|
+ videoSize: number | undefined
|
|
|
+): Promise<VideoMediaMetadata> {
|
|
|
+ const { store, event } = ctx
|
|
|
+ const videoMedia =
|
|
|
+ existingVideoMedia ||
|
|
|
+ new VideoMediaMetadata({
|
|
|
+ createdInBlock: event.blockNumber,
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ createdById: '1',
|
|
|
+ updatedById: '1',
|
|
|
+ })
|
|
|
|
|
|
- return result as Partial<T>
|
|
|
+ // integrate media-related data
|
|
|
+ const mediaMetadata = {
|
|
|
+ size: videoSize,
|
|
|
+ pixelWidth: metadata.mediaPixelWidth,
|
|
|
+ pixelHeight: metadata.mediaPixelHeight,
|
|
|
}
|
|
|
+ integrateMeta(videoMedia, mediaMetadata, ['pixelWidth', 'pixelHeight', 'size'])
|
|
|
+ videoMedia.updatedAt = new Date(event.blockTimestamp)
|
|
|
+ videoMedia.encoding = await processVideoMediaEncoding(ctx, videoMedia.encoding, metadata.mediaType || {})
|
|
|
+ await store.save<VideoMediaMetadata>(videoMedia)
|
|
|
|
|
|
- // this should never happen
|
|
|
- logger.error('Not implemented metadata type', { type })
|
|
|
- throw new Error(`Not implemented metadata type`)
|
|
|
+ return videoMedia
|
|
|
}
|
|
|
|
|
|
export async function convertContentActorToChannelOwner(
|
|
@@ -372,180 +263,60 @@ export async function convertContentActorToChannelOwner(
|
|
|
throw new Error('Not-implemented ContentActor type used')
|
|
|
}
|
|
|
|
|
|
-export function convertContentActorToDataObjectOwner(
|
|
|
- contentActor: ContentActor,
|
|
|
- channelId: number
|
|
|
-): typeof DataObjectOwner {
|
|
|
- const owner = new DataObjectOwnerChannel()
|
|
|
- owner.channelId = channelId.toString()
|
|
|
-
|
|
|
- return owner
|
|
|
-
|
|
|
- /* contentActor is irrelevant now -> all video/channel content belongs to the channel
|
|
|
- if (contentActor.isMember) {
|
|
|
- const owner = new DataObjectOwnerMember()
|
|
|
- owner.member = contentActor.asMember.toBn()
|
|
|
-
|
|
|
- return owner
|
|
|
- }
|
|
|
-
|
|
|
- if (contentActor.isLead || contentActor.isCurator) {
|
|
|
- const owner = new DataObjectOwnerChannel()
|
|
|
- owner.channel = channelId
|
|
|
-
|
|
|
- return owner
|
|
|
+function processPublishedBeforeJoystream(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentValue: Date | undefined,
|
|
|
+ metadata: IPublishedBeforeJoystream
|
|
|
+): Date | undefined {
|
|
|
+ if (!isSet(metadata)) {
|
|
|
+ return currentValue
|
|
|
}
|
|
|
|
|
|
- logger.error('Not implemented ContentActor type', {contentActor: contentActor.toString()})
|
|
|
- throw 'Not-implemented ContentActor type used'
|
|
|
- */
|
|
|
-}
|
|
|
-
|
|
|
-function handlePublishedBeforeJoystream(
|
|
|
- video: Partial<Video>,
|
|
|
- metadata: PublishedBeforeJoystreamMetadata.AsObject
|
|
|
-): PropertyChange<Date> {
|
|
|
- // is publish being unset
|
|
|
- if ('isPublished' in metadata && !metadata.isPublished) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
+ // Property is beeing unset
|
|
|
+ if (!metadata.isPublished) {
|
|
|
+ return undefined
|
|
|
}
|
|
|
|
|
|
// try to parse timestamp from publish date
|
|
|
- const timestamp = metadata.date ? Date.parse(metadata.date) : NaN
|
|
|
+ const timestamp = isSet(metadata.date) ? Date.parse(metadata.date) : NaN
|
|
|
|
|
|
// ensure date is valid
|
|
|
if (isNaN(timestamp)) {
|
|
|
invalidMetadata(`Invalid date used for publishedBeforeJoystream`, {
|
|
|
timestamp,
|
|
|
})
|
|
|
- return PropertyChange.newNoChange()
|
|
|
+ return currentValue
|
|
|
}
|
|
|
|
|
|
// set new date
|
|
|
- return PropertyChange.newChange(new Date(timestamp))
|
|
|
-}
|
|
|
-
|
|
|
-interface IConvertAssetParameters {
|
|
|
- rawAsset: NewAsset
|
|
|
- store: DatabaseManager
|
|
|
- blockNumber: number
|
|
|
- contentOwner: typeof DataObjectOwner
|
|
|
-}
|
|
|
-
|
|
|
-/*
|
|
|
- Converts event asset into data object or list of URLs fit to be saved to db.
|
|
|
-*/
|
|
|
-async function convertAsset(parameters: IConvertAssetParameters): Promise<AssetStorageOrUrls> {
|
|
|
- // is asset describing list of URLs?
|
|
|
- if (parameters.rawAsset.isUrls) {
|
|
|
- const urls = parameters.rawAsset.asUrls.toArray().map((item) => item.toString())
|
|
|
-
|
|
|
- return urls
|
|
|
- }
|
|
|
-
|
|
|
- // !parameters.rawAsset.isUrls && parameters.rawAsset.isUpload // asset is in storage
|
|
|
-
|
|
|
- // prepare data object
|
|
|
- const contentParameters: ContentParameters = parameters.rawAsset.asUpload
|
|
|
- const dataObject = await prepareDataObject(contentParameters, parameters.blockNumber, parameters.contentOwner)
|
|
|
-
|
|
|
- return dataObject
|
|
|
-}
|
|
|
-
|
|
|
-interface IExtractAssetParameters {
|
|
|
- assetIndex: number | undefined
|
|
|
- assets: NewAsset[]
|
|
|
- store: DatabaseManager
|
|
|
- blockNumber: number
|
|
|
- contentOwner: typeof DataObjectOwner
|
|
|
+ return new Date(timestamp)
|
|
|
}
|
|
|
|
|
|
-/*
|
|
|
- Selects asset from provided set of assets and prepares asset data fit to be saved to db.
|
|
|
-*/
|
|
|
-async function extractAsset(parameters: IExtractAssetParameters): Promise<PropertyChange<AssetStorageOrUrls>> {
|
|
|
- // is asset being unset?
|
|
|
- if (parameters.assetIndex === undefined) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
+async function processNewAsset(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ asset: NewAsset,
|
|
|
+ owner: typeof DataObjectOwner
|
|
|
+): Promise<typeof Asset> {
|
|
|
+ if (asset.isUrls) {
|
|
|
+ const urls = asset.asUrls.toArray().map((url) => url.toString())
|
|
|
+ const resultAsset = new AssetExternal()
|
|
|
+ resultAsset.urls = JSON.stringify(urls)
|
|
|
+ return resultAsset
|
|
|
+ } else if (asset.isUpload) {
|
|
|
+ const contentParameters: ContentParameters = asset.asUpload
|
|
|
+ const dataObject = await createDataObject(ctx, contentParameters, owner)
|
|
|
+
|
|
|
+ const resultAsset = new AssetJoystreamStorage()
|
|
|
+ resultAsset.dataObjectId = dataObject.id
|
|
|
+ return resultAsset
|
|
|
+ } else {
|
|
|
+ unexpectedData('Unrecognized asset type', asset.type)
|
|
|
}
|
|
|
-
|
|
|
- // ensure asset index is valid
|
|
|
- if (parameters.assetIndex >= parameters.assets.length) {
|
|
|
- invalidMetadata(`Non-existing asset extraction requested`, {
|
|
|
- assetsProvided: parameters.assets.length,
|
|
|
- assetIndex: parameters.assetIndex,
|
|
|
- })
|
|
|
- return PropertyChange.newNoChange()
|
|
|
- }
|
|
|
-
|
|
|
- // convert asset to data object record
|
|
|
- const asset = await convertAsset({
|
|
|
- rawAsset: parameters.assets[parameters.assetIndex],
|
|
|
- store: parameters.store,
|
|
|
- blockNumber: parameters.blockNumber,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
-
|
|
|
- return PropertyChange.newChange(asset)
|
|
|
}
|
|
|
|
|
|
-/*
|
|
|
- As a temporary messure to overcome yet-to-be-implemented features in Hydra, we are using redudant information
|
|
|
- to describe asset state. This function introduces all redudant data needed to be saved to db.
|
|
|
-
|
|
|
- Changes `result` argument!
|
|
|
-*/
|
|
|
-function integrateAsset<T>(
|
|
|
- propertyName: string,
|
|
|
- result: Record<string, unknown>,
|
|
|
- asset: PropertyChange<AssetStorageOrUrls>
|
|
|
-): void {
|
|
|
- // helpers - property names
|
|
|
- const nameUrl = propertyName + 'Urls'
|
|
|
- const nameDataObject = propertyName + 'DataObject'
|
|
|
- const nameAvailability = propertyName + 'Availability'
|
|
|
-
|
|
|
- if (asset.isNoChange()) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (asset.isUnset()) {
|
|
|
- result[nameUrl] = []
|
|
|
- result[nameAvailability] = AssetAvailability.INVALID
|
|
|
- result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
|
|
|
-
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const newValue = asset.getValue() as AssetStorageOrUrls
|
|
|
-
|
|
|
- // is asset available on external URL(s)
|
|
|
- if (!isAssetInStorage(newValue)) {
|
|
|
- // (un)set asset's properties
|
|
|
- result[nameUrl] = newValue
|
|
|
- result[nameAvailability] = AssetAvailability.ACCEPTED
|
|
|
- result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
|
|
|
-
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // asset saved in storage
|
|
|
-
|
|
|
- // prepare conversion table between liaison judgment and asset availability
|
|
|
- const conversionTable = {
|
|
|
- [LiaisonJudgement.ACCEPTED]: AssetAvailability.ACCEPTED,
|
|
|
- [LiaisonJudgement.PENDING]: AssetAvailability.PENDING,
|
|
|
- }
|
|
|
-
|
|
|
- // (un)set asset's properties
|
|
|
- result[nameUrl] = [] // plan deletion (will have effect when saved to db)
|
|
|
- result[nameAvailability] = conversionTable[newValue.liaisonJudgement]
|
|
|
- result[nameDataObject] = newValue
|
|
|
-}
|
|
|
-
|
|
|
-function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): number | undefined {
|
|
|
+function extractVideoSize(assets: NewAsset[], assetIndex: number | null | undefined): number | undefined {
|
|
|
// escape if no asset is required
|
|
|
- if (assetIndex === undefined) {
|
|
|
+ if (!isSet(assetIndex)) {
|
|
|
return undefined
|
|
|
}
|
|
|
|
|
@@ -572,14 +343,15 @@ function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): n
|
|
|
return videoSize
|
|
|
}
|
|
|
|
|
|
-async function prepareLanguage(
|
|
|
- languageIso: string | undefined,
|
|
|
- store: DatabaseManager,
|
|
|
- blockNumber: number
|
|
|
-): Promise<PropertyChange<Language>> {
|
|
|
- // is language being unset?
|
|
|
- if (languageIso === undefined) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
+async function processLanguage(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentLanguage: Language | undefined,
|
|
|
+ languageIso: string | undefined
|
|
|
+): Promise<Language | undefined> {
|
|
|
+ const { event, store } = ctx
|
|
|
+
|
|
|
+ if (!isSet(languageIso)) {
|
|
|
+ return currentLanguage
|
|
|
}
|
|
|
|
|
|
// validate language string
|
|
@@ -588,22 +360,23 @@ async function prepareLanguage(
|
|
|
// ensure language string is valid
|
|
|
if (!isValidIso) {
|
|
|
invalidMetadata(`Invalid language ISO-639-1 provided`, languageIso)
|
|
|
- return PropertyChange.newNoChange()
|
|
|
+ return currentLanguage
|
|
|
}
|
|
|
|
|
|
// load language
|
|
|
- const language = await store.get(Language, { where: { iso: languageIso } as FindConditions<Language> })
|
|
|
+ const existingLanguage = await store.get(Language, { where: { iso: languageIso } })
|
|
|
|
|
|
// return existing language if any
|
|
|
- if (language) {
|
|
|
- return PropertyChange.newChange(language)
|
|
|
+ if (existingLanguage) {
|
|
|
+ return existingLanguage
|
|
|
}
|
|
|
|
|
|
// create new language
|
|
|
const newLanguage = new Language({
|
|
|
iso: languageIso,
|
|
|
- createdInBlock: blockNumber,
|
|
|
-
|
|
|
+ createdInBlock: event.blockNumber,
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ updatedAt: new Date(event.blockTimestamp),
|
|
|
// TODO: remove these lines after Hydra auto-fills the values when cascading save (remove them on all places)
|
|
|
createdById: '1',
|
|
|
updatedById: '1',
|
|
@@ -611,30 +384,40 @@ async function prepareLanguage(
|
|
|
|
|
|
await store.save<Language>(newLanguage)
|
|
|
|
|
|
- return PropertyChange.newChange(newLanguage)
|
|
|
+ return newLanguage
|
|
|
}
|
|
|
|
|
|
-async function prepareLicense(licenseProtobuf: LicenseMetadata.AsObject | undefined): Promise<License | undefined> {
|
|
|
- // NOTE: Deletion of any previous license should take place in appropriate event handling function
|
|
|
- // and not here even it might appear so.
|
|
|
+async function processLicense(
|
|
|
+ ctx: StoreContext & EventContext,
|
|
|
+ existingLicense: License | undefined,
|
|
|
+ metadata: ILicense | null | undefined
|
|
|
+): Promise<License | undefined> {
|
|
|
+ const { store, event } = ctx
|
|
|
|
|
|
- // is license being unset?
|
|
|
- if (licenseProtobuf === undefined) {
|
|
|
- return undefined
|
|
|
+ if (!isSet(metadata)) {
|
|
|
+ return existingLicense
|
|
|
}
|
|
|
|
|
|
- // license is meant to be deleted
|
|
|
- if (isLicenseEmpty(licenseProtobuf)) {
|
|
|
- return new License({})
|
|
|
+ if (isLicenseEmpty(metadata)) {
|
|
|
+ // license is meant to be deleted
|
|
|
+ if (existingLicense) {
|
|
|
+ await store.remove<License>(existingLicense)
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
}
|
|
|
|
|
|
- // crete new license
|
|
|
- const license = new License({
|
|
|
- ...licenseProtobuf,
|
|
|
+ // license is meant to be created/updated
|
|
|
+ const license =
|
|
|
+ existingLicense ||
|
|
|
+ new License({
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ createdById: '1',
|
|
|
+ updatedById: '1',
|
|
|
+ })
|
|
|
+ license.updatedAt = new Date(event.blockTimestamp)
|
|
|
+ integrateMeta(license, metadata, ['attribution', 'code', 'customText'])
|
|
|
|
|
|
- createdById: '1',
|
|
|
- updatedById: '1',
|
|
|
- })
|
|
|
+ await store.save<License>(license)
|
|
|
|
|
|
return license
|
|
|
}
|
|
@@ -643,94 +426,50 @@ async function prepareLicense(licenseProtobuf: LicenseMetadata.AsObject | undefi
|
|
|
Checks if protobof contains license with some fields filled or is empty object (`{}` or `{someKey: undefined, ...}`).
|
|
|
Empty object means deletion is requested.
|
|
|
*/
|
|
|
-function isLicenseEmpty(licenseObject: LicenseMetadata.AsObject): boolean {
|
|
|
- const somePropertySet = Object.entries(licenseObject).reduce((acc, [key, value]) => {
|
|
|
- return acc || value !== undefined
|
|
|
- }, false)
|
|
|
+function isLicenseEmpty(licenseObject: ILicense): boolean {
|
|
|
+ const somePropertySet = Object.values(licenseObject).some((v) => isSet(v))
|
|
|
|
|
|
return !somePropertySet
|
|
|
}
|
|
|
|
|
|
-function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject, videoSize: number | undefined): RawVideoMetadata {
|
|
|
- const rawMeta = {
|
|
|
- encoding: {
|
|
|
- codecName: PropertyChange.fromObjectProperty<string, 'codecName', MediaTypeMetadata.AsObject>(
|
|
|
- videoProtobuf.mediaType || {},
|
|
|
- 'codecName'
|
|
|
- ),
|
|
|
- container: PropertyChange.fromObjectProperty<string, 'container', MediaTypeMetadata.AsObject>(
|
|
|
- videoProtobuf.mediaType || {},
|
|
|
- 'container'
|
|
|
- ),
|
|
|
- mimeMediaType: PropertyChange.fromObjectProperty<string, 'mimeMediaType', MediaTypeMetadata.AsObject>(
|
|
|
- videoProtobuf.mediaType || {},
|
|
|
- 'mimeMediaType'
|
|
|
- ),
|
|
|
- },
|
|
|
- pixelWidth: PropertyChange.fromObjectProperty<number, 'mediaPixelWidth', VideoMetadata.AsObject>(
|
|
|
- videoProtobuf,
|
|
|
- 'mediaPixelWidth'
|
|
|
- ),
|
|
|
- pixelHeight: PropertyChange.fromObjectProperty<number, 'mediaPixelHeight', VideoMetadata.AsObject>(
|
|
|
- videoProtobuf,
|
|
|
- 'mediaPixelHeight'
|
|
|
- ),
|
|
|
- size: videoSize === undefined ? PropertyChange.newNoChange() : PropertyChange.newChange(videoSize),
|
|
|
- } as RawVideoMetadata
|
|
|
-
|
|
|
- return rawMeta
|
|
|
-}
|
|
|
-
|
|
|
-async function prepareVideoCategory(
|
|
|
- categoryId: number | undefined,
|
|
|
- store: DatabaseManager
|
|
|
-): Promise<PropertyChange<VideoCategory>> {
|
|
|
- // is category being unset?
|
|
|
- if (categoryId === undefined) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
- }
|
|
|
+async function processVideoCategory(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentCategory: VideoCategory | undefined,
|
|
|
+ categoryId: number
|
|
|
+): Promise<VideoCategory | undefined> {
|
|
|
+ const { store } = ctx
|
|
|
|
|
|
// load video category
|
|
|
const category = await store.get(VideoCategory, {
|
|
|
- where: { id: categoryId.toString() } as FindConditions<VideoCategory>,
|
|
|
+ where: { id: categoryId.toString() },
|
|
|
})
|
|
|
|
|
|
// ensure video category exists
|
|
|
if (!category) {
|
|
|
invalidMetadata('Non-existing video category association with video requested', categoryId)
|
|
|
- return PropertyChange.newNoChange()
|
|
|
+ return currentCategory
|
|
|
}
|
|
|
|
|
|
- return PropertyChange.newChange(category)
|
|
|
+ return category
|
|
|
}
|
|
|
|
|
|
-function convertMetadataToObject<T>(metadata: jspb.Message): T {
|
|
|
- const metaAsObject = metadata.toObject()
|
|
|
- const result = {} as T
|
|
|
-
|
|
|
- for (const key in metaAsObject) {
|
|
|
- const funcNameBase = key.charAt(0).toUpperCase() + key.slice(1)
|
|
|
- const hasFuncName = 'has' + funcNameBase
|
|
|
- const isSet =
|
|
|
- funcNameBase === 'PersonsList' // there is no `VideoMetadata.hasPersonsList` method from unkown reason -> create exception
|
|
|
- ? true
|
|
|
- : metadata[hasFuncName]()
|
|
|
+async function processChannelCategory(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentCategory: ChannelCategory | undefined,
|
|
|
+ categoryId: number
|
|
|
+): Promise<ChannelCategory | undefined> {
|
|
|
+ const { store } = ctx
|
|
|
|
|
|
- if (!isSet) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- const getFuncName = 'get' + funcNameBase
|
|
|
- const value = metadata[getFuncName]()
|
|
|
-
|
|
|
- // TODO: check that recursion trully works
|
|
|
- if (value instanceof jspb.Message) {
|
|
|
- result[key] = convertMetadataToObject(value)
|
|
|
- continue
|
|
|
- }
|
|
|
+ // load video category
|
|
|
+ const category = await store.get(ChannelCategory, {
|
|
|
+ where: { id: categoryId.toString() },
|
|
|
+ })
|
|
|
|
|
|
- result[key] = metaAsObject[key]
|
|
|
+ // ensure video category exists
|
|
|
+ if (!category) {
|
|
|
+ invalidMetadata('Non-existing channel category association with channel requested', categoryId)
|
|
|
+ return currentCategory
|
|
|
}
|
|
|
|
|
|
- return result
|
|
|
+ return category
|
|
|
}
|