aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/videos/recommendations
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/videos/recommendations')
-rw-r--r--client/src/app/videos/recommendations/recent-videos-recommendation.service.spec.ts66
-rw-r--r--client/src/app/videos/recommendations/recent-videos-recommendation.service.ts39
-rw-r--r--client/src/app/videos/recommendations/recommendations.module.ts25
-rw-r--r--client/src/app/videos/recommendations/recommendations.service.ts8
-rw-r--r--client/src/app/videos/recommendations/recommended-videos.component.html11
-rw-r--r--client/src/app/videos/recommendations/recommended-videos.component.ts31
-rw-r--r--client/src/app/videos/recommendations/recommended-videos.store.spec.ts22
-rw-r--r--client/src/app/videos/recommendations/recommended-videos.store.ts32
8 files changed, 234 insertions, 0 deletions
diff --git a/client/src/app/videos/recommendations/recent-videos-recommendation.service.spec.ts b/client/src/app/videos/recommendations/recent-videos-recommendation.service.spec.ts
new file mode 100644
index 000000000..f9055b82c
--- /dev/null
+++ b/client/src/app/videos/recommendations/recent-videos-recommendation.service.spec.ts
@@ -0,0 +1,66 @@
1import { RecentVideosRecommendationService } from '@app/videos/recommendations/recent-videos-recommendation.service'
2import { VideosProvider } from '@app/shared/video/video.service'
3import { EMPTY, of } from 'rxjs'
4import Mock = jest.Mock
5
6describe('"Recent Videos" Recommender', () => {
7 describe('getRecommendations', () => {
8 let videosService: VideosProvider
9 let service: RecentVideosRecommendationService
10 let getVideosMock: Mock<any>
11 beforeEach(() => {
12 getVideosMock = jest.fn(() => EMPTY)
13 videosService = {
14 getVideos: getVideosMock
15 }
16 service = new RecentVideosRecommendationService(videosService)
17 })
18 it('should filter out the given UUID from the results', async (done) => {
19 const vids = [
20 { uuid: 'uuid1' },
21 { uuid: 'uuid2' }
22 ]
23 getVideosMock.mockReturnValueOnce(of({ videos: vids }))
24 const result = await service.getRecommendations('uuid1').toPromise()
25 const uuids = result.map(v => v.uuid)
26 expect(uuids).toEqual(['uuid2'])
27 done()
28 })
29 it('should return 5 results when the given UUID is NOT in the first 5 results', async (done) => {
30 const vids = [
31 { uuid: 'uuid2' },
32 { uuid: 'uuid3' },
33 { uuid: 'uuid4' },
34 { uuid: 'uuid5' },
35 { uuid: 'uuid6' },
36 { uuid: 'uuid7' }
37 ]
38 getVideosMock.mockReturnValueOnce(of({ videos: vids }))
39 const result = await service.getRecommendations('uuid1').toPromise()
40 expect(result.length).toEqual(5)
41 done()
42 })
43 it('should return 5 results when the given UUID IS PRESENT in the first 5 results', async (done) => {
44 const vids = [
45 { uuid: 'uuid1' },
46 { uuid: 'uuid2' },
47 { uuid: 'uuid3' },
48 { uuid: 'uuid4' },
49 { uuid: 'uuid5' },
50 { uuid: 'uuid6' }
51 ]
52 getVideosMock
53 .mockReturnValueOnce(of({ videos: vids }))
54 const result = await service.getRecommendations('uuid1').toPromise()
55 expect(result.length).toEqual(5)
56 done()
57 })
58 it('should fetch an extra result in case the given UUID is in the list', async (done) => {
59 await service.getRecommendations('uuid1').toPromise()
60 let expectedSize = service.pageSize + 1
61 let params = { currentPage: jasmine.anything(), itemsPerPage: expectedSize }
62 expect(getVideosMock).toHaveBeenCalledWith(params, jasmine.anything())
63 done()
64 })
65 })
66})
diff --git a/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts b/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts
new file mode 100644
index 000000000..708d67699
--- /dev/null
+++ b/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts
@@ -0,0 +1,39 @@
1import { Inject, Injectable } from '@angular/core'
2import { RecommendationService } from '@app/videos/recommendations/recommendations.service'
3import { Video } from '@app/shared/video/video.model'
4import { VideoService, VideosProvider } from '@app/shared/video/video.service'
5import { map } from 'rxjs/operators'
6import { Observable } from 'rxjs'
7
8/**
9 * Provides "recommendations" by providing the most recently uploaded videos.
10 */
11@Injectable()
12export class RecentVideosRecommendationService implements RecommendationService {
13
14 readonly pageSize = 5
15
16 constructor (
17 @Inject(VideoService) private videos: VideosProvider
18 ) {
19 }
20
21 getRecommendations (uuid: string): Observable<Video[]> {
22 return this.fetchPage(1)
23 .pipe(
24 map(vids => {
25 const otherVideos = vids.filter(v => v.uuid !== uuid)
26 return otherVideos.slice(0, this.pageSize)
27 })
28 )
29 }
30
31 private fetchPage (page: number): Observable<Video[]> {
32 let pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
33 return this.videos.getVideos(pagination, '-createdAt')
34 .pipe(
35 map(v => v.videos)
36 )
37 }
38
39}
diff --git a/client/src/app/videos/recommendations/recommendations.module.ts b/client/src/app/videos/recommendations/recommendations.module.ts
new file mode 100644
index 000000000..5a46ea739
--- /dev/null
+++ b/client/src/app/videos/recommendations/recommendations.module.ts
@@ -0,0 +1,25 @@
1import { NgModule } from '@angular/core'
2import { RecommendedVideosComponent } from '@app/videos/recommendations/recommended-videos.component'
3import { RecommendedVideosStore } from '@app/videos/recommendations/recommended-videos.store'
4import { CommonModule } from '@angular/common'
5import { SharedModule } from '@app/shared'
6import { RecentVideosRecommendationService } from '@app/videos/recommendations/recent-videos-recommendation.service'
7
8@NgModule({
9 imports: [
10 SharedModule,
11 CommonModule
12 ],
13 declarations: [
14 RecommendedVideosComponent
15 ],
16 exports: [
17 RecommendedVideosComponent
18 ],
19 providers: [
20 RecommendedVideosStore,
21 RecentVideosRecommendationService
22 ]
23})
24export class RecommendationsModule {
25}
diff --git a/client/src/app/videos/recommendations/recommendations.service.ts b/client/src/app/videos/recommendations/recommendations.service.ts
new file mode 100644
index 000000000..44cbda9b7
--- /dev/null
+++ b/client/src/app/videos/recommendations/recommendations.service.ts
@@ -0,0 +1,8 @@
1import { Video } from '@app/shared/video/video.model'
2import { Observable } from 'rxjs'
3
4export type UUID = string
5
6export interface RecommendationService {
7 getRecommendations (uuid: UUID): Observable<Video[]>
8}
diff --git a/client/src/app/videos/recommendations/recommended-videos.component.html b/client/src/app/videos/recommendations/recommended-videos.component.html
new file mode 100644
index 000000000..7cfaffec2
--- /dev/null
+++ b/client/src/app/videos/recommendations/recommended-videos.component.html
@@ -0,0 +1,11 @@
1<div class="other-videos">
2 <div i18n class="title-page title-page-single">
3 Other videos
4 </div>
5
6 <ng-container *ngIf="hasVideos$ | async">
7 <div *ngFor="let video of (videos$ | async)">
8 <my-video-miniature [video]="video" [user]="user"></my-video-miniature>
9 </div>
10 </ng-container>
11</div>
diff --git a/client/src/app/videos/recommendations/recommended-videos.component.ts b/client/src/app/videos/recommendations/recommended-videos.component.ts
new file mode 100644
index 000000000..aa4dd0ee2
--- /dev/null
+++ b/client/src/app/videos/recommendations/recommended-videos.component.ts
@@ -0,0 +1,31 @@
1import { Component, Input, OnChanges } from '@angular/core'
2import { Observable } from 'rxjs'
3import { Video } from '@app/shared/video/video.model'
4import { RecommendedVideosStore } from '@app/videos/recommendations/recommended-videos.store'
5import { User } from '@app/shared'
6
7@Component({
8 selector: 'my-recommended-videos',
9 templateUrl: './recommended-videos.component.html'
10})
11export class RecommendedVideosComponent implements OnChanges {
12 @Input() inputVideo: Video
13 @Input() user: User
14
15 readonly hasVideos$: Observable<boolean>
16 readonly videos$: Observable<Video[]>
17
18 constructor (
19 private store: RecommendedVideosStore
20 ) {
21 this.videos$ = this.store.recommendations$
22 this.hasVideos$ = this.store.hasRecommendations$
23 }
24
25 public ngOnChanges (): void {
26 if (this.inputVideo) {
27 this.store.requestNewRecommendations(this.inputVideo.uuid)
28 }
29 }
30
31}
diff --git a/client/src/app/videos/recommendations/recommended-videos.store.spec.ts b/client/src/app/videos/recommendations/recommended-videos.store.spec.ts
new file mode 100644
index 000000000..e12a3f520
--- /dev/null
+++ b/client/src/app/videos/recommendations/recommended-videos.store.spec.ts
@@ -0,0 +1,22 @@
1import { RecommendedVideosStore } from '@app/videos/recommendations/recommended-videos.store'
2import { RecommendationService } from '@app/videos/recommendations/recommendations.service'
3
4describe('RecommendedVideosStore', () => {
5 describe('requestNewRecommendations', () => {
6 let store: RecommendedVideosStore
7 let service: RecommendationService
8 beforeEach(() => {
9 service = {
10 getRecommendations: jest.fn(() => new Promise((r) => r()))
11 }
12 store = new RecommendedVideosStore(service)
13 })
14 it('should pull new videos from the service one time when given the same UUID twice', () => {
15 store.requestNewRecommendations('some-uuid')
16 store.requestNewRecommendations('some-uuid')
17 // Requests aren't fulfilled until someone asks for them (ie: subscribes)
18 store.recommendations$.subscribe()
19 expect(service.getRecommendations).toHaveBeenCalledTimes(1)
20 })
21 })
22})
diff --git a/client/src/app/videos/recommendations/recommended-videos.store.ts b/client/src/app/videos/recommendations/recommended-videos.store.ts
new file mode 100644
index 000000000..689adeb1f
--- /dev/null
+++ b/client/src/app/videos/recommendations/recommended-videos.store.ts
@@ -0,0 +1,32 @@
1import { Inject, Injectable } from '@angular/core'
2import { Observable, ReplaySubject } from 'rxjs'
3import { Video } from '@app/shared/video/video.model'
4import { RecentVideosRecommendationService } from '@app/videos/recommendations/recent-videos-recommendation.service'
5import { RecommendationService, UUID } from '@app/videos/recommendations/recommendations.service'
6import { map, switchMap, take } from 'rxjs/operators'
7
8/**
9 * This store is intended to provide data for the RecommendedVideosComponent.
10 */
11@Injectable()
12export class RecommendedVideosStore {
13 public readonly recommendations$: Observable<Video[]>
14 public readonly hasRecommendations$: Observable<boolean>
15 private readonly requestsForLoad$$ = new ReplaySubject<UUID>(1)
16
17 constructor (
18 @Inject(RecentVideosRecommendationService) private recommendations: RecommendationService
19 ) {
20 this.recommendations$ = this.requestsForLoad$$.pipe(
21 switchMap(requestedUUID => recommendations.getRecommendations(requestedUUID)
22 .pipe(take(1))
23 ))
24 this.hasRecommendations$ = this.recommendations$.pipe(
25 map(otherVideos => otherVideos.length > 0)
26 )
27 }
28
29 requestNewRecommendations (videoUUID: string) {
30 this.requestsForLoad$$.next(videoUUID)
31 }
32}