Browse Source

Merge pull request #2371 from ondratra/query_node_video_metadata

Query node video metadata
Mokhtar Naamani 3 years ago
parent
commit
43e824dded

+ 14 - 14
query-node/generated/graphql-server/generated/binding.ts

@@ -858,12 +858,12 @@ export interface DataObjectWhereInput {
   typeId_lt?: Int | null
   typeId_lte?: Int | null
   typeId_in?: Int[] | Int | null
-  size_eq?: Int | null
-  size_gt?: Int | null
-  size_gte?: Int | null
-  size_lt?: Int | null
-  size_lte?: Int | null
-  size_in?: Int[] | Int | null
+  size_eq?: Float | null
+  size_gt?: Float | null
+  size_gte?: Float | null
+  size_lt?: Float | null
+  size_lte?: Float | null
+  size_in?: Float[] | Float | null
   liaisonId_eq?: ID_Input | null
   liaisonId_in?: ID_Output[] | ID_Output | null
   liaisonJudgement_eq?: LiaisonJudgement | null
@@ -1318,12 +1318,12 @@ export interface VideoMediaMetadataWhereInput {
   pixelHeight_lt?: Int | null
   pixelHeight_lte?: Int | null
   pixelHeight_in?: Int[] | Int | null
-  size_eq?: Int | null
-  size_gt?: Int | null
-  size_gte?: Int | null
-  size_lt?: Int | null
-  size_lte?: Int | null
-  size_in?: Int[] | Int | null
+  size_eq?: Float | null
+  size_gt?: Float | null
+  size_gte?: Float | null
+  size_lt?: Float | null
+  size_lte?: Float | null
+  size_in?: Float[] | Float | null
   createdInBlock_eq?: Int | null
   createdInBlock_gt?: Int | null
   createdInBlock_gte?: Int | null
@@ -1664,7 +1664,7 @@ export interface DataObject extends BaseGraphQLObject {
   owner: DataObjectOwner
   createdInBlock: Int
   typeId: Int
-  size: Int
+  size: Float
   liaison?: Worker | null
   liaisonId?: String | null
   liaisonJudgement: LiaisonJudgement
@@ -1974,7 +1974,7 @@ export interface VideoMediaMetadata extends BaseGraphQLObject {
   encodingId?: String | null
   pixelWidth?: Int | null
   pixelHeight?: Int | null
-  size?: Int | null
+  size?: Float | null
   video?: Video | null
   createdInBlock: Int
 }

+ 12 - 12
query-node/generated/graphql-server/generated/classes.ts

@@ -2269,22 +2269,22 @@ export class VideoMediaMetadataWhereInput {
   @TypeGraphQLField(() => [Int], { nullable: true })
   pixelHeight_in?: number[];
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_eq?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_gt?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_gte?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_lt?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_lte?: number;
 
-  @TypeGraphQLField(() => [Int], { nullable: true })
+  @TypeGraphQLField(() => [Float], { nullable: true })
   size_in?: number[];
 
   @TypeGraphQLField(() => Int, { nullable: true })
@@ -3127,22 +3127,22 @@ export class DataObjectWhereInput {
   @TypeGraphQLField(() => [Int], { nullable: true })
   typeId_in?: number[];
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_eq?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_gt?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_gte?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_lt?: number;
 
-  @TypeGraphQLField(() => Int, { nullable: true })
+  @TypeGraphQLField(() => Float, { nullable: true })
   size_lte?: number;
 
-  @TypeGraphQLField(() => [Int], { nullable: true })
+  @TypeGraphQLField(() => [Float], { nullable: true })
   size_in?: number[];
 
   @TypeGraphQLField(() => ID, { nullable: true })

+ 14 - 14
query-node/generated/graphql-server/generated/schema.graphql

@@ -477,7 +477,7 @@ type DataObject implements BaseGraphQLObject {
   typeId: Int!
 
   """Content size in bytes"""
-  size: Int!
+  size: Float!
   liaison: Worker
   liaisonId: String
 
@@ -863,12 +863,12 @@ input DataObjectWhereInput {
   typeId_lt: Int
   typeId_lte: Int
   typeId_in: [Int!]
-  size_eq: Int
-  size_gt: Int
-  size_gte: Int
-  size_lt: Int
-  size_lte: Int
-  size_in: [Int!]
+  size_eq: Float
+  size_gt: Float
+  size_gte: Float
+  size_lt: Float
+  size_lte: Float
+  size_in: [Float!]
   liaisonId_eq: ID
   liaisonId_in: [ID!]
   liaisonJudgement_eq: LiaisonJudgement
@@ -1769,7 +1769,7 @@ type VideoMediaMetadata implements BaseGraphQLObject {
   pixelHeight: Int
 
   """Video media size in bytes"""
-  size: Int
+  size: Float
   video: Video
   createdInBlock: Int!
 }
@@ -1859,12 +1859,12 @@ input VideoMediaMetadataWhereInput {
   pixelHeight_lt: Int
   pixelHeight_lte: Int
   pixelHeight_in: [Int!]
-  size_eq: Int
-  size_gt: Int
-  size_gte: Int
-  size_lt: Int
-  size_lte: Int
-  size_in: [Int!]
+  size_eq: Float
+  size_gt: Float
+  size_gte: Float
+  size_lt: Float
+  size_lte: Float
+  size_in: [Float!]
   createdInBlock_eq: Int
   createdInBlock_gt: Int
   createdInBlock_gte: Int

+ 6 - 2
query-node/generated/graphql-server/src/modules/data-object/data-object.model.ts

@@ -1,4 +1,4 @@
-import { BaseModel, IntField, Model, ManyToOne, OneToMany, EnumField, StringField } from 'warthog';
+import { BaseModel, FloatField, IntField, Model, ManyToOne, OneToMany, EnumField, StringField } from 'warthog';
 
 import { Column } from 'typeorm';
 import { Field } from 'type-graphql';
@@ -32,7 +32,11 @@ export class DataObject extends BaseModel {
   })
   typeId!: number;
 
-  @IntField({
+  // Size is meant to be integer, but since `IntField` represents only 4-bytes long number
+  // (sadly, `dataType: bigint` settings only fixes DB, but GraphQL server still uses 4-bytes)
+  // `NumericField` seems to always return string (when using transform directive number<->string)
+  // `FloatField` field fixes this issue.
+  @FloatField({
     description: `Content size in bytes`,
   })
   size!: number;

+ 6 - 2
query-node/generated/graphql-server/src/modules/video-media-metadata/video-media-metadata.model.ts

@@ -1,4 +1,4 @@
-import { BaseModel, IntField, Model, ManyToOne, OneToOne, OneToOneJoin, StringField } from 'warthog';
+import { BaseModel, IntField, FloatField, Model, ManyToOne, OneToOne, OneToOneJoin, StringField } from 'warthog';
 
 import { VideoMediaEncoding } from '../video-media-encoding/video-media-encoding.model';
 import { Video } from '../video/video.model';
@@ -24,7 +24,11 @@ export class VideoMediaMetadata extends BaseModel {
   })
   pixelHeight?: number;
 
-  @IntField({
+  // Size is meant to be integer, but since `IntField` represents only 4-bytes long number
+  // (sadly, `dataType: bigint` settings only fixes DB, but GraphQL server still uses 4-bytes)
+  // `NumericField` seems to always return string (when using transform directive number<->string)
+  // `FloatField` field fixes this issue.
+  @FloatField({
     nullable: true,
     description: `Video media size in bytes`,
   })

+ 78 - 50
query-node/mappings/src/content/utils.ts

@@ -31,6 +31,7 @@ import {
 
 import {
   invalidMetadata,
+  inconsistentState,
   logger,
   prepareDataObject,
 } from '../common'
@@ -107,18 +108,37 @@ export interface IReadProtobufArgumentsWithAssets extends IReadProtobufArguments
 */
 export class PropertyChange<T> {
 
-  static newUnset<T>() {
+  static newUnset<T>(): PropertyChange<T> {
     return new PropertyChange<T>('unset')
   }
 
-  static newNoChange<T>() {
+  static newNoChange<T>(): PropertyChange<T> {
     return new PropertyChange<T>('nochange')
   }
 
-  static newChange<T>(value: T) {
+  static newChange<T>(value: T): PropertyChange<T> {
     return new PropertyChange<T>('change', value)
   }
 
+  /*
+    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>()
+    }
+
+    if (object[key] === undefined) {
+      return PropertyChange.newUnset<T>()
+    }
+
+    return PropertyChange.newChange<T>(object[key] as T)
+  }
+
   private type: string
   private value?: T
 
@@ -162,6 +182,17 @@ export class PropertyChange<T> {
   }
 }
 
+export interface RawVideoMetadata {
+  encoding: {
+    codecName: PropertyChange<string>
+    container: PropertyChange<string>
+    mimeMediaType: PropertyChange<string>
+  }
+  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.
 */
@@ -264,9 +295,11 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
     // prepare media meta information if needed
     if ('mediaType' in metaAsObject || 'mediaPixelWidth' in metaAsObject || 'mediaPixelHeight' in metaAsObject) {
       // prepare video file size if poosible
-      const videoSize = await extractVideoSize(parameters.assets, metaAsObject.video)
+      const videoSize = extractVideoSize(parameters.assets, metaAsObject.video)
 
-      result.mediaMetadata = await prepareVideoMetadata(metaAsObject, videoSize, parameters.blockNumber)
+      // 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, parameters.blockNumber) as unknown as VideoMediaMetadata
 
       // remove extra values
       delete metaAsObject.mediaType
@@ -312,12 +345,10 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
       language.integrateInto(result, 'language')
     }
 
-    // prepare information about media published somewhere else before Joystream if needed.
-    if (metaAsObject.publishedBeforeJoystream && metaAsObject.publishedBeforeJoystream.isPublished) {
-      // this will change the `channel`!
-      handlePublishedBeforeJoystream(result, metaAsObject.publishedBeforeJoystream.date)
-    } else {
-      delete metaAsObject.publishedBeforeJoystream // make sure the object is unset
+    if (metaAsObject.publishedBeforeJoystream) {
+      const publishedBeforeJoystream = handlePublishedBeforeJoystream(result, metaAsObject.publishedBeforeJoystream)
+      delete metaAsObject.publishedBeforeJoystream // make sure temporary value will not interfere
+      publishedBeforeJoystream.integrateInto(result, 'publishedBeforeJoystream')
     }
 
     return result as Partial<T>
@@ -338,7 +369,7 @@ export async function convertContentActorToChannelOwner(db: DatabaseManager, con
 
     // ensure member exists
     if (!member) {
-      invalidMetadata(`Actor is non-existing member`, memberId)
+      inconsistentState(`Actor is non-existing member`, memberId)
       return {} // this will keep fields unchanged
     }
 
@@ -354,7 +385,7 @@ export async function convertContentActorToChannelOwner(db: DatabaseManager, con
 
     // ensure curator group exists
     if (!curatorGroup) {
-      invalidMetadata('Actor is non-existing curator group', curatorGroupId)
+      inconsistentState('Actor is non-existing curator group', curatorGroupId)
       return {} // this will keep fields unchanged
     }
 
@@ -396,16 +427,27 @@ export function convertContentActorToDataObjectOwner(contentActor: ContentActor,
   */
 }
 
-function handlePublishedBeforeJoystream(video: Partial<Video>, publishedAtString?: string) {
-  // published elsewhere before Joystream
-  if (publishedAtString) {
-    video.publishedBeforeJoystream = new Date(publishedAtString)
+function handlePublishedBeforeJoystream(video: Partial<Video>, metadata: PublishedBeforeJoystreamMetadata.AsObject): PropertyChange<Date> {
+  // is publish being unset
+  if ('isPublished' in metadata && !metadata.isPublished) {
+    return PropertyChange.newUnset()
+  }
 
-    return
+  // try to parse timestamp from publish date
+  const timestamp = metadata.date
+    ? Date.parse(metadata.date)
+    : NaN
+
+  // ensure date is valid
+  if (isNaN(timestamp)) {
+    invalidMetadata(`Invalid date used for publishedBeforeJoystream`, {
+      timestamp
+    })
+    return PropertyChange.newNoChange()
   }
 
-  // unset publish info
-  video.publishedBeforeJoystream = undefined // plan deletion (will have effect when saved to db)
+  // set new date
+  return PropertyChange.newChange(new Date(timestamp))
 }
 
 interface IConvertAssetParameters {
@@ -522,7 +564,7 @@ function integrateAsset<T>(propertyName: string, result: Object, asset: Property
   result[nameDataObject] = newValue
 }
 
-async function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): Promise<number | undefined> {
+function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): number | undefined {
   // escape if no asset is required
   if (assetIndex === undefined) {
     return undefined
@@ -610,35 +652,21 @@ async function prepareLicense(licenseProtobuf: LicenseMetadata.AsObject | undefi
   return license
 }
 
-async function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject, videoSize: number | undefined, blockNumber: number): Promise<VideoMediaMetadata> {
-  // TODO: handle situations when only some metadata are set (e.g. pixelWidth and mediaType is defined, but pixelHeight is missing)
-  // TODO: handle update of VideoMediaEncoding and VideoMediaMetadata
-  //       right now when only some of mediaType(mb partial), mediaPixelWidth, or mediaPixelHeight is set, the update discards previous values
-  // create new encoding info
-  const encoding = new VideoMediaEncoding({
-    ...videoProtobuf.mediaType,
-
-    createdById: '1',
-    updatedById: '1',
-  })
-
-  // create new video metadata
-  const videoMeta = new VideoMediaMetadata({
-    encoding,
-    pixelWidth: videoProtobuf.mediaPixelWidth,
-    pixelHeight: videoProtobuf.mediaPixelHeight,
-    createdInBlock: blockNumber,
-
-    createdById: '1',
-    updatedById: '1',
-  })
-
-  // fill in video size if provided
-  if (videoSize !== undefined) {
-    videoMeta.size = videoSize
-  }
-
-  return videoMeta
+function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject, videoSize: number | undefined, blockNumber: number): 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, db: DatabaseManager): Promise<PropertyChange<VideoCategory>> {

+ 67 - 4
query-node/mappings/src/content/video.ts

@@ -16,7 +16,8 @@ import {
 import {
   convertContentActorToDataObjectOwner,
   readProtobuf,
-  readProtobufWithAssets
+  readProtobufWithAssets,
+  RawVideoMetadata,
 } from './utils'
 
 // primary entities
@@ -25,6 +26,8 @@ import {
   Channel,
   Video,
   VideoCategory,
+  VideoMediaEncoding,
+  VideoMediaMetadata,
 } from 'query-node'
 
 // secondary entities
@@ -182,6 +185,9 @@ export async function content_VideoCreated(
     inconsistentState('Trying to add video to non-existing channel', channelId)
   }
 
+  // prepare video media metadata (if any)
+  const fixedProtobuf = integrateVideoMediaMetadata(null, protobufContent, event.blockNumber)
+
   // create new video
   const video = new Video({
     // main data
@@ -203,7 +209,7 @@ export async function content_VideoCreated(
     updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
 
     // integrate metadata
-    ...protobufContent
+    ...fixedProtobuf
   })
 
   // save video
@@ -226,7 +232,7 @@ export async function content_VideoUpdated(
   } = new Content.VideoUpdatedEvent(event).data
 
   // load video
-  const video = await db.get(Video, { where: { id: videoId.toString() } as FindConditions<Video> })
+  const video = await db.get(Video, { where: { id: videoId.toString() } as FindConditions<Video>, relations: ['channel', 'license'] })
 
   // ensure video exists
   if (!video) {
@@ -249,11 +255,14 @@ export async function content_VideoUpdated(
       }
     )
 
+    // prepare video media metadata (if any)
+    const fixedProtobuf = integrateVideoMediaMetadata(video, protobufContent, event.blockNumber)
+
     // remember original license
     const originalLicense = video.license
 
     // update all fields read from protobuf
-    for (let [key, value] of Object.entries(protobufContent)) {
+    for (let [key, value] of Object.entries(fixedProtobuf)) {
       video[key] = value
     }
 
@@ -392,3 +401,57 @@ export async function content_FeaturedVideosSet(
   // emit log event
   logger.info('New featured videos have been set', {videoIds})
 }
+
+/////////////////// Helpers ////////////////////////////////////////////////////
+
+/*
+  Integrates video metadata-related data into existing data (if any) or creates a new record.
+
+  NOTE: type hack - `RawVideoMetadata` is accepted for `metadata` instead of `Partial<Video>`
+        see `prepareVideoMetadata()` in `utils.ts` for more info
+*/
+function integrateVideoMediaMetadata(
+  existingRecord: Video | null,
+  metadata: Partial<Video>,
+  blockNumber: number,
+): Partial<Video> {
+  if (!metadata.mediaMetadata) {
+    return metadata
+  }
+
+  // fix TS type
+  const rawMediaMetadata = metadata.mediaMetadata as unknown as RawVideoMetadata
+
+  // ensure encoding object
+  const encoding = (existingRecord && existingRecord.mediaMetadata && existingRecord.mediaMetadata.encoding)
+    || new VideoMediaEncoding({
+        createdById: '1',
+        updatedById: '1',
+      })
+
+  // integrate media encoding-related data
+  rawMediaMetadata.encoding.codecName.integrateInto(encoding, 'codecName')
+  rawMediaMetadata.encoding.container.integrateInto(encoding, 'container')
+  rawMediaMetadata.encoding.mimeMediaType.integrateInto(encoding, 'mimeMediaType')
+
+  // ensure media metadata object
+  const mediaMetadata = (existingRecord && existingRecord.mediaMetadata) || new VideoMediaMetadata({
+    createdInBlock: blockNumber,
+
+    createdById: '1',
+    updatedById: '1',
+  })
+
+  // integrate media-related data
+  rawMediaMetadata.pixelWidth.integrateInto(mediaMetadata, 'pixelWidth')
+  rawMediaMetadata.pixelHeight.integrateInto(mediaMetadata, 'pixelHeight')
+  rawMediaMetadata.size.integrateInto(mediaMetadata, 'size')
+
+  // connect encoding to media metadata object
+  mediaMetadata.encoding = encoding
+
+  return {
+    ...metadata,
+    mediaMetadata
+  }
+}