Add client hooks
authorChocobozzz <me@florianbigard.com>
Mon, 22 Jul 2019 13:40:13 +0000 (15:40 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Wed, 24 Jul 2019 08:58:16 +0000 (10:58 +0200)
31 files changed:
client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
client/src/app/+accounts/account-videos/account-videos.component.ts
client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
client/src/app/app.component.ts
client/src/app/core/core.module.ts
client/src/app/core/plugins/hooks.service.ts [new file with mode: 0644]
client/src/app/core/plugins/plugin.service.ts
client/src/app/search/search.component.ts
client/src/app/search/search.service.ts
client/src/app/shared/overview/overview.service.ts
client/src/app/shared/video/abstract-video-list.ts
client/src/app/shared/video/video.service.ts
client/src/app/shared/video/videos-selection.component.ts
client/src/app/videos/+video-edit/video-update.resolver.ts
client/src/app/videos/+video-watch/comment/video-comment.service.ts
client/src/app/videos/+video-watch/comment/video-comments.component.ts
client/src/app/videos/+video-watch/video-watch-playlist.component.ts
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/app/videos/recommendations/recent-videos-recommendation.service.ts
client/src/app/videos/video-list/video-local.component.ts
client/src/app/videos/video-list/video-recently-added.component.ts
client/src/app/videos/video-list/video-trending.component.ts
client/src/app/videos/video-list/video-user-subscriptions.component.ts
client/tsconfig.json
shared/core-utils/plugins/hooks.ts
shared/models/plugins/client-hook.model.ts [new file with mode: 0644]
shared/models/plugins/plugin-client-scope.type.ts [new file with mode: 0644]
shared/models/plugins/plugin-scope.type.ts [deleted file]
shared/models/plugins/server-hook.model.ts
shared/models/server/server-config.model.ts

index a8d4237e81242c5cabf834e0d5794cfd1913881e..4d07d653f9adeafdf5e55c9fa8f32d08270bccc3 100644 (file)
@@ -68,7 +68,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
         switchMap(res => from(res.data)),
         concatMap(videoChannel => {
           return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort)
-            .pipe(map(data => ({ videoChannel, videos: data.videos })))
+            .pipe(map(data => ({ videoChannel, videos: data.data })))
         })
       )
       .subscribe(({ videoChannel, videos }) => {
index 5a99aadcee5c876e5a8e268dc5ad1434f3ede06e..ac4477c182dcc7c0f1f14e7daf97f6d93b2b4019 100644 (file)
@@ -69,8 +69,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
     return this.videoService
                .getAccountVideos(this.account, newPagination, this.sort)
                .pipe(
-                 tap(({ totalVideos }) => {
-                   this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos })
+                 tap(({ total }) => {
+                   this.titlePage = this.i18n('Published {{total}} videos', { total })
                  })
                )
   }
index 03f34412cabdd6e372104589278211cc79874105..d5122aeba5adb1279ed1b29190f8fb188b1985f1 100644 (file)
@@ -131,9 +131,9 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
 
   private loadElements () {
     this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
-        .subscribe(({ totalVideos, videos }) => {
-          this.videos = this.videos.concat(videos)
-          this.pagination.totalItems = totalVideos
+        .subscribe(({ total, data }) => {
+          this.videos = this.videos.concat(data)
+          this.pagination.totalItems = total
         })
   }
 
index 629fd4450b9762714b4df3bd423c725f1cf032e9..c1dc25aaf3b2706410ffc25e8562805ef3e05c5e 100644 (file)
@@ -71,8 +71,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
     return this.videoService
                .getVideoChannelVideos(this.videoChannel, newPagination, this.sort)
                .pipe(
-                 tap(({ totalVideos }) => {
-                   this.titlePage = this.i18n('Published {{totalVideos}} videos', { totalVideos })
+                 tap(({ total }) => {
+                   this.titlePage = this.i18n('Published {{total}} videos', { total })
                  })
                )
   }
index 0ebd628fce46b1f5da440a7276cefa813323f820..bde97c68b6a03573041d4a75542dad1aa57c2bf2 100644 (file)
@@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { fromEvent } from 'rxjs'
 import { ViewportScroller } from '@angular/common'
 import { PluginService } from '@app/core/plugins/plugin.service'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-app',
@@ -33,7 +34,8 @@ export class AppComponent implements OnInit {
     private redirectService: RedirectService,
     private screenService: ScreenService,
     private hotkeysService: HotkeysService,
-    private themeService: ThemeService
+    private themeService: ThemeService,
+    private hooks: HooksService
   ) { }
 
   get serverVersion () {
@@ -206,7 +208,7 @@ export class AppComponent implements OnInit {
 
     await this.pluginService.loadPluginsByScope('common')
 
-    this.pluginService.runHook('action:application.loaded')
+    this.hooks.runAction('action:application.init')
   }
 
   private initHotkeys () {
index 436c0dfb8bba25e09a014c4b76e57bf065870ecb..5943af4da527c4ac62f341bba5b9e6dec5fc99ff 100644 (file)
@@ -22,6 +22,7 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification
 import { ServerConfigResolver } from './routing/server-config-resolver.service'
 import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
 import { PluginService } from '@app/core/plugins/plugin.service'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @NgModule({
   imports: [
@@ -63,6 +64,7 @@ import { PluginService } from '@app/core/plugins/plugin.service'
     UnloggedGuard,
 
     PluginService,
+    HooksService,
 
     RedirectService,
     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 (file)
index 0000000..80c5786
--- /dev/null
@@ -0,0 +1,44 @@
+import { Injectable } from '@angular/core'
+import { PluginService } from '@app/core/plugins/plugin.service'
+import { ClientActionHookName, ClientFilterHookName } from '@shared/models/plugins/client-hook.model'
+import { from, Observable } from 'rxjs'
+import { mergeMap, switchMap } from 'rxjs/operators'
+import { ServerService } from '@app/core/server'
+import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
+
+type RawFunction<U, T> = (params: U) => T
+type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
+
+@Injectable()
+export class HooksService {
+  constructor (
+    private server: ServerService,
+    private pluginService: PluginService
+  ) { }
+
+  wrapObject<T, U extends ClientFilterHookName> (result: T, hookName: U) {
+    return this.pluginService.runHook(hookName, result)
+  }
+
+  wrapObsFun
+    <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName>
+    (fun: ObservableFunction<P, R>, params: P, scope: PluginClientScope, hookParamName: H1, hookResultName: H2) {
+    return from(this.pluginService.ensurePluginsAreLoaded(scope))
+      .pipe(
+        mergeMap(() => this.wrapObject(params, hookParamName)),
+        switchMap(params => fun(params)),
+        mergeMap(result => this.pluginService.runHook(hookResultName, result, params))
+      )
+  }
+
+  async wrapFun<U, T, V extends ClientFilterHookName> (fun: RawFunction<U, T>, params: U, hookName: V) {
+    const result = fun(params)
+
+    return this.pluginService.runHook(hookName, result, params)
+  }
+
+  runAction<T, U extends ClientActionHookName> (hookName: U, params?: T) {
+    this.pluginService.runHook(hookName, params)
+                 .catch((err: any) => console.error('Fatal hook error.', { err }))
+  }
+}
index af330c2eb204f617448074a10d163e3a75b058e7..14310f093670b053039a68d33ee3c68ae990d5f7 100644 (file)
@@ -3,11 +3,13 @@ import { Router } from '@angular/router'
 import { ServerConfigPlugin } from '@shared/models'
 import { ServerService } from '@app/core/server/server.service'
 import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
-import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
 import { environment } from '../../../environments/environment'
 import { RegisterHookOptions } from '@shared/models/plugins/register-hook.model'
 import { ReplaySubject } from 'rxjs'
 import { first, shareReplay } from 'rxjs/operators'
+import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
+import { ClientHook, ClientHookName } from '@shared/models/plugins/client-hook.model'
+import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
 
 interface HookStructValue extends RegisterHookOptions {
   plugin: ServerConfigPlugin
@@ -21,14 +23,18 @@ type PluginInfo = {
 }
 
 @Injectable()
-export class PluginService {
-  pluginsLoaded = new ReplaySubject<boolean>(1)
+export class PluginService implements ClientHook {
+  pluginsBuilt = new ReplaySubject<boolean>(1)
+
+  pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject<boolean> } = {
+    common: new ReplaySubject<boolean>(1),
+    'video-watch': new ReplaySubject<boolean>(1)
+  }
 
   private plugins: ServerConfigPlugin[] = []
   private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
-  private loadedPlugins: { [ name: string ]: boolean } = {}
   private loadedScripts: { [ script: string ]: boolean } = {}
-  private loadedScopes: PluginScope[] = []
+  private loadedScopes: PluginClientScope[] = []
 
   private hooks: { [ name: string ]: HookStructValue[] } = {}
 
@@ -45,12 +51,18 @@ export class PluginService {
 
         this.buildScopeStruct()
 
-        this.pluginsLoaded.next(true)
+        this.pluginsBuilt.next(true)
       })
   }
 
-  ensurePluginsAreLoaded () {
-    return this.pluginsLoaded.asObservable()
+  ensurePluginsAreBuilt () {
+    return this.pluginsBuilt.asObservable()
+               .pipe(first(), shareReplay())
+               .toPromise()
+  }
+
+  ensurePluginsAreLoaded (scope: PluginClientScope) {
+    return this.pluginsLoaded[scope].asObservable()
                .pipe(first(), shareReplay())
                .toPromise()
   }
@@ -90,9 +102,9 @@ export class PluginService {
     }
   }
 
-  async loadPluginsByScope (scope: PluginScope, isReload = false) {
+  async loadPluginsByScope (scope: PluginClientScope, isReload = false) {
     try {
-      await this.ensurePluginsAreLoaded()
+      await this.ensurePluginsAreBuilt()
 
       if (!isReload) this.loadedScopes.push(scope)
 
@@ -111,32 +123,24 @@ export class PluginService {
       }
 
       await Promise.all(promises)
+
+      this.pluginsLoaded[scope].next(true)
     } catch (err) {
       console.error('Cannot load plugins by scope %s.', scope, err)
     }
   }
 
-  async runHook (hookName: string, param?: any) {
-    let result = param
-
-    if (!this.hooks[hookName]) return result
+  async runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
+    if (!this.hooks[hookName]) return Promise.resolve(result)
 
-    const wait = hookName.startsWith('static:')
+    const hookType = getHookType(hookName)
 
     for (const hook of this.hooks[hookName]) {
-      try {
-        const p = hook.handler(param)
-
-        if (wait) {
-          result = await p
-        } else if (p.catch) {
-          p.catch((err: Error) => {
-            console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
-          })
-        }
-      } catch (err) {
-        console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
-      }
+      console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
+
+      result = await internalRunHook(hook.handler, hookType, result, params, err => {
+        console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
+      })
     }
 
     return result
index a7ddbe1f8fb78437c3b8fd98fd67aaf9e08324e0..b1d732d68d3b361e1da5d587188cc71ca610ce2e 100644 (file)
@@ -10,6 +10,7 @@ import { AdvancedSearch } from '@app/search/advanced-search.model'
 import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
 import { immutableAssign } from '@app/shared/misc/utils'
 import { Video } from '@app/shared/video/video.model'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-search',
@@ -41,7 +42,8 @@ export class SearchComponent implements OnInit, OnDestroy {
     private metaService: MetaService,
     private notifier: Notifier,
     private searchService: SearchService,
-    private authService: AuthService
+    private authService: AuthService,
+    private hooks: HooksService
   ) { }
 
   get user () {
@@ -93,18 +95,18 @@ export class SearchComponent implements OnInit, OnDestroy {
 
   search () {
     forkJoin([
-      this.searchService.searchVideos(this.currentSearch, this.pagination, this.advancedSearch),
-      this.searchService.searchVideoChannels(this.currentSearch, immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }))
+      this.getVideosObs(),
+      this.getVideoChannelObs()
     ])
       .subscribe(
         ([ videosResult, videoChannelsResult ]) => {
           this.results = this.results
                              .concat(videoChannelsResult.data)
-                             .concat(videosResult.videos)
-          this.pagination.totalItems = videosResult.totalVideos + videoChannelsResult.total
+                             .concat(videosResult.data)
+          this.pagination.totalItems = videosResult.total + videoChannelsResult.total
 
           // Focus on channels if there are no enough videos
-          if (this.firstSearch === true && videosResult.videos.length < this.pagination.itemsPerPage) {
+          if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) {
             this.resetPagination()
             this.firstSearch = false
 
@@ -117,7 +119,6 @@ export class SearchComponent implements OnInit, OnDestroy {
 
         err => this.notifier.error(err.message)
       )
-
   }
 
   onNearOfBottom () {
@@ -163,4 +164,35 @@ export class SearchComponent implements OnInit, OnDestroy {
       queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search })
     })
   }
+
+  private getVideosObs () {
+    const params = {
+      search: this.currentSearch,
+      componentPagination: this.pagination,
+      advancedSearch: this.advancedSearch
+    }
+
+    return this.hooks.wrapObsFun(
+      this.searchService.searchVideos.bind(this.searchService),
+      params,
+      'common',
+      'filter:api.search.videos.list.params',
+      'filter:api.search.videos.list.result'
+    )
+  }
+
+  private getVideoChannelObs () {
+    const params = {
+      search: this.currentSearch,
+      componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage })
+    }
+
+    return this.hooks.wrapObsFun(
+      this.searchService.searchVideoChannels.bind(this.searchService),
+      params,
+      'common',
+      'filter:api.search.video-channels.list.params',
+      'filter:api.search.video-channels.list.result'
+    )
+  }
 }
index cd3bdad356443812c0e5429192c16bc80d39d58a..8f137a321b222dd7af0f84f64821e3b4d8282ea5 100644 (file)
@@ -23,13 +23,14 @@ export class SearchService {
     private videoService: VideoService
   ) {}
 
-  searchVideos (
+  searchVideos (parameters: {
     search: string,
     componentPagination: ComponentPagination,
     advancedSearch: AdvancedSearch
-  ): Observable<{ videos: Video[], totalVideos: number }> {
-    const url = SearchService.BASE_SEARCH_URL + 'videos'
+  }): Observable<ResultList<Video>> {
+    const { search, componentPagination, advancedSearch } = parameters
 
+    const url = SearchService.BASE_SEARCH_URL + 'videos'
     const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
 
     let params = new HttpParams()
@@ -48,12 +49,13 @@ export class SearchService {
                )
   }
 
-  searchVideoChannels (
+  searchVideoChannels (parameters: {
     search: string,
     componentPagination: ComponentPagination
-  ): Observable<{ data: VideoChannel[], total: number }> {
-    const url = SearchService.BASE_SEARCH_URL + 'video-channels'
+  }): Observable<ResultList<VideoChannel>> {
+    const { search, componentPagination } = parameters
 
+    const url = SearchService.BASE_SEARCH_URL + 'video-channels'
     const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
 
     let params = new HttpParams()
index 98dba2d9767cccb843cf964026f3d50300b2d0d9..bd4068925486b2d6bfa40fccd38090a34b954b04 100644 (file)
@@ -45,7 +45,7 @@ export class OverviewService {
           of(object.videos)
             .pipe(
               switchMap(videos => this.videosService.extractVideos({ total: 0, data: videos })),
-              map(result => result.videos),
+              map(result => result.data),
               tap(videos => {
                 videosOverviewResult[key].push(immutableAssign(object, { videos }))
               })
index cf4b5ef8eb85e78050c021b3eedefa4402ecf405..8a247a9af726c87fac3c3970216c355ba2ff4063 100644 (file)
@@ -13,6 +13,7 @@ import { Notifier, ServerService } from '@app/core'
 import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date'
+import { ResultList } from '@shared/models'
 
 enum GroupDate {
   UNKNOWN = 0,
@@ -73,7 +74,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
   private groupedDateLabels: { [id in GroupDate]: string }
   private groupedDates: { [id: number]: GroupDate } = {}
 
-  abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }>
+  abstract getVideosObservable (page: number): Observable<ResultList<Video>>
 
   abstract generateSyndicationList (): void
 
@@ -138,12 +139,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
   }
 
   loadMoreVideos () {
-    const observable = this.getVideosObservable(this.pagination.currentPage)
-
-    observable.subscribe(
-      ({ videos, totalVideos }) => {
-        this.pagination.totalItems = totalVideos
-        this.videos = this.videos.concat(videos)
+    this.getVideosObservable(this.pagination.currentPage).subscribe(
+      ({ data, total }) => {
+        this.pagination.totalItems = total
+        this.videos = this.videos.concat(data)
 
         if (this.groupByDate) this.buildGroupedDateLabels()
 
index 871bc9e46455ccf7d47af6862c1b9b373ed0b290..d1af13c93c6cde3f14a063619f00762ca52fbbe4 100644 (file)
@@ -41,7 +41,7 @@ export interface VideosProvider {
     filter?: VideoFilter,
     categoryOneOf?: number,
     languageOneOf?: string[]
-  }): Observable<{ videos: Video[], totalVideos: number }>
+  }): Observable<ResultList<Video>>
 }
 
 @Injectable()
@@ -65,11 +65,11 @@ export class VideoService implements VideosProvider {
     return VideoService.BASE_VIDEO_URL + uuid + '/watching'
   }
 
-  getVideo (uuid: string): Observable<VideoDetails> {
+  getVideo (options: { videoId: string }): Observable<VideoDetails> {
     return this.serverService.localeObservable
                .pipe(
                  switchMap(translations => {
-                   return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid)
+                   return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + options.videoId)
                               .pipe(map(videoHash => ({ videoHash, translations })))
                  }),
                  map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
@@ -123,7 +123,7 @@ export class VideoService implements VideosProvider {
                .pipe(catchError(err => this.restExtractor.handleError(err)))
   }
 
-  getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> {
+  getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<ResultList<Video>> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -141,7 +141,7 @@ export class VideoService implements VideosProvider {
     account: Account,
     videoPagination: ComponentPagination,
     sort: VideoSortField
-  ): Observable<{ videos: Video[], totalVideos: number }> {
+  ): Observable<ResultList<Video>> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -159,7 +159,7 @@ export class VideoService implements VideosProvider {
     videoChannel: VideoChannel,
     videoPagination: ComponentPagination,
     sort: VideoSortField
-  ): Observable<{ videos: Video[], totalVideos: number }> {
+  ): Observable<ResultList<Video>> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -176,7 +176,7 @@ export class VideoService implements VideosProvider {
   getPlaylistVideos (
     videoPlaylistId: number | string,
     videoPagination: ComponentPagination
-  ): Observable<{ videos: Video[], totalVideos: number }> {
+  ): Observable<ResultList<Video>> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -190,10 +190,11 @@ export class VideoService implements VideosProvider {
                )
   }
 
-  getUserSubscriptionVideos (
+  getUserSubscriptionVideos (parameters: {
     videoPagination: ComponentPagination,
     sort: VideoSortField
-  ): Observable<{ videos: Video[], totalVideos: number }> {
+  }): Observable<ResultList<Video>> {
+    const { videoPagination, sort } = parameters
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -213,7 +214,7 @@ export class VideoService implements VideosProvider {
     filter?: VideoFilter,
     categoryOneOf?: number,
     languageOneOf?: string[]
-  }): Observable<{ videos: Video[], totalVideos: number }> {
+  }): Observable<ResultList<Video>> {
     const { videoPagination, sort, filter, categoryOneOf, languageOneOf } = parameters
 
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
@@ -344,7 +345,7 @@ export class VideoService implements VideosProvider {
                      videos.push(new Video(videoJson, translations))
                    }
 
-                   return { videos, totalVideos }
+                   return { total: totalVideos, data: videos }
                  })
                )
   }
index d69f7b70e3208c977417a458fa27a59bf4d75c69..994e0fa1ec0774ec1322386f3a0fc17c318a76d8 100644 (file)
@@ -21,6 +21,7 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
 import { VideoSortField } from '@app/shared/video/sort-field.type'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { ResultList } from '@shared/models'
 
 export type SelectionType = { [ id: number ]: boolean }
 
@@ -33,7 +34,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
   @Input() pagination: ComponentPagination
   @Input() titlePage: string
   @Input() miniatureDisplayOptions: MiniatureDisplayOptions
-  @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<{ videos: Video[], totalVideos: number }>
+  @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
   @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective>
 
   @Output() selectionChange = new EventEmitter<SelectionType>()
index 384458127e79eb694baec3ff0b77d20e8ae0a648..4ac517d961e613edaf692be3b03487972708d17b 100644 (file)
@@ -18,7 +18,7 @@ export class VideoUpdateResolver implements Resolve<any> {
   resolve (route: ActivatedRouteSnapshot) {
     const uuid: string = route.params[ 'uuid' ]
 
-    return this.videoService.getVideo(uuid)
+    return this.videoService.getVideo({ videoId: uuid })
                .pipe(
                  switchMap(video => {
                    return forkJoin([
index b8e5878c5b9c8391dedd419b70984e5eb5d222b6..eb608a1a36a9cec2ddfe57d03dee0642af3c23e7 100644 (file)
@@ -48,11 +48,13 @@ export class VideoCommentService {
                )
   }
 
-  getVideoCommentThreads (
+  getVideoCommentThreads (parameters: {
     videoId: number | string,
     componentPagination: ComponentPagination,
     sort: VideoSortField
-  ): Observable<{ comments: VideoComment[], totalComments: number}> {
+  }): Observable<{ comments: VideoComment[], totalComments: number}> {
+    const { videoId, componentPagination, sort } = parameters
+
     const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
 
     let params = new HttpParams()
@@ -67,7 +69,11 @@ export class VideoCommentService {
                )
   }
 
-  getVideoThreadComments (videoId: number | string, threadId: number): Observable<VideoCommentThreadTree> {
+  getVideoThreadComments (parameters: {
+    videoId: number | string,
+    threadId: number
+  }): Observable<VideoCommentThreadTree> {
+    const { videoId, threadId } = parameters
     const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}`
 
     return this.authHttp
index 3acddbe6ac25338276408ec8d05ecf7cb55afcd2..3c1a0986c7833f8fa64ea98a7f25d311fe3b7c9b 100644 (file)
@@ -12,6 +12,7 @@ import { VideoComment } from './video-comment.model'
 import { VideoCommentService } from './video-comment.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { Syndication } from '@app/shared/video/syndication.model'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-video-comments',
@@ -45,7 +46,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
     private confirmService: ConfirmService,
     private videoCommentService: VideoCommentService,
     private activatedRoute: ActivatedRoute,
-    private i18n: I18n
+    private i18n: I18n,
+    private hooks: HooksService
   ) {}
 
   ngOnInit () {
@@ -73,8 +75,20 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
   viewReplies (commentId: number, highlightThread = false) {
     this.threadLoading[commentId] = true
 
-    this.videoCommentService.getVideoThreadComments(this.video.id, commentId)
-      .subscribe(
+    const params = {
+      videoId: this.video.id,
+      threadId: commentId
+    }
+
+    const obs = this.hooks.wrapObsFun(
+      this.videoCommentService.getVideoThreadComments.bind(this.videoCommentService),
+      params,
+      'video-watch',
+      'filter:api.video-watch.video-thread-replies.list.params',
+      'filter:api.video-watch.video-thread-replies.list.result'
+    )
+
+    obs.subscribe(
         res => {
           this.threadComments[commentId] = res
           this.threadLoading[commentId] = false
@@ -91,16 +105,29 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
       )
   }
 
-  loadMoreComments () {
-    this.videoCommentService.getVideoCommentThreads(this.video.id, this.componentPagination, this.sort)
-      .subscribe(
-        res => {
-          this.comments = this.comments.concat(res.comments)
-          this.componentPagination.totalItems = res.totalComments
-        },
+  loadMoreThreads () {
+    const params = {
+      videoId: this.video.id,
+      componentPagination: this.componentPagination,
+      sort: this.sort
+    }
 
-        err => this.notifier.error(err.message)
-      )
+    const obs = this.hooks.wrapObsFun(
+      this.videoCommentService.getVideoCommentThreads.bind(this.videoCommentService),
+      params,
+      'video-watch',
+      'filter:api.video-watch.video-threads.list.params',
+      'filter:api.video-watch.video-threads.list.result'
+    )
+
+    obs.subscribe(
+      res => {
+        this.comments = this.comments.concat(res.comments)
+        this.componentPagination.totalItems = res.totalComments
+      },
+
+      err => this.notifier.error(err.message)
+    )
   }
 
   onCommentThreadCreated (comment: VideoComment) {
@@ -169,7 +196,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
     this.componentPagination.currentPage++
 
     if (hasMoreItems(this.componentPagination)) {
-      this.loadMoreComments()
+      this.loadMoreThreads()
     }
   }
 
@@ -197,7 +224,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
 
       this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid)
 
-      this.loadMoreComments()
+      this.loadMoreThreads()
     }
   }
 
index bccdaf7b20d779feaf993b0ba02277f2598fa993..2fb0cb0e565c6824265e0f28d7d00cec0ef9d17d 100644 (file)
@@ -66,11 +66,11 @@ export class VideoWatchPlaylistComponent {
 
   loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
     this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination)
-        .subscribe(({ totalVideos, videos }) => {
-          this.playlistVideos = this.playlistVideos.concat(videos)
-          this.playlistPagination.totalItems = totalVideos
+        .subscribe(({ total, data }) => {
+          this.playlistVideos = this.playlistVideos.concat(data)
+          this.playlistPagination.totalItems = total
 
-          if (totalVideos === 0) {
+          if (total === 0) {
             this.noPlaylistVideos = true
             return
           }
index 6d8bb4b3f1403c12f0eacda0ba571888e20e831f..eed2ec0486546d3a9a2ad56f591e9cc5cc20df21 100644 (file)
@@ -33,6 +33,7 @@ import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
 import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
 import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
 import { PluginService } from '@app/core/plugins/plugin.service'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-video-watch',
@@ -93,6 +94,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     private videoCaptionService: VideoCaptionService,
     private i18n: I18n,
     private hotkeysService: HotkeysService,
+    private hooks: HooksService,
     @Inject(LOCALE_ID) private localeId: string
   ) {}
 
@@ -131,7 +133,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
     this.theaterEnabled = getStoredTheater()
 
-    this.pluginService.runHook('action:video-watch.loaded')
+    this.hooks.runAction('action:video-watch.init')
   }
 
   ngOnDestroy () {
@@ -246,9 +248,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
     if (this.player) this.player.pause()
 
+    const videoObs = this.hooks.wrapObsFun(
+      this.videoService.getVideo.bind(this.videoService),
+      { videoId },
+      'video-watch',
+      'filter:api.video-watch.video.get.params',
+      'filter:api.video-watch.video.get.result'
+    )
+
     // Video did change
     forkJoin(
-      this.videoService.getVideo(videoId),
+      videoObs,
       this.videoCaptionService.listCaptions(videoId)
     )
       .pipe(
@@ -486,6 +496,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
     this.setOpenGraphTags()
     this.checkUserRating()
+
+    this.hooks.runAction('action:video-watch.video.loaded')
   }
 
   private setRating (nextRating: UserVideoRateType) {
index f975ff6ef1abf070729c9b592b70e7a78d386584..a1e65c27cddd99d93bd5cda764482b0fdffca53d 100644 (file)
@@ -33,20 +33,24 @@ export class RecentVideosRecommendationService implements RecommendationService
   private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
     const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
     const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' })
-                                    .pipe(map(v => v.videos))
+                                    .pipe(map(v => v.data))
 
     if (!recommendation.tags || recommendation.tags.length === 0) return defaultSubscription
 
-    return this.searchService.searchVideos('',
-      pagination,
-      new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' })
-    ).pipe(
-      map(v => v.videos),
-      switchMap(videos => {
-        if (videos.length <= 1) return defaultSubscription
-
-        return of(videos)
-      })
-    )
+    const params = {
+      search: '',
+      componentPagination: pagination,
+      advancedSearch: new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' })
+    }
+
+    return this.searchService.searchVideos(params)
+               .pipe(
+                 map(v => v.data),
+                 switchMap(videos => {
+                   if (videos.length <= 1) return defaultSubscription
+
+                   return of(videos)
+                 })
+               )
   }
 }
index 5de4a13afdb175b03c396ba559fac8d4ad005dee..81b6ce49334444a3a584847727ff791e2d8ada86 100644 (file)
@@ -10,6 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { UserRight } from '../../../../../shared/models/users'
 import { Notifier, ServerService } from '@app/core'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-videos-local',
@@ -31,7 +32,8 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
     protected notifier: Notifier,
     protected authService: AuthService,
     protected screenService: ScreenService,
-    private videoService: VideoService
+    private videoService: VideoService,
+    private hooks: HooksService
   ) {
     super()
 
@@ -55,14 +57,21 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
 
   getVideosObservable (page: number) {
     const newPagination = immutableAssign(this.pagination, { currentPage: page })
-
-    return this.videoService.getVideos({
+    const params = {
       videoPagination: newPagination,
       sort: this.sort,
       filter: this.filter,
       categoryOneOf: this.categoryOneOf,
       languageOneOf: this.languageOneOf
-    })
+    }
+
+    return this.hooks.wrapObsFun(
+      this.videoService.getVideos.bind(this.videoService),
+      params,
+      'common',
+      'filter:api.videos.list.local.params',
+      'filter:api.videos.list.local.result'
+    )
   }
 
   generateSyndicationList () {
index 19522e6b4d5cdb51575882e0a41cf34c8ed7c960..638e7caed24611764801061fa5d7a1061e21d552 100644 (file)
@@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { Notifier, ServerService } from '@app/core'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-videos-recently-added',
@@ -29,7 +30,8 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
     protected notifier: Notifier,
     protected authService: AuthService,
     protected screenService: ScreenService,
-    private videoService: VideoService
+    private videoService: VideoService,
+    private hooks: HooksService
   ) {
     super()
 
@@ -48,14 +50,20 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
 
   getVideosObservable (page: number) {
     const newPagination = immutableAssign(this.pagination, { currentPage: page })
-
-    return this.videoService.getVideos({
+    const params = {
       videoPagination: newPagination,
       sort: this.sort,
-      filter: undefined,
       categoryOneOf: this.categoryOneOf,
       languageOneOf: this.languageOneOf
-    })
+    }
+
+    return this.hooks.wrapObsFun(
+      this.videoService.getVideos.bind(this.videoService),
+      params,
+      'common',
+      'filter:api.videos.list.recently-added.params',
+      'filter:api.videos.list.recently-added.result'
+    )
   }
 
   generateSyndicationList () {
index 5f1d5055b6c91dc10840644de7bbed23e494852b..0e69bfd645e23cc0fd981717f246f305ea8f1679 100644 (file)
@@ -8,6 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { Notifier, ServerService } from '@app/core'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-videos-trending',
@@ -28,7 +29,8 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
     protected notifier: Notifier,
     protected authService: AuthService,
     protected screenService: ScreenService,
-    private videoService: VideoService
+    private videoService: VideoService,
+    private hooks: HooksService
   ) {
     super()
   }
@@ -61,13 +63,20 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
 
   getVideosObservable (page: number) {
     const newPagination = immutableAssign(this.pagination, { currentPage: page })
-    return this.videoService.getVideos({
+    const params = {
       videoPagination: newPagination,
       sort: this.sort,
-      filter: undefined,
       categoryOneOf: this.categoryOneOf,
       languageOneOf: this.languageOneOf
-    })
+    }
+
+    return this.hooks.wrapObsFun(
+      this.videoService.getVideos.bind(this.videoService),
+      params,
+      'common',
+      'filter:api.videos.list.trending.params',
+      'filter:api.videos.list.trending.result'
+    )
   }
 
   generateSyndicationList () {
index 3caa371d8886562d5ee67609c3bf0a60b1d849d1..ac325aeff1f25829e158990141e77f45deb846c6 100644 (file)
@@ -9,6 +9,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
 import { Notifier, ServerService } from '@app/core'
+import { HooksService } from '@app/core/plugins/hooks.service'
 
 @Component({
   selector: 'my-videos-user-subscriptions',
@@ -29,7 +30,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
     protected notifier: Notifier,
     protected authService: AuthService,
     protected screenService: ScreenService,
-    private videoService: VideoService
+    private videoService: VideoService,
+    private hooks: HooksService
   ) {
     super()
 
@@ -46,8 +48,18 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
 
   getVideosObservable (page: number) {
     const newPagination = immutableAssign(this.pagination, { currentPage: page })
+    const params = {
+      videoPagination: newPagination,
+      sort: this.sort
+    }
 
-    return this.videoService.getUserSubscriptionVideos(newPagination, this.sort)
+    return this.hooks.wrapObsFun(
+      this.videoService.getUserSubscriptionVideos.bind(this.videoService),
+      params,
+      'common',
+      'filter:api.videos.list.user-subscriptions.params',
+      'filter:api.videos.list.user-subscriptions.result'
+    )
   }
 
   generateSyndicationList () {
index e46528d1c9117717cd9c5bc8d304ec88d2aa64b7..ac9a3522d536a23f33768d9453261aa44bdaab12 100644 (file)
@@ -12,6 +12,7 @@
     "noImplicitThis": true,
     "suppressImplicitAnyIndexErrors":true,
     "alwaysStrict": true,
+    "strictBindCallApply": true,
     "target": "es5",
     "typeRoots": [
       "node_modules/@types"
index 3d59a7428d492901fd044b2a7bf1561dad24ef1f..5405e0529ad0eeab00bc08ed6cee44d84a8b5ef6 100644 (file)
@@ -29,7 +29,7 @@ async function internalRunHook <T> (handler: Function, hookType: HookType, resul
     }
 
     if (hookType === HookType.ACTION) {
-      if (isCatchable(p)) p.catch(err => onError(err))
+      if (isCatchable(p)) p.catch((err: any) => onError(err))
 
       return undefined
     }
diff --git a/shared/models/plugins/client-hook.model.ts b/shared/models/plugins/client-hook.model.ts
new file mode 100644 (file)
index 0000000..8940000
--- /dev/null
@@ -0,0 +1,39 @@
+export type ClientFilterHookName =
+  'filter:api.videos.list.trending.params' |
+  'filter:api.videos.list.trending.result' |
+
+  'filter:api.videos.list.local.params' |
+  'filter:api.videos.list.local.result' |
+
+  'filter:api.videos.list.recently-added.params' |
+  'filter:api.videos.list.recently-added.result' |
+
+  'filter:api.videos.list.user-subscriptions.params' |
+  'filter:api.videos.list.user-subscriptions.result' |
+
+  'filter:api.video-watch.video.get.params' |
+  'filter:api.video-watch.video.get.result' |
+
+  'filter:api.video-watch.video-threads.list.params' |
+  'filter:api.video-watch.video-threads.list.result' |
+
+  'filter:api.video-watch.video-thread-replies.list.params' |
+  'filter:api.video-watch.video-thread-replies.list.result' |
+
+  'filter:api.search.videos.list.params' |
+  'filter:api.search.videos.list.result' |
+  'filter:api.search.video-channels.list.params' |
+  'filter:api.search.video-channels.list.result'
+
+export type ClientActionHookName =
+  'action:application.init' |
+
+  'action:video-watch.init' |
+
+  'action:video-watch.video.loaded'
+
+export type ClientHookName = ClientActionHookName | ClientFilterHookName
+
+export interface ClientHook {
+  runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T>
+}
diff --git a/shared/models/plugins/plugin-client-scope.type.ts b/shared/models/plugins/plugin-client-scope.type.ts
new file mode 100644 (file)
index 0000000..a2112ee
--- /dev/null
@@ -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 (file)
index b63ae43..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export type PluginScope = 'common' | 'video-watch'
index a7f88f3c40f377ef9dc70c6881b4a0410488bdb9..6729e2dabab4f581be6bb4b90366835e8ef634a9 100644 (file)
@@ -30,5 +30,5 @@ export type ServerActionHookName =
 export type ServerHookName = ServerFilterHookName | ServerActionHookName
 
 export interface ServerHook {
-  runHook (hookName: ServerHookName, params?: any)
+  runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T>
 }
index 3498f86d77be9db4439a05d042baf51c8db8d67c..49bb01708cbae88876d9a7d7d5e549741044869c 100644 (file)
@@ -1,11 +1,12 @@
 import { NSFWPolicyType } from '../videos/nsfw-policy.type'
 import { ClientScript } from '../plugins/plugin-package-json.model'
+import { PluginClientScope } from '../plugins/plugin-scope.type'
 
 export interface ServerConfigPlugin {
   name: string
   version: string
   description: string
-  clientScripts: { [name: string]: ClientScript }
+  clientScripts: { [name in PluginClientScope]: ClientScript }
 }
 
 export interface ServerConfigTheme extends ServerConfigPlugin {