video.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import BN from 'bn.js'
  2. import { fixBlockTimestamp } from '../eventFix'
  3. import { SubstrateEvent } from '@dzlzv/hydra-common'
  4. import { DatabaseManager } from '@dzlzv/hydra-db-utils'
  5. import { FindConditions, In } from 'typeorm'
  6. import { Content } from '../../../generated/types'
  7. import { inconsistentState, logger, getNextId } from '../common'
  8. import { convertContentActorToDataObjectOwner, readProtobuf, readProtobufWithAssets, RawVideoMetadata } from './utils'
  9. import {
  10. AssetAvailability,
  11. Channel,
  12. License,
  13. Video,
  14. VideoCategory,
  15. VideoMediaEncoding,
  16. VideoMediaMetadata,
  17. } from 'query-node'
  18. // Joystream types
  19. import { ChannelId } from '@joystream/types/augment'
  20. // eslint-disable-next-line @typescript-eslint/naming-convention
  21. export async function content_VideoCategoryCreated(db: DatabaseManager, event: SubstrateEvent) {
  22. // read event data
  23. const { videoCategoryId, videoCategoryCreationParameters, contentActor } = new Content.VideoCategoryCreatedEvent(
  24. event
  25. ).data
  26. // read metadata
  27. const protobufContent = await readProtobuf(new VideoCategory(), {
  28. metadata: videoCategoryCreationParameters.meta,
  29. db,
  30. event,
  31. })
  32. // create new video category
  33. const videoCategory = new VideoCategory({
  34. // main data
  35. id: videoCategoryId.toString(),
  36. videos: [],
  37. createdInBlock: event.blockNumber,
  38. // fill in auto-generated fields
  39. createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  40. updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  41. // integrate metadata
  42. ...protobufContent,
  43. })
  44. // save video category
  45. await db.save<VideoCategory>(videoCategory)
  46. // emit log event
  47. logger.info('Video category has been created', { id: videoCategoryId })
  48. }
  49. // eslint-disable-next-line @typescript-eslint/naming-convention
  50. export async function content_VideoCategoryUpdated(db: DatabaseManager, event: SubstrateEvent) {
  51. // read event data
  52. const { videoCategoryId, videoCategoryUpdateParameters, contentActor } = new Content.VideoCategoryUpdatedEvent(
  53. event
  54. ).data
  55. // load video category
  56. const videoCategory = await db.get(VideoCategory, {
  57. where: { id: videoCategoryId.toString() } as FindConditions<VideoCategory>,
  58. })
  59. // ensure video category exists
  60. if (!videoCategory) {
  61. return inconsistentState('Non-existing video category update requested', videoCategoryId)
  62. }
  63. // read metadata
  64. const protobufContent = await readProtobuf(new VideoCategory(), {
  65. metadata: videoCategoryUpdateParameters.new_meta,
  66. db,
  67. event,
  68. })
  69. // update all fields read from protobuf
  70. for (const [key, value] of Object.entries(protobufContent)) {
  71. videoCategory[key] = value
  72. }
  73. // set last update time
  74. videoCategory.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
  75. // save video category
  76. await db.save<VideoCategory>(videoCategory)
  77. // emit log event
  78. logger.info('Video category has been updated', { id: videoCategoryId })
  79. }
  80. // eslint-disable-next-line @typescript-eslint/naming-convention
  81. export async function content_VideoCategoryDeleted(db: DatabaseManager, event: SubstrateEvent) {
  82. // read event data
  83. const { videoCategoryId } = new Content.VideoCategoryDeletedEvent(event).data
  84. // load video category
  85. const videoCategory = await db.get(VideoCategory, {
  86. where: { id: videoCategoryId.toString() } as FindConditions<VideoCategory>,
  87. })
  88. // ensure video category exists
  89. if (!videoCategory) {
  90. return inconsistentState('Non-existing video category deletion requested', videoCategoryId)
  91. }
  92. // remove video category
  93. await db.remove<VideoCategory>(videoCategory)
  94. // emit log event
  95. logger.info('Video category has been deleted', { id: videoCategoryId })
  96. }
  97. /// ///////////////// Video //////////////////////////////////////////////////////
  98. // eslint-disable-next-line @typescript-eslint/naming-convention
  99. export async function content_VideoCreated(db: DatabaseManager, event: SubstrateEvent) {
  100. // read event data
  101. const { channelId, videoId, videoCreationParameters, contentActor } = new Content.VideoCreatedEvent(event).data
  102. // read metadata
  103. const protobufContent = await readProtobufWithAssets(new Video(), {
  104. metadata: videoCreationParameters.meta,
  105. db,
  106. event,
  107. assets: videoCreationParameters.assets,
  108. contentOwner: convertContentActorToDataObjectOwner(contentActor, channelId.toNumber()),
  109. })
  110. // load channel
  111. const channel = await db.get(Channel, { where: { id: channelId.toString() } as FindConditions<Channel> })
  112. // ensure channel exists
  113. if (!channel) {
  114. return inconsistentState('Trying to add video to non-existing channel', channelId)
  115. }
  116. // prepare video media metadata (if any)
  117. const fixedProtobuf = await integrateVideoMediaMetadata(db, null, protobufContent, event)
  118. const licenseIsEmpty = fixedProtobuf.license && !Object.keys(fixedProtobuf.license).length
  119. if (licenseIsEmpty) {
  120. // license deletion was requested - ignore it and consider it empty
  121. delete fixedProtobuf.license
  122. }
  123. // create new video
  124. const video = new Video({
  125. // main data
  126. id: videoId.toString(),
  127. isCensored: false,
  128. channel,
  129. createdInBlock: event.blockNumber,
  130. isFeatured: false,
  131. // default values for properties that might or might not be filled by metadata
  132. thumbnailPhotoUrls: [],
  133. thumbnailPhotoAvailability: AssetAvailability.INVALID,
  134. mediaUrls: [],
  135. mediaAvailability: AssetAvailability.INVALID,
  136. // fill in auto-generated fields
  137. createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  138. updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  139. // integrate metadata
  140. ...fixedProtobuf,
  141. })
  142. // save video
  143. await db.save<Video>(video)
  144. // emit log event
  145. logger.info('Video has been created', { id: videoId })
  146. }
  147. // eslint-disable-next-line @typescript-eslint/naming-convention
  148. export async function content_VideoUpdated(db: DatabaseManager, event: SubstrateEvent) {
  149. // read event data
  150. const { videoId, videoUpdateParameters, contentActor } = new Content.VideoUpdatedEvent(event).data
  151. // load video
  152. const video = await db.get(Video, {
  153. where: { id: videoId.toString() } as FindConditions<Video>,
  154. relations: ['channel', 'license'],
  155. })
  156. // ensure video exists
  157. if (!video) {
  158. return inconsistentState('Non-existing video update requested', videoId)
  159. }
  160. // prepare changed metadata
  161. const newMetadata = videoUpdateParameters.new_meta.unwrapOr(null)
  162. // license must be deleted AFTER video is saved - plan a license deletion by assigning it to this variable
  163. let licenseToDelete: License | null = null
  164. // update metadata if it was changed
  165. if (newMetadata) {
  166. const protobufContent = await readProtobufWithAssets(new Video(), {
  167. metadata: newMetadata,
  168. db,
  169. event,
  170. assets: videoUpdateParameters.assets.unwrapOr([]),
  171. contentOwner: convertContentActorToDataObjectOwner(contentActor, new BN(video.channel.id).toNumber()),
  172. })
  173. // prepare video media metadata (if any)
  174. const fixedProtobuf = await integrateVideoMediaMetadata(db, video, protobufContent, event)
  175. // remember original license
  176. const originalLicense = video.license
  177. // update all fields read from protobuf
  178. for (const [key, value] of Object.entries(fixedProtobuf)) {
  179. video[key] = value
  180. }
  181. // license has changed - plan old license delete
  182. if (originalLicense && video.license !== originalLicense) {
  183. ;[video.license, licenseToDelete] = handleLicenseUpdate(originalLicense, video.license)
  184. } else if (!Object.keys(video.license || {}).length) {
  185. // license deletion was requested event no license exists?
  186. delete video.license // ensure license is empty
  187. }
  188. }
  189. // set last update time
  190. video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
  191. // save video
  192. await db.save<Video>(video)
  193. // delete old license if it's planned
  194. if (licenseToDelete) {
  195. await db.remove<License>(licenseToDelete)
  196. }
  197. // emit log event
  198. logger.info('Video has been updated', { id: videoId })
  199. }
  200. // eslint-disable-next-line @typescript-eslint/naming-convention
  201. export async function content_VideoDeleted(db: DatabaseManager, event: SubstrateEvent) {
  202. // read event data
  203. const { videoId } = new Content.VideoDeletedEvent(event).data
  204. // load video
  205. const video = await db.get(Video, { where: { id: videoId.toString() } as FindConditions<Video> })
  206. // ensure video exists
  207. if (!video) {
  208. return inconsistentState('Non-existing video deletion requested', videoId)
  209. }
  210. // remove video
  211. await db.remove<Video>(video)
  212. // emit log event
  213. logger.info('Video has been deleted', { id: videoId })
  214. }
  215. // eslint-disable-next-line @typescript-eslint/naming-convention
  216. export async function content_VideoCensorshipStatusUpdated(db: DatabaseManager, event: SubstrateEvent) {
  217. // read event data
  218. const { videoId, isCensored } = new Content.VideoCensorshipStatusUpdatedEvent(event).data
  219. // load video
  220. const video = await db.get(Video, { where: { id: videoId.toString() } as FindConditions<Video> })
  221. // ensure video exists
  222. if (!video) {
  223. return inconsistentState('Non-existing video censoring requested', videoId)
  224. }
  225. // update video
  226. video.isCensored = isCensored.isTrue
  227. // set last update time
  228. video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
  229. // save video
  230. await db.save<Video>(video)
  231. // emit log event
  232. logger.info('Video censorship status has been updated', { id: videoId, isCensored: isCensored.isTrue })
  233. }
  234. // eslint-disable-next-line @typescript-eslint/naming-convention
  235. export async function content_FeaturedVideosSet(db: DatabaseManager, event: SubstrateEvent) {
  236. // read event data
  237. const { videoId: videoIds } = new Content.FeaturedVideosSetEvent(event).data
  238. // load old featured videos
  239. const existingFeaturedVideos = await db.getMany(Video, { where: { isFeatured: true } as FindConditions<Video> })
  240. // comparsion utility
  241. const isSame = (videoIdA: string) => (videoIdB: string) => videoIdA === videoIdB
  242. // calculate diff sets
  243. const toRemove = existingFeaturedVideos.filter(
  244. (existingFV) => !videoIds.map((item) => item.toString()).some(isSame(existingFV.id))
  245. )
  246. const toAdd = videoIds.filter(
  247. (video) => !existingFeaturedVideos.map((item) => item.id).some(isSame(video.toString()))
  248. )
  249. // escape if no featured video needs to be added or removed
  250. if (!toRemove.length && !toAdd.length) {
  251. // emit log event
  252. logger.info('Featured videos unchanged')
  253. return
  254. }
  255. // mark previously featured videos as not-featured
  256. await Promise.all(
  257. toRemove.map(async (video) => {
  258. video.isFeatured = false
  259. // set last update time
  260. video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
  261. await db.save<Video>(video)
  262. })
  263. )
  264. // escape if no featured video needs to be added
  265. if (!toAdd.length) {
  266. // emit log event
  267. logger.info('Some featured videos have been unset.', { videoIds: toRemove.map((item) => item.id.toString()) })
  268. return
  269. }
  270. // read videos previously not-featured videos that are meant to be featured
  271. const videosToAdd = await db.getMany(Video, {
  272. where: {
  273. id: In(toAdd.map((item) => item.toString())),
  274. } as FindConditions<Video>,
  275. })
  276. if (videosToAdd.length !== toAdd.length) {
  277. return inconsistentState('At least one non-existing video featuring requested', toAdd)
  278. }
  279. // mark previously not-featured videos as featured
  280. await Promise.all(
  281. videosToAdd.map(async (video) => {
  282. video.isFeatured = true
  283. // set last update time
  284. video.updatedAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
  285. await db.save<Video>(video)
  286. })
  287. )
  288. // emit log event
  289. logger.info('New featured videos have been set', { videoIds })
  290. }
  291. /// ///////////////// Helpers ////////////////////////////////////////////////////
  292. /*
  293. Integrates video metadata-related data into existing data (if any) or creates a new record.
  294. NOTE: type hack - `RawVideoMetadata` is accepted for `metadata` instead of `Partial<Video>`
  295. see `prepareVideoMetadata()` in `utils.ts` for more info
  296. */
  297. async function integrateVideoMediaMetadata(
  298. db: DatabaseManager,
  299. existingRecord: Video | null,
  300. metadata: Partial<Video>,
  301. event: SubstrateEvent
  302. ): Promise<Partial<Video>> {
  303. if (!metadata.mediaMetadata) {
  304. return metadata
  305. }
  306. const now = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())
  307. // fix TS type
  308. const rawMediaMetadata = (metadata.mediaMetadata as unknown) as RawVideoMetadata
  309. // ensure encoding object
  310. const encoding =
  311. (existingRecord && existingRecord.mediaMetadata && existingRecord.mediaMetadata.encoding) ||
  312. new VideoMediaEncoding({
  313. createdAt: now,
  314. updatedAt: now,
  315. createdById: '1',
  316. updatedById: '1',
  317. })
  318. // integrate media encoding-related data
  319. rawMediaMetadata.encoding.codecName.integrateInto(encoding, 'codecName')
  320. rawMediaMetadata.encoding.container.integrateInto(encoding, 'container')
  321. rawMediaMetadata.encoding.mimeMediaType.integrateInto(encoding, 'mimeMediaType')
  322. // ensure media metadata object
  323. const mediaMetadata =
  324. (existingRecord && existingRecord.mediaMetadata) ||
  325. new VideoMediaMetadata({
  326. createdInBlock: event.blockNumber,
  327. createdAt: now,
  328. updatedAt: now,
  329. createdById: '1',
  330. updatedById: '1',
  331. })
  332. // integrate media-related data
  333. rawMediaMetadata.pixelWidth.integrateInto(mediaMetadata, 'pixelWidth')
  334. rawMediaMetadata.pixelHeight.integrateInto(mediaMetadata, 'pixelHeight')
  335. rawMediaMetadata.size.integrateInto(mediaMetadata, 'size')
  336. // connect encoding to media metadata object
  337. mediaMetadata.encoding = encoding
  338. // ensure predictable ids
  339. if (!mediaMetadata.encoding.id) {
  340. mediaMetadata.encoding.id = await getNextId(db)
  341. }
  342. if (!mediaMetadata.id) {
  343. mediaMetadata.id = await getNextId(db)
  344. }
  345. /// ///////////////// update updatedAt if needed ///////////////////////////////
  346. const encodingNoChange =
  347. true &&
  348. rawMediaMetadata.encoding.codecName.isNoChange() &&
  349. rawMediaMetadata.encoding.container.isNoChange() &&
  350. rawMediaMetadata.encoding.mimeMediaType.isNoChange()
  351. const mediaMetadataNoChange =
  352. encodingNoChange &&
  353. rawMediaMetadata.encoding.codecName.isNoChange() &&
  354. rawMediaMetadata.encoding.container.isNoChange() &&
  355. rawMediaMetadata.encoding.mimeMediaType.isNoChange()
  356. if (!encodingNoChange) {
  357. // encoding changed?
  358. mediaMetadata.encoding.updatedAt = now
  359. }
  360. if (!mediaMetadataNoChange) {
  361. // metadata changed?
  362. mediaMetadata.updatedAt = now
  363. }
  364. /// ////////////////////////////////////////////////////////////////////////////
  365. return {
  366. ...metadata,
  367. mediaMetadata,
  368. }
  369. }
  370. // returns tuple `[newLicenseForVideo, oldLicenseToBeDeleted]`
  371. function handleLicenseUpdate(originalLicense, newLicense): [License | undefined, License | null] {
  372. const isNewEmpty = !Object.keys(newLicense).length
  373. if (!originalLicense && isNewEmpty) {
  374. return [undefined, null]
  375. }
  376. if (!originalLicense) {
  377. // && !isNewEmpty
  378. return [newLicense, null]
  379. }
  380. if (!isNewEmpty) {
  381. // && originalLicense
  382. return [
  383. new License({
  384. ...originalLicense,
  385. ...newLicense,
  386. }),
  387. null,
  388. ]
  389. }
  390. // originalLicense && isNewEmpty
  391. return [originalLicense, null]
  392. }