123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- import BN from 'bn.js'
- import { fixBlockTimestamp } from '../eventFix'
- import { SubstrateEvent } from '@dzlzv/hydra-common'
- import { DatabaseManager } from '@dzlzv/hydra-db-utils'
- import { FindConditions, In } from 'typeorm'
- import { Content } from '../../../generated/types'
- import { inconsistentState, logger, getNextId } from '../common'
- import { convertContentActorToDataObjectOwner, readProtobuf, readProtobufWithAssets, RawVideoMetadata } from './utils'
- import {
- AssetAvailability,
- Channel,
- License,
- Video,
- VideoCategory,
- VideoMediaEncoding,
- VideoMediaMetadata,
- } from 'query-node'
- // Joystream types
- import { ChannelId } from '@joystream/types/augment'
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_VideoCategoryCreated(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { videoCategoryId, videoCategoryCreationParameters, contentActor } = new Content.VideoCategoryCreatedEvent(
- event
- ).data
- // read metadata
- const protobufContent = await readProtobuf(new VideoCategory(), {
- metadata: videoCategoryCreationParameters.meta,
- db,
- event,
- })
- // create new video category
- const videoCategory = new VideoCategory({
- // main data
- id: videoCategoryId.toString(),
- videos: [],
- createdInBlock: event.blockNumber,
- // fill in auto-generated fields
- createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
- updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
- // integrate metadata
- ...protobufContent,
- })
- // save video category
- await db.save<VideoCategory>(videoCategory)
- // emit log event
- logger.info('Video category has been created', { id: videoCategoryId })
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_VideoCategoryUpdated(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { videoCategoryId, videoCategoryUpdateParameters, contentActor } = new Content.VideoCategoryUpdatedEvent(
- event
- ).data
- // load video category
- const videoCategory = await db.get(VideoCategory, {
- where: { id: videoCategoryId.toString() } as FindConditions<VideoCategory>,
- })
- // ensure video category exists
- if (!videoCategory) {
- return inconsistentState('Non-existing video category update requested', videoCategoryId)
- }
- // read metadata
- const protobufContent = await readProtobuf(new VideoCategory(), {
- metadata: videoCategoryUpdateParameters.new_meta,
- db,
- event,
- })
- // update all fields read from protobuf
- for (const [key, value] of Object.entries(protobufContent)) {
- videoCategory[key] = value
- }
- // set last update time
- videoCategory.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
- // save video category
- await db.save<VideoCategory>(videoCategory)
- // emit log event
- logger.info('Video category has been updated', { id: videoCategoryId })
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_VideoCategoryDeleted(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { videoCategoryId } = new Content.VideoCategoryDeletedEvent(event).data
- // load video category
- const videoCategory = await db.get(VideoCategory, {
- where: { id: videoCategoryId.toString() } as FindConditions<VideoCategory>,
- })
- // ensure video category exists
- if (!videoCategory) {
- return inconsistentState('Non-existing video category deletion requested', videoCategoryId)
- }
- // remove video category
- await db.remove<VideoCategory>(videoCategory)
- // emit log event
- logger.info('Video category has been deleted', { id: videoCategoryId })
- }
- /// ///////////////// Video //////////////////////////////////////////////////////
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_VideoCreated(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { channelId, videoId, videoCreationParameters, contentActor } = new Content.VideoCreatedEvent(event).data
- // read metadata
- const protobufContent = await readProtobufWithAssets(new Video(), {
- metadata: videoCreationParameters.meta,
- db,
- event,
- assets: videoCreationParameters.assets,
- contentOwner: convertContentActorToDataObjectOwner(contentActor, channelId.toNumber()),
- })
- // load channel
- const channel = await db.get(Channel, { where: { id: channelId.toString() } as FindConditions<Channel> })
- // ensure channel exists
- if (!channel) {
- return inconsistentState('Trying to add video to non-existing channel', channelId)
- }
- // prepare video media metadata (if any)
- const fixedProtobuf = await integrateVideoMediaMetadata(db, null, protobufContent, event)
- const licenseIsEmpty = fixedProtobuf.license && !Object.keys(fixedProtobuf.license).length
- if (licenseIsEmpty) {
- // license deletion was requested - ignore it and consider it empty
- delete fixedProtobuf.license
- }
- // create new video
- const video = new Video({
- // main data
- id: videoId.toString(),
- isCensored: false,
- channel,
- createdInBlock: event.blockNumber,
- isFeatured: false,
- // default values for properties that might or might not be filled by metadata
- thumbnailPhotoUrls: [],
- thumbnailPhotoAvailability: AssetAvailability.INVALID,
- mediaUrls: [],
- mediaAvailability: AssetAvailability.INVALID,
- // fill in auto-generated fields
- createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
- updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
- // integrate metadata
- ...fixedProtobuf,
- })
- // save video
- await db.save<Video>(video)
- // emit log event
- logger.info('Video has been created', { id: videoId })
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_VideoUpdated(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { videoId, videoUpdateParameters, contentActor } = new Content.VideoUpdatedEvent(event).data
- // load video
- const video = await db.get(Video, {
- where: { id: videoId.toString() } as FindConditions<Video>,
- relations: ['channel', 'license'],
- })
- // ensure video exists
- if (!video) {
- return inconsistentState('Non-existing video update requested', videoId)
- }
- // prepare changed metadata
- const newMetadata = videoUpdateParameters.new_meta.unwrapOr(null)
- // license must be deleted AFTER video is saved - plan a license deletion by assigning it to this variable
- let licenseToDelete: License | null = null
- // update metadata if it was changed
- if (newMetadata) {
- const protobufContent = await readProtobufWithAssets(new Video(), {
- metadata: newMetadata,
- db,
- event,
- assets: videoUpdateParameters.assets.unwrapOr([]),
- contentOwner: convertContentActorToDataObjectOwner(contentActor, new BN(video.channel.id).toNumber()),
- })
- // prepare video media metadata (if any)
- const fixedProtobuf = await integrateVideoMediaMetadata(db, video, protobufContent, event)
- // remember original license
- const originalLicense = video.license
- // update all fields read from protobuf
- for (const [key, value] of Object.entries(fixedProtobuf)) {
- video[key] = value
- }
- // license has changed - plan old license delete
- if (originalLicense && video.license !== originalLicense) {
- ;[video.license, licenseToDelete] = handleLicenseUpdate(originalLicense, video.license)
- } else if (!Object.keys(video.license || {}).length) {
- // license deletion was requested event no license exists?
- delete video.license // ensure license is empty
- }
- }
- // set last update time
- video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
- // save video
- await db.save<Video>(video)
- // delete old license if it's planned
- if (licenseToDelete) {
- await db.remove<License>(licenseToDelete)
- }
- // emit log event
- logger.info('Video has been updated', { id: videoId })
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_VideoDeleted(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { videoId } = new Content.VideoDeletedEvent(event).data
- // load video
- const video = await db.get(Video, { where: { id: videoId.toString() } as FindConditions<Video> })
- // ensure video exists
- if (!video) {
- return inconsistentState('Non-existing video deletion requested', videoId)
- }
- // remove video
- await db.remove<Video>(video)
- // emit log event
- logger.info('Video has been deleted', { id: videoId })
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_VideoCensorshipStatusUpdated(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { videoId, isCensored } = new Content.VideoCensorshipStatusUpdatedEvent(event).data
- // load video
- const video = await db.get(Video, { where: { id: videoId.toString() } as FindConditions<Video> })
- // ensure video exists
- if (!video) {
- return inconsistentState('Non-existing video censoring requested', videoId)
- }
- // update video
- video.isCensored = isCensored.isTrue
- // set last update time
- video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
- // save video
- await db.save<Video>(video)
- // emit log event
- logger.info('Video censorship status has been updated', { id: videoId, isCensored: isCensored.isTrue })
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export async function content_FeaturedVideosSet(db: DatabaseManager, event: SubstrateEvent) {
- // read event data
- const { videoId: videoIds } = new Content.FeaturedVideosSetEvent(event).data
- // load old featured videos
- const existingFeaturedVideos = await db.getMany(Video, { where: { isFeatured: true } as FindConditions<Video> })
- // comparsion utility
- const isSame = (videoIdA: string) => (videoIdB: string) => videoIdA === videoIdB
- // calculate diff sets
- const toRemove = existingFeaturedVideos.filter(
- (existingFV) => !videoIds.map((item) => item.toString()).some(isSame(existingFV.id))
- )
- const toAdd = videoIds.filter(
- (video) => !existingFeaturedVideos.map((item) => item.id).some(isSame(video.toString()))
- )
- // escape if no featured video needs to be added or removed
- if (!toRemove.length && !toAdd.length) {
- // emit log event
- logger.info('Featured videos unchanged')
- return
- }
- // mark previously featured videos as not-featured
- await Promise.all(
- toRemove.map(async (video) => {
- video.isFeatured = false
- // set last update time
- video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
- await db.save<Video>(video)
- })
- )
- // escape if no featured video needs to be added
- if (!toAdd.length) {
- // emit log event
- logger.info('Some featured videos have been unset.', { videoIds: toRemove.map((item) => item.id.toString()) })
- return
- }
- // read videos previously not-featured videos that are meant to be featured
- const videosToAdd = await db.getMany(Video, {
- where: {
- id: In(toAdd.map((item) => item.toString())),
- } as FindConditions<Video>,
- })
- if (videosToAdd.length !== toAdd.length) {
- return inconsistentState('At least one non-existing video featuring requested', toAdd)
- }
- // mark previously not-featured videos as featured
- await Promise.all(
- videosToAdd.map(async (video) => {
- video.isFeatured = true
- // set last update time
- video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
- await db.save<Video>(video)
- })
- )
- // 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
- */
- async function integrateVideoMediaMetadata(
- db: DatabaseManager,
- existingRecord: Video | null,
- metadata: Partial<Video>,
- event: SubstrateEvent
- ): Promise<Partial<Video>> {
- if (!metadata.mediaMetadata) {
- return metadata
- }
- const now = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
- // fix TS type
- const rawMediaMetadata = (metadata.mediaMetadata as unknown) as RawVideoMetadata
- // ensure encoding object
- const encoding =
- (existingRecord && existingRecord.mediaMetadata && existingRecord.mediaMetadata.encoding) ||
- new VideoMediaEncoding({
- createdAt: now,
- updatedAt: now,
- 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: event.blockNumber,
- createdAt: now,
- updatedAt: now,
- 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
- // ensure predictable ids
- if (!mediaMetadata.encoding.id) {
- mediaMetadata.encoding.id = await getNextId(db)
- }
- if (!mediaMetadata.id) {
- mediaMetadata.id = await getNextId(db)
- }
- /// ///////////////// update updatedAt if needed ///////////////////////////////
- const encodingNoChange =
- true &&
- rawMediaMetadata.encoding.codecName.isNoChange() &&
- rawMediaMetadata.encoding.container.isNoChange() &&
- rawMediaMetadata.encoding.mimeMediaType.isNoChange()
- const mediaMetadataNoChange =
- encodingNoChange &&
- rawMediaMetadata.encoding.codecName.isNoChange() &&
- rawMediaMetadata.encoding.container.isNoChange() &&
- rawMediaMetadata.encoding.mimeMediaType.isNoChange()
- if (!encodingNoChange) {
- // encoding changed?
- mediaMetadata.encoding.updatedAt = now
- }
- if (!mediaMetadataNoChange) {
- // metadata changed?
- mediaMetadata.updatedAt = now
- }
- /// ////////////////////////////////////////////////////////////////////////////
- return {
- ...metadata,
- mediaMetadata,
- }
- }
- // returns tuple `[newLicenseForVideo, oldLicenseToBeDeleted]`
- function handleLicenseUpdate(originalLicense, newLicense): [License | undefined, License | null] {
- const isNewEmpty = !Object.keys(newLicense).length
- if (!originalLicense && isNewEmpty) {
- return [undefined, null]
- }
- if (!originalLicense) {
- // && !isNewEmpty
- return [newLicense, null]
- }
- if (!isNewEmpty) {
- // && originalLicense
- return [
- new License({
- ...originalLicense,
- ...newLicense,
- }),
- null,
- ]
- }
- // originalLicense && isNewEmpty
- return [originalLicense, null]
- }
|