aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-07-22 15:40:13 +0200
committerChocobozzz <chocobozzz@cpy.re>2019-07-24 10:58:16 +0200
commit93cae47925e4dd68b7d34a41927b2740b4fab1b4 (patch)
treef649ab49fab1886b434e164591990cc99b234466
parent587568e1cc0e33c023c1ac62dd28fef313285250 (diff)
downloadPeerTube-93cae47925e4dd68b7d34a41927b2740b4fab1b4.tar.gz
PeerTube-93cae47925e4dd68b7d34a41927b2740b4fab1b4.tar.zst
PeerTube-93cae47925e4dd68b7d34a41927b2740b4fab1b4.zip
Add client hooks
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts2
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts4
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts6
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts4
-rw-r--r--client/src/app/app.component.ts6
-rw-r--r--client/src/app/core/core.module.ts2
-rw-r--r--client/src/app/core/plugins/hooks.service.ts44
-rw-r--r--client/src/app/core/plugins/plugin.service.ts60
-rw-r--r--client/src/app/search/search.component.ts46
-rw-r--r--client/src/app/search/search.service.ts14
-rw-r--r--client/src/app/shared/overview/overview.service.ts2
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts13
-rw-r--r--client/src/app/shared/video/video.service.ts23
-rw-r--r--client/src/app/shared/video/videos-selection.component.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-update.resolver.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts12
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.ts55
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.ts8
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts16
-rw-r--r--client/src/app/videos/recommendations/recent-videos-recommendation.service.ts28
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts17
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts18
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts17
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts16
-rw-r--r--client/tsconfig.json1
-rw-r--r--shared/core-utils/plugins/hooks.ts2
-rw-r--r--shared/models/plugins/client-hook.model.ts39
-rw-r--r--shared/models/plugins/plugin-client-scope.type.ts1
-rw-r--r--shared/models/plugins/plugin-scope.type.ts1
-rw-r--r--shared/models/plugins/server-hook.model.ts2
-rw-r--r--shared/models/server/server-config.model.ts3
31 files changed, 341 insertions, 126 deletions
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index a8d4237e8..4d07d653f 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -68,7 +68,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
68 switchMap(res => from(res.data)), 68 switchMap(res => from(res.data)),
69 concatMap(videoChannel => { 69 concatMap(videoChannel => {
70 return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort) 70 return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort)
71 .pipe(map(data => ({ videoChannel, videos: data.videos }))) 71 .pipe(map(data => ({ videoChannel, videos: data.data })))
72 }) 72 })
73 ) 73 )
74 .subscribe(({ videoChannel, videos }) => { 74 .subscribe(({ videoChannel, videos }) => {
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 5a99aadce..ac4477c18 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -69,8 +69,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
69 return this.videoService 69 return this.videoService
70 .getAccountVideos(this.account, newPagination, this.sort) 70 .getAccountVideos(this.account, newPagination, this.sort)
71 .pipe( 71 .pipe(
72 tap(({ totalVideos }) => { 72 tap(({ total }) => {
73 this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos }) 73 this.titlePage = this.i18n('Published {{total}} videos', { total })
74 }) 74 })
75 ) 75 )
76 } 76 }
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
index 03f34412c..d5122aeba 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
@@ -131,9 +131,9 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
131 131
132 private loadElements () { 132 private loadElements () {
133 this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination) 133 this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
134 .subscribe(({ totalVideos, videos }) => { 134 .subscribe(({ total, data }) => {
135 this.videos = this.videos.concat(videos) 135 this.videos = this.videos.concat(data)
136 this.pagination.totalItems = totalVideos 136 this.pagination.totalItems = total
137 }) 137 })
138 } 138 }
139 139
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 629fd4450..c1dc25aaf 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -71,8 +71,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
71 return this.videoService 71 return this.videoService
72 .getVideoChannelVideos(this.videoChannel, newPagination, this.sort) 72 .getVideoChannelVideos(this.videoChannel, newPagination, this.sort)
73 .pipe( 73 .pipe(
74 tap(({ totalVideos }) => { 74 tap(({ total }) => {
75 this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos }) 75 this.titlePage = this.i18n('Published {{total}} videos', { total })
76 }) 76 })
77 ) 77 )
78 } 78 }
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 0ebd628fc..bde97c68b 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
10import { fromEvent } from 'rxjs' 10import { fromEvent } from 'rxjs'
11import { ViewportScroller } from '@angular/common' 11import { ViewportScroller } from '@angular/common'
12import { PluginService } from '@app/core/plugins/plugin.service' 12import { PluginService } from '@app/core/plugins/plugin.service'
13import { HooksService } from '@app/core/plugins/hooks.service'
13 14
14@Component({ 15@Component({
15 selector: 'my-app', 16 selector: 'my-app',
@@ -33,7 +34,8 @@ export class AppComponent implements OnInit {
33 private redirectService: RedirectService, 34 private redirectService: RedirectService,
34 private screenService: ScreenService, 35 private screenService: ScreenService,
35 private hotkeysService: HotkeysService, 36 private hotkeysService: HotkeysService,
36 private themeService: ThemeService 37 private themeService: ThemeService,
38 private hooks: HooksService
37 ) { } 39 ) { }
38 40
39 get serverVersion () { 41 get serverVersion () {
@@ -206,7 +208,7 @@ export class AppComponent implements OnInit {
206 208
207 await this.pluginService.loadPluginsByScope('common') 209 await this.pluginService.loadPluginsByScope('common')
208 210
209 this.pluginService.runHook('action:application.loaded') 211 this.hooks.runAction('action:application.init')
210 } 212 }
211 213
212 private initHotkeys () { 214 private initHotkeys () {
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 436c0dfb8..5943af4da 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -22,6 +22,7 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification
22import { ServerConfigResolver } from './routing/server-config-resolver.service' 22import { ServerConfigResolver } from './routing/server-config-resolver.service'
23import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' 23import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
24import { PluginService } from '@app/core/plugins/plugin.service' 24import { PluginService } from '@app/core/plugins/plugin.service'
25import { HooksService } from '@app/core/plugins/hooks.service'
25 26
26@NgModule({ 27@NgModule({
27 imports: [ 28 imports: [
@@ -63,6 +64,7 @@ import { PluginService } from '@app/core/plugins/plugin.service'
63 UnloggedGuard, 64 UnloggedGuard,
64 65
65 PluginService, 66 PluginService,
67 HooksService,
66 68
67 RedirectService, 69 RedirectService,
68 Notifier, 70 Notifier,
diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts
new file mode 100644
index 000000000..80c57869c
--- /dev/null
+++ b/client/src/app/core/plugins/hooks.service.ts
@@ -0,0 +1,44 @@
1import { Injectable } from '@angular/core'
2import { PluginService } from '@app/core/plugins/plugin.service'
3import { ClientActionHookName, ClientFilterHookName } from '@shared/models/plugins/client-hook.model'
4import { from, Observable } from 'rxjs'
5import { mergeMap, switchMap } from 'rxjs/operators'
6import { ServerService } from '@app/core/server'
7import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
8
9type RawFunction<U, T> = (params: U) => T
10type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
11
12@Injectable()
13export class HooksService {
14 constructor (
15 private server: ServerService,
16 private pluginService: PluginService
17 ) { }
18
19 wrapObject<T, U extends ClientFilterHookName> (result: T, hookName: U) {
20 return this.pluginService.runHook(hookName, result)
21 }
22
23 wrapObsFun
24 <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName>
25 (fun: ObservableFunction<P, R>, params: P, scope: PluginClientScope, hookParamName: H1, hookResultName: H2) {
26 return from(this.pluginService.ensurePluginsAreLoaded(scope))
27 .pipe(
28 mergeMap(() => this.wrapObject(params, hookParamName)),
29 switchMap(params => fun(params)),
30 mergeMap(result => this.pluginService.runHook(hookResultName, result, params))
31 )
32 }
33
34 async wrapFun<U, T, V extends ClientFilterHookName> (fun: RawFunction<U, T>, params: U, hookName: V) {
35 const result = fun(params)
36
37 return this.pluginService.runHook(hookName, result, params)
38 }
39
40 runAction<T, U extends ClientActionHookName> (hookName: U, params?: T) {
41 this.pluginService.runHook(hookName, params)
42 .catch((err: any) => console.error('Fatal hook error.', { err }))
43 }
44}
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index af330c2eb..14310f093 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -3,11 +3,13 @@ import { Router } from '@angular/router'
3import { ServerConfigPlugin } from '@shared/models' 3import { ServerConfigPlugin } from '@shared/models'
4import { ServerService } from '@app/core/server/server.service' 4import { ServerService } from '@app/core/server/server.service'
5import { ClientScript } from '@shared/models/plugins/plugin-package-json.model' 5import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
6import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
7import { environment } from '../../../environments/environment' 6import { environment } from '../../../environments/environment'
8import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model' 7import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model'
9import { ReplaySubject } from 'rxjs' 8import { ReplaySubject } from 'rxjs'
10import { first, shareReplay } from 'rxjs/operators' 9import { first, shareReplay } from 'rxjs/operators'
10import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
11import { ClientHook, ClientHookName } from '@shared/models/plugins/client-hook.model'
12import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
11 13
12interface HookStructValue extends RegisterHookOptions { 14interface HookStructValue extends RegisterHookOptions {
13 plugin: ServerConfigPlugin 15 plugin: ServerConfigPlugin
@@ -21,14 +23,18 @@ type PluginInfo = {
21} 23}
22 24
23@Injectable() 25@Injectable()
24export class PluginService { 26export class PluginService implements ClientHook {
25 pluginsLoaded = new ReplaySubject<boolean>(1) 27 pluginsBuilt = new ReplaySubject<boolean>(1)
28
29 pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject<boolean> } = {
30 common: new ReplaySubject<boolean>(1),
31 'video-watch': new ReplaySubject<boolean>(1)
32 }
26 33
27 private plugins: ServerConfigPlugin[] = [] 34 private plugins: ServerConfigPlugin[] = []
28 private scopes: { [ scopeName: string ]: PluginInfo[] } = {} 35 private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
29 private loadedPlugins: { [ name: string ]: boolean } = {}
30 private loadedScripts: { [ script: string ]: boolean } = {} 36 private loadedScripts: { [ script: string ]: boolean } = {}
31 private loadedScopes: PluginScope[] = [] 37 private loadedScopes: PluginClientScope[] = []
32 38
33 private hooks: { [ name: string ]: HookStructValue[] } = {} 39 private hooks: { [ name: string ]: HookStructValue[] } = {}
34 40
@@ -45,12 +51,18 @@ export class PluginService {
45 51
46 this.buildScopeStruct() 52 this.buildScopeStruct()
47 53
48 this.pluginsLoaded.next(true) 54 this.pluginsBuilt.next(true)
49 }) 55 })
50 } 56 }
51 57
52 ensurePluginsAreLoaded () { 58 ensurePluginsAreBuilt () {
53 return this.pluginsLoaded.asObservable() 59 return this.pluginsBuilt.asObservable()
60 .pipe(first(), shareReplay())
61 .toPromise()
62 }
63
64 ensurePluginsAreLoaded (scope: PluginClientScope) {
65 return this.pluginsLoaded[scope].asObservable()
54 .pipe(first(), shareReplay()) 66 .pipe(first(), shareReplay())
55 .toPromise() 67 .toPromise()
56 } 68 }
@@ -90,9 +102,9 @@ export class PluginService {
90 } 102 }
91 } 103 }
92 104
93 async loadPluginsByScope (scope: PluginScope, isReload = false) { 105 async loadPluginsByScope (scope: PluginClientScope, isReload = false) {
94 try { 106 try {
95 await this.ensurePluginsAreLoaded() 107 await this.ensurePluginsAreBuilt()
96 108
97 if (!isReload) this.loadedScopes.push(scope) 109 if (!isReload) this.loadedScopes.push(scope)
98 110
@@ -111,32 +123,24 @@ export class PluginService {
111 } 123 }
112 124
113 await Promise.all(promises) 125 await Promise.all(promises)
126
127 this.pluginsLoaded[scope].next(true)
114 } catch (err) { 128 } catch (err) {
115 console.error('Cannot load plugins by scope %s.', scope, err) 129 console.error('Cannot load plugins by scope %s.', scope, err)
116 } 130 }
117 } 131 }
118 132
119 async runHook (hookName: string, param?: any) { 133 async runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
120 let result = param 134 if (!this.hooks[hookName]) return Promise.resolve(result)
121
122 if (!this.hooks[hookName]) return result
123 135
124 const wait = hookName.startsWith('static:') 136 const hookType = getHookType(hookName)
125 137
126 for (const hook of this.hooks[hookName]) { 138 for (const hook of this.hooks[hookName]) {
127 try { 139 console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
128 const p = hook.handler(param) 140
129 141 result = await internalRunHook(hook.handler, hookType, result, params, err => {
130 if (wait) { 142 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
131 result = await p 143 })
132 } else if (p.catch) {
133 p.catch((err: Error) => {
134 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
135 })
136 }
137 } catch (err) {
138 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
139 }
140 } 144 }
141 145
142 return result 146 return result
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts
index a7ddbe1f8..b1d732d68 100644
--- a/client/src/app/search/search.component.ts
+++ b/client/src/app/search/search.component.ts
@@ -10,6 +10,7 @@ import { AdvancedSearch } from '@app/search/advanced-search.model'
10import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 10import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
11import { immutableAssign } from '@app/shared/misc/utils' 11import { immutableAssign } from '@app/shared/misc/utils'
12import { Video } from '@app/shared/video/video.model' 12import { Video } from '@app/shared/video/video.model'
13import { HooksService } from '@app/core/plugins/hooks.service'
13 14
14@Component({ 15@Component({
15 selector: 'my-search', 16 selector: 'my-search',
@@ -41,7 +42,8 @@ export class SearchComponent implements OnInit, OnDestroy {
41 private metaService: MetaService, 42 private metaService: MetaService,
42 private notifier: Notifier, 43 private notifier: Notifier,
43 private searchService: SearchService, 44 private searchService: SearchService,
44 private authService: AuthService 45 private authService: AuthService,
46 private hooks: HooksService
45 ) { } 47 ) { }
46 48
47 get user () { 49 get user () {
@@ -93,18 +95,18 @@ export class SearchComponent implements OnInit, OnDestroy {
93 95
94 search () { 96 search () {
95 forkJoin([ 97 forkJoin([
96 this.searchService.searchVideos(this.currentSearch, this.pagination, this.advancedSearch), 98 this.getVideosObs(),
97 this.searchService.searchVideoChannels(this.currentSearch, immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage })) 99 this.getVideoChannelObs()
98 ]) 100 ])
99 .subscribe( 101 .subscribe(
100 ([ videosResult, videoChannelsResult ]) => { 102 ([ videosResult, videoChannelsResult ]) => {
101 this.results = this.results 103 this.results = this.results
102 .concat(videoChannelsResult.data) 104 .concat(videoChannelsResult.data)
103 .concat(videosResult.videos) 105 .concat(videosResult.data)
104 this.pagination.totalItems = videosResult.totalVideos + videoChannelsResult.total 106 this.pagination.totalItems = videosResult.total + videoChannelsResult.total
105 107
106 // Focus on channels if there are no enough videos 108 // Focus on channels if there are no enough videos
107 if (this.firstSearch === true && videosResult.videos.length < this.pagination.itemsPerPage) { 109 if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) {
108 this.resetPagination() 110 this.resetPagination()
109 this.firstSearch = false 111 this.firstSearch = false
110 112
@@ -117,7 +119,6 @@ export class SearchComponent implements OnInit, OnDestroy {
117 119
118 err => this.notifier.error(err.message) 120 err => this.notifier.error(err.message)
119 ) 121 )
120
121 } 122 }
122 123
123 onNearOfBottom () { 124 onNearOfBottom () {
@@ -163,4 +164,35 @@ export class SearchComponent implements OnInit, OnDestroy {
163 queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search }) 164 queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search })
164 }) 165 })
165 } 166 }
167
168 private getVideosObs () {
169 const params = {
170 search: this.currentSearch,
171 componentPagination: this.pagination,
172 advancedSearch: this.advancedSearch
173 }
174
175 return this.hooks.wrapObsFun(
176 this.searchService.searchVideos.bind(this.searchService),
177 params,
178 'common',
179 'filter:api.search.videos.list.params',
180 'filter:api.search.videos.list.result'
181 )
182 }
183
184 private getVideoChannelObs () {
185 const params = {
186 search: this.currentSearch,
187 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage })
188 }
189
190 return this.hooks.wrapObsFun(
191 this.searchService.searchVideoChannels.bind(this.searchService),
192 params,
193 'common',
194 'filter:api.search.video-channels.list.params',
195 'filter:api.search.video-channels.list.result'
196 )
197 }
166} 198}
diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts
index cd3bdad35..8f137a321 100644
--- a/client/src/app/search/search.service.ts
+++ b/client/src/app/search/search.service.ts
@@ -23,13 +23,14 @@ export class SearchService {
23 private videoService: VideoService 23 private videoService: VideoService
24 ) {} 24 ) {}
25 25
26 searchVideos ( 26 searchVideos (parameters: {
27 search: string, 27 search: string,
28 componentPagination: ComponentPagination, 28 componentPagination: ComponentPagination,
29 advancedSearch: AdvancedSearch 29 advancedSearch: AdvancedSearch
30 ): Observable<{ videos: Video[], totalVideos: number }> { 30 }): Observable<ResultList<Video>> {
31 const url = SearchService.BASE_SEARCH_URL + 'videos' 31 const { search, componentPagination, advancedSearch } = parameters
32 32
33 const url = SearchService.BASE_SEARCH_URL + 'videos'
33 const pagination = this.restService.componentPaginationToRestPagination(componentPagination) 34 const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
34 35
35 let params = new HttpParams() 36 let params = new HttpParams()
@@ -48,12 +49,13 @@ export class SearchService {
48 ) 49 )
49 } 50 }
50 51
51 searchVideoChannels ( 52 searchVideoChannels (parameters: {
52 search: string, 53 search: string,
53 componentPagination: ComponentPagination 54 componentPagination: ComponentPagination
54 ): Observable<{ data: VideoChannel[], total: number }> { 55 }): Observable<ResultList<VideoChannel>> {
55 const url = SearchService.BASE_SEARCH_URL + 'video-channels' 56 const { search, componentPagination } = parameters
56 57
58 const url = SearchService.BASE_SEARCH_URL + 'video-channels'
57 const pagination = this.restService.componentPaginationToRestPagination(componentPagination) 59 const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
58 60
59 let params = new HttpParams() 61 let params = new HttpParams()
diff --git a/client/src/app/shared/overview/overview.service.ts b/client/src/app/shared/overview/overview.service.ts
index 98dba2d97..bd4068925 100644
--- a/client/src/app/shared/overview/overview.service.ts
+++ b/client/src/app/shared/overview/overview.service.ts
@@ -45,7 +45,7 @@ export class OverviewService {
45 of(object.videos) 45 of(object.videos)
46 .pipe( 46 .pipe(
47 switchMap(videos => this.videosService.extractVideos({ total: 0, data: videos })), 47 switchMap(videos => this.videosService.extractVideos({ total: 0, data: videos })),
48 map(result => result.videos), 48 map(result => result.data),
49 tap(videos => { 49 tap(videos => {
50 videosOverviewResult[key].push(immutableAssign(object, { videos })) 50 videosOverviewResult[key].push(immutableAssign(object, { videos }))
51 }) 51 })
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index cf4b5ef8e..8a247a9af 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -13,6 +13,7 @@ import { Notifier, ServerService } from '@app/core'
13import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 13import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
14import { I18n } from '@ngx-translate/i18n-polyfill' 14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' 15import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date'
16import { ResultList } from '@shared/models'
16 17
17enum GroupDate { 18enum GroupDate {
18 UNKNOWN = 0, 19 UNKNOWN = 0,
@@ -73,7 +74,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
73 private groupedDateLabels: { [id in GroupDate]: string } 74 private groupedDateLabels: { [id in GroupDate]: string }
74 private groupedDates: { [id: number]: GroupDate } = {} 75 private groupedDates: { [id: number]: GroupDate } = {}
75 76
76 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> 77 abstract getVideosObservable (page: number): Observable<ResultList<Video>>
77 78
78 abstract generateSyndicationList (): void 79 abstract generateSyndicationList (): void
79 80
@@ -138,12 +139,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
138 } 139 }
139 140
140 loadMoreVideos () { 141 loadMoreVideos () {
141 const observable = this.getVideosObservable(this.pagination.currentPage) 142 this.getVideosObservable(this.pagination.currentPage).subscribe(
142 143 ({ data, total }) => {
143 observable.subscribe( 144 this.pagination.totalItems = total
144 ({ videos, totalVideos }) => { 145 this.videos = this.videos.concat(data)
145 this.pagination.totalItems = totalVideos
146 this.videos = this.videos.concat(videos)
147 146
148 if (this.groupByDate) this.buildGroupedDateLabels() 147 if (this.groupByDate) this.buildGroupedDateLabels()
149 148
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index 871bc9e46..d1af13c93 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -41,7 +41,7 @@ export interface VideosProvider {
41 filter?: VideoFilter, 41 filter?: VideoFilter,
42 categoryOneOf?: number, 42 categoryOneOf?: number,
43 languageOneOf?: string[] 43 languageOneOf?: string[]
44 }): Observable<{ videos: Video[], totalVideos: number }> 44 }): Observable<ResultList<Video>>
45} 45}
46 46
47@Injectable() 47@Injectable()
@@ -65,11 +65,11 @@ export class VideoService implements VideosProvider {
65 return VideoService.BASE_VIDEO_URL + uuid + '/watching' 65 return VideoService.BASE_VIDEO_URL + uuid + '/watching'
66 } 66 }
67 67
68 getVideo (uuid: string): Observable<VideoDetails> { 68 getVideo (options: { videoId: string }): Observable<VideoDetails> {
69 return this.serverService.localeObservable 69 return this.serverService.localeObservable
70 .pipe( 70 .pipe(
71 switchMap(translations => { 71 switchMap(translations => {
72 return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid) 72 return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + options.videoId)
73 .pipe(map(videoHash => ({ videoHash, translations }))) 73 .pipe(map(videoHash => ({ videoHash, translations })))
74 }), 74 }),
75 map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), 75 map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
@@ -123,7 +123,7 @@ export class VideoService implements VideosProvider {
123 .pipe(catchError(err => this.restExtractor.handleError(err))) 123 .pipe(catchError(err => this.restExtractor.handleError(err)))
124 } 124 }
125 125
126 getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> { 126 getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<ResultList<Video>> {
127 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 127 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
128 128
129 let params = new HttpParams() 129 let params = new HttpParams()
@@ -141,7 +141,7 @@ export class VideoService implements VideosProvider {
141 account: Account, 141 account: Account,
142 videoPagination: ComponentPagination, 142 videoPagination: ComponentPagination,
143 sort: VideoSortField 143 sort: VideoSortField
144 ): Observable<{ videos: Video[], totalVideos: number }> { 144 ): Observable<ResultList<Video>> {
145 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 145 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
146 146
147 let params = new HttpParams() 147 let params = new HttpParams()
@@ -159,7 +159,7 @@ export class VideoService implements VideosProvider {
159 videoChannel: VideoChannel, 159 videoChannel: VideoChannel,
160 videoPagination: ComponentPagination, 160 videoPagination: ComponentPagination,
161 sort: VideoSortField 161 sort: VideoSortField
162 ): Observable<{ videos: Video[], totalVideos: number }> { 162 ): Observable<ResultList<Video>> {
163 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 163 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
164 164
165 let params = new HttpParams() 165 let params = new HttpParams()
@@ -176,7 +176,7 @@ export class VideoService implements VideosProvider {
176 getPlaylistVideos ( 176 getPlaylistVideos (
177 videoPlaylistId: number | string, 177 videoPlaylistId: number | string,
178 videoPagination: ComponentPagination 178 videoPagination: ComponentPagination
179 ): Observable<{ videos: Video[], totalVideos: number }> { 179 ): Observable<ResultList<Video>> {
180 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 180 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
181 181
182 let params = new HttpParams() 182 let params = new HttpParams()
@@ -190,10 +190,11 @@ export class VideoService implements VideosProvider {
190 ) 190 )
191 } 191 }
192 192
193 getUserSubscriptionVideos ( 193 getUserSubscriptionVideos (parameters: {
194 videoPagination: ComponentPagination, 194 videoPagination: ComponentPagination,
195 sort: VideoSortField 195 sort: VideoSortField
196 ): Observable<{ videos: Video[], totalVideos: number }> { 196 }): Observable<ResultList<Video>> {
197 const { videoPagination, sort } = parameters
197 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 198 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
198 199
199 let params = new HttpParams() 200 let params = new HttpParams()
@@ -213,7 +214,7 @@ export class VideoService implements VideosProvider {
213 filter?: VideoFilter, 214 filter?: VideoFilter,
214 categoryOneOf?: number, 215 categoryOneOf?: number,
215 languageOneOf?: string[] 216 languageOneOf?: string[]
216 }): Observable<{ videos: Video[], totalVideos: number }> { 217 }): Observable<ResultList<Video>> {
217 const { videoPagination, sort, filter, categoryOneOf, languageOneOf } = parameters 218 const { videoPagination, sort, filter, categoryOneOf, languageOneOf } = parameters
218 219
219 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 220 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
@@ -344,7 +345,7 @@ export class VideoService implements VideosProvider {
344 videos.push(new Video(videoJson, translations)) 345 videos.push(new Video(videoJson, translations))
345 } 346 }
346 347
347 return { videos, totalVideos } 348 return { total: totalVideos, data: videos }
348 }) 349 })
349 ) 350 )
350 } 351 }
diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts
index d69f7b70e..994e0fa1e 100644
--- a/client/src/app/shared/video/videos-selection.component.ts
+++ b/client/src/app/shared/video/videos-selection.component.ts
@@ -21,6 +21,7 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
21import { VideoSortField } from '@app/shared/video/sort-field.type' 21import { VideoSortField } from '@app/shared/video/sort-field.type'
22import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 22import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
23import { I18n } from '@ngx-translate/i18n-polyfill' 23import { I18n } from '@ngx-translate/i18n-polyfill'
24import { ResultList } from '@shared/models'
24 25
25export type SelectionType = { [ id: number ]: boolean } 26export type SelectionType = { [ id: number ]: boolean }
26 27
@@ -33,7 +34,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
33 @Input() pagination: ComponentPagination 34 @Input() pagination: ComponentPagination
34 @Input() titlePage: string 35 @Input() titlePage: string
35 @Input() miniatureDisplayOptions: MiniatureDisplayOptions 36 @Input() miniatureDisplayOptions: MiniatureDisplayOptions
36 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<{ videos: Video[], totalVideos: number }> 37 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
37 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective> 38 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective>
38 39
39 @Output() selectionChange = new EventEmitter<SelectionType>() 40 @Output() selectionChange = new EventEmitter<SelectionType>()
diff --git a/client/src/app/videos/+video-edit/video-update.resolver.ts b/client/src/app/videos/+video-edit/video-update.resolver.ts
index 384458127..4ac517d96 100644
--- a/client/src/app/videos/+video-edit/video-update.resolver.ts
+++ b/client/src/app/videos/+video-edit/video-update.resolver.ts
@@ -18,7 +18,7 @@ export class VideoUpdateResolver implements Resolve<any> {
18 resolve (route: ActivatedRouteSnapshot) { 18 resolve (route: ActivatedRouteSnapshot) {
19 const uuid: string = route.params[ 'uuid' ] 19 const uuid: string = route.params[ 'uuid' ]
20 20
21 return this.videoService.getVideo(uuid) 21 return this.videoService.getVideo({ videoId: uuid })
22 .pipe( 22 .pipe(
23 switchMap(video => { 23 switchMap(video => {
24 return forkJoin([ 24 return forkJoin([
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
index b8e5878c5..eb608a1a3 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
@@ -48,11 +48,13 @@ export class VideoCommentService {
48 ) 48 )
49 } 49 }
50 50
51 getVideoCommentThreads ( 51 getVideoCommentThreads (parameters: {
52 videoId: number | string, 52 videoId: number | string,
53 componentPagination: ComponentPagination, 53 componentPagination: ComponentPagination,
54 sort: VideoSortField 54 sort: VideoSortField
55 ): Observable<{ comments: VideoComment[], totalComments: number}> { 55 }): Observable<{ comments: VideoComment[], totalComments: number}> {
56 const { videoId, componentPagination, sort } = parameters
57
56 const pagination = this.restService.componentPaginationToRestPagination(componentPagination) 58 const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
57 59
58 let params = new HttpParams() 60 let params = new HttpParams()
@@ -67,7 +69,11 @@ export class VideoCommentService {
67 ) 69 )
68 } 70 }
69 71
70 getVideoThreadComments (videoId: number | string, threadId: number): Observable<VideoCommentThreadTree> { 72 getVideoThreadComments (parameters: {
73 videoId: number | string,
74 threadId: number
75 }): Observable<VideoCommentThreadTree> {
76 const { videoId, threadId } = parameters
71 const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` 77 const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}`
72 78
73 return this.authHttp 79 return this.authHttp
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
index 3acddbe6a..3c1a0986c 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
@@ -12,6 +12,7 @@ import { VideoComment } from './video-comment.model'
12import { VideoCommentService } from './video-comment.service' 12import { VideoCommentService } from './video-comment.service'
13import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Syndication } from '@app/shared/video/syndication.model' 14import { Syndication } from '@app/shared/video/syndication.model'
15import { HooksService } from '@app/core/plugins/hooks.service'
15 16
16@Component({ 17@Component({
17 selector: 'my-video-comments', 18 selector: 'my-video-comments',
@@ -45,7 +46,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
45 private confirmService: ConfirmService, 46 private confirmService: ConfirmService,
46 private videoCommentService: VideoCommentService, 47 private videoCommentService: VideoCommentService,
47 private activatedRoute: ActivatedRoute, 48 private activatedRoute: ActivatedRoute,
48 private i18n: I18n 49 private i18n: I18n,
50 private hooks: HooksService
49 ) {} 51 ) {}
50 52
51 ngOnInit () { 53 ngOnInit () {
@@ -73,8 +75,20 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
73 viewReplies (commentId: number, highlightThread = false) { 75 viewReplies (commentId: number, highlightThread = false) {
74 this.threadLoading[commentId] = true 76 this.threadLoading[commentId] = true
75 77
76 this.videoCommentService.getVideoThreadComments(this.video.id, commentId) 78 const params = {
77 .subscribe( 79 videoId: this.video.id,
80 threadId: commentId
81 }
82
83 const obs = this.hooks.wrapObsFun(
84 this.videoCommentService.getVideoThreadComments.bind(this.videoCommentService),
85 params,
86 'video-watch',
87 'filter:api.video-watch.video-thread-replies.list.params',
88 'filter:api.video-watch.video-thread-replies.list.result'
89 )
90
91 obs.subscribe(
78 res => { 92 res => {
79 this.threadComments[commentId] = res 93 this.threadComments[commentId] = res
80 this.threadLoading[commentId] = false 94 this.threadLoading[commentId] = false
@@ -91,16 +105,29 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
91 ) 105 )
92 } 106 }
93 107
94 loadMoreComments () { 108 loadMoreThreads () {
95 this.videoCommentService.getVideoCommentThreads(this.video.id, this.componentPagination, this.sort) 109 const params = {
96 .subscribe( 110 videoId: this.video.id,
97 res => { 111 componentPagination: this.componentPagination,
98 this.comments = this.comments.concat(res.comments) 112 sort: this.sort
99 this.componentPagination.totalItems = res.totalComments 113 }
100 },
101 114
102 err => this.notifier.error(err.message) 115 const obs = this.hooks.wrapObsFun(
103 ) 116 this.videoCommentService.getVideoCommentThreads.bind(this.videoCommentService),
117 params,
118 'video-watch',
119 'filter:api.video-watch.video-threads.list.params',
120 'filter:api.video-watch.video-threads.list.result'
121 )
122
123 obs.subscribe(
124 res => {
125 this.comments = this.comments.concat(res.comments)
126 this.componentPagination.totalItems = res.totalComments
127 },
128
129 err => this.notifier.error(err.message)
130 )
104 } 131 }
105 132
106 onCommentThreadCreated (comment: VideoComment) { 133 onCommentThreadCreated (comment: VideoComment) {
@@ -169,7 +196,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
169 this.componentPagination.currentPage++ 196 this.componentPagination.currentPage++
170 197
171 if (hasMoreItems(this.componentPagination)) { 198 if (hasMoreItems(this.componentPagination)) {
172 this.loadMoreComments() 199 this.loadMoreThreads()
173 } 200 }
174 } 201 }
175 202
@@ -197,7 +224,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
197 224
198 this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid) 225 this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid)
199 226
200 this.loadMoreComments() 227 this.loadMoreThreads()
201 } 228 }
202 } 229 }
203 230
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
index bccdaf7b2..2fb0cb0e5 100644
--- a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
@@ -66,11 +66,11 @@ export class VideoWatchPlaylistComponent {
66 66
67 loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { 67 loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
68 this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination) 68 this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination)
69 .subscribe(({ totalVideos, videos }) => { 69 .subscribe(({ total, data }) => {
70 this.playlistVideos = this.playlistVideos.concat(videos) 70 this.playlistVideos = this.playlistVideos.concat(data)
71 this.playlistPagination.totalItems = totalVideos 71 this.playlistPagination.totalItems = total
72 72
73 if (totalVideos === 0) { 73 if (total === 0) {
74 this.noPlaylistVideos = true 74 this.noPlaylistVideos = true
75 return 75 return
76 } 76 }
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 6d8bb4b3f..eed2ec048 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -33,6 +33,7 @@ import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
33import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' 33import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
34import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage' 34import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
35import { PluginService } from '@app/core/plugins/plugin.service' 35import { PluginService } from '@app/core/plugins/plugin.service'
36import { HooksService } from '@app/core/plugins/hooks.service'
36 37
37@Component({ 38@Component({
38 selector: 'my-video-watch', 39 selector: 'my-video-watch',
@@ -93,6 +94,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
93 private videoCaptionService: VideoCaptionService, 94 private videoCaptionService: VideoCaptionService,
94 private i18n: I18n, 95 private i18n: I18n,
95 private hotkeysService: HotkeysService, 96 private hotkeysService: HotkeysService,
97 private hooks: HooksService,
96 @Inject(LOCALE_ID) private localeId: string 98 @Inject(LOCALE_ID) private localeId: string
97 ) {} 99 ) {}
98 100
@@ -131,7 +133,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
131 133
132 this.theaterEnabled = getStoredTheater() 134 this.theaterEnabled = getStoredTheater()
133 135
134 this.pluginService.runHook('action:video-watch.loaded') 136 this.hooks.runAction('action:video-watch.init')
135 } 137 }
136 138
137 ngOnDestroy () { 139 ngOnDestroy () {
@@ -246,9 +248,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
246 248
247 if (this.player) this.player.pause() 249 if (this.player) this.player.pause()
248 250
251 const videoObs = this.hooks.wrapObsFun(
252 this.videoService.getVideo.bind(this.videoService),
253 { videoId },
254 'video-watch',
255 'filter:api.video-watch.video.get.params',
256 'filter:api.video-watch.video.get.result'
257 )
258
249 // Video did change 259 // Video did change
250 forkJoin( 260 forkJoin(
251 this.videoService.getVideo(videoId), 261 videoObs,
252 this.videoCaptionService.listCaptions(videoId) 262 this.videoCaptionService.listCaptions(videoId)
253 ) 263 )
254 .pipe( 264 .pipe(
@@ -486,6 +496,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
486 496
487 this.setOpenGraphTags() 497 this.setOpenGraphTags()
488 this.checkUserRating() 498 this.checkUserRating()
499
500 this.hooks.runAction('action:video-watch.video.loaded')
489 } 501 }
490 502
491 private setRating (nextRating: UserVideoRateType) { 503 private setRating (nextRating: UserVideoRateType) {
diff --git a/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts b/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts
index f975ff6ef..a1e65c27c 100644
--- a/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts
+++ b/client/src/app/videos/recommendations/recent-videos-recommendation.service.ts
@@ -33,20 +33,24 @@ export class RecentVideosRecommendationService implements RecommendationService
33 private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> { 33 private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
34 const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 } 34 const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
35 const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' }) 35 const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' })
36 .pipe(map(v => v.videos)) 36 .pipe(map(v => v.data))
37 37
38 if (!recommendation.tags || recommendation.tags.length === 0) return defaultSubscription 38 if (!recommendation.tags || recommendation.tags.length === 0) return defaultSubscription
39 39
40 return this.searchService.searchVideos('', 40 const params = {
41 pagination, 41 search: '',
42 new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' }) 42 componentPagination: pagination,
43 ).pipe( 43 advancedSearch: new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' })
44 map(v => v.videos), 44 }
45 switchMap(videos => { 45
46 if (videos.length <= 1) return defaultSubscription 46 return this.searchService.searchVideos(params)
47 47 .pipe(
48 return of(videos) 48 map(v => v.data),
49 }) 49 switchMap(videos => {
50 ) 50 if (videos.length <= 1) return defaultSubscription
51
52 return of(videos)
53 })
54 )
51 } 55 }
52} 56}
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index 5de4a13af..81b6ce493 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
10import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { UserRight } from '../../../../../shared/models/users' 11import { UserRight } from '../../../../../shared/models/users'
12import { Notifier, ServerService } from '@app/core' 12import { Notifier, ServerService } from '@app/core'
13import { HooksService } from '@app/core/plugins/hooks.service'
13 14
14@Component({ 15@Component({
15 selector: 'my-videos-local', 16 selector: 'my-videos-local',
@@ -31,7 +32,8 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
31 protected notifier: Notifier, 32 protected notifier: Notifier,
32 protected authService: AuthService, 33 protected authService: AuthService,
33 protected screenService: ScreenService, 34 protected screenService: ScreenService,
34 private videoService: VideoService 35 private videoService: VideoService,
36 private hooks: HooksService
35 ) { 37 ) {
36 super() 38 super()
37 39
@@ -55,14 +57,21 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
55 57
56 getVideosObservable (page: number) { 58 getVideosObservable (page: number) {
57 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 59 const newPagination = immutableAssign(this.pagination, { currentPage: page })
58 60 const params = {
59 return this.videoService.getVideos({
60 videoPagination: newPagination, 61 videoPagination: newPagination,
61 sort: this.sort, 62 sort: this.sort,
62 filter: this.filter, 63 filter: this.filter,
63 categoryOneOf: this.categoryOneOf, 64 categoryOneOf: this.categoryOneOf,
64 languageOneOf: this.languageOneOf 65 languageOneOf: this.languageOneOf
65 }) 66 }
67
68 return this.hooks.wrapObsFun(
69 this.videoService.getVideos.bind(this.videoService),
70 params,
71 'common',
72 'filter:api.videos.list.local.params',
73 'filter:api.videos.list.local.result'
74 )
66 } 75 }
67 76
68 generateSyndicationList () { 77 generateSyndicationList () {
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index 19522e6b4..638e7caed 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
8import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { ScreenService } from '@app/shared/misc/screen.service' 9import { ScreenService } from '@app/shared/misc/screen.service'
10import { Notifier, ServerService } from '@app/core' 10import { Notifier, ServerService } from '@app/core'
11import { HooksService } from '@app/core/plugins/hooks.service'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-recently-added', 14 selector: 'my-videos-recently-added',
@@ -29,7 +30,8 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
29 protected notifier: Notifier, 30 protected notifier: Notifier,
30 protected authService: AuthService, 31 protected authService: AuthService,
31 protected screenService: ScreenService, 32 protected screenService: ScreenService,
32 private videoService: VideoService 33 private videoService: VideoService,
34 private hooks: HooksService
33 ) { 35 ) {
34 super() 36 super()
35 37
@@ -48,14 +50,20 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
48 50
49 getVideosObservable (page: number) { 51 getVideosObservable (page: number) {
50 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 52 const newPagination = immutableAssign(this.pagination, { currentPage: page })
51 53 const params = {
52 return this.videoService.getVideos({
53 videoPagination: newPagination, 54 videoPagination: newPagination,
54 sort: this.sort, 55 sort: this.sort,
55 filter: undefined,
56 categoryOneOf: this.categoryOneOf, 56 categoryOneOf: this.categoryOneOf,
57 languageOneOf: this.languageOneOf 57 languageOneOf: this.languageOneOf
58 }) 58 }
59
60 return this.hooks.wrapObsFun(
61 this.videoService.getVideos.bind(this.videoService),
62 params,
63 'common',
64 'filter:api.videos.list.recently-added.params',
65 'filter:api.videos.list.recently-added.result'
66 )
59 } 67 }
60 68
61 generateSyndicationList () { 69 generateSyndicationList () {
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index 5f1d5055b..0e69bfd64 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
8import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { ScreenService } from '@app/shared/misc/screen.service' 9import { ScreenService } from '@app/shared/misc/screen.service'
10import { Notifier, ServerService } from '@app/core' 10import { Notifier, ServerService } from '@app/core'
11import { HooksService } from '@app/core/plugins/hooks.service'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-trending', 14 selector: 'my-videos-trending',
@@ -28,7 +29,8 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
28 protected notifier: Notifier, 29 protected notifier: Notifier,
29 protected authService: AuthService, 30 protected authService: AuthService,
30 protected screenService: ScreenService, 31 protected screenService: ScreenService,
31 private videoService: VideoService 32 private videoService: VideoService,
33 private hooks: HooksService
32 ) { 34 ) {
33 super() 35 super()
34 } 36 }
@@ -61,13 +63,20 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
61 63
62 getVideosObservable (page: number) { 64 getVideosObservable (page: number) {
63 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 65 const newPagination = immutableAssign(this.pagination, { currentPage: page })
64 return this.videoService.getVideos({ 66 const params = {
65 videoPagination: newPagination, 67 videoPagination: newPagination,
66 sort: this.sort, 68 sort: this.sort,
67 filter: undefined,
68 categoryOneOf: this.categoryOneOf, 69 categoryOneOf: this.categoryOneOf,
69 languageOneOf: this.languageOneOf 70 languageOneOf: this.languageOneOf
70 }) 71 }
72
73 return this.hooks.wrapObsFun(
74 this.videoService.getVideos.bind(this.videoService),
75 params,
76 'common',
77 'filter:api.videos.list.trending.params',
78 'filter:api.videos.list.trending.result'
79 )
71 } 80 }
72 81
73 generateSyndicationList () { 82 generateSyndicationList () {
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
index 3caa371d8..ac325aeff 100644
--- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -9,6 +9,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
9import { ScreenService } from '@app/shared/misc/screen.service' 9import { ScreenService } from '@app/shared/misc/screen.service'
10import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 10import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
11import { Notifier, ServerService } from '@app/core' 11import { Notifier, ServerService } from '@app/core'
12import { HooksService } from '@app/core/plugins/hooks.service'
12 13
13@Component({ 14@Component({
14 selector: 'my-videos-user-subscriptions', 15 selector: 'my-videos-user-subscriptions',
@@ -29,7 +30,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
29 protected notifier: Notifier, 30 protected notifier: Notifier,
30 protected authService: AuthService, 31 protected authService: AuthService,
31 protected screenService: ScreenService, 32 protected screenService: ScreenService,
32 private videoService: VideoService 33 private videoService: VideoService,
34 private hooks: HooksService
33 ) { 35 ) {
34 super() 36 super()
35 37
@@ -46,8 +48,18 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
46 48
47 getVideosObservable (page: number) { 49 getVideosObservable (page: number) {
48 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 50 const newPagination = immutableAssign(this.pagination, { currentPage: page })
51 const params = {
52 videoPagination: newPagination,
53 sort: this.sort
54 }
49 55
50 return this.videoService.getUserSubscriptionVideos(newPagination, this.sort) 56 return this.hooks.wrapObsFun(
57 this.videoService.getUserSubscriptionVideos.bind(this.videoService),
58 params,
59 'common',
60 'filter:api.videos.list.user-subscriptions.params',
61 'filter:api.videos.list.user-subscriptions.result'
62 )
51 } 63 }
52 64
53 generateSyndicationList () { 65 generateSyndicationList () {
diff --git a/client/tsconfig.json b/client/tsconfig.json
index e46528d1c..ac9a3522d 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -12,6 +12,7 @@
12 "noImplicitThis": true, 12 "noImplicitThis": true,
13 "suppressImplicitAnyIndexErrors":true, 13 "suppressImplicitAnyIndexErrors":true,
14 "alwaysStrict": true, 14 "alwaysStrict": true,
15 "strictBindCallApply": true,
15 "target": "es5", 16 "target": "es5",
16 "typeRoots": [ 17 "typeRoots": [
17 "node_modules/@types" 18 "node_modules/@types"
diff --git a/shared/core-utils/plugins/hooks.ts b/shared/core-utils/plugins/hooks.ts
index 3d59a7428..5405e0529 100644
--- a/shared/core-utils/plugins/hooks.ts
+++ b/shared/core-utils/plugins/hooks.ts
@@ -29,7 +29,7 @@ async function internalRunHook <T> (handler: Function, hookType: HookType, resul
29 } 29 }
30 30
31 if (hookType === HookType.ACTION) { 31 if (hookType === HookType.ACTION) {
32 if (isCatchable(p)) p.catch(err => onError(err)) 32 if (isCatchable(p)) p.catch((err: any) => onError(err))
33 33
34 return undefined 34 return undefined
35 } 35 }
diff --git a/shared/models/plugins/client-hook.model.ts b/shared/models/plugins/client-hook.model.ts
new file mode 100644
index 000000000..89400003e
--- /dev/null
+++ b/shared/models/plugins/client-hook.model.ts
@@ -0,0 +1,39 @@
1export type ClientFilterHookName =
2 'filter:api.videos.list.trending.params' |
3 'filter:api.videos.list.trending.result' |
4
5 'filter:api.videos.list.local.params' |
6 'filter:api.videos.list.local.result' |
7
8 'filter:api.videos.list.recently-added.params' |
9 'filter:api.videos.list.recently-added.result' |
10
11 'filter:api.videos.list.user-subscriptions.params' |
12 'filter:api.videos.list.user-subscriptions.result' |
13
14 'filter:api.video-watch.video.get.params' |
15 'filter:api.video-watch.video.get.result' |
16
17 'filter:api.video-watch.video-threads.list.params' |
18 'filter:api.video-watch.video-threads.list.result' |
19
20 'filter:api.video-watch.video-thread-replies.list.params' |
21 'filter:api.video-watch.video-thread-replies.list.result' |
22
23 'filter:api.search.videos.list.params' |
24 'filter:api.search.videos.list.result' |
25 'filter:api.search.video-channels.list.params' |
26 'filter:api.search.video-channels.list.result'
27
28export type ClientActionHookName =
29 'action:application.init' |
30
31 'action:video-watch.init' |
32
33 'action:video-watch.video.loaded'
34
35export type ClientHookName = ClientActionHookName | ClientFilterHookName
36
37export interface ClientHook {
38 runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T>
39}
diff --git a/shared/models/plugins/plugin-client-scope.type.ts b/shared/models/plugins/plugin-client-scope.type.ts
new file mode 100644
index 000000000..a2112eed7
--- /dev/null
+++ b/shared/models/plugins/plugin-client-scope.type.ts
@@ -0,0 +1 @@
export type PluginClientScope = 'common' | 'video-watch'
diff --git a/shared/models/plugins/plugin-scope.type.ts b/shared/models/plugins/plugin-scope.type.ts
deleted file mode 100644
index b63ae43ec..000000000
--- a/shared/models/plugins/plugin-scope.type.ts
+++ /dev/null
@@ -1 +0,0 @@
1export type PluginScope = 'common' | 'video-watch'
diff --git a/shared/models/plugins/server-hook.model.ts b/shared/models/plugins/server-hook.model.ts
index a7f88f3c4..6729e2dab 100644
--- a/shared/models/plugins/server-hook.model.ts
+++ b/shared/models/plugins/server-hook.model.ts
@@ -30,5 +30,5 @@ export type ServerActionHookName =
30export type ServerHookName = ServerFilterHookName | ServerActionHookName 30export type ServerHookName = ServerFilterHookName | ServerActionHookName
31 31
32export interface ServerHook { 32export interface ServerHook {
33 runHook (hookName: ServerHookName, params?: any) 33 runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T>
34} 34}
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 3498f86d7..49bb01708 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -1,11 +1,12 @@
1import { NSFWPolicyType } from '../videos/nsfw-policy.type' 1import { NSFWPolicyType } from '../videos/nsfw-policy.type'
2import { ClientScript } from '../plugins/plugin-package-json.model' 2import { ClientScript } from '../plugins/plugin-package-json.model'
3import { PluginClientScope } from '../plugins/plugin-scope.type'
3 4
4export interface ServerConfigPlugin { 5export interface ServerConfigPlugin {
5 name: string 6 name: string
6 version: string 7 version: string
7 description: string 8 description: string
8 clientScripts: { [name: string]: ClientScript } 9 clientScripts: { [name in PluginClientScope]: ClientScript }
9} 10}
10 11
11export interface ServerConfigTheme extends ServerConfigPlugin { 12export interface ServerConfigTheme extends ServerConfigPlugin {