Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Video } from '@app/shared/shared-main/video/video.model'
import { VideoService } from '@app/shared/shared-main/video/video.service'
import { AdvancedSearch } from '@app/shared/shared-search/advanced-search.model'
import { SearchService } from '@app/shared/shared-search/search.service'
import { HTMLServerConfig } from '@peertube/peertube-models'
import { HTMLServerConfig, VideoRecommendationPolicy } from '@peertube/peertube-models'
import { Observable, of } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'

Expand Down Expand Up @@ -59,12 +59,17 @@ export class VideoRecommendationService {
return this.userService.getAnonymousOrLoggedUser()
.pipe(
switchMap(user => {
const defaultSubscription = this.videos.listVideos({
const defaultSubscription = this.videos.listRecommendationVideos({
skipCount: true,
videoPagination: pagination,
sort: '-publishedAt'
sort: '-publishedAt',
currentVideo
}).pipe(map(v => v.data))

if (currentVideo.recommendationPolicy.id !== VideoRecommendationPolicy.ANY_VIDEOS) {
return defaultSubscription
}

const searchIndexConfig = this.config.search.searchIndex
if (searchIndexConfig.enabled === true && searchIndexConfig.disableLocalSearch === true) {
return defaultSubscription
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ type UpdateFromAPIOptions = {
| 'waitTranscoding'
| 'support'
| 'commentsPolicy'
| 'recommendationPolicy'
| 'downloadEnabled'
| 'pluginData'
| 'scheduledUpdate'
Expand Down Expand Up @@ -404,6 +405,7 @@ export class VideoEdit {
waitTranscoding: video.waitTranscoding ?? null,
support: video.support ?? '',
commentsPolicy: video.commentsPolicy?.id ?? null,
recommendationPolicy: video.recommendationPolicy?.id ?? null,

downloadEnabled: video.downloadEnabled ?? null,

Expand Down Expand Up @@ -558,6 +560,7 @@ export class VideoEdit {
if (values.waitTranscoding !== undefined) this.common.waitTranscoding = values.waitTranscoding
if (values.support !== undefined) this.common.support = values.support
if (values.commentsPolicy !== undefined) this.common.commentsPolicy = values.commentsPolicy
if (values.recommendationPolicy !== undefined) this.common.recommendationPolicy = values.recommendationPolicy
if (values.downloadEnabled !== undefined) this.common.downloadEnabled = values.downloadEnabled
if (values.thumbnailfile !== undefined) this.common.thumbnailfile = values.thumbnailfile
if (values.pluginData !== undefined) this.common.pluginData = values.pluginData
Expand Down Expand Up @@ -643,6 +646,7 @@ export class VideoEdit {
nsfwSummary: this.common.nsfwSummary,

commentsPolicy: this.common.commentsPolicy,
recommendationPolicy: this.common.recommendationPolicy,
waitTranscoding: this.common.waitTranscoding,
channelId: this.common.channelId,
privacy: this.common.privacy,
Expand Down Expand Up @@ -712,6 +716,7 @@ export class VideoEdit {
nsfwSummary: this.common.nsfwSummary || null,
waitTranscoding: this.common.waitTranscoding,
commentsPolicy: this.common.commentsPolicy,
recommendationPolicy: this.common.recommendationPolicy,
downloadEnabled: this.common.downloadEnabled,
thumbnailfile: this.common.thumbnailfile,
scheduleUpdate: this.common.scheduleUpdate || null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ <h2 class="form-title">
</div>
</my-select-radio>
</div>

<div class="form-group">
<my-select-radio i18n-label label="Recommendation policy" [items]="recommendationPolicies" inputId="recommendationPolicy" formControlName="recommendationPolicy">
<div class="form-group-description" i18n>
Control which videos appear in recommendations for this video
</div>
</my-select-radio>
</div>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ServerService } from '@app/core'
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
import { VIDEO_NSFW_SUMMARY_VALIDATOR } from '@app/shared/form-validators/video-validators'
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
import { HTMLServerConfig, VideoCommentPolicyType, ConstantLabel } from '@peertube/peertube-models'
import { HTMLServerConfig, VideoCommentPolicyType, VideoRecommendationPolicyType, ConstantLabel } from '@peertube/peertube-models'
import debug from 'debug'
import { Subscription } from 'rxjs'
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
Expand All @@ -26,6 +26,7 @@ type Form = {
nsfwSummary: FormControl<string>

commentPolicies: FormControl<VideoCommentPolicyType>
recommendationPolicies: FormControl<VideoRecommendationPolicyType>

videoPrivacyEmbedEnableAllowlist: FormControl<boolean>
videoPrivacyEmbedAllowlistDomains: FormControl<string>
Expand Down Expand Up @@ -58,6 +59,8 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
validationMessages: FormReactiveMessages = {}

commentPolicies: ConstantLabel<VideoCommentPolicyType>[] = []
recommendationPolicies: ConstantLabel<VideoRecommendationPolicyType>[] = []

serverConfig: HTMLServerConfig

private updatedSub: Subscription
Expand All @@ -69,6 +72,9 @@ export class VideoModerationComponent implements OnInit, OnDestroy {

this.serverService.getCommentPolicies()
.subscribe(res => this.commentPolicies = res)

this.serverService.getRecommendationPolicies()
.subscribe(res => this.recommendationPolicies = res)
}

ngOnDestroy () {
Expand All @@ -81,6 +87,7 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
const defaultValues = { ...videoEdit.toCommonFormPatch(), ...videoEdit.toEmbedPrivacyFormPatch() }
const obj: BuildFormArgument = {
commentsPolicy: null,
recommendationPolicy: null,
nsfw: null,
nsfwFlagViolent: null,
nsfwFlagSex: null,
Expand Down
22 changes: 22 additions & 0 deletions client/src/app/core/server/server.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ServerConfig,
ServerStats,
VideoCommentPolicy,
VideoRecommendationPolicy,
ConstantLabel,
VideoLicenceType,
VideoPlaylistPrivacyType,
Expand Down Expand Up @@ -133,6 +134,27 @@ export class ServerService {
])
}

getRecommendationPolicies () {
return of([
{
id: VideoRecommendationPolicy.ANY_VIDEOS,
label: $localize`Allow any videos in recommendations`
},
{
id: VideoRecommendationPolicy.ONLY_LOCAL_VIDEOS,
label: $localize`Only allow instance-local videos in recommendations`
},
{
id: VideoRecommendationPolicy.ONLY_CHANNEL_VIDEOS,
label: $localize`Only allow same channel videos in recommendations`
},
{
id: VideoRecommendationPolicy.ONLY_OWNER_VIDEOS,
label: $localize`Only allow owner videos in recommendations`
}
])
}

getVideoCategories () {
if (!this.videoCategoriesObservable) {
this.videoCategoriesObservable = this.loadAttributeEnum<number>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.mode
import {
ConstantLabel,
VideoCommentPolicyType,
VideoRecommendationPolicyType,
VideoDetails as VideoDetailsServerModel,
VideoEmbedPrivacyPolicy,
VideoEmbedPrivacyPolicyType,
Expand All @@ -22,6 +23,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
downloadEnabled: boolean

commentsPolicy: ConstantLabel<VideoCommentPolicyType>
recommendationPolicy: ConstantLabel<VideoRecommendationPolicyType>

likesPercent: number
dislikesPercent: number
Expand All @@ -48,6 +50,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
this.support = hash.support
this.commentsPolicy = hash.commentsPolicy
this.downloadEnabled = hash.downloadEnabled
this.recommendationPolicy = hash.recommendationPolicy

this.inputFileUpdatedAt = hash.inputFileUpdatedAt

Expand Down
15 changes: 13 additions & 2 deletions client/src/app/shared/shared-main/video/video.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ export class VideoService {
)
}

listRecommendationVideos (
options: VideoListParams & {
currentVideo: Pick<Video, 'uuid'>
}
): Observable<ResultList<Video>> {
return this.listVideos({ ...options, currentVideo: options.currentVideo })
}

listAccountVideos (
options: VideoListParams & {
account: Pick<Account, 'nameWithHost'>
Expand All @@ -183,16 +191,19 @@ export class VideoService {
listVideos (
optionsArg: VideoListParams & {
videoChannel?: Pick<VideoChannel, 'nameWithHost'>
currentVideo?: Pick<Video, 'uuid'>
account?: Pick<Account, 'nameWithHost'>
}
): Observable<ResultList<Video>> {
const { account, videoChannel, ...options } = optionsArg
const { account, videoChannel, currentVideo, ...options } = optionsArg

let params = new HttpParams()
params = this.buildVideoListParams({ params, ...options })

let url: string
if (videoChannel) {
if (currentVideo) {
url = VideoService.BASE_VIDEO_URL + '/' + currentVideo.uuid + '/recommendations'
} else if (videoChannel) {
url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos'
} else if (account) {
url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos'
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/videos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export * from './video-rate.type.js'
export * from './video-schedule-update.model.js'
export * from './video-sort-field.type.js'
export * from './video-state.enum.js'
export * from './video-recommendation-policies.enum.js'
export * from './video-source.model.js'

export * from './video-streaming-playlist.model.js'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { VideoCommentPolicyType } from './comment/video-comment-policy.enum.js'
import { VideoRecommendationPolicyType } from './video-recommendation-policies.enum.js'
import { VideoPrivacyType } from './video-privacy.enum.js'
import { VideoScheduleUpdate } from './video-schedule-update.model.js'

Expand Down Expand Up @@ -30,4 +31,6 @@ export interface VideoCreateUpdateCommon {
scheduleUpdate?: VideoScheduleUpdate
originallyPublishedAt?: Date | string
videoPasswords?: string[]

recommendationPolicy?: VideoRecommendationPolicyType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const VideoRecommendationPolicy = {
ANY_VIDEOS: 1,
ONLY_LOCAL_VIDEOS: 2,
ONLY_OWNER_VIDEOS: 3,
ONLY_CHANNEL_VIDEOS: 4
} as const

export type VideoRecommendationPolicyType = typeof VideoRecommendationPolicy[keyof typeof VideoRecommendationPolicy]
8 changes: 8 additions & 0 deletions packages/models/src/videos/video.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Account, AccountSummary } from '../actors/index.js'
import { VideoChannel, VideoChannelSummary } from './channel/video-channel.model.js'
import { VideoCommentPolicyType } from './comment/video-comment-policy.enum.js'
import { VideoRecommendationPolicyType } from './video-recommendation-policies.enum.js'
import { VideoEmbedPrivacyPolicyType } from './embed-privacy/video-embed-privacy-policy.enum.js'
import { VideoFile } from './file/index.js'
import { LiveVideoScheduleEdit } from './live/live-video-schedule.model.js'
Expand Down Expand Up @@ -87,6 +88,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
}

pluginData?: any

recommendationPolicy?: ConstantLabel<VideoRecommendationPolicyType>
}

// Not included by default, needs query params
Expand Down Expand Up @@ -118,6 +121,11 @@ export interface VideoDetails extends Video {
account: Account
tags: string[]

recommendationPolicy: {
id: VideoRecommendationPolicyType
label: string
}

commentsPolicy: {
id: VideoCommentPolicyType
label: string
Expand Down
19 changes: 19 additions & 0 deletions packages/server-commands/src/videos/videos-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,25 @@ export class VideosCommand extends AbstractCommand {
}

// ---------------------------------------------------------------------------
listRecommendations (
options: OverrideCommandOptions & VideosCommonQuery & {
id: number | string
}
) {
const path = '/api/v1/videos/' + options.id + '/recommendations'

const query = this.buildListQuery(options)

return this.getRequestBody<ResultList<Video>>({
...options,

path,
query: { sort: 'name', ...query },
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}


list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
const path = '/api/v1/videos'
Expand Down
87 changes: 87 additions & 0 deletions packages/tests/src/api/videos/video-recommendation-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { VideoRecommendationPolicy } from '@peertube/peertube-models'
import {
cleanupTests,
createSingleServer,
PeerTubeServer,
setAccessTokensToServers,
waitJobs
} from '@peertube/peertube-server-commands'
import { expect } from 'chai'

describe('Video recommendation policy (server-side)', function () {
let server: PeerTubeServer
let videoA: any

before(async function () {
this.timeout(30000)

server = await createSingleServer(1, {})
await setAccessTokensToServers([ server ])

// Create 2 videos in same channel
videoA = await server.videos.upload({ attributes: { name: 'VideoA' } })
// Fetch video details
videoA = await server.videos.get({ id: videoA.id })

await server.videos.upload({ attributes: { name: 'VideoB' } })

// Create another channel + video
const channel = await server.channels.create({ attributes: { displayName: 'Other channel', name: 'other' } })
await server.videos.upload({
attributes: { name: 'VideoC', channelId: channel.id }
})

// Create another user + video
const token = await server.users.generateUserAndToken('user2')
await server.videos.upload({
token,
attributes: { name: 'VideoD' }
})

await waitJobs(server)
})

it('only-channel-videos: should only return videos from same channel', async function () {
await server.videos.update({ id: videoA.id, attributes: { recommendationPolicy: VideoRecommendationPolicy.ONLY_CHANNEL_VIDEOS } })

const res = await server.videos.listRecommendations({ id: videoA.uuid })

const videos = res.data
expect(videos.length).to.be.greaterThan(0)
expect(videos.every(v => v.channel.id === videoA.channel.id)).to.equal(true)
})

it('only-owner-videos: should only return videos from same owner', async function () {
await server.videos.update({ id: videoA.id, attributes: { recommendationPolicy: VideoRecommendationPolicy.ONLY_OWNER_VIDEOS } })

const res = await server.videos.listRecommendations({ id: videoA.uuid })

const videos = res.data
expect(videos.length).to.be.greaterThan(0)
expect(videos.every(v => v.account.id === videoA.account.id)).to.equal(true)
})

it('only-local-videos: should only return local videos', async function () {
await server.videos.update({ id: videoA.id, attributes: { recommendationPolicy: VideoRecommendationPolicy.ONLY_LOCAL_VIDEOS } })

const res = await server.videos.listRecommendations({ id: videoA.uuid })

const videos = res.data
expect(videos.every(v => v.isLocal === true)).to.equal(true)
})

it('any-videos: should allow mixed results', async function () {
await server.videos.update({ id: videoA.id, attributes: { recommendationPolicy: VideoRecommendationPolicy.ANY_VIDEOS } })

const res = await server.videos.listRecommendations({ id: videoA.uuid })

const videos = res.data
const hasDifferentChannel = videos.some(v => v.channel.id !== videoA.channel.id)

expect(hasDifferentChannel).to.equal(true)
})

after(async function () {
await cleanupTests([ server ])
})
})
Loading
Loading