Просмотр исходного кода

Feature/video relevance (#120)

* Introduce new video property and scheduler to update it

* Adjust equation and change score to float

* Schedule updates in required places

* Add mutation to change video weights

* Adjust env weights

* Fix for newness weight on creation

* CR fixes

* CR fixes v2

* Update src/mappings/content/video.ts

* Update src/mappings/content/video.ts

---------

Co-authored-by: Leszek Wiesner <leszek@jsgenesis.com>
WRadoslaw 1 год назад
Родитель
Сommit
c39505e90e

+ 4 - 1
.env

@@ -19,6 +19,9 @@ SUPPORT_NO_CATEGORY_VIDEOS=true
 SUPPORT_NEW_CATEGORIES=true
 KILL_SWITCH_ON=false
 VIDEO_VIEW_PER_IP_TIME_LIMIT=86400 # 86400 seconds = 24 hours
+VIDEO_RELEVANCE_VIEWS_TICK=50 # every 50 views video relevance score will be recalculated
+RELEVANCE_WEIGHTS="[1, 0.03, 0.3, 0.5]" # [newness (30 days - days since created) weight, views weight, comments weight, rections weights]
+
 # Operator API secret
 OPERATOR_SECRET=this-is-not-so-secret-change-it
 # Processor configuration
@@ -29,4 +32,4 @@ APP_PRIVATE_KEY=this-is-not-so-secret-change-it
 TRUSTED_REVERSE_PROXIES=1
 
 # Debug settings
-SQD_DEBUG=api:*
+SQD_DEBUG=api:*

+ 105 - 105
db/migrations/1680280600588-Data.js → db/migrations/1682581339585-Data.js

@@ -1,24 +1,9 @@
-module.exports = class Data1680280600588 {
-    name = 'Data1680280600588'
+module.exports = class Data1682581339585 {
+    name = 'Data1682581339585'
 
     async up(db) {
-        await db.query(`CREATE TABLE "bid" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "auction_id" character varying, "nft_id" character varying, "bidder_id" character varying, "amount" numeric NOT NULL, "is_canceled" boolean NOT NULL, "created_in_block" integer NOT NULL, "index_in_block" integer NOT NULL, "previous_top_bid_id" character varying, CONSTRAINT "PK_ed405dda320051aca2dcb1a50bb" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_9e594e5a61c0f3cb25679f6ba8" ON "bid" ("auction_id") `)
-        await db.query(`CREATE INDEX "IDX_3caf2d6b31d2fe45a2b85b8191" ON "bid" ("nft_id") `)
-        await db.query(`CREATE INDEX "IDX_e7618559409a903a897164156b" ON "bid" ("bidder_id") `)
-        await db.query(`CREATE INDEX "IDX_32cb73025ec49c87f4c594a265" ON "bid" ("previous_top_bid_id") `)
-        await db.query(`CREATE TABLE "auction" ("id" character varying NOT NULL, "nft_id" character varying, "winning_member_id" character varying, "starting_price" numeric NOT NULL, "buy_now_price" numeric, "auction_type" jsonb NOT NULL, "top_bid_id" character varying, "starts_at_block" integer NOT NULL, "ended_at_block" integer, "is_canceled" boolean NOT NULL, "is_completed" boolean NOT NULL, CONSTRAINT "PK_9dc876c629273e71646cf6dfa67" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_cfb47e97e60c9d1462576f85a8" ON "auction" ("nft_id") `)
-        await db.query(`CREATE INDEX "IDX_a3127ec87cccc5696b92cac4e0" ON "auction" ("winning_member_id") `)
-        await db.query(`CREATE INDEX "IDX_1673ad4b059742fbabfc40b275" ON "auction" ("top_bid_id") `)
-        await db.query(`CREATE TABLE "auction_whitelisted_member" ("id" character varying NOT NULL, "auction_id" character varying, "member_id" character varying, CONSTRAINT "AuctionWhitelistedMember_auction_member" UNIQUE ("auction_id", "member_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_f20264ca8e878696fbc25f11bd5" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_d5ae4854487c7658b64225be30" ON "auction_whitelisted_member" ("member_id") `)
-        await db.query(`CREATE INDEX "IDX_5468573a96fa51c03743de5912" ON "auction_whitelisted_member" ("auction_id", "member_id") `)
-        await db.query(`CREATE TABLE "banned_member" ("id" character varying NOT NULL, "member_id" character varying, "channel_id" character varying, CONSTRAINT "BannedMember_member_channel" UNIQUE ("member_id", "channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_ebdf9a9c6d88f1116a5f2d0815d" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_ed36c6c26bf5410796c2fc21f7" ON "banned_member" ("channel_id") `)
-        await db.query(`CREATE INDEX "IDX_f29ff095bdb945975deca021ad" ON "banned_member" ("member_id", "channel_id") `)
-        await db.query(`CREATE TABLE "membership" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "handle" text NOT NULL, "controller_account" text NOT NULL, "total_channels_created" integer NOT NULL, CONSTRAINT "Membership_handle" UNIQUE ("handle") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_83c1afebef3059472e7c37e8de8" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_1298811c0de5f11198fd43df72" ON "membership" ("handle") `)
+        await db.query(`CREATE TABLE "event" ("id" character varying NOT NULL, "in_block" integer NOT NULL, "in_extrinsic" text, "index_in_block" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "data" jsonb NOT NULL, CONSTRAINT "PK_30c2f3bbaf6d34a55f8ae6e4614" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_8f3f220c4e717207d841d4e6d4" ON "event" ("in_extrinsic") `)
         await db.query(`CREATE TABLE "storage_bucket" ("id" character varying NOT NULL, "operator_status" jsonb NOT NULL, "accepting_new_bags" boolean NOT NULL, "data_objects_size_limit" numeric NOT NULL, "data_object_count_limit" numeric NOT NULL, "data_objects_count" numeric NOT NULL, "data_objects_size" numeric NOT NULL, CONSTRAINT "PK_97cd0c3fe7f51e34216822e5f91" PRIMARY KEY ("id"))`)
         await db.query(`CREATE TABLE "storage_bucket_bag" ("id" character varying NOT NULL, "storage_bucket_id" character varying, "bag_id" character varying, CONSTRAINT "StorageBucketBag_storageBucket_bag" UNIQUE ("storage_bucket_id", "bag_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_9d54c04557134225652d566cc82" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_aaf00b2c7d0cba49f97da14fbb" ON "storage_bucket_bag" ("bag_id") `)
@@ -34,6 +19,9 @@ module.exports = class Data1680280600588 {
         await db.query(`CREATE TABLE "storage_bag" ("id" character varying NOT NULL, "owner" jsonb NOT NULL, CONSTRAINT "PK_242aecdc788d9b22bcbb9ade19a" PRIMARY KEY ("id"))`)
         await db.query(`CREATE TABLE "storage_data_object" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "is_accepted" boolean NOT NULL, "size" numeric NOT NULL, "storage_bag_id" character varying, "ipfs_hash" text NOT NULL, "type" jsonb, "state_bloat_bond" numeric NOT NULL, "unset_at" TIMESTAMP WITH TIME ZONE, "resolved_urls" text array NOT NULL, CONSTRAINT "PK_61f224a4aef08f580a5ab4aadf0" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_ff8014300b8039dbaed764f51b" ON "storage_data_object" ("storage_bag_id") `)
+        await db.query(`CREATE TABLE "banned_member" ("id" character varying NOT NULL, "member_id" character varying, "channel_id" character varying, CONSTRAINT "BannedMember_member_channel" UNIQUE ("member_id", "channel_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_ebdf9a9c6d88f1116a5f2d0815d" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_ed36c6c26bf5410796c2fc21f7" ON "banned_member" ("channel_id") `)
+        await db.query(`CREATE INDEX "IDX_f29ff095bdb945975deca021ad" ON "banned_member" ("member_id", "channel_id") `)
         await db.query(`CREATE TABLE "app" ("id" character varying NOT NULL, "name" text NOT NULL, "owner_member_id" character varying, "website_url" text, "use_uri" text, "small_icon" text, "medium_icon" text, "big_icon" text, "one_liner" text, "description" text, "terms_of_service" text, "platforms" text array, "category" text, "auth_key" text, CONSTRAINT "App_name" UNIQUE ("name") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_9478629fc093d229df09e560aea" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_f36adbb7b096ceeb6f3e80ad14" ON "app" ("name") `)
         await db.query(`CREATE INDEX "IDX_c9cc395bbc485f70a15be64553" ON "app" ("owner_member_id") `)
@@ -66,7 +54,7 @@ module.exports = class Data1680280600588 {
         await db.query(`CREATE TABLE "video_reaction" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "reaction" character varying(6) NOT NULL, "member_id" character varying, "video_id" character varying, CONSTRAINT "PK_504876585c394f4ab33665dd44b" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_73dda64f53bbc7ec7035d5e7f0" ON "video_reaction" ("member_id") `)
         await db.query(`CREATE INDEX "IDX_436a3836eb47acb5e1e3c88dde" ON "video_reaction" ("video_id") `)
-        await db.query(`CREATE TABLE "video" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "channel_id" character varying, "category_id" character varying, "title" text, "description" text, "duration" integer, "thumbnail_photo_id" character varying, "language" text, "has_marketing" boolean, "published_before_joystream" TIMESTAMP WITH TIME ZONE, "is_public" boolean, "is_censored" boolean NOT NULL, "is_excluded" boolean NOT NULL, "is_explicit" boolean, "license_id" character varying, "media_id" character varying, "video_state_bloat_bond" numeric NOT NULL, "created_in_block" integer NOT NULL, "is_comment_section_enabled" boolean NOT NULL, "pinned_comment_id" character varying, "comments_count" integer NOT NULL, "is_reaction_feature_enabled" boolean NOT NULL, "reactions_count_by_reaction_id" jsonb, "reactions_count" integer NOT NULL, "views_num" integer NOT NULL, "entry_app_id" character varying, "yt_video_id" text, CONSTRAINT "PK_1a2f3856250765d72e7e1636c8e" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE TABLE "video" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "channel_id" character varying, "category_id" character varying, "title" text, "description" text, "duration" integer, "thumbnail_photo_id" character varying, "language" text, "has_marketing" boolean, "published_before_joystream" TIMESTAMP WITH TIME ZONE, "is_public" boolean, "is_censored" boolean NOT NULL, "is_excluded" boolean NOT NULL, "is_explicit" boolean, "license_id" character varying, "media_id" character varying, "video_state_bloat_bond" numeric NOT NULL, "created_in_block" integer NOT NULL, "is_comment_section_enabled" boolean NOT NULL, "pinned_comment_id" character varying, "comments_count" integer NOT NULL, "is_reaction_feature_enabled" boolean NOT NULL, "reactions_count_by_reaction_id" jsonb, "reactions_count" integer NOT NULL, "views_num" integer NOT NULL, "entry_app_id" character varying, "yt_video_id" text, "video_relevance" numeric NOT NULL, CONSTRAINT "PK_1a2f3856250765d72e7e1636c8e" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_81b11ef99a9db9ef1aed040d75" ON "video" ("channel_id") `)
         await db.query(`CREATE INDEX "IDX_2a5c61f32e9636ee10821e9a58" ON "video" ("category_id") `)
         await db.query(`CREATE INDEX "IDX_8530d052cc79b420f7ce2b4e09" ON "video" ("thumbnail_photo_id") `)
@@ -75,17 +63,22 @@ module.exports = class Data1680280600588 {
         await db.query(`CREATE INDEX "IDX_2db879ed42e3308fe65e679672" ON "video" ("media_id") `)
         await db.query(`CREATE INDEX "IDX_54f88a7decf7d22fd9bd9fa439" ON "video" ("pinned_comment_id") `)
         await db.query(`CREATE INDEX "IDX_6c49ad08c44d36d11f77c426e4" ON "video" ("entry_app_id") `)
+        await db.query(`CREATE TABLE "bid" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "auction_id" character varying, "nft_id" character varying, "bidder_id" character varying, "amount" numeric NOT NULL, "is_canceled" boolean NOT NULL, "created_in_block" integer NOT NULL, "index_in_block" integer NOT NULL, "previous_top_bid_id" character varying, CONSTRAINT "PK_ed405dda320051aca2dcb1a50bb" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_9e594e5a61c0f3cb25679f6ba8" ON "bid" ("auction_id") `)
+        await db.query(`CREATE INDEX "IDX_3caf2d6b31d2fe45a2b85b8191" ON "bid" ("nft_id") `)
+        await db.query(`CREATE INDEX "IDX_e7618559409a903a897164156b" ON "bid" ("bidder_id") `)
+        await db.query(`CREATE INDEX "IDX_32cb73025ec49c87f4c594a265" ON "bid" ("previous_top_bid_id") `)
         await db.query(`CREATE TABLE "owned_nft" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "video_id" character varying NOT NULL, "owner" jsonb NOT NULL, "transactional_status" jsonb, "creator_royalty" numeric, "last_sale_price" numeric, "last_sale_date" TIMESTAMP WITH TIME ZONE, "is_featured" boolean NOT NULL, CONSTRAINT "OwnedNft_video" UNIQUE ("video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_466896e39b9ec953f4f2545622" UNIQUE ("video_id"), CONSTRAINT "PK_5e0c289b350e863668fff44bb56" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_466896e39b9ec953f4f2545622" ON "owned_nft" ("video_id") `)
-        await db.query(`CREATE TABLE "storage_bucket_operator_metadata" ("id" character varying NOT NULL, "storage_bucket_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "StorageBucketOperatorMetadata_storageBucket" UNIQUE ("storage_bucket_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_7beffc9530b3f307bc1169cb52" UNIQUE ("storage_bucket_id"), CONSTRAINT "PK_9846a397400ae1a39b21fbd02d4" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_7beffc9530b3f307bc1169cb52" ON "storage_bucket_operator_metadata" ("storage_bucket_id") `)
-        await db.query(`CREATE TABLE "distribution_bucket_family_metadata" ("id" character varying NOT NULL, "family_id" character varying NOT NULL, "region" text, "description" text, "areas" jsonb, "latency_test_targets" text array, CONSTRAINT "DistributionBucketFamilyMetadata_family" UNIQUE ("family_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_dd93ca0ea24f3e7a02f11c4c14" UNIQUE ("family_id"), CONSTRAINT "PK_df7a270835bb313d3ef17bdee2f" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_dd93ca0ea24f3e7a02f11c4c14" ON "distribution_bucket_family_metadata" ("family_id") `)
-        await db.query(`CREATE INDEX "IDX_5510d3b244a63d6ec702faa426" ON "distribution_bucket_family_metadata" ("region") `)
-        await db.query(`CREATE TABLE "distribution_bucket_operator_metadata" ("id" character varying NOT NULL, "distirbution_bucket_operator_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "DistributionBucketOperatorMetadata_distirbutionBucketOperator" UNIQUE ("distirbution_bucket_operator_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_69ec9bdc975b95f7dff94a7106" UNIQUE ("distirbution_bucket_operator_id"), CONSTRAINT "PK_9bbecaa12f30e3826922688274f" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_69ec9bdc975b95f7dff94a7106" ON "distribution_bucket_operator_metadata" ("distirbution_bucket_operator_id") `)
-        await db.query(`CREATE TABLE "event" ("id" character varying NOT NULL, "in_block" integer NOT NULL, "in_extrinsic" text, "index_in_block" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "data" jsonb NOT NULL, CONSTRAINT "PK_30c2f3bbaf6d34a55f8ae6e4614" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_8f3f220c4e717207d841d4e6d4" ON "event" ("in_extrinsic") `)
+        await db.query(`CREATE TABLE "auction" ("id" character varying NOT NULL, "nft_id" character varying, "winning_member_id" character varying, "starting_price" numeric NOT NULL, "buy_now_price" numeric, "auction_type" jsonb NOT NULL, "top_bid_id" character varying, "starts_at_block" integer NOT NULL, "ended_at_block" integer, "is_canceled" boolean NOT NULL, "is_completed" boolean NOT NULL, CONSTRAINT "PK_9dc876c629273e71646cf6dfa67" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_cfb47e97e60c9d1462576f85a8" ON "auction" ("nft_id") `)
+        await db.query(`CREATE INDEX "IDX_a3127ec87cccc5696b92cac4e0" ON "auction" ("winning_member_id") `)
+        await db.query(`CREATE INDEX "IDX_1673ad4b059742fbabfc40b275" ON "auction" ("top_bid_id") `)
+        await db.query(`CREATE TABLE "auction_whitelisted_member" ("id" character varying NOT NULL, "auction_id" character varying, "member_id" character varying, CONSTRAINT "AuctionWhitelistedMember_auction_member" UNIQUE ("auction_id", "member_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_f20264ca8e878696fbc25f11bd5" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_d5ae4854487c7658b64225be30" ON "auction_whitelisted_member" ("member_id") `)
+        await db.query(`CREATE INDEX "IDX_5468573a96fa51c03743de5912" ON "auction_whitelisted_member" ("auction_id", "member_id") `)
+        await db.query(`CREATE TABLE "membership" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "handle" text NOT NULL, "controller_account" text NOT NULL, "total_channels_created" integer NOT NULL, CONSTRAINT "Membership_handle" UNIQUE ("handle") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "PK_83c1afebef3059472e7c37e8de8" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_1298811c0de5f11198fd43df72" ON "membership" ("handle") `)
         await db.query(`CREATE TABLE "notification" ("id" character varying NOT NULL, "member_id" character varying, "event_id" character varying, CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_ac8de39626657d3c0e909d9d82" ON "notification" ("member_id") `)
         await db.query(`CREATE INDEX "IDX_122be1f0696e0255acf95f9e33" ON "notification" ("event_id") `)
@@ -95,10 +88,12 @@ module.exports = class Data1680280600588 {
         await db.query(`CREATE TABLE "nft_activity" ("id" character varying NOT NULL, "member_id" character varying, "event_id" character varying, CONSTRAINT "PK_1553b1bbf8000039875a6e31536" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_18a65713a9fd0715c7a980f5d5" ON "nft_activity" ("member_id") `)
         await db.query(`CREATE INDEX "IDX_94d325a753f2c08fdd416eb095" ON "nft_activity" ("event_id") `)
-        await db.query(`CREATE TABLE "member_metadata" ("id" character varying NOT NULL, "name" text, "avatar" jsonb, "about" text, "member_id" character varying NOT NULL, CONSTRAINT "MemberMetadata_member" UNIQUE ("member_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_e7e4d350f82ae2383894f465ed" UNIQUE ("member_id"), CONSTRAINT "PK_d3fcc374696465f3c0ac3ba8708" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_e7e4d350f82ae2383894f465ed" ON "member_metadata" ("member_id") `)
-        await db.query(`CREATE TABLE "curator_group" ("id" character varying NOT NULL, "is_active" boolean NOT NULL, CONSTRAINT "PK_0b4c0ab279d72bcbf4e16b65ff1" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE TABLE "curator" ("id" character varying NOT NULL, CONSTRAINT "PK_5791051a62d2c2dfc593d38ab57" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE TABLE "video_hero" ("id" character varying NOT NULL, "video_id" character varying, "hero_title" text NOT NULL, "hero_video_cut_url" text NOT NULL, "hero_poster_url" text NOT NULL, "activated_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_f3b63979879773378afac0b9495" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_9feac5d9713a9f07e32eb8ba7a" ON "video_hero" ("video_id") `)
+        await db.query(`CREATE TABLE "video_media_encoding" ("id" character varying NOT NULL, "codec_name" text, "container" text, "mime_media_type" text, CONSTRAINT "PK_52e25874f8d8a381e154d1125e0" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE TABLE "video_media_metadata" ("id" character varying NOT NULL, "encoding_id" character varying, "pixel_width" integer, "pixel_height" integer, "size" numeric, "video_id" character varying NOT NULL, "created_in_block" integer NOT NULL, CONSTRAINT "VideoMediaMetadata_video" UNIQUE ("video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_4dc101240e8e1536b770aee202" UNIQUE ("video_id"), CONSTRAINT "PK_86a13815734e589cd86d0465e2d" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_5944dc5896cb16bd395414a0ce" ON "video_media_metadata" ("encoding_id") `)
+        await db.query(`CREATE INDEX "IDX_4dc101240e8e1536b770aee202" ON "video_media_metadata" ("video_id") `)
         await db.query(`CREATE TABLE "video_view_event" ("id" character varying NOT NULL, "video_id" text NOT NULL, "ip" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_2efd85597a6a7a704fc4d0f7701" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_2e29fba63e12a2b1818e0782d7" ON "video_view_event" ("video_id") `)
         await db.query(`CREATE INDEX "IDX_2529e6da5b4b7410d7245eef78" ON "video_view_event" ("ip") `)
@@ -112,25 +107,19 @@ module.exports = class Data1680280600588 {
         await db.query(`CREATE TABLE "channel_follow" ("id" character varying NOT NULL, "ip" text NOT NULL, "channel_id" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_9410df2b9a316af3f0d216f9487" PRIMARY KEY ("id"))`)
         await db.query(`CREATE INDEX "IDX_9bba17db99b72836523ab6c16f" ON "channel_follow" ("ip") `)
         await db.query(`CREATE INDEX "IDX_9bc0651dda94437ec18764a260" ON "channel_follow" ("channel_id") `)
-        await db.query(`CREATE TABLE "video_hero" ("id" character varying NOT NULL, "video_id" character varying, "hero_title" text NOT NULL, "hero_video_cut_url" text NOT NULL, "hero_poster_url" text NOT NULL, "activated_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_f3b63979879773378afac0b9495" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_9feac5d9713a9f07e32eb8ba7a" ON "video_hero" ("video_id") `)
-        await db.query(`CREATE TABLE "video_media_encoding" ("id" character varying NOT NULL, "codec_name" text, "container" text, "mime_media_type" text, CONSTRAINT "PK_52e25874f8d8a381e154d1125e0" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE TABLE "video_media_metadata" ("id" character varying NOT NULL, "encoding_id" character varying, "pixel_width" integer, "pixel_height" integer, "size" numeric, "video_id" character varying NOT NULL, "created_in_block" integer NOT NULL, CONSTRAINT "VideoMediaMetadata_video" UNIQUE ("video_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_4dc101240e8e1536b770aee202" UNIQUE ("video_id"), CONSTRAINT "PK_86a13815734e589cd86d0465e2d" PRIMARY KEY ("id"))`)
-        await db.query(`CREATE INDEX "IDX_5944dc5896cb16bd395414a0ce" ON "video_media_metadata" ("encoding_id") `)
-        await db.query(`CREATE INDEX "IDX_4dc101240e8e1536b770aee202" ON "video_media_metadata" ("video_id") `)
+        await db.query(`CREATE TABLE "storage_bucket_operator_metadata" ("id" character varying NOT NULL, "storage_bucket_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "StorageBucketOperatorMetadata_storageBucket" UNIQUE ("storage_bucket_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_7beffc9530b3f307bc1169cb52" UNIQUE ("storage_bucket_id"), CONSTRAINT "PK_9846a397400ae1a39b21fbd02d4" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_7beffc9530b3f307bc1169cb52" ON "storage_bucket_operator_metadata" ("storage_bucket_id") `)
+        await db.query(`CREATE TABLE "distribution_bucket_family_metadata" ("id" character varying NOT NULL, "family_id" character varying NOT NULL, "region" text, "description" text, "areas" jsonb, "latency_test_targets" text array, CONSTRAINT "DistributionBucketFamilyMetadata_family" UNIQUE ("family_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_dd93ca0ea24f3e7a02f11c4c14" UNIQUE ("family_id"), CONSTRAINT "PK_df7a270835bb313d3ef17bdee2f" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_dd93ca0ea24f3e7a02f11c4c14" ON "distribution_bucket_family_metadata" ("family_id") `)
+        await db.query(`CREATE INDEX "IDX_5510d3b244a63d6ec702faa426" ON "distribution_bucket_family_metadata" ("region") `)
+        await db.query(`CREATE TABLE "distribution_bucket_operator_metadata" ("id" character varying NOT NULL, "distirbution_bucket_operator_id" character varying NOT NULL, "node_endpoint" text, "node_location" jsonb, "extra" text, CONSTRAINT "DistributionBucketOperatorMetadata_distirbutionBucketOperator" UNIQUE ("distirbution_bucket_operator_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_69ec9bdc975b95f7dff94a7106" UNIQUE ("distirbution_bucket_operator_id"), CONSTRAINT "PK_9bbecaa12f30e3826922688274f" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_69ec9bdc975b95f7dff94a7106" ON "distribution_bucket_operator_metadata" ("distirbution_bucket_operator_id") `)
+        await db.query(`CREATE TABLE "curator_group" ("id" character varying NOT NULL, "is_active" boolean NOT NULL, CONSTRAINT "PK_0b4c0ab279d72bcbf4e16b65ff1" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE TABLE "curator" ("id" character varying NOT NULL, CONSTRAINT "PK_5791051a62d2c2dfc593d38ab57" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE TABLE "member_metadata" ("id" character varying NOT NULL, "name" text, "avatar" jsonb, "about" text, "member_id" character varying NOT NULL, CONSTRAINT "MemberMetadata_member" UNIQUE ("member_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_e7e4d350f82ae2383894f465ed" UNIQUE ("member_id"), CONSTRAINT "PK_d3fcc374696465f3c0ac3ba8708" PRIMARY KEY ("id"))`)
+        await db.query(`CREATE INDEX "IDX_e7e4d350f82ae2383894f465ed" ON "member_metadata" ("member_id") `)
         await db.query(`CREATE TABLE "next_entity_id" ("entity_name" character varying NOT NULL, "next_id" bigint NOT NULL, CONSTRAINT "PK_09a3b40db622a65096e7344d7ae" PRIMARY KEY ("entity_name"))`)
         await db.query(`CREATE TABLE "gateway_config" ("id" character varying NOT NULL, "value" text NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_db1fa5a857fb6292eee4c493e6f" PRIMARY KEY ("id"))`)
-        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d" FOREIGN KEY ("auction_id") REFERENCES "auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912" FOREIGN KEY ("nft_id") REFERENCES "owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_e7618559409a903a897164156b7" FOREIGN KEY ("bidder_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_32cb73025ec49c87f4c594a265f" FOREIGN KEY ("previous_top_bid_id") REFERENCES "bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "auction" ADD CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88" FOREIGN KEY ("nft_id") REFERENCES "owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "auction" ADD CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09" FOREIGN KEY ("winning_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "auction" ADD CONSTRAINT "FK_1673ad4b059742fbabfc40b275c" FOREIGN KEY ("top_bid_id") REFERENCES "bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5" FOREIGN KEY ("auction_id") REFERENCES "auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_d5ae4854487c7658b64225be305" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "banned_member" ADD CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "banned_member" ADD CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74" FOREIGN KEY ("channel_id") REFERENCES "channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "storage_bucket_bag" ADD CONSTRAINT "FK_791e2f82e3919ffcef8712aa1b9" FOREIGN KEY ("storage_bucket_id") REFERENCES "storage_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "storage_bucket_bag" ADD CONSTRAINT "FK_aaf00b2c7d0cba49f97da14fbba" FOREIGN KEY ("bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "distribution_bucket_operator" ADD CONSTRAINT "FK_678dc5427cdde0cd4fef2c07a43" FOREIGN KEY ("distribution_bucket_id") REFERENCES "distribution_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
@@ -138,6 +127,8 @@ module.exports = class Data1680280600588 {
         await db.query(`ALTER TABLE "distribution_bucket_bag" ADD CONSTRAINT "FK_8a807921f1aae60d4ba94895826" FOREIGN KEY ("distribution_bucket_id") REFERENCES "distribution_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "distribution_bucket_bag" ADD CONSTRAINT "FK_a9810100aee7584680f197c8ff0" FOREIGN KEY ("bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "storage_data_object" ADD CONSTRAINT "FK_ff8014300b8039dbaed764f51bc" FOREIGN KEY ("storage_bag_id") REFERENCES "storage_bag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "banned_member" ADD CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "banned_member" ADD CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74" FOREIGN KEY ("channel_id") REFERENCES "channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "app" ADD CONSTRAINT "FK_c9cc395bbc485f70a15be64553e" FOREIGN KEY ("owner_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_25c85bc448b5e236a4c1a5f7895" FOREIGN KEY ("owner_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_a77e12f3d8c6ced020e179a5e94" FOREIGN KEY ("cover_photo_id") REFERENCES "storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
@@ -163,40 +154,34 @@ module.exports = class Data1680280600588 {
         await db.query(`ALTER TABLE "video" ADD CONSTRAINT "FK_2db879ed42e3308fe65e6796729" FOREIGN KEY ("media_id") REFERENCES "storage_data_object"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "video" ADD CONSTRAINT "FK_54f88a7decf7d22fd9bd9fa439a" FOREIGN KEY ("pinned_comment_id") REFERENCES "comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "video" ADD CONSTRAINT "FK_6c49ad08c44d36d11f77c426e43" FOREIGN KEY ("entry_app_id") REFERENCES "app"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d" FOREIGN KEY ("auction_id") REFERENCES "auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912" FOREIGN KEY ("nft_id") REFERENCES "owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_e7618559409a903a897164156b7" FOREIGN KEY ("bidder_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "bid" ADD CONSTRAINT "FK_32cb73025ec49c87f4c594a265f" FOREIGN KEY ("previous_top_bid_id") REFERENCES "bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "owned_nft" ADD CONSTRAINT "FK_466896e39b9ec953f4f2545622d" FOREIGN KEY ("video_id") REFERENCES "video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "storage_bucket_operator_metadata" ADD CONSTRAINT "FK_7beffc9530b3f307bc1169cb524" FOREIGN KEY ("storage_bucket_id") REFERENCES "storage_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "distribution_bucket_family_metadata" ADD CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149" FOREIGN KEY ("family_id") REFERENCES "distribution_bucket_family"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" ADD CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069" FOREIGN KEY ("distirbution_bucket_operator_id") REFERENCES "distribution_bucket_operator"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "auction" ADD CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88" FOREIGN KEY ("nft_id") REFERENCES "owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "auction" ADD CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09" FOREIGN KEY ("winning_member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "auction" ADD CONSTRAINT "FK_1673ad4b059742fbabfc40b275c" FOREIGN KEY ("top_bid_id") REFERENCES "bid"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5" FOREIGN KEY ("auction_id") REFERENCES "auction"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "auction_whitelisted_member" ADD CONSTRAINT "FK_d5ae4854487c7658b64225be305" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_ac8de39626657d3c0e909d9d82f" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_122be1f0696e0255acf95f9e336" FOREIGN KEY ("event_id") REFERENCES "event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "nft_history_entry" ADD CONSTRAINT "FK_57f51d35ecab042478fe2e31c19" FOREIGN KEY ("nft_id") REFERENCES "owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "nft_history_entry" ADD CONSTRAINT "FK_d1a28b178f5d028d048d40ce208" FOREIGN KEY ("event_id") REFERENCES "event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "nft_activity" ADD CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "nft_activity" ADD CONSTRAINT "FK_94d325a753f2c08fdd416eb095f" FOREIGN KEY ("event_id") REFERENCES "event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
-        await db.query(`ALTER TABLE "member_metadata" ADD CONSTRAINT "FK_e7e4d350f82ae2383894f465ede" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "video_hero" ADD CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1" FOREIGN KEY ("video_id") REFERENCES "video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "video_media_metadata" ADD CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0" FOREIGN KEY ("encoding_id") REFERENCES "video_media_encoding"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
         await db.query(`ALTER TABLE "video_media_metadata" ADD CONSTRAINT "FK_4dc101240e8e1536b770aee202a" FOREIGN KEY ("video_id") REFERENCES "video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "storage_bucket_operator_metadata" ADD CONSTRAINT "FK_7beffc9530b3f307bc1169cb524" FOREIGN KEY ("storage_bucket_id") REFERENCES "storage_bucket"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "distribution_bucket_family_metadata" ADD CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149" FOREIGN KEY ("family_id") REFERENCES "distribution_bucket_family"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" ADD CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069" FOREIGN KEY ("distirbution_bucket_operator_id") REFERENCES "distribution_bucket_operator"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+        await db.query(`ALTER TABLE "member_metadata" ADD CONSTRAINT "FK_e7e4d350f82ae2383894f465ede" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
     }
 
     async down(db) {
-        await db.query(`DROP TABLE "bid"`)
-        await db.query(`DROP INDEX "public"."IDX_9e594e5a61c0f3cb25679f6ba8"`)
-        await db.query(`DROP INDEX "public"."IDX_3caf2d6b31d2fe45a2b85b8191"`)
-        await db.query(`DROP INDEX "public"."IDX_e7618559409a903a897164156b"`)
-        await db.query(`DROP INDEX "public"."IDX_32cb73025ec49c87f4c594a265"`)
-        await db.query(`DROP TABLE "auction"`)
-        await db.query(`DROP INDEX "public"."IDX_cfb47e97e60c9d1462576f85a8"`)
-        await db.query(`DROP INDEX "public"."IDX_a3127ec87cccc5696b92cac4e0"`)
-        await db.query(`DROP INDEX "public"."IDX_1673ad4b059742fbabfc40b275"`)
-        await db.query(`DROP TABLE "auction_whitelisted_member"`)
-        await db.query(`DROP INDEX "public"."IDX_d5ae4854487c7658b64225be30"`)
-        await db.query(`DROP INDEX "public"."IDX_5468573a96fa51c03743de5912"`)
-        await db.query(`DROP TABLE "banned_member"`)
-        await db.query(`DROP INDEX "public"."IDX_ed36c6c26bf5410796c2fc21f7"`)
-        await db.query(`DROP INDEX "public"."IDX_f29ff095bdb945975deca021ad"`)
-        await db.query(`DROP TABLE "membership"`)
-        await db.query(`DROP INDEX "public"."IDX_1298811c0de5f11198fd43df72"`)
+        await db.query(`DROP TABLE "event"`)
+        await db.query(`DROP INDEX "public"."IDX_8f3f220c4e717207d841d4e6d4"`)
         await db.query(`DROP TABLE "storage_bucket"`)
         await db.query(`DROP TABLE "storage_bucket_bag"`)
         await db.query(`DROP INDEX "public"."IDX_aaf00b2c7d0cba49f97da14fbb"`)
@@ -212,6 +197,9 @@ module.exports = class Data1680280600588 {
         await db.query(`DROP TABLE "storage_bag"`)
         await db.query(`DROP TABLE "storage_data_object"`)
         await db.query(`DROP INDEX "public"."IDX_ff8014300b8039dbaed764f51b"`)
+        await db.query(`DROP TABLE "banned_member"`)
+        await db.query(`DROP INDEX "public"."IDX_ed36c6c26bf5410796c2fc21f7"`)
+        await db.query(`DROP INDEX "public"."IDX_f29ff095bdb945975deca021ad"`)
         await db.query(`DROP TABLE "app"`)
         await db.query(`DROP INDEX "public"."IDX_f36adbb7b096ceeb6f3e80ad14"`)
         await db.query(`DROP INDEX "public"."IDX_c9cc395bbc485f70a15be64553"`)
@@ -253,17 +241,22 @@ module.exports = class Data1680280600588 {
         await db.query(`DROP INDEX "public"."IDX_2db879ed42e3308fe65e679672"`)
         await db.query(`DROP INDEX "public"."IDX_54f88a7decf7d22fd9bd9fa439"`)
         await db.query(`DROP INDEX "public"."IDX_6c49ad08c44d36d11f77c426e4"`)
+        await db.query(`DROP TABLE "bid"`)
+        await db.query(`DROP INDEX "public"."IDX_9e594e5a61c0f3cb25679f6ba8"`)
+        await db.query(`DROP INDEX "public"."IDX_3caf2d6b31d2fe45a2b85b8191"`)
+        await db.query(`DROP INDEX "public"."IDX_e7618559409a903a897164156b"`)
+        await db.query(`DROP INDEX "public"."IDX_32cb73025ec49c87f4c594a265"`)
         await db.query(`DROP TABLE "owned_nft"`)
         await db.query(`DROP INDEX "public"."IDX_466896e39b9ec953f4f2545622"`)
-        await db.query(`DROP TABLE "storage_bucket_operator_metadata"`)
-        await db.query(`DROP INDEX "public"."IDX_7beffc9530b3f307bc1169cb52"`)
-        await db.query(`DROP TABLE "distribution_bucket_family_metadata"`)
-        await db.query(`DROP INDEX "public"."IDX_dd93ca0ea24f3e7a02f11c4c14"`)
-        await db.query(`DROP INDEX "public"."IDX_5510d3b244a63d6ec702faa426"`)
-        await db.query(`DROP TABLE "distribution_bucket_operator_metadata"`)
-        await db.query(`DROP INDEX "public"."IDX_69ec9bdc975b95f7dff94a7106"`)
-        await db.query(`DROP TABLE "event"`)
-        await db.query(`DROP INDEX "public"."IDX_8f3f220c4e717207d841d4e6d4"`)
+        await db.query(`DROP TABLE "auction"`)
+        await db.query(`DROP INDEX "public"."IDX_cfb47e97e60c9d1462576f85a8"`)
+        await db.query(`DROP INDEX "public"."IDX_a3127ec87cccc5696b92cac4e0"`)
+        await db.query(`DROP INDEX "public"."IDX_1673ad4b059742fbabfc40b275"`)
+        await db.query(`DROP TABLE "auction_whitelisted_member"`)
+        await db.query(`DROP INDEX "public"."IDX_d5ae4854487c7658b64225be30"`)
+        await db.query(`DROP INDEX "public"."IDX_5468573a96fa51c03743de5912"`)
+        await db.query(`DROP TABLE "membership"`)
+        await db.query(`DROP INDEX "public"."IDX_1298811c0de5f11198fd43df72"`)
         await db.query(`DROP TABLE "notification"`)
         await db.query(`DROP INDEX "public"."IDX_ac8de39626657d3c0e909d9d82"`)
         await db.query(`DROP INDEX "public"."IDX_122be1f0696e0255acf95f9e33"`)
@@ -273,10 +266,12 @@ module.exports = class Data1680280600588 {
         await db.query(`DROP TABLE "nft_activity"`)
         await db.query(`DROP INDEX "public"."IDX_18a65713a9fd0715c7a980f5d5"`)
         await db.query(`DROP INDEX "public"."IDX_94d325a753f2c08fdd416eb095"`)
-        await db.query(`DROP TABLE "member_metadata"`)
-        await db.query(`DROP INDEX "public"."IDX_e7e4d350f82ae2383894f465ed"`)
-        await db.query(`DROP TABLE "curator_group"`)
-        await db.query(`DROP TABLE "curator"`)
+        await db.query(`DROP TABLE "video_hero"`)
+        await db.query(`DROP INDEX "public"."IDX_9feac5d9713a9f07e32eb8ba7a"`)
+        await db.query(`DROP TABLE "video_media_encoding"`)
+        await db.query(`DROP TABLE "video_media_metadata"`)
+        await db.query(`DROP INDEX "public"."IDX_5944dc5896cb16bd395414a0ce"`)
+        await db.query(`DROP INDEX "public"."IDX_4dc101240e8e1536b770aee202"`)
         await db.query(`DROP TABLE "video_view_event"`)
         await db.query(`DROP INDEX "public"."IDX_2e29fba63e12a2b1818e0782d7"`)
         await db.query(`DROP INDEX "public"."IDX_2529e6da5b4b7410d7245eef78"`)
@@ -290,25 +285,19 @@ module.exports = class Data1680280600588 {
         await db.query(`DROP TABLE "channel_follow"`)
         await db.query(`DROP INDEX "public"."IDX_9bba17db99b72836523ab6c16f"`)
         await db.query(`DROP INDEX "public"."IDX_9bc0651dda94437ec18764a260"`)
-        await db.query(`DROP TABLE "video_hero"`)
-        await db.query(`DROP INDEX "public"."IDX_9feac5d9713a9f07e32eb8ba7a"`)
-        await db.query(`DROP TABLE "video_media_encoding"`)
-        await db.query(`DROP TABLE "video_media_metadata"`)
-        await db.query(`DROP INDEX "public"."IDX_5944dc5896cb16bd395414a0ce"`)
-        await db.query(`DROP INDEX "public"."IDX_4dc101240e8e1536b770aee202"`)
+        await db.query(`DROP TABLE "storage_bucket_operator_metadata"`)
+        await db.query(`DROP INDEX "public"."IDX_7beffc9530b3f307bc1169cb52"`)
+        await db.query(`DROP TABLE "distribution_bucket_family_metadata"`)
+        await db.query(`DROP INDEX "public"."IDX_dd93ca0ea24f3e7a02f11c4c14"`)
+        await db.query(`DROP INDEX "public"."IDX_5510d3b244a63d6ec702faa426"`)
+        await db.query(`DROP TABLE "distribution_bucket_operator_metadata"`)
+        await db.query(`DROP INDEX "public"."IDX_69ec9bdc975b95f7dff94a7106"`)
+        await db.query(`DROP TABLE "curator_group"`)
+        await db.query(`DROP TABLE "curator"`)
+        await db.query(`DROP TABLE "member_metadata"`)
+        await db.query(`DROP INDEX "public"."IDX_e7e4d350f82ae2383894f465ed"`)
         await db.query(`DROP TABLE "next_entity_id"`)
         await db.query(`DROP TABLE "gateway_config"`)
-        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d"`)
-        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912"`)
-        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_e7618559409a903a897164156b7"`)
-        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_32cb73025ec49c87f4c594a265f"`)
-        await db.query(`ALTER TABLE "auction" DROP CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88"`)
-        await db.query(`ALTER TABLE "auction" DROP CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09"`)
-        await db.query(`ALTER TABLE "auction" DROP CONSTRAINT "FK_1673ad4b059742fbabfc40b275c"`)
-        await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5"`)
-        await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_d5ae4854487c7658b64225be305"`)
-        await db.query(`ALTER TABLE "banned_member" DROP CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e"`)
-        await db.query(`ALTER TABLE "banned_member" DROP CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74"`)
         await db.query(`ALTER TABLE "storage_bucket_bag" DROP CONSTRAINT "FK_791e2f82e3919ffcef8712aa1b9"`)
         await db.query(`ALTER TABLE "storage_bucket_bag" DROP CONSTRAINT "FK_aaf00b2c7d0cba49f97da14fbba"`)
         await db.query(`ALTER TABLE "distribution_bucket_operator" DROP CONSTRAINT "FK_678dc5427cdde0cd4fef2c07a43"`)
@@ -316,6 +305,8 @@ module.exports = class Data1680280600588 {
         await db.query(`ALTER TABLE "distribution_bucket_bag" DROP CONSTRAINT "FK_8a807921f1aae60d4ba94895826"`)
         await db.query(`ALTER TABLE "distribution_bucket_bag" DROP CONSTRAINT "FK_a9810100aee7584680f197c8ff0"`)
         await db.query(`ALTER TABLE "storage_data_object" DROP CONSTRAINT "FK_ff8014300b8039dbaed764f51bc"`)
+        await db.query(`ALTER TABLE "banned_member" DROP CONSTRAINT "FK_b94ea874da235d9b6fbc35cf58e"`)
+        await db.query(`ALTER TABLE "banned_member" DROP CONSTRAINT "FK_ed36c6c26bf5410796c2fc21f74"`)
         await db.query(`ALTER TABLE "app" DROP CONSTRAINT "FK_c9cc395bbc485f70a15be64553e"`)
         await db.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_25c85bc448b5e236a4c1a5f7895"`)
         await db.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_a77e12f3d8c6ced020e179a5e94"`)
@@ -341,19 +332,28 @@ module.exports = class Data1680280600588 {
         await db.query(`ALTER TABLE "video" DROP CONSTRAINT "FK_2db879ed42e3308fe65e6796729"`)
         await db.query(`ALTER TABLE "video" DROP CONSTRAINT "FK_54f88a7decf7d22fd9bd9fa439a"`)
         await db.query(`ALTER TABLE "video" DROP CONSTRAINT "FK_6c49ad08c44d36d11f77c426e43"`)
+        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_9e594e5a61c0f3cb25679f6ba8d"`)
+        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_3caf2d6b31d2fe45a2b85b81912"`)
+        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_e7618559409a903a897164156b7"`)
+        await db.query(`ALTER TABLE "bid" DROP CONSTRAINT "FK_32cb73025ec49c87f4c594a265f"`)
         await db.query(`ALTER TABLE "owned_nft" DROP CONSTRAINT "FK_466896e39b9ec953f4f2545622d"`)
-        await db.query(`ALTER TABLE "storage_bucket_operator_metadata" DROP CONSTRAINT "FK_7beffc9530b3f307bc1169cb524"`)
-        await db.query(`ALTER TABLE "distribution_bucket_family_metadata" DROP CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149"`)
-        await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" DROP CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069"`)
+        await db.query(`ALTER TABLE "auction" DROP CONSTRAINT "FK_cfb47e97e60c9d1462576f85a88"`)
+        await db.query(`ALTER TABLE "auction" DROP CONSTRAINT "FK_a3127ec87cccc5696b92cac4e09"`)
+        await db.query(`ALTER TABLE "auction" DROP CONSTRAINT "FK_1673ad4b059742fbabfc40b275c"`)
+        await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_aad797677bc7c7c7dc1f1d397f5"`)
+        await db.query(`ALTER TABLE "auction_whitelisted_member" DROP CONSTRAINT "FK_d5ae4854487c7658b64225be305"`)
         await db.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_ac8de39626657d3c0e909d9d82f"`)
         await db.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_122be1f0696e0255acf95f9e336"`)
         await db.query(`ALTER TABLE "nft_history_entry" DROP CONSTRAINT "FK_57f51d35ecab042478fe2e31c19"`)
         await db.query(`ALTER TABLE "nft_history_entry" DROP CONSTRAINT "FK_d1a28b178f5d028d048d40ce208"`)
         await db.query(`ALTER TABLE "nft_activity" DROP CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54"`)
         await db.query(`ALTER TABLE "nft_activity" DROP CONSTRAINT "FK_94d325a753f2c08fdd416eb095f"`)
-        await db.query(`ALTER TABLE "member_metadata" DROP CONSTRAINT "FK_e7e4d350f82ae2383894f465ede"`)
         await db.query(`ALTER TABLE "video_hero" DROP CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1"`)
         await db.query(`ALTER TABLE "video_media_metadata" DROP CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0"`)
         await db.query(`ALTER TABLE "video_media_metadata" DROP CONSTRAINT "FK_4dc101240e8e1536b770aee202a"`)
+        await db.query(`ALTER TABLE "storage_bucket_operator_metadata" DROP CONSTRAINT "FK_7beffc9530b3f307bc1169cb524"`)
+        await db.query(`ALTER TABLE "distribution_bucket_family_metadata" DROP CONSTRAINT "FK_dd93ca0ea24f3e7a02f11c4c149"`)
+        await db.query(`ALTER TABLE "distribution_bucket_operator_metadata" DROP CONSTRAINT "FK_69ec9bdc975b95f7dff94a71069"`)
+        await db.query(`ALTER TABLE "member_metadata" DROP CONSTRAINT "FK_e7e4d350f82ae2383894f465ede"`)
     }
 }

+ 3 - 0
schema/videos.graphql

@@ -120,6 +120,9 @@ type Video @entity {
 
   "Video ID coming from YPP"
   ytVideoId: String
+
+  "Video relevance score based on the views, reactions, comments and update date"
+  videoRelevance: Float!
 }
 
 type VideoFeaturedInCategory @entity @index(fields: ["category", "video"], unique: true) {

+ 5 - 0
src/mappings/content/commentsAndReactions.ts

@@ -43,6 +43,7 @@ import {
   genericEventFields,
   metaprotocolTransactionFailure,
   commentCountersManager,
+  videoRelevanceManager,
 } from '../utils'
 import { getChannelOwnerMemberByChannelId } from './utils'
 
@@ -256,6 +257,8 @@ export async function processReactVideoMessage(
 
   await processVideoReaction(overlay, block, memberId, video, reactionType, existingReaction)
 
+  videoRelevanceManager.scheduleRecalcForVideo(video.id)
+
   return new MetaprotocolTransactionResultOK()
 }
 
@@ -400,6 +403,7 @@ export async function processCreateCommentMessage(
   // schedule comment counters update
   commentCountersManager.scheduleRecalcForComment(comment.parentCommentId)
   commentCountersManager.scheduleRecalcForVideo(comment.videoId)
+  videoRelevanceManager.scheduleRecalcForVideo(comment.videoId)
 
   // add CommentCreated event
   const event = overlay.getRepository(Event).new({
@@ -533,6 +537,7 @@ export async function processDeleteCommentMessage(
   // schedule comment counters update
   commentCountersManager.scheduleRecalcForComment(comment.parentCommentId)
   commentCountersManager.scheduleRecalcForVideo(comment.videoId)
+  videoRelevanceManager.scheduleRecalcForVideo(comment.videoId)
 
   // update the comment
   comment.text = ''

+ 2 - 0
src/mappings/content/metadata.ts

@@ -63,6 +63,7 @@ import {
   genericEventFields,
   invalidMetadata,
   metaprotocolTransactionFailure,
+  videoRelevanceManager,
 } from '../utils'
 import { AsDecoded, ASSETS_MAP, EntityAssetProps, EntityAssetsMap, MetaNumberProps } from './utils'
 
@@ -571,6 +572,7 @@ export async function processModerateCommentMessage(
   // schedule comment counters updates
   commentCountersManager.scheduleRecalcForComment(comment.parentCommentId)
   commentCountersManager.scheduleRecalcForVideo(comment.videoId)
+  videoRelevanceManager.scheduleRecalcForVideo(comment.videoId)
 
   comment.text = ''
   comment.status = CommentStatus.MODERATED

+ 12 - 0
src/mappings/content/video.ts

@@ -12,6 +12,8 @@ import { deserializeMetadata, u8aToBytes } from '../utils'
 import { processVideoMetadata } from './metadata'
 import { deleteVideo, encodeAssets, processAppActionMetadata, processNft } from './utils'
 import { generateAppActionCommitment } from '@joystream/js/utils'
+import { config, ConfigVariable } from '../../utils/config'
+import { NEWNESS_SECONDS_DIVIDER } from '../../utils/VideoRelevanceManager'
 
 export async function processVideoCreatedEvent({
   overlay,
@@ -26,6 +28,10 @@ export async function processVideoCreatedEvent({
 
   const videoId = contentId.toString()
   const viewsNum = await overlay.getEm().getRepository(VideoViewEvent).countBy({ videoId })
+  const [newnessWeight, viewsWeight] = await config.get(
+    ConfigVariable.RelevanceWeights,
+    overlay.getEm()
+  )
   const video = overlay.getRepository(Video).new({
     id: videoId,
     createdAt: new Date(block.timestamp),
@@ -39,6 +45,12 @@ export async function processVideoCreatedEvent({
     commentsCount: 0,
     reactionsCount: 0,
     viewsNum,
+    // First we need to dic by 1k to match postgres epoch (in seconds) then apply the further dividers
+    videoRelevance: +(
+      (30 - (Date.now() - new Date(block.timestamp).getTime()) / (1000 * NEWNESS_SECONDS_DIVIDER)) *
+        newnessWeight +
+      viewsNum * viewsWeight
+    ).toFixed(2),
   })
 
   // fetch related channel and owner

+ 2 - 0
src/mappings/utils.ts

@@ -15,8 +15,10 @@ import { Bytes } from '@polkadot/types/primitive'
 import { createType } from '@joystream/types'
 import { u8aToHex } from '@polkadot/util'
 import { CommentCountersManager } from '../utils/CommentsCountersManager'
+import { VideoRelevanceManager } from '../utils/VideoRelevanceManager'
 
 export const commentCountersManager = new CommentCountersManager()
+export const videoRelevanceManager = new VideoRelevanceManager()
 
 export const JOYSTREAM_SS58_PREFIX = 126
 

+ 2 - 1
src/processor.ts

@@ -88,7 +88,7 @@ import { Event } from './types/support'
 import { assertAssignable } from './utils/misc'
 import { EntityManagerOverlay } from './utils/overlay'
 import { EventNames, EventHandler, eventConstructors, EventInstance } from './utils/events'
-import { commentCountersManager } from './mappings/utils'
+import { commentCountersManager, videoRelevanceManager } from './mappings/utils'
 import { EntityManager } from 'typeorm'
 
 const defaultEventOptions = {
@@ -278,6 +278,7 @@ async function processEvent<EventName extends EventNames>(
 async function afterDbUpdate(em: EntityManager) {
   await commentCountersManager.updateVideoCommentsCounters(em)
   await commentCountersManager.updateParentRepliesCounters(em)
+  await videoRelevanceManager.updateVideoRelevanceValue(em)
 }
 
 processor.run(new TypeormDatabase({ isolationLevel: 'READ COMMITTED' }), async (ctx) => {

+ 16 - 0
src/server-extension/resolvers/AdminResolver/index.ts

@@ -20,7 +20,9 @@ import {
   SetVideoHeroInput,
   SetVideoHeroResult,
   SetVideoViewPerIpTimeLimitInput,
+  SetVideoWeightsInput,
   VideoViewPerIpTimeLimit,
+  VideoWeights,
 } from './types'
 import { config, ConfigVariable } from '../../../utils/config'
 import { OperatorOnly } from '../middleware'
@@ -44,12 +46,26 @@ import { generateAppActionCommitment } from '@joystream/js/utils'
 import { AppAction } from '@joystream/metadata-protobuf'
 import { withHiddenEntities } from '../../../utils/sql'
 import { processCommentsCensorshipStatusUpdate } from './utils'
+import { videoRelevanceManager } from '../../../mappings/utils'
 
 @Resolver()
 export class AdminResolver {
   // Set by depenency injection
   constructor(private em: () => Promise<EntityManager>) {}
 
+  @UseMiddleware(OperatorOnly)
+  @Mutation(() => VideoWeights)
+  async setVideoWeights(@Args() args: SetVideoWeightsInput): Promise<VideoWeights> {
+    const em = await this.em()
+    await config.set(
+      ConfigVariable.RelevanceWeights,
+      [args.newnessWeight, args.viewsWeight, args.commentsWeight, args.reactionsWeight],
+      em
+    )
+    await videoRelevanceManager.updateVideoRelevanceValue(em, true)
+    return { isApplied: true }
+  }
+
   @UseMiddleware(OperatorOnly)
   @Mutation(() => KillSwitch)
   async setKillSwitch(@Args() args: SetKillSwitchInput): Promise<KillSwitch> {

+ 22 - 1
src/server-extension/resolvers/AdminResolver/types.ts

@@ -1,6 +1,27 @@
-import { ArgsType, Field, InputType, Int, ObjectType, registerEnumType } from 'type-graphql'
+import { ArgsType, Field, Float, InputType, Int, ObjectType, registerEnumType } from 'type-graphql'
 import { AppAction } from '@joystream/metadata-protobuf'
 
+@ArgsType()
+export class SetVideoWeightsInput {
+  @Field(() => Float, { nullable: false })
+  newnessWeight!: number
+
+  @Field(() => Float, { nullable: false })
+  viewsWeight!: number
+
+  @Field(() => Float, { nullable: false })
+  commentsWeight!: number
+
+  @Field(() => Float, { nullable: false })
+  reactionsWeight!: number
+}
+
+@ObjectType()
+export class VideoWeights {
+  @Field(() => Boolean, { nullable: false })
+  isApplied!: boolean
+}
+
 @ArgsType()
 export class SetKillSwitchInput {
   @Field(() => Boolean, { nullable: false })

+ 4 - 0
src/server-extension/resolvers/AdminResolver/utils.ts

@@ -1,14 +1,18 @@
 import { EntityManager, In } from 'typeorm'
 import { CommentCountersManager } from '../../../utils/CommentsCountersManager'
 import { Comment } from '../../../model'
+import { VideoRelevanceManager } from '../../../utils/VideoRelevanceManager'
 
 export async function processCommentsCensorshipStatusUpdate(em: EntityManager, ids: string[]) {
   const manager = new CommentCountersManager()
+  const videoRelevanceManager = new VideoRelevanceManager()
   const comments = await em.getRepository(Comment).find({ where: { id: In(ids) } })
   comments.forEach((c) => {
     manager.scheduleRecalcForComment(c.parentCommentId)
     manager.scheduleRecalcForVideo(c.videoId)
+    videoRelevanceManager.scheduleRecalcForVideo(c.videoId)
   })
   await manager.updateVideoCommentsCounters(em)
   await manager.updateParentRepliesCounters(em)
+  await videoRelevanceManager.updateVideoRelevanceValue(em)
 }

+ 9 - 2
src/server-extension/resolvers/VideosResolver/index.ts

@@ -9,14 +9,14 @@ import {
 } from './types'
 import { VideosConnection } from '../baseTypes'
 import { Context } from '@subsquid/openreader/lib/context'
-import { VideoViewEvent, Video, Report } from '../../../model'
+import { Report, Video, VideoViewEvent } from '../../../model'
 import { ensureArray } from '@subsquid/openreader/lib/util/util'
 import { UserInputError } from 'apollo-server-core'
 import { parseOrderBy } from '@subsquid/openreader/lib/opencrud/orderBy'
 import { parseWhere } from '@subsquid/openreader/lib/opencrud/where'
 import {
-  RelayConnectionRequest,
   decodeRelayConnectionCursor,
+  RelayConnectionRequest,
 } from '@subsquid/openreader/lib/ir/connection'
 import { AnyFields } from '@subsquid/openreader/lib/ir/fields'
 import {
@@ -36,6 +36,7 @@ import { ContextWithIP } from '../../check'
 import { randomAsHex } from '@polkadot/util-crypto'
 import { isObject } from 'lodash'
 import { has } from '../../../utils/misc'
+import { videoRelevanceManager } from '../../../mappings/utils'
 
 @Resolver()
 export class VideosResolver {
@@ -210,7 +211,13 @@ export class VideosResolver {
         timestamp: new Date(),
         videoId,
       })
+
+      const tick = await config.get(ConfigVariable.VideoRelevanceViewsTick, em)
+      if (video.viewsNum % tick === 0) {
+        videoRelevanceManager.scheduleRecalcForVideo(videoId)
+      }
       await em.save([video, video.channel, newView])
+      await videoRelevanceManager.updateVideoRelevanceValue(em)
       return {
         videoId,
         viewsNum: video.viewsNum,

+ 42 - 0
src/utils/VideoRelevanceManager.ts

@@ -0,0 +1,42 @@
+import { EntityManager } from 'typeorm'
+import { config, ConfigVariable } from './config'
+
+// constant used to parse seconds from creation
+export const NEWNESS_SECONDS_DIVIDER = 60 * 60 * 24
+
+export class VideoRelevanceManager {
+  private videosToUpdate: Set<string> = new Set()
+
+  scheduleRecalcForVideo(id: string | null | undefined) {
+    id && this.videosToUpdate.add(id)
+  }
+
+  async updateVideoRelevanceValue(em: EntityManager, forceUpdateAll?: boolean) {
+    if (this.videosToUpdate.size || forceUpdateAll) {
+      const [newnessWeight, viewsWeight, commentsWeight, reactionsWeight] = await config.get(
+        ConfigVariable.RelevanceWeights,
+        em
+      )
+      await em.query(`
+        UPDATE "video"
+        SET
+          "video_relevance" = ROUND(
+          ((30 - (extract(epoch from now() - created_at) / ${NEWNESS_SECONDS_DIVIDER})) * ${newnessWeight}) +
+          (views_num * ${viewsWeight}) +
+          (
+            comments_count * ${commentsWeight} 
+          ) +
+          (
+            reactions_count * ${reactionsWeight} 
+          ), 2)
+        ${
+          forceUpdateAll
+            ? ''
+            : `WHERE "id" IN (${[...this.videosToUpdate.values()]
+                .map((id) => `'${id}'`)
+                .join(', ')})`
+        }`)
+      this.videosToUpdate.clear()
+    }
+  }
+}

+ 9 - 0
src/utils/config.ts

@@ -6,6 +6,8 @@ export enum ConfigVariable {
   SupportNewCategories = 'SUPPORT_NEW_CATEGORIES',
   KillSwitch = 'KILL_SWITCH_ON',
   VideoViewPerIpTimeLimit = 'VIDEO_VIEW_PER_IP_TIME_LIMIT',
+  VideoRelevanceViewsTick = 'VIDEO_RELEVANCE_VIEWS_TICK',
+  RelevanceWeights = 'RELEVANCE_WEIGHTS',
   AppPrivateKey = 'APP_PRIVATE_KEY',
 }
 
@@ -24,11 +26,18 @@ const stringType = {
   deserialize: (v: string) => v,
 }
 
+const jsonType = <T>() => ({
+  serialize: (v: T) => JSON.stringify(v),
+  deserialize: (v: string) => JSON.parse(v) as T,
+})
+
 export const configVariables = {
   [ConfigVariable.SupportNoCategoryVideo]: boolType,
   [ConfigVariable.SupportNewCategories]: boolType,
   [ConfigVariable.KillSwitch]: boolType,
   [ConfigVariable.VideoViewPerIpTimeLimit]: numberType,
+  [ConfigVariable.VideoRelevanceViewsTick]: numberType,
+  [ConfigVariable.RelevanceWeights]: jsonType<[number, number, number, number]>(),
   [ConfigVariable.AppPrivateKey]: stringType,
 } as const