aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html2
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.ts7
-rw-r--r--client/src/app/core/server/server.service.ts2
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts3
-rw-r--r--client/src/assets/images/feather/award.svg1
-rw-r--r--config/default.yaml3
-rw-r--r--config/production.yaml.example3
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/models/video/video-query-builder.ts33
-rw-r--r--server/models/video/video.ts6
-rw-r--r--server/tests/api/check-params/config.ts2
-rw-r--r--server/tests/api/server/config.ts2
-rw-r--r--server/tests/api/videos/single-server.ts8
-rw-r--r--shared/extra-utils/server/config.ts2
-rw-r--r--shared/models/videos/video-sort-field.type.ts3
15 files changed, 57 insertions, 22 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 796aa12ed..48678a194 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -271,6 +271,7 @@
271 <option i18n value="/videos/overview">Discover videos</option> 271 <option i18n value="/videos/overview">Discover videos</option>
272 <optgroup i18n-label label="Trending pages"> 272 <optgroup i18n-label label="Trending pages">
273 <option i18n value="/videos/trending">Default trending page</option> 273 <option i18n value="/videos/trending">Default trending page</option>
274 <option i18n value="/videos/trending?alg=best" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('best')">Best videos</option>
274 <option i18n value="/videos/trending?alg=hot" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('hot')">Hot videos</option> 275 <option i18n value="/videos/trending?alg=hot" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('hot')">Hot videos</option>
275 <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-viewed')">Most viewed videos</option> 276 <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-viewed')">Most viewed videos</option>
276 <option i18n value="/videos/trending?alg=most-liked" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-liked')">Most liked videos</option> 277 <option i18n value="/videos/trending?alg=most-liked" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-liked')">Most liked videos</option>
@@ -288,6 +289,7 @@
288 <label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label> 289 <label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
289 <div class="peertube-select-container"> 290 <div class="peertube-select-container">
290 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> 291 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
292 <option i18n value="best">Best videos</option>
291 <option i18n value="hot">Hot videos</option> 293 <option i18n value="hot">Hot videos</option>
292 <option i18n value="most-viewed">Most viewed videos</option> 294 <option i18n value="most-viewed">Most viewed videos</option>
293 <option i18n value="most-liked">Most liked videos</option> 295 <option i18n value="most-liked">Most liked videos</option>
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
index 33eaa2c1e..a4a1e358f 100644
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
@@ -36,6 +36,13 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
36 36
37 this.buttons = [ 37 this.buttons = [
38 { 38 {
39 label: $localize`:A variant of Trending videos based on the number of recent interactions, minus user history:Best`,
40 iconName: 'award',
41 value: 'best',
42 tooltip: $localize`Videos totalizing the most interactions for recent videos, minus user history`,
43 hidden: true
44 },
45 {
39 label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`, 46 label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`,
40 iconName: 'flame', 47 iconName: 'flame',
41 value: 'hot', 48 value: 'hot',
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 39739afd0..11288fc54 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -131,7 +131,7 @@ export class ServerService {
131 videos: { 131 videos: {
132 intervalDays: 0, 132 intervalDays: 0,
133 algorithms: { 133 algorithms: {
134 enabled: [ 'hot', 'most-viewed', 'most-liked' ], 134 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
135 default: 'most-viewed' 135 default: 'most-viewed'
136 } 136 }
137 } 137 }
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts
index def488df0..3af517927 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -71,7 +71,8 @@ const icons = {
71 'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default, 71 'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default,
72 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, 72 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
73 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, 73 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default,
74 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default 74 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default,
75 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default
75} 76}
76 77
77export type GlobalIconName = keyof typeof icons 78export type GlobalIconName = keyof typeof icons
diff --git a/client/src/assets/images/feather/award.svg b/client/src/assets/images/feather/award.svg
new file mode 100644
index 000000000..be70d5a13
--- /dev/null
+++ b/client/src/assets/images/feather/award.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-award"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg> \ No newline at end of file
diff --git a/config/default.yaml b/config/default.yaml
index 22488da99..95df2e06c 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -110,7 +110,8 @@ trending:
110 interval_days: 7 # Compute trending videos for the last x days 110 interval_days: 7 # Compute trending videos for the last x days
111 algorithms: 111 algorithms:
112 enabled: 112 enabled:
113 - 'hot' # adaptation of the Reddit 'Hot' algorithm 113 - 'best' # adaptation of Reddit's 'Best' algorithm (Hot minus History)
114 - 'hot' # adaptation of Reddit's 'Hot' algorithm
114 - 'most-viewed' # default, used initially by PeerTube as the trending page 115 - 'most-viewed' # default, used initially by PeerTube as the trending page
115 - 'most-liked' 116 - 'most-liked'
116 default: 'most-viewed' 117 default: 'most-viewed'
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 66c981dd5..13a646918 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -108,7 +108,8 @@ trending:
108 interval_days: 7 # Compute trending videos for the last x days 108 interval_days: 7 # Compute trending videos for the last x days
109 algorithms: 109 algorithms:
110 enabled: 110 enabled:
111 - 'hot' # adaptation of the Reddit 'Hot' algorithm 111 - 'best' # adaptation of Reddit's 'Best' algorithm (Hot minus History)
112 - 'hot' # adaptation of Reddit's 'Hot' algorithm
112 - 'most-viewed' # default, used initially by PeerTube as the trending page 113 - 'most-viewed' # default, used initially by PeerTube as the trending page
113 - 'most-liked' 114 - 'most-liked'
114 default: 'most-viewed' 115 default: 'most-viewed'
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 0fab872a9..9d9b3966c 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -72,7 +72,7 @@ const SORTABLE_COLUMNS = {
72 FOLLOWERS: [ 'createdAt', 'state', 'score' ], 72 FOLLOWERS: [ 'createdAt', 'state', 'score' ],
73 FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ], 73 FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
74 74
75 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot' ], 75 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
76 76
77 // Don't forget to update peertube-search-index with the same values 77 // Don't forget to update peertube-search-index with the same values
78 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], 78 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index e2145fb9a..822d0c89b 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -31,8 +31,8 @@ export type BuildVideosQueryOptions = {
31 31
32 videoPlaylistId?: number 32 videoPlaylistId?: number
33 33
34 trendingAlgorithm?: string // best, hot, or any other algorithm implemented
34 trendingDays?: number 35 trendingDays?: number
35 hot?: boolean
36 36
37 user?: MUserAccountId 37 user?: MUserAccountId
38 historyOfUser?: MUserId 38 historyOfUser?: MUserId
@@ -252,7 +252,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
252 attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') 252 attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
253 253
254 group = 'GROUP BY "video"."id"' 254 group = 'GROUP BY "video"."id"'
255 } else if (options.hot) { 255 } else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) {
256 /** 256 /**
257 * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, 257 * "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
258 * with fixed weights only applied to their log values. 258 * with fixed weights only applied to their log values.
@@ -269,28 +269,39 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
269 */ 269 */
270 const weights = { 270 const weights = {
271 like: 3, 271 like: 3,
272 dislike: 3, 272 dislike: -3,
273 view: 1 / 12, 273 view: 1 / 12,
274 comment: 2 // a comment takes more time than a like to do, but can be done multiple times 274 comment: 2, // a comment takes more time than a like to do, but can be done multiple times
275 history: -2
275 } 276 }
276 277
277 joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') 278 joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"')
278 279
279 attributes.push( 280 let attribute =
280 `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) 281 `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+)
281 `- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) 282 `+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
282 `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) 283 `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+)
283 `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+) 284 `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+)
284 '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days) 285 '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' // base score (in number of half-days)
285 'AS "score"' 286
286 ) 287 if (options.trendingAlgorithm === 'best' && options.user) {
288 joins.push(
289 'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser'
290 )
291 replacements.bestUser = options.user.id
292
293 attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} `
294 }
295
296 attribute += 'AS "score"'
297 attributes.push(attribute)
287 298
288 group = 'GROUP BY "video"."id"' 299 group = 'GROUP BY "video"."id"'
289 } 300 }
290 } 301 }
291 302
292 if (options.historyOfUser) { 303 if (options.historyOfUser) {
293 joins.push('INNER JOIN "userVideoHistory" on "video"."id" = "userVideoHistory"."videoId"') 304 joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"')
294 305
295 and.push('"userVideoHistory"."userId" = :historyOfUser') 306 and.push('"userVideoHistory"."userId" = :historyOfUser')
296 replacements.historyOfUser = options.historyOfUser.id 307 replacements.historyOfUser = options.historyOfUser.id
@@ -410,7 +421,7 @@ function buildOrder (value: string) {
410 421
411 if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' 422 if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
412 423
413 if ([ 'trending', 'hot' ].includes(field.toLowerCase())) { // Sort by aggregation 424 if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation
414 return `ORDER BY "score" ${direction}, "video"."views" ${direction}` 425 return `ORDER BY "score" ${direction}, "video"."views" ${direction}`
415 } 426 }
416 427
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index ea6c9d44b..0ecb8d600 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1090,7 +1090,9 @@ export class VideoModel extends Model {
1090 const trendingDays = options.sort.endsWith('trending') 1090 const trendingDays = options.sort.endsWith('trending')
1091 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS 1091 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
1092 : undefined 1092 : undefined
1093 const hot = options.sort.endsWith('hot') 1093 let trendingAlgorithm
1094 if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot'
1095 if (options.sort.endsWith('best')) trendingAlgorithm = 'best'
1094 1096
1095 const serverActor = await getServerActor() 1097 const serverActor = await getServerActor()
1096 1098
@@ -1120,7 +1122,7 @@ export class VideoModel extends Model {
1120 user: options.user, 1122 user: options.user,
1121 historyOfUser: options.historyOfUser, 1123 historyOfUser: options.historyOfUser,
1122 trendingDays, 1124 trendingDays,
1123 hot, 1125 trendingAlgorithm,
1124 search: options.search 1126 search: options.search
1125 } 1127 }
1126 1128
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index e6309b5f7..d6c20f7af 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -141,7 +141,7 @@ describe('Test config API validators', function () {
141 trending: { 141 trending: {
142 videos: { 142 videos: {
143 algorithms: { 143 algorithms: {
144 enabled: [ 'hot', 'most-viewed', 'most-liked' ], 144 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
145 default: 'most-viewed' 145 default: 'most-viewed'
146 } 146 }
147 } 147 }
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index e5bab0b77..26df8373e 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -375,7 +375,7 @@ describe('Test config', function () {
375 trending: { 375 trending: {
376 videos: { 376 videos: {
377 algorithms: { 377 algorithms: {
378 enabled: [ 'hot', 'most-viewed', 'most-liked' ], 378 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
379 default: 'hot' 379 default: 'hot'
380 } 380 }
381 } 381 }
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index 52c6800c1..da90223b8 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -363,6 +363,14 @@ describe('Test a single server', function () {
363 expect(videos.length).to.equal(2) 363 expect(videos.length).to.equal(2)
364 }) 364 })
365 365
366 it('Should list and sort by best in descending order', async function () {
367 const res = await getVideosListPagination(server.url, 0, 2, '-best')
368
369 const videos = res.body.data
370 expect(res.body.total).to.equal(6)
371 expect(videos.length).to.equal(2)
372 })
373
366 it('Should update a video', async function () { 374 it('Should update a video', async function () {
367 const attributes = { 375 const attributes = {
368 name: 'my super video updated', 376 name: 'my super video updated',
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
index 8998da8b6..db5a473ca 100644
--- a/shared/extra-utils/server/config.ts
+++ b/shared/extra-utils/server/config.ts
@@ -164,7 +164,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
164 trending: { 164 trending: {
165 videos: { 165 videos: {
166 algorithms: { 166 algorithms: {
167 enabled: [ 'hot', 'most-viewed', 'most-liked' ], 167 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
168 default: 'hot' 168 default: 'hot'
169 } 169 }
170 } 170 }
diff --git a/shared/models/videos/video-sort-field.type.ts b/shared/models/videos/video-sort-field.type.ts
index 97687f84b..5073848b8 100644
--- a/shared/models/videos/video-sort-field.type.ts
+++ b/shared/models/videos/video-sort-field.type.ts
@@ -8,4 +8,5 @@ export type VideoSortField =
8 8
9 // trending sorts 9 // trending sorts
10 'trending' | '-trending' | 10 'trending' | '-trending' |
11 'hot' | '-hot' 11 'hot' | '-hot' |
12 'best' | '-best'