utils.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common'
  2. import { FindConditions } from 'typeorm'
  3. import {
  4. IVideoMetadata,
  5. IPublishedBeforeJoystream,
  6. ILicense,
  7. IMediaType,
  8. IChannelMetadata,
  9. } from '@joystream/metadata-protobuf'
  10. import { integrateMeta, isSet, isValidLanguageCode } from '@joystream/metadata-protobuf/utils'
  11. import { invalidMetadata, inconsistentState, logger } from '../common'
  12. import {
  13. // primary entities
  14. CuratorGroup,
  15. Channel,
  16. Video,
  17. VideoCategory,
  18. // secondary entities
  19. Language,
  20. License,
  21. VideoMediaMetadata,
  22. // asset
  23. Asset,
  24. Membership,
  25. VideoMediaEncoding,
  26. ChannelCategory,
  27. AssetNone,
  28. } from 'query-node/dist/model'
  29. // Joystream types
  30. import { NewAsset, ContentActor } from '@joystream/types/augment'
  31. import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
  32. import BN from 'bn.js'
  33. export async function processChannelMetadata(
  34. ctx: EventContext & StoreContext,
  35. channel: Channel,
  36. meta: DecodedMetadataObject<IChannelMetadata>,
  37. assets: NewAsset[]
  38. ): Promise<Channel> {
  39. // TODO: Assets processing (Storage v2)
  40. // const assetsOwner = new DataObjectOwnerChannel()
  41. // assetsOwner.channelId = channel.id
  42. // const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
  43. integrateMeta(channel, meta, ['title', 'description', 'isPublic'])
  44. // prepare channel category if needed
  45. if (isSet(meta.category)) {
  46. channel.category = await processChannelCategory(ctx, channel.category, parseInt(meta.category))
  47. }
  48. channel.coverPhoto = new AssetNone()
  49. channel.avatarPhoto = new AssetNone()
  50. // // prepare cover photo asset if needed
  51. // if (isSet(meta.coverPhoto)) {
  52. // const asset = findAssetByIndex(processedAssets, meta.coverPhoto, 'channel cover photo')
  53. // if (asset) {
  54. // channel.coverPhoto = asset
  55. // }
  56. // }
  57. // // prepare avatar photo asset if needed
  58. // if (isSet(meta.avatarPhoto)) {
  59. // const asset = findAssetByIndex(processedAssets, meta.avatarPhoto, 'channel avatar photo')
  60. // if (asset) {
  61. // channel.avatarPhoto = asset
  62. // }
  63. // }
  64. // prepare language if needed
  65. if (isSet(meta.language)) {
  66. channel.language = await processLanguage(ctx, channel.language, meta.language)
  67. }
  68. return channel
  69. }
  70. export async function processVideoMetadata(
  71. ctx: EventContext & StoreContext,
  72. channel: Channel,
  73. video: Video,
  74. meta: DecodedMetadataObject<IVideoMetadata>,
  75. assets: NewAsset[]
  76. ): Promise<Video> {
  77. // TODO: Assets processing (Storage v2)
  78. // const assetsOwner = new DataObjectOwnerChannel()
  79. // assetsOwner.channelId = channel.id
  80. // const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
  81. integrateMeta(video, meta, ['title', 'description', 'duration', 'hasMarketing', 'isExplicit', 'isPublic'])
  82. // prepare video category if needed
  83. if (meta.category) {
  84. video.category = await processVideoCategory(ctx, video.category, parseInt(meta.category))
  85. }
  86. // prepare media meta information if needed
  87. if (isSet(meta.mediaType) || isSet(meta.mediaPixelWidth) || isSet(meta.mediaPixelHeight)) {
  88. // prepare video file size if poosible
  89. const videoSize = 0 // TODO: extractVideoSize(assets, meta.video)
  90. video.mediaMetadata = await processVideoMediaMetadata(ctx, video.mediaMetadata, meta, videoSize)
  91. }
  92. // prepare license if needed
  93. if (isSet(meta.license)) {
  94. await updateVideoLicense(ctx, video, meta.license)
  95. }
  96. video.thumbnailPhoto = new AssetNone()
  97. video.media = new AssetNone()
  98. // // prepare thumbnail photo asset if needed
  99. // if (isSet(meta.thumbnailPhoto)) {
  100. // const asset = findAssetByIndex(processedAssets, meta.thumbnailPhoto, 'thumbnail photo')
  101. // if (asset) {
  102. // video.thumbnailPhoto = asset
  103. // }
  104. // }
  105. // // prepare video asset if needed
  106. // if (isSet(meta.video)) {
  107. // const asset = findAssetByIndex(processedAssets, meta.video, 'video')
  108. // if (asset) {
  109. // video.media = asset
  110. // }
  111. // }
  112. // prepare language if needed
  113. if (isSet(meta.language)) {
  114. video.language = await processLanguage(ctx, video.language, meta.language)
  115. }
  116. if (isSet(meta.publishedBeforeJoystream)) {
  117. video.publishedBeforeJoystream = processPublishedBeforeJoystream(
  118. ctx,
  119. video.publishedBeforeJoystream,
  120. meta.publishedBeforeJoystream
  121. )
  122. }
  123. return video
  124. }
  125. function findAssetByIndex(assets: typeof Asset[], index: number, name?: string): typeof Asset | null {
  126. if (assets[index]) {
  127. return assets[index]
  128. } else {
  129. invalidMetadata(`Invalid${name ? ' ' + name : ''} asset index`, {
  130. numberOfAssets: assets.length,
  131. requestedAssetIndex: index,
  132. })
  133. return null
  134. }
  135. }
  136. async function processVideoMediaEncoding(
  137. { store, event }: StoreContext & EventContext,
  138. existingVideoMediaEncoding: VideoMediaEncoding | undefined,
  139. metadata: DecodedMetadataObject<IMediaType>
  140. ): Promise<VideoMediaEncoding> {
  141. const encoding =
  142. existingVideoMediaEncoding ||
  143. new VideoMediaEncoding({
  144. createdAt: new Date(event.blockTimestamp),
  145. createdById: '1',
  146. updatedById: '1',
  147. })
  148. // integrate media encoding-related data
  149. integrateMeta(encoding, metadata, ['codecName', 'container', 'mimeMediaType'])
  150. encoding.updatedAt = new Date(event.blockTimestamp)
  151. await store.save<VideoMediaEncoding>(encoding)
  152. return encoding
  153. }
  154. async function processVideoMediaMetadata(
  155. ctx: StoreContext & EventContext,
  156. existingVideoMedia: VideoMediaMetadata | undefined,
  157. metadata: DecodedMetadataObject<IVideoMetadata>,
  158. videoSize: number | undefined
  159. ): Promise<VideoMediaMetadata> {
  160. const { store, event } = ctx
  161. const videoMedia =
  162. existingVideoMedia ||
  163. new VideoMediaMetadata({
  164. createdInBlock: event.blockNumber,
  165. createdAt: new Date(event.blockTimestamp),
  166. createdById: '1',
  167. updatedById: '1',
  168. })
  169. // integrate media-related data
  170. const mediaMetadata = {
  171. size: isSet(videoSize) ? new BN(videoSize.toString()) : undefined,
  172. pixelWidth: metadata.mediaPixelWidth,
  173. pixelHeight: metadata.mediaPixelHeight,
  174. }
  175. integrateMeta(videoMedia, mediaMetadata, ['pixelWidth', 'pixelHeight', 'size'])
  176. videoMedia.updatedAt = new Date(event.blockTimestamp)
  177. videoMedia.encoding = await processVideoMediaEncoding(ctx, videoMedia.encoding, metadata.mediaType || {})
  178. await store.save<VideoMediaMetadata>(videoMedia)
  179. return videoMedia
  180. }
  181. export async function convertContentActorToChannelOwner(
  182. store: DatabaseManager,
  183. contentActor: ContentActor
  184. ): Promise<{
  185. ownerMember?: Membership
  186. ownerCuratorGroup?: CuratorGroup
  187. }> {
  188. if (contentActor.isMember) {
  189. const memberId = contentActor.asMember.toNumber()
  190. const member = await store.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
  191. // ensure member exists
  192. if (!member) {
  193. return inconsistentState(`Actor is non-existing member`, memberId)
  194. }
  195. return {
  196. ownerMember: member,
  197. ownerCuratorGroup: undefined, // this will clear the field
  198. }
  199. }
  200. if (contentActor.isCurator) {
  201. const curatorGroupId = contentActor.asCurator[0].toNumber()
  202. const curatorGroup = await store.get(CuratorGroup, {
  203. where: { id: curatorGroupId.toString() } as FindConditions<CuratorGroup>,
  204. })
  205. // ensure curator group exists
  206. if (!curatorGroup) {
  207. return inconsistentState('Actor is non-existing curator group', curatorGroupId)
  208. }
  209. return {
  210. ownerMember: undefined, // this will clear the field
  211. ownerCuratorGroup: curatorGroup,
  212. }
  213. }
  214. // TODO: contentActor.isLead
  215. logger.error('Not implemented ContentActor type', { contentActor: contentActor.toString() })
  216. throw new Error('Not-implemented ContentActor type used')
  217. }
  218. function processPublishedBeforeJoystream(
  219. ctx: EventContext & StoreContext,
  220. currentValue: Date | undefined,
  221. metadata: DecodedMetadataObject<IPublishedBeforeJoystream>
  222. ): Date | undefined {
  223. if (!isSet(metadata)) {
  224. return currentValue
  225. }
  226. // Property is beeing unset
  227. if (!metadata.isPublished) {
  228. return undefined
  229. }
  230. // try to parse timestamp from publish date
  231. const timestamp = isSet(metadata.date) ? Date.parse(metadata.date) : NaN
  232. // ensure date is valid
  233. if (isNaN(timestamp)) {
  234. invalidMetadata(`Invalid date used for publishedBeforeJoystream`, {
  235. timestamp,
  236. })
  237. return currentValue
  238. }
  239. // set new date
  240. return new Date(timestamp)
  241. }
  242. // TODO: Assets processing (Storage v2)
  243. // async function processNewAsset(
  244. // ctx: EventContext & StoreContext,
  245. // asset: NewAsset,
  246. // owner: typeof DataObjectOwner
  247. // ): Promise<typeof Asset> {
  248. // if (asset.isUrls) {
  249. // const urls = asset.asUrls.toArray().map((url) => url.toString())
  250. // const resultAsset = new AssetExternal()
  251. // resultAsset.urls = JSON.stringify(urls)
  252. // return resultAsset
  253. // } else if (asset.isUpload) {
  254. // const contentParameters: ContentParameters = asset.asUpload
  255. // const dataObject = await createDataObject(ctx, contentParameters, owner)
  256. // const resultAsset = new AssetJoystreamStorage()
  257. // resultAsset.dataObjectId = dataObject.id
  258. // return resultAsset
  259. // } else {
  260. // unexpectedData('Unrecognized asset type', asset.type)
  261. // }
  262. // }
  263. // function extractVideoSize(assets: NewAsset[], assetIndex: number | null | undefined): number | undefined {
  264. // // escape if no asset is required
  265. // if (!isSet(assetIndex)) {
  266. // return undefined
  267. // }
  268. // // ensure asset index is valid
  269. // if (assetIndex > assets.length) {
  270. // invalidMetadata(`Non-existing asset video size extraction requested`, { assetsProvided: assets.length, assetIndex })
  271. // return undefined
  272. // }
  273. // const rawAsset = assets[assetIndex]
  274. // // escape if asset is describing URLs (can't get size)
  275. // if (rawAsset.isUrls) {
  276. // return undefined
  277. // }
  278. // // !rawAsset.isUrls && rawAsset.isUpload // asset is in storage
  279. // // convert generic content parameters coming from processor to custom Joystream data type
  280. // const customContentParameters = new Custom_ContentParameters(registry, rawAsset.asUpload.toJSON() as any)
  281. // // extract video size
  282. // const videoSize = customContentParameters.size_in_bytes.toNumber()
  283. // return videoSize
  284. // }
  285. async function processLanguage(
  286. ctx: EventContext & StoreContext,
  287. currentLanguage: Language | undefined,
  288. languageIso: string | undefined
  289. ): Promise<Language | undefined> {
  290. const { event, store } = ctx
  291. if (!isSet(languageIso)) {
  292. return currentLanguage
  293. }
  294. // ensure language string is valid
  295. if (!isValidLanguageCode(languageIso)) {
  296. invalidMetadata(`Invalid language ISO-639-1 provided`, languageIso)
  297. return currentLanguage
  298. }
  299. // load language
  300. const existingLanguage = await store.get(Language, { where: { iso: languageIso } })
  301. // return existing language if any
  302. if (existingLanguage) {
  303. return existingLanguage
  304. }
  305. // create new language
  306. const newLanguage = new Language({
  307. iso: languageIso,
  308. createdInBlock: event.blockNumber,
  309. createdAt: new Date(event.blockTimestamp),
  310. updatedAt: new Date(event.blockTimestamp),
  311. // TODO: remove these lines after Hydra auto-fills the values when cascading save (remove them on all places)
  312. createdById: '1',
  313. updatedById: '1',
  314. })
  315. await store.save<Language>(newLanguage)
  316. return newLanguage
  317. }
  318. async function updateVideoLicense(
  319. ctx: StoreContext & EventContext,
  320. video: Video,
  321. licenseMetadata: ILicense | null | undefined
  322. ): Promise<void> {
  323. const { store, event } = ctx
  324. if (!isSet(licenseMetadata)) {
  325. return
  326. }
  327. const previousLicense = video.license
  328. let license: License | null = null
  329. if (!isLicenseEmpty(licenseMetadata)) {
  330. // license is meant to be created/updated
  331. license =
  332. previousLicense ||
  333. new License({
  334. createdAt: new Date(event.blockTimestamp),
  335. createdById: '1',
  336. updatedById: '1',
  337. })
  338. license.updatedAt = new Date(event.blockTimestamp)
  339. integrateMeta(license, licenseMetadata, ['attribution', 'code', 'customText'])
  340. await store.save<License>(license)
  341. }
  342. // Update license (and potentially remove foreign key reference)
  343. // FIXME: Note that we MUST to provide "null" here in order to unset a relation,
  344. // See: https://github.com/Joystream/hydra/issues/435
  345. video.license = license as License | undefined
  346. video.updatedAt = new Date(ctx.event.blockTimestamp)
  347. await store.save<Video>(video)
  348. // Safely remove previous license if needed
  349. if (previousLicense && !license) {
  350. await store.remove<License>(previousLicense)
  351. }
  352. }
  353. /*
  354. Checks if protobof contains license with some fields filled or is empty object (`{}` or `{someKey: undefined, ...}`).
  355. Empty object means deletion is requested.
  356. */
  357. function isLicenseEmpty(licenseObject: ILicense): boolean {
  358. const somePropertySet = Object.values(licenseObject).some((v) => isSet(v))
  359. return !somePropertySet
  360. }
  361. async function processVideoCategory(
  362. ctx: EventContext & StoreContext,
  363. currentCategory: VideoCategory | undefined,
  364. categoryId: number
  365. ): Promise<VideoCategory | undefined> {
  366. const { store } = ctx
  367. // load video category
  368. const category = await store.get(VideoCategory, {
  369. where: { id: categoryId.toString() },
  370. })
  371. // ensure video category exists
  372. if (!category) {
  373. invalidMetadata('Non-existing video category association with video requested', categoryId)
  374. return currentCategory
  375. }
  376. return category
  377. }
  378. async function processChannelCategory(
  379. ctx: EventContext & StoreContext,
  380. currentCategory: ChannelCategory | undefined,
  381. categoryId: number
  382. ): Promise<ChannelCategory | undefined> {
  383. const { store } = ctx
  384. // load video category
  385. const category = await store.get(ChannelCategory, {
  386. where: { id: categoryId.toString() },
  387. })
  388. // ensure video category exists
  389. if (!category) {
  390. invalidMetadata('Non-existing channel category association with channel requested', categoryId)
  391. return currentCategory
  392. }
  393. return category
  394. }