Autoplay next recommended video (#2137)
authorLoveIsGrief <LoveIsGrief@users.noreply.github.com>
Tue, 24 Sep 2019 06:48:01 +0000 (08:48 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 24 Sep 2019 06:48:01 +0000 (08:48 +0200)
* 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

...again

14 files changed:
.gitignore
client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
client/src/app/shared/users/user.model.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/app/videos/recommendations/recommended-videos.component.ts
server/controllers/api/users/me.ts
server/helpers/custom-validators/users.ts
server/models/account/user.ts
server/tests/api/check-params/users.ts
server/tests/api/users/users.ts
shared/models/users/user-update-me.model.ts
shared/models/users/user.model.ts

index 3a91facb48b6c390726c3e090ca13d8196d90f2c..fbf8fdf3c2c233e72870b74ad99279568dba58b6 100644 (file)
@@ -37,6 +37,8 @@
 /scripts/i18n/generate-iso639-target.ts
 
 # Other
+/dump.rdb
+/.theia/
 /profiling/
 /*.zip
 /*.tar.xz
index a11238925da04f521331278a6d55cb29d2d407e2..06fd9833a5005e0f015e7d580ddeb34ec1fd6ccf 100644 (file)
       inputName="autoPlayVideo" formControlName="autoPlayVideo"
       i18n-labelText labelText="Automatically plays video"
     ></my-peertube-checkbox>
+
+    <my-peertube-checkbox
+      inputName="autoPlayNextVideo" formControlName="autoPlayNextVideo"
+      i18n-labelText labelText="Automatically starts playing next video"
+    ></my-peertube-checkbox>
   </div>
 
   <input type="submit" i18n-value value="Save" [disabled]="!form.valid">
index 4fb82808285a5012959e4a0666b4514bfab68fa8..99eee23b8d6fe53bbcf6b4e938af5c477d27d680 100644 (file)
@@ -36,6 +36,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
       nsfwPolicy: null,
       webTorrentEnabled: null,
       autoPlayVideo: null,
+      autoPlayNextVideo: null,
       videoLanguages: null
     })
 
@@ -57,6 +58,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
         nsfwPolicy: this.user.nsfwPolicy,
         webTorrentEnabled: this.user.webTorrentEnabled,
         autoPlayVideo: this.user.autoPlayVideo === true,
+        autoPlayNextVideo: this.user.autoPlayNextVideo,
         videoLanguages
       })
     })
@@ -66,6 +68,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
     const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
     const webTorrentEnabled = this.form.value['webTorrentEnabled']
     const autoPlayVideo = this.form.value['autoPlayVideo']
+    const autoPlayNextVideo = this.form.value['autoPlayNextVideo']
 
     let videoLanguages: string[] = this.form.value['videoLanguages']
     if (Array.isArray(videoLanguages)) {
@@ -84,6 +87,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
       nsfwPolicy,
       webTorrentEnabled,
       autoPlayVideo,
+      autoPlayNextVideo,
       videoLanguages
     }
 
index 656b73dd25547afb4bed3e3986fc590a31866bf0..e0b3f1faff46403f8b21fec48425568d835f67c7 100644 (file)
@@ -16,6 +16,7 @@ export class User implements UserServerModel {
   adminFlags?: UserAdminFlag
 
   autoPlayVideo: boolean
+  autoPlayNextVideo: boolean
   webTorrentEnabled: boolean
   videosHistoryEnabled: boolean
   videoLanguages: string[]
index 6a02f630ad81532811cc7274258b70f0f546766b..cd60c407f1e43ffc17be413f9988eecfc796eeee 100644 (file)
       <my-video-comments [video]="video" [user]="user"></my-video-comments>
     </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 class="row privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
index 21a24113fa9c0ae4916155b4fed508ec2cede124..1e7991738c2fb64e93387f2ca13422846be6f1c5 100644 (file)
@@ -35,6 +35,7 @@ import { getStoredTheater } from '../../../assets/player/peertube-player-local-s
 import { PluginService } from '@app/core/plugins/plugin.service'
 import { HooksService } from '@app/core/plugins/hooks.service'
 import { PlatformLocation } from '@angular/common'
+import { randomInt } from '@shared/core-utils/miscs/miscs'
 
 @Component({
   selector: 'my-video-watch',
@@ -69,6 +70,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   remoteServerDown = false
   hotkeys: Hotkey[]
 
+  private nextVideoUuid = ''
   private currentTime: number
   private paramsSub: Subscription
   private queryParamsSub: Subscription
@@ -217,6 +219,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     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 () {
     this.redirectService.redirectToHomepage()
   }
@@ -477,6 +486,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
       this.player.one('ended', () => {
         if (this.playlist) {
           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')
   }
 
+  private autoplayNext () {
+    if (this.nextVideoUuid) {
+      this.router.navigate([ '/videos/watch', this.nextVideoUuid ])
+    }
+  }
+
   private setRating (nextRating: UserVideoRateType) {
     const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = {
       like: this.videoService.setVideoLike,
index 68fd750ccb9f64786e74fa993fde45c2947dd34f..7e0fb88567ad442b14d44d42726ac5cf8943c221 100644 (file)
@@ -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 { Video } from '@app/shared/video/video.model'
 import { RecommendationInfo } from '@app/shared/video/recommendation-info.model'
@@ -12,6 +12,7 @@ import { User } from '@app/shared'
 export class RecommendedVideosComponent implements OnChanges {
   @Input() inputRecommendation: RecommendationInfo
   @Input() user: User
+  @Output() gotRecommendations = new EventEmitter<Video[]>()
 
   readonly hasVideos$: Observable<boolean>
   readonly videos$: Observable<Video[]>
@@ -21,6 +22,7 @@ export class RecommendedVideosComponent implements OnChanges {
   ) {
     this.videos$ = this.store.recommendations$
     this.hasVideos$ = this.store.hasRecommendations$
+    this.videos$.subscribe(videos => this.gotRecommendations.emit(videos))
   }
 
   public ngOnChanges (): void {
index bf872ca52d9c0206462bfc26a7e5f92e52d50418..cfc346c35ad2a2d0d22884565fcbb78631ab4e66 100644 (file)
@@ -175,6 +175,7 @@ async function updateMe (req: express.Request, res: express.Response) {
   if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
   if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
   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.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
   if (body.theme !== undefined) user.theme = body.theme
index 68e84d9ebb79ef90ced517129bb5b21ef9ffb76a..16a95f1204f28f0a33221aef427756226d4fe213 100644 (file)
@@ -65,6 +65,10 @@ function isUserBlockedValid (value: any) {
   return isBooleanValid(value)
 }
 
+function isUserAutoPlayNextVideoValid (value: any) {
+  return isBooleanValid(value)
+}
+
 function isNoInstanceConfigWarningModal (value: any) {
   return isBooleanValid(value)
 }
@@ -106,6 +110,7 @@ export {
   isUserNSFWPolicyValid,
   isUserWebTorrentEnabledValid,
   isUserAutoPlayVideoValid,
+  isUserAutoPlayNextVideoValid,
   isUserDisplayNameValid,
   isUserDescriptionValid,
   isNoInstanceConfigWarningModal,
index 451e1fd6b94d36c49a18b6719ebaec302178bb75..38c6d474aa2e6916ce7170bcf28a58512a214666 100644 (file)
@@ -25,6 +25,7 @@ import {
   isNoInstanceConfigWarningModal,
   isUserAdminFlagsValid,
   isUserAutoPlayVideoValid,
+  isUserAutoPlayNextVideoValid,
   isUserBlockedReasonValid,
   isUserBlockedValid,
   isUserEmailVerifiedValid,
@@ -160,6 +161,12 @@ export class UserModel extends Model<UserModel> {
   @Column
   autoPlayVideo: boolean
 
+  @AllowNull(false)
+  @Default(false)
+  @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean'))
+  @Column
+  autoPlayNextVideo: boolean
+
   @AllowNull(true)
   @Default(null)
   @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
@@ -597,6 +604,7 @@ export class UserModel extends Model<UserModel> {
       webTorrentEnabled: this.webTorrentEnabled,
       videosHistoryEnabled: this.videosHistoryEnabled,
       autoPlayVideo: this.autoPlayVideo,
+      autoPlayNextVideo: this.autoPlayNextVideo,
       videoLanguages: this.videoLanguages,
 
       role: this.role,
index 9d7ff898463fd80c76fc8d303eeb4fccd7aea9b2..5d5af284c5ea8dab12dffb8b7abf240a6b5c0608 100644 (file)
@@ -418,6 +418,14 @@ describe('Test users API validators', function () {
       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 () {
       const fields = {
         videosHistoryEnabled: -1
index 95b1bb62603357324217039ab2306bddb00ed7e7..ca06942e73c0caa6ecd407f4bae8b7993e58bd53 100644 (file)
@@ -481,6 +481,19 @@ describe('Test users', function () {
       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 () {
       await updateMyUser({
         url: server.url,
index 99b9a65bd7cbfbcd9fbb90124fc0e9daad84ae5b..0a833f84cc55fccf72ad77cec7d48fc30df6fb68 100644 (file)
@@ -7,6 +7,7 @@ export interface UserUpdateMe {
 
   webTorrentEnabled?: boolean
   autoPlayVideo?: boolean
+  autoPlayNextVideo?: boolean
   videosHistoryEnabled?: boolean
   videoLanguages?: string[]
 
index f67d262b036aa7ce62ad5648ca6db74923e5254e..1ca8ddcbaabf409231dfdf81ecef89cb38918794 100644 (file)
@@ -17,6 +17,7 @@ export interface User {
   adminFlags?: UserAdminFlag
 
   autoPlayVideo: boolean
+  autoPlayNextVideo: boolean
   webTorrentEnabled: boolean
   videosHistoryEnabled: boolean
   videoLanguages: string[]