diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts index 1696e6709..98e45ffc0 100644 --- a/client/src/app/core/rest/rest.service.ts +++ b/client/src/app/core/rest/rest.service.ts @@ -44,13 +44,21 @@ export class RestService { return newParams } + addArrayParams (params: HttpParams, name: string, values: (string | number)[]) { + for (const v of values) { + params = params.append(name, v) + } + + return params + } + addObjectParams (params: HttpParams, object: { [ name: string ]: any }) { for (const name of Object.keys(object)) { const value = object[name] if (value === undefined || value === null) continue if (Array.isArray(value)) { - for (const v of value) params = params.append(name, v) + params = this.addArrayParams(params, name, value) } else { params = params.append(name, value) } diff --git a/client/src/app/helpers/index.ts b/client/src/app/helpers/index.ts index cc61255ba..beff749ec 100644 --- a/client/src/app/helpers/index.ts +++ b/client/src/app/helpers/index.ts @@ -1,5 +1,6 @@ export * from './locales' export * from './constants' export * from './i18n-utils' +export * from './rxjs' export * from './utils' export * from './zone' diff --git a/client/src/app/helpers/rxjs.ts b/client/src/app/helpers/rxjs.ts new file mode 100644 index 000000000..eb051f868 --- /dev/null +++ b/client/src/app/helpers/rxjs.ts @@ -0,0 +1,29 @@ +import { uniq } from 'lodash-es' +import { asyncScheduler, Observable } from 'rxjs' +import { bufferTime, distinctUntilChanged, filter, map, observeOn, share, switchMap } from 'rxjs/operators' +import { NgZone } from '@angular/core' +import { enterZone, leaveZone } from './zone' + +function buildBulkObservable (options: { + ngZone: NgZone + notifierObservable: Observable + time: number + bulkGet: (params: T[]) => Observable +}) { + const { ngZone, notifierObservable, time, bulkGet } = options + + return notifierObservable.pipe( + distinctUntilChanged(), + // We leave Angular zone so Protractor does not get stuck + bufferTime(time, leaveZone(ngZone, asyncScheduler)), + filter(params => params.length !== 0), + map(params => uniq(params)), + observeOn(enterZone(ngZone, asyncScheduler)), + switchMap(params => bulkGet(params)), + share() + ) +} + +export { + buildBulkObservable +} diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts index 231e52d0a..089728a51 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts +++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts @@ -65,15 +65,15 @@ export class CustomMarkupService { for (const selector of Object.keys(this.htmlBuilders)) { rootElement.querySelectorAll(selector) - .forEach((e: HTMLElement) => { - try { - const element = this.execHTMLBuilder(selector, e) - // Insert as first child - e.insertBefore(element, e.firstChild) - } catch (err) { - console.error('Cannot inject component %s.', selector, err) - } - }) + .forEach((e: HTMLElement) => { + try { + const element = this.execHTMLBuilder(selector, e) + // Insert as first child + e.insertBefore(element, e.firstChild) + } catch (err) { + console.error('Cannot inject component %s.', selector, err) + } + }) } const loadedPromises: Promise[] = [] diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts index 7043a7ec9..bb099deae 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts @@ -2,8 +2,9 @@ import { from } from 'rxjs' import { finalize, map, switchMap, tap } from 'rxjs/operators' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { MarkdownService, Notifier, UserService } from '@app/core' +import { FindInBulkService } from '@app/shared/shared-search' import { Video, VideoSortField } from '@shared/models/videos' -import { VideoChannel, VideoChannelService, VideoService } from '../../shared-main' +import { VideoChannel, VideoService } from '../../shared-main' import { CustomMarkupComponent } from './shared' /* @@ -29,14 +30,14 @@ export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, O constructor ( private markdown: MarkdownService, - private channelService: VideoChannelService, + private findInBulk: FindInBulkService, private videoService: VideoService, private userService: UserService, private notifier: Notifier ) { } ngOnInit () { - this.channelService.getVideoChannel(this.name) + this.findInBulk.getChannel(this.name) .pipe( tap(channel => this.channel = channel), switchMap(() => from(this.markdown.textMarkdownToHTML(this.channel.description))), diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts index ff8cc01db..97d31c4a7 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts @@ -1,8 +1,9 @@ import { finalize } from 'rxjs/operators' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { Notifier } from '@app/core' +import { FindInBulkService } from '@app/shared/shared-search' import { MiniatureDisplayOptions } from '../../shared-video-miniature' -import { VideoPlaylist, VideoPlaylistService } from '../../shared-video-playlist' +import { VideoPlaylist } from '../../shared-video-playlist' import { CustomMarkupComponent } from './shared' /* @@ -33,12 +34,12 @@ export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, } constructor ( - private playlistService: VideoPlaylistService, + private findInBulkService: FindInBulkService, private notifier: Notifier ) { } ngOnInit () { - this.playlistService.getVideoPlaylist(this.uuid) + this.findInBulkService.getPlaylist(this.uuid) .pipe(finalize(() => this.loaded.emit(true))) .subscribe( playlist => this.playlist = playlist, diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts index 47518abfd..ba61aaf51 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts @@ -4,6 +4,7 @@ import { AuthService, Notifier } from '@app/core' import { Video, VideoService } from '../../shared-main' import { MiniatureDisplayOptions } from '../../shared-video-miniature' import { CustomMarkupComponent } from './shared' +import { FindInBulkService } from '@app/shared/shared-search' /* * Markup component that creates a video miniature only @@ -35,7 +36,7 @@ export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnI constructor ( private auth: AuthService, - private videoService: VideoService, + private findInBulk: FindInBulkService, private notifier: Notifier ) { } @@ -50,7 +51,7 @@ export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnI } } - this.videoService.getVideo({ videoId: this.uuid }) + this.findInBulk.getVideo(this.uuid) .pipe(finalize(() => this.loaded.emit(true))) .subscribe( video => this.video = video, diff --git a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts index dccd64709..27e976d13 100644 --- a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts +++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core' import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' import { SharedGlobalIconModule } from '../shared-icons' import { SharedMainModule } from '../shared-main' +import { SharedSearchModule } from '../shared-search' import { SharedVideoMiniatureModule } from '../shared-video-miniature' import { SharedVideoPlaylistModule } from '../shared-video-playlist' import { CustomMarkupContainerComponent } from './custom-markup-container.component' @@ -26,7 +27,8 @@ import { SharedGlobalIconModule, SharedVideoMiniatureModule, SharedVideoPlaylistModule, - SharedActorImageModule + SharedActorImageModule, + SharedSearchModule ], declarations: [ diff --git a/client/src/app/shared/shared-search/find-in-bulk.service.ts b/client/src/app/shared/shared-search/find-in-bulk.service.ts new file mode 100644 index 000000000..0383d8648 --- /dev/null +++ b/client/src/app/shared/shared-search/find-in-bulk.service.ts @@ -0,0 +1,118 @@ +import * as debug from 'debug' +import { Observable, Subject } from 'rxjs' +import { map } from 'rxjs/operators' +import { Injectable, NgZone } from '@angular/core' +import { buildBulkObservable } from '@app/helpers' +import { ResultList } from '@shared/models/common' +import { Video, VideoChannel } from '../shared-main' +import { VideoPlaylist } from '../shared-video-playlist' +import { SearchService } from './search.service' + +const logger = debug('peertube:search:FindInBulkService') + +type BulkObservables

= { + notifier: Subject

+ result: Observable +} + +@Injectable() +export class FindInBulkService { + + private getVideoInBulk: BulkObservables> + private getChannelInBulk: BulkObservables> + private getPlaylistInBulk: BulkObservables> + + constructor ( + private searchService: SearchService, + private ngZone: NgZone + ) { + this.getVideoInBulk = this.buildBulkObservableObject(this.getVideosInBulk.bind(this)) + this.getChannelInBulk = this.buildBulkObservableObject(this.getChannelsInBulk.bind(this)) + this.getPlaylistInBulk = this.buildBulkObservableObject(this.getPlaylistsInBulk.bind(this)) + } + + getVideo (uuid: string): Observable

(options: { + observableObject: BulkObservables> + param: P + finder: (d: R) => boolean + }) { + const { observableObject, param, finder } = options + + return new Observable(obs => { + observableObject.result + .pipe( + map(({ data }) => data), + map(data => data.find(finder)) + ) + .subscribe(result => { + obs.next(result) + obs.complete() + }) + + observableObject.notifier.next(param) + }) + } + + private getVideosInBulk (uuids: string[]) { + logger('Fetching videos %s.', uuids.join(', ')) + + return this.searchService.searchVideos({ uuids }) + } + + private getChannelsInBulk (handles: string[]) { + logger('Fetching channels %s.', handles.join(', ')) + + return this.searchService.searchVideoChannels({ handles }) + } + + private getPlaylistsInBulk (uuids: string[]) { + logger('Fetching playlists %s.', uuids.join(', ')) + + return this.searchService.searchVideoPlaylists({ uuids }) + } + + private buildBulkObservableObject (bulkGet: (params: T[]) => Observable) { + const notifier = new Subject() + + return { + notifier, + + result: buildBulkObservable({ + time: 500, + bulkGet, + ngZone: this.ngZone, + notifierObservable: notifier.asObservable() + }) + } + } +} diff --git a/client/src/app/shared/shared-search/index.ts b/client/src/app/shared/shared-search/index.ts index f687f6767..0235893c4 100644 --- a/client/src/app/shared/shared-search/index.ts +++ b/client/src/app/shared/shared-search/index.ts @@ -1,3 +1,4 @@ export * from './advanced-search.model' +export * from './find-in-bulk.service' export * from './search.service' export * from './shared-search.module' diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts index a1603da98..fdfab0e0e 100644 --- a/client/src/app/shared/shared-search/search.service.ts +++ b/client/src/app/shared/shared-search/search.service.ts @@ -32,11 +32,12 @@ export class SearchService { } searchVideos (parameters: { - search: string + search?: string componentPagination?: ComponentPaginationLight advancedSearch?: AdvancedSearch + uuids?: string[] }): Observable> { - const { search, componentPagination, advancedSearch } = parameters + const { search, uuids, componentPagination, advancedSearch } = parameters const url = SearchService.BASE_SEARCH_URL + 'videos' let pagination: RestPagination @@ -49,6 +50,7 @@ export class SearchService { params = this.restService.addRestGetParams(params, pagination) if (search) params = params.append('search', search) + if (uuids) params = this.restService.addArrayParams(params, 'uuids', uuids) if (advancedSearch) { const advancedSearchObject = advancedSearch.toVideosAPIObject() @@ -64,11 +66,12 @@ export class SearchService { } searchVideoChannels (parameters: { - search: string + search?: string advancedSearch?: AdvancedSearch componentPagination?: ComponentPaginationLight + handles?: string[] }): Observable> { - const { search, advancedSearch, componentPagination } = parameters + const { search, advancedSearch, componentPagination, handles } = parameters const url = SearchService.BASE_SEARCH_URL + 'video-channels' @@ -81,6 +84,7 @@ export class SearchService { params = this.restService.addRestGetParams(params, pagination) if (search) params = params.append('search', search) + if (handles) params = this.restService.addArrayParams(params, 'handles', handles) if (advancedSearch) { const advancedSearchObject = advancedSearch.toChannelAPIObject() @@ -96,11 +100,12 @@ export class SearchService { } searchVideoPlaylists (parameters: { - search: string + search?: string advancedSearch?: AdvancedSearch componentPagination?: ComponentPaginationLight + uuids?: string[] }): Observable> { - const { search, advancedSearch, componentPagination } = parameters + const { search, advancedSearch, componentPagination, uuids } = parameters const url = SearchService.BASE_SEARCH_URL + 'video-playlists' @@ -113,6 +118,7 @@ export class SearchService { params = this.restService.addRestGetParams(params, pagination) if (search) params = params.append('search', search) + if (uuids) params = this.restService.addArrayParams(params, 'uuids', uuids) if (advancedSearch) { const advancedSearchObject = advancedSearch.toPlaylistAPIObject() diff --git a/client/src/app/shared/shared-search/shared-search.module.ts b/client/src/app/shared/shared-search/shared-search.module.ts index be4ef5e3f..8b5492400 100644 --- a/client/src/app/shared/shared-search/shared-search.module.ts +++ b/client/src/app/shared/shared-search/shared-search.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core' import { SharedMainModule } from '../shared-main' import { SharedVideoPlaylistModule } from '../shared-video-playlist' +import { FindInBulkService } from './find-in-bulk.service' import { SearchService } from './search.service' @NgModule({ @@ -16,6 +17,7 @@ import { SearchService } from './search.service' ], providers: [ + FindInBulkService, SearchService ] }) diff --git a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts index eb1fdf91c..bb44660d2 100644 --- a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts +++ b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts @@ -1,11 +1,10 @@ import * as debug from 'debug' -import { uniq } from 'lodash-es' -import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' -import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators' +import { merge, Observable, of, ReplaySubject, Subject } from 'rxjs' +import { catchError, filter, map, switchMap, tap } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable, NgZone } from '@angular/core' import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' -import { enterZone, leaveZone } from '@app/helpers' +import { buildBulkObservable } from '@app/helpers' import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' import { ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models' import { environment } from '../../../environments/environment' @@ -35,15 +34,12 @@ export class UserSubscriptionService { private ngZone: NgZone ) { this.existsObservable = merge( - this.existsSubject.pipe( - // We leave Angular zone so Protractor does not get stuck - bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), - filter(uris => uris.length !== 0), - map(uris => uniq(uris)), - observeOn(enterZone(this.ngZone, asyncScheduler)), - switchMap(uris => this.doSubscriptionsExist(uris)), - share() - ), + buildBulkObservable({ + time: 500, + ngZone: this.ngZone, + notifierObservable: this.existsSubject, + bulkGet: this.doSubscriptionsExist.bind(this) + }), this.myAccountSubscriptionCacheSubject ) diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index 1b87e0b2a..a3f1393ff 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts @@ -1,11 +1,10 @@ import * as debug from 'debug' -import { uniq } from 'lodash-es' -import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' -import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap, distinctUntilChanged } from 'rxjs/operators' +import { merge, Observable, of, ReplaySubject, Subject } from 'rxjs' +import { catchError, filter, map, share, switchMap, tap } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable, NgZone } from '@angular/core' import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' -import { enterZone, leaveZone, objectToFormData } from '@app/helpers' +import { buildBulkObservable, objectToFormData } from '@app/helpers' import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main' import { ResultList, @@ -52,16 +51,12 @@ export class VideoPlaylistService { private ngZone: NgZone ) { this.videoExistsInPlaylistObservable = merge( - this.videoExistsInPlaylistNotifier.pipe( - distinctUntilChanged(), - // We leave Angular zone so Protractor does not get stuck - bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), - filter(videoIds => videoIds.length !== 0), - map(videoIds => uniq(videoIds)), - observeOn(enterZone(this.ngZone, asyncScheduler)), - switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)), - share() - ), + buildBulkObservable({ + time: 500, + ngZone: this.ngZone, + bulkGet: this.doVideosExistInPlaylist.bind(this), + notifierObservable: this.videoExistsInPlaylistNotifier + }), this.videoExistsInPlaylistCacheSubject )