mirror of https://github.com/Chocobozzz/PeerTube
add best trending strategy based on Reddit's best
inspired from https://www.reddit.com/r/changelog/comments/7spgg0/best_is_the_new_hotness/ this implementation only adds freshness, and doesn't personalize based on subscribed communities yet.pull/3695/head
parent
f6267b6101
commit
3d4e112d16
|
@ -271,6 +271,7 @@
|
|||
<option i18n value="/videos/overview">Discover videos</option>
|
||||
<optgroup i18n-label label="Trending pages">
|
||||
<option i18n value="/videos/trending">Default trending page</option>
|
||||
<option i18n value="/videos/trending?alg=best" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('best')">Best videos</option>
|
||||
<option i18n value="/videos/trending?alg=hot" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('hot')">Hot videos</option>
|
||||
<option i18n value="/videos/trending?alg=most-viewed" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-viewed')">Most viewed videos</option>
|
||||
<option i18n value="/videos/trending?alg=most-liked" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-liked')">Most liked videos</option>
|
||||
|
@ -288,6 +289,7 @@
|
|||
<label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
|
||||
<option i18n value="best">Best videos</option>
|
||||
<option i18n value="hot">Hot videos</option>
|
||||
<option i18n value="most-viewed">Most viewed videos</option>
|
||||
<option i18n value="most-liked">Most liked videos</option>
|
||||
|
|
|
@ -35,6 +35,13 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
|
|||
super(data)
|
||||
|
||||
this.buttons = [
|
||||
{
|
||||
label: $localize`:A variant of Trending videos based on the number of recent interactions, minus user history:Best`,
|
||||
iconName: 'award',
|
||||
value: 'best',
|
||||
tooltip: $localize`Videos totalizing the most interactions for recent videos, minus user history`,
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`,
|
||||
iconName: 'flame',
|
||||
|
|
|
@ -131,7 +131,7 @@ export class ServerService {
|
|||
videos: {
|
||||
intervalDays: 0,
|
||||
algorithms: {
|
||||
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
|
||||
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
|
||||
default: 'most-viewed'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,8 @@ const icons = {
|
|||
'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default,
|
||||
'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
|
||||
'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default,
|
||||
'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default
|
||||
'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default,
|
||||
'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default
|
||||
}
|
||||
|
||||
export type GlobalIconName = keyof typeof icons
|
||||
|
|
|
@ -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>
|
After Width: | Height: | Size: 325 B |
|
@ -110,7 +110,8 @@ trending:
|
|||
interval_days: 7 # Compute trending videos for the last x days
|
||||
algorithms:
|
||||
enabled:
|
||||
- 'hot' # adaptation of the Reddit 'Hot' algorithm
|
||||
- 'best' # adaptation of Reddit's 'Best' algorithm (Hot minus History)
|
||||
- 'hot' # adaptation of Reddit's 'Hot' algorithm
|
||||
- 'most-viewed' # default, used initially by PeerTube as the trending page
|
||||
- 'most-liked'
|
||||
default: 'most-viewed'
|
||||
|
|
|
@ -108,7 +108,8 @@ trending:
|
|||
interval_days: 7 # Compute trending videos for the last x days
|
||||
algorithms:
|
||||
enabled:
|
||||
- 'hot' # adaptation of the Reddit 'Hot' algorithm
|
||||
- 'best' # adaptation of Reddit's 'Best' algorithm (Hot minus History)
|
||||
- 'hot' # adaptation of Reddit's 'Hot' algorithm
|
||||
- 'most-viewed' # default, used initially by PeerTube as the trending page
|
||||
- 'most-liked'
|
||||
default: 'most-viewed'
|
||||
|
|
|
@ -72,7 +72,7 @@ const SORTABLE_COLUMNS = {
|
|||
FOLLOWERS: [ 'createdAt', 'state', 'score' ],
|
||||
FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
|
||||
|
||||
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot' ],
|
||||
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
|
||||
|
||||
// Don't forget to update peertube-search-index with the same values
|
||||
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
|
||||
|
|
|
@ -31,8 +31,8 @@ export type BuildVideosQueryOptions = {
|
|||
|
||||
videoPlaylistId?: number
|
||||
|
||||
trendingAlgorithm?: string // best, hot, or any other algorithm implemented
|
||||
trendingDays?: number
|
||||
hot?: boolean
|
||||
|
||||
user?: MUserAccountId
|
||||
historyOfUser?: MUserId
|
||||
|
@ -252,7 +252,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
|
|||
attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
|
||||
|
||||
group = 'GROUP BY "video"."id"'
|
||||
} else if (options.hot) {
|
||||
} else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) {
|
||||
/**
|
||||
* "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
|
||||
* with fixed weights only applied to their log values.
|
||||
|
@ -269,28 +269,39 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
|
|||
*/
|
||||
const weights = {
|
||||
like: 3,
|
||||
dislike: 3,
|
||||
dislike: -3,
|
||||
view: 1 / 12,
|
||||
comment: 2 // a comment takes more time than a like to do, but can be done multiple times
|
||||
comment: 2, // a comment takes more time than a like to do, but can be done multiple times
|
||||
history: -2
|
||||
}
|
||||
|
||||
joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"')
|
||||
|
||||
attributes.push(
|
||||
let attribute =
|
||||
`LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+)
|
||||
`- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
|
||||
`+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
|
||||
`+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+)
|
||||
`+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+)
|
||||
'+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
|
||||
'AS "score"'
|
||||
)
|
||||
'+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' // base score (in number of half-days)
|
||||
|
||||
if (options.trendingAlgorithm === 'best' && options.user) {
|
||||
joins.push(
|
||||
'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser'
|
||||
)
|
||||
replacements.bestUser = options.user.id
|
||||
|
||||
attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} `
|
||||
}
|
||||
|
||||
attribute += 'AS "score"'
|
||||
attributes.push(attribute)
|
||||
|
||||
group = 'GROUP BY "video"."id"'
|
||||
}
|
||||
}
|
||||
|
||||
if (options.historyOfUser) {
|
||||
joins.push('INNER JOIN "userVideoHistory" on "video"."id" = "userVideoHistory"."videoId"')
|
||||
joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"')
|
||||
|
||||
and.push('"userVideoHistory"."userId" = :historyOfUser')
|
||||
replacements.historyOfUser = options.historyOfUser.id
|
||||
|
@ -410,7 +421,7 @@ function buildOrder (value: string) {
|
|||
|
||||
if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
|
||||
|
||||
if ([ 'trending', 'hot' ].includes(field.toLowerCase())) { // Sort by aggregation
|
||||
if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation
|
||||
return `ORDER BY "score" ${direction}, "video"."views" ${direction}`
|
||||
}
|
||||
|
||||
|
|
|
@ -1090,7 +1090,9 @@ export class VideoModel extends Model {
|
|||
const trendingDays = options.sort.endsWith('trending')
|
||||
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
|
||||
: undefined
|
||||
const hot = options.sort.endsWith('hot')
|
||||
let trendingAlgorithm
|
||||
if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot'
|
||||
if (options.sort.endsWith('best')) trendingAlgorithm = 'best'
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
|
@ -1120,7 +1122,7 @@ export class VideoModel extends Model {
|
|||
user: options.user,
|
||||
historyOfUser: options.historyOfUser,
|
||||
trendingDays,
|
||||
hot,
|
||||
trendingAlgorithm,
|
||||
search: options.search
|
||||
}
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@ describe('Test config API validators', function () {
|
|||
trending: {
|
||||
videos: {
|
||||
algorithms: {
|
||||
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
|
||||
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
|
||||
default: 'most-viewed'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -375,7 +375,7 @@ describe('Test config', function () {
|
|||
trending: {
|
||||
videos: {
|
||||
algorithms: {
|
||||
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
|
||||
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
|
||||
default: 'hot'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -363,6 +363,14 @@ describe('Test a single server', function () {
|
|||
expect(videos.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('Should list and sort by best in descending order', async function () {
|
||||
const res = await getVideosListPagination(server.url, 0, 2, '-best')
|
||||
|
||||
const videos = res.body.data
|
||||
expect(res.body.total).to.equal(6)
|
||||
expect(videos.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('Should update a video', async function () {
|
||||
const attributes = {
|
||||
name: 'my super video updated',
|
||||
|
|
|
@ -164,7 +164,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
|
|||
trending: {
|
||||
videos: {
|
||||
algorithms: {
|
||||
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
|
||||
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
|
||||
default: 'hot'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@ export type VideoSortField =
|
|||
|
||||
// trending sorts
|
||||
'trending' | '-trending' |
|
||||
'hot' | '-hot'
|
||||
'hot' | '-hot' |
|
||||
'best' | '-best'
|
||||
|
|
Loading…
Reference in New Issue