mirror of https://github.com/Chocobozzz/PeerTube
Autoplay next recommended video (#2137)
* Start working on autoplay of next video * Ignore changes made by gitpod * Apply changes from PR#1370 * Correct the spelling of recommendations * Fix linting errors * Move boolean check to existing onEnded handler * Pick a random video until the recommendations are improved * Add simple tests for autoPlayNextVideo * Fix lint ...againpull/2158/head
parent
32d7f2b754
commit
6aa5414813
|
@ -37,6 +37,8 @@
|
||||||
/scripts/i18n/generate-iso639-target.ts
|
/scripts/i18n/generate-iso639-target.ts
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
|
/dump.rdb
|
||||||
|
/.theia/
|
||||||
/profiling/
|
/profiling/
|
||||||
/*.zip
|
/*.zip
|
||||||
/*.tar.xz
|
/*.tar.xz
|
||||||
|
|
|
@ -47,6 +47,11 @@
|
||||||
inputName="autoPlayVideo" formControlName="autoPlayVideo"
|
inputName="autoPlayVideo" formControlName="autoPlayVideo"
|
||||||
i18n-labelText labelText="Automatically plays video"
|
i18n-labelText labelText="Automatically plays video"
|
||||||
></my-peertube-checkbox>
|
></my-peertube-checkbox>
|
||||||
|
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="autoPlayNextVideo" formControlName="autoPlayNextVideo"
|
||||||
|
i18n-labelText labelText="Automatically starts playing next video"
|
||||||
|
></my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="submit" i18n-value value="Save" [disabled]="!form.valid">
|
<input type="submit" i18n-value value="Save" [disabled]="!form.valid">
|
||||||
|
|
|
@ -36,6 +36,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
|
||||||
nsfwPolicy: null,
|
nsfwPolicy: null,
|
||||||
webTorrentEnabled: null,
|
webTorrentEnabled: null,
|
||||||
autoPlayVideo: null,
|
autoPlayVideo: null,
|
||||||
|
autoPlayNextVideo: null,
|
||||||
videoLanguages: null
|
videoLanguages: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
|
||||||
nsfwPolicy: this.user.nsfwPolicy,
|
nsfwPolicy: this.user.nsfwPolicy,
|
||||||
webTorrentEnabled: this.user.webTorrentEnabled,
|
webTorrentEnabled: this.user.webTorrentEnabled,
|
||||||
autoPlayVideo: this.user.autoPlayVideo === true,
|
autoPlayVideo: this.user.autoPlayVideo === true,
|
||||||
|
autoPlayNextVideo: this.user.autoPlayNextVideo,
|
||||||
videoLanguages
|
videoLanguages
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -66,6 +68,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
|
||||||
const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
|
const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
|
||||||
const webTorrentEnabled = this.form.value['webTorrentEnabled']
|
const webTorrentEnabled = this.form.value['webTorrentEnabled']
|
||||||
const autoPlayVideo = this.form.value['autoPlayVideo']
|
const autoPlayVideo = this.form.value['autoPlayVideo']
|
||||||
|
const autoPlayNextVideo = this.form.value['autoPlayNextVideo']
|
||||||
|
|
||||||
let videoLanguages: string[] = this.form.value['videoLanguages']
|
let videoLanguages: string[] = this.form.value['videoLanguages']
|
||||||
if (Array.isArray(videoLanguages)) {
|
if (Array.isArray(videoLanguages)) {
|
||||||
|
@ -84,6 +87,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
|
||||||
nsfwPolicy,
|
nsfwPolicy,
|
||||||
webTorrentEnabled,
|
webTorrentEnabled,
|
||||||
autoPlayVideo,
|
autoPlayVideo,
|
||||||
|
autoPlayNextVideo,
|
||||||
videoLanguages
|
videoLanguages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ export class User implements UserServerModel {
|
||||||
adminFlags?: UserAdminFlag
|
adminFlags?: UserAdminFlag
|
||||||
|
|
||||||
autoPlayVideo: boolean
|
autoPlayVideo: boolean
|
||||||
|
autoPlayNextVideo: boolean
|
||||||
webTorrentEnabled: boolean
|
webTorrentEnabled: boolean
|
||||||
videosHistoryEnabled: boolean
|
videosHistoryEnabled: boolean
|
||||||
videoLanguages: string[]
|
videoLanguages: string[]
|
||||||
|
|
|
@ -199,7 +199,11 @@
|
||||||
<my-video-comments [video]="video" [user]="user"></my-video-comments>
|
<my-video-comments [video]="video" [user]="user"></my-video-comments>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-recommended-videos [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" [user]="user"></my-recommended-videos>
|
<my-recommended-videos
|
||||||
|
[inputRecommendation]="{ uuid: video.uuid, tags: video.tags }"
|
||||||
|
[user]="user"
|
||||||
|
(gotRecommendations)="onRecommendations($event)"
|
||||||
|
></my-recommended-videos>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
|
<div class="row privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { getStoredTheater } from '../../../assets/player/peertube-player-local-s
|
||||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
import { PlatformLocation } from '@angular/common'
|
import { PlatformLocation } from '@angular/common'
|
||||||
|
import { randomInt } from '@shared/core-utils/miscs/miscs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-watch',
|
selector: 'my-video-watch',
|
||||||
|
@ -69,6 +70,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
remoteServerDown = false
|
remoteServerDown = false
|
||||||
hotkeys: Hotkey[]
|
hotkeys: Hotkey[]
|
||||||
|
|
||||||
|
private nextVideoUuid = ''
|
||||||
private currentTime: number
|
private currentTime: number
|
||||||
private paramsSub: Subscription
|
private paramsSub: Subscription
|
||||||
private queryParamsSub: Subscription
|
private queryParamsSub: Subscription
|
||||||
|
@ -217,6 +219,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
return this.video.tags
|
return this.video.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRecommendations (videos: Video[]) {
|
||||||
|
if (videos.length > 0) {
|
||||||
|
// Pick a random video until the recommendations are improved
|
||||||
|
this.nextVideoUuid = videos[randomInt(0,videos.length - 1)].uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onVideoRemoved () {
|
onVideoRemoved () {
|
||||||
this.redirectService.redirectToHomepage()
|
this.redirectService.redirectToHomepage()
|
||||||
}
|
}
|
||||||
|
@ -477,6 +486,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
this.player.one('ended', () => {
|
this.player.one('ended', () => {
|
||||||
if (this.playlist) {
|
if (this.playlist) {
|
||||||
this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
|
this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
|
||||||
|
} else if (this.user && this.user.autoPlayNextVideo) {
|
||||||
|
this.zone.run(() => this.autoplayNext())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -500,6 +511,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
this.hooks.runAction('action:video-watch.video.loaded', 'video-watch')
|
this.hooks.runAction('action:video-watch.video.loaded', 'video-watch')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private autoplayNext () {
|
||||||
|
if (this.nextVideoUuid) {
|
||||||
|
this.router.navigate([ '/videos/watch', this.nextVideoUuid ])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setRating (nextRating: UserVideoRateType) {
|
private setRating (nextRating: UserVideoRateType) {
|
||||||
const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = {
|
const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = {
|
||||||
like: this.videoService.setVideoLike,
|
like: this.videoService.setVideoLike,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, Input, OnChanges } from '@angular/core'
|
import { Component, Input, Output, OnChanges, EventEmitter } from '@angular/core'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { Video } from '@app/shared/video/video.model'
|
import { Video } from '@app/shared/video/video.model'
|
||||||
import { RecommendationInfo } from '@app/shared/video/recommendation-info.model'
|
import { RecommendationInfo } from '@app/shared/video/recommendation-info.model'
|
||||||
|
@ -12,6 +12,7 @@ import { User } from '@app/shared'
|
||||||
export class RecommendedVideosComponent implements OnChanges {
|
export class RecommendedVideosComponent implements OnChanges {
|
||||||
@Input() inputRecommendation: RecommendationInfo
|
@Input() inputRecommendation: RecommendationInfo
|
||||||
@Input() user: User
|
@Input() user: User
|
||||||
|
@Output() gotRecommendations = new EventEmitter<Video[]>()
|
||||||
|
|
||||||
readonly hasVideos$: Observable<boolean>
|
readonly hasVideos$: Observable<boolean>
|
||||||
readonly videos$: Observable<Video[]>
|
readonly videos$: Observable<Video[]>
|
||||||
|
@ -21,6 +22,7 @@ export class RecommendedVideosComponent implements OnChanges {
|
||||||
) {
|
) {
|
||||||
this.videos$ = this.store.recommendations$
|
this.videos$ = this.store.recommendations$
|
||||||
this.hasVideos$ = this.store.hasRecommendations$
|
this.hasVideos$ = this.store.hasRecommendations$
|
||||||
|
this.videos$.subscribe(videos => this.gotRecommendations.emit(videos))
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnChanges (): void {
|
public ngOnChanges (): void {
|
||||||
|
|
|
@ -175,6 +175,7 @@ async function updateMe (req: express.Request, res: express.Response) {
|
||||||
if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
|
if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
|
||||||
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
|
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
|
||||||
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
||||||
|
if (body.autoPlayNextVideo !== undefined) user.autoPlayNextVideo = body.autoPlayNextVideo
|
||||||
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
|
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
|
||||||
if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
|
if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
|
||||||
if (body.theme !== undefined) user.theme = body.theme
|
if (body.theme !== undefined) user.theme = body.theme
|
||||||
|
|
|
@ -65,6 +65,10 @@ function isUserBlockedValid (value: any) {
|
||||||
return isBooleanValid(value)
|
return isBooleanValid(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUserAutoPlayNextVideoValid (value: any) {
|
||||||
|
return isBooleanValid(value)
|
||||||
|
}
|
||||||
|
|
||||||
function isNoInstanceConfigWarningModal (value: any) {
|
function isNoInstanceConfigWarningModal (value: any) {
|
||||||
return isBooleanValid(value)
|
return isBooleanValid(value)
|
||||||
}
|
}
|
||||||
|
@ -106,6 +110,7 @@ export {
|
||||||
isUserNSFWPolicyValid,
|
isUserNSFWPolicyValid,
|
||||||
isUserWebTorrentEnabledValid,
|
isUserWebTorrentEnabledValid,
|
||||||
isUserAutoPlayVideoValid,
|
isUserAutoPlayVideoValid,
|
||||||
|
isUserAutoPlayNextVideoValid,
|
||||||
isUserDisplayNameValid,
|
isUserDisplayNameValid,
|
||||||
isUserDescriptionValid,
|
isUserDescriptionValid,
|
||||||
isNoInstanceConfigWarningModal,
|
isNoInstanceConfigWarningModal,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
isNoInstanceConfigWarningModal,
|
isNoInstanceConfigWarningModal,
|
||||||
isUserAdminFlagsValid,
|
isUserAdminFlagsValid,
|
||||||
isUserAutoPlayVideoValid,
|
isUserAutoPlayVideoValid,
|
||||||
|
isUserAutoPlayNextVideoValid,
|
||||||
isUserBlockedReasonValid,
|
isUserBlockedReasonValid,
|
||||||
isUserBlockedValid,
|
isUserBlockedValid,
|
||||||
isUserEmailVerifiedValid,
|
isUserEmailVerifiedValid,
|
||||||
|
@ -160,6 +161,12 @@ export class UserModel extends Model<UserModel> {
|
||||||
@Column
|
@Column
|
||||||
autoPlayVideo: boolean
|
autoPlayVideo: boolean
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Default(false)
|
||||||
|
@Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean'))
|
||||||
|
@Column
|
||||||
|
autoPlayNextVideo: boolean
|
||||||
|
|
||||||
@AllowNull(true)
|
@AllowNull(true)
|
||||||
@Default(null)
|
@Default(null)
|
||||||
@Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
|
@Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
|
||||||
|
@ -597,6 +604,7 @@ export class UserModel extends Model<UserModel> {
|
||||||
webTorrentEnabled: this.webTorrentEnabled,
|
webTorrentEnabled: this.webTorrentEnabled,
|
||||||
videosHistoryEnabled: this.videosHistoryEnabled,
|
videosHistoryEnabled: this.videosHistoryEnabled,
|
||||||
autoPlayVideo: this.autoPlayVideo,
|
autoPlayVideo: this.autoPlayVideo,
|
||||||
|
autoPlayNextVideo: this.autoPlayNextVideo,
|
||||||
videoLanguages: this.videoLanguages,
|
videoLanguages: this.videoLanguages,
|
||||||
|
|
||||||
role: this.role,
|
role: this.role,
|
||||||
|
|
|
@ -418,6 +418,14 @@ describe('Test users API validators', function () {
|
||||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
|
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid autoPlayNextVideo attribute', async function () {
|
||||||
|
const fields = {
|
||||||
|
autoPlayNextVideo: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
|
||||||
|
})
|
||||||
|
|
||||||
it('Should fail with an invalid videosHistoryEnabled attribute', async function () {
|
it('Should fail with an invalid videosHistoryEnabled attribute', async function () {
|
||||||
const fields = {
|
const fields = {
|
||||||
videosHistoryEnabled: -1
|
videosHistoryEnabled: -1
|
||||||
|
|
|
@ -481,6 +481,19 @@ describe('Test users', function () {
|
||||||
expect(user.autoPlayVideo).to.be.false
|
expect(user.autoPlayVideo).to.be.false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should be able to change the autoPlayNextVideo attribute', async function () {
|
||||||
|
await updateMyUser({
|
||||||
|
url: server.url,
|
||||||
|
accessToken: accessTokenUser,
|
||||||
|
autoPlayNextVideo: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await getMyUserInformation(server.url, accessTokenUser)
|
||||||
|
const user = res.body
|
||||||
|
|
||||||
|
expect(user.autoPlayNextVideo).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
it('Should be able to change the email attribute', async function () {
|
it('Should be able to change the email attribute', async function () {
|
||||||
await updateMyUser({
|
await updateMyUser({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
|
|
@ -7,6 +7,7 @@ export interface UserUpdateMe {
|
||||||
|
|
||||||
webTorrentEnabled?: boolean
|
webTorrentEnabled?: boolean
|
||||||
autoPlayVideo?: boolean
|
autoPlayVideo?: boolean
|
||||||
|
autoPlayNextVideo?: boolean
|
||||||
videosHistoryEnabled?: boolean
|
videosHistoryEnabled?: boolean
|
||||||
videoLanguages?: string[]
|
videoLanguages?: string[]
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export interface User {
|
||||||
adminFlags?: UserAdminFlag
|
adminFlags?: UserAdminFlag
|
||||||
|
|
||||||
autoPlayVideo: boolean
|
autoPlayVideo: boolean
|
||||||
|
autoPlayNextVideo: boolean
|
||||||
webTorrentEnabled: boolean
|
webTorrentEnabled: boolean
|
||||||
videosHistoryEnabled: boolean
|
videosHistoryEnabled: boolean
|
||||||
videoLanguages: string[]
|
videoLanguages: string[]
|
||||||
|
|
Loading…
Reference in New Issue