From 5fb2e2888ce032c638e4b75d07458642f0833e52 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 29 May 2020 16:16:24 +0200 Subject: First implem global search --- client/src/app/search/advanced-search.model.ts | 21 ++++- .../src/app/search/channel-lazy-load.resolver.ts | 45 ++++++++++ .../src/app/search/search-filters.component.html | 64 ++++++++------ client/src/app/search/search-filters.component.ts | 8 +- client/src/app/search/search-routing.module.ts | 20 ++++- client/src/app/search/search.component.html | 15 ++-- client/src/app/search/search.component.ts | 98 +++++++++++++++------- client/src/app/search/search.module.ts | 14 ++-- client/src/app/search/search.service.ts | 48 +++++++---- client/src/app/search/video-lazy-load.resolver.ts | 43 ++++++++++ 10 files changed, 290 insertions(+), 86 deletions(-) create mode 100644 client/src/app/search/channel-lazy-load.resolver.ts create mode 100644 client/src/app/search/video-lazy-load.resolver.ts (limited to 'client/src/app/search') diff --git a/client/src/app/search/advanced-search.model.ts b/client/src/app/search/advanced-search.model.ts index 50f00bc27..643cc9a29 100644 --- a/client/src/app/search/advanced-search.model.ts +++ b/client/src/app/search/advanced-search.model.ts @@ -1,3 +1,4 @@ +import { SearchTargetType } from '@shared/models/search/search-target-query.model' import { NSFWQuery } from '../../../../shared/models/search' export class AdvancedSearch { @@ -23,6 +24,11 @@ export class AdvancedSearch { sort: string + searchTarget: SearchTargetType + + // Filters we don't want to count, because they are mandatory + private silentFilters = new Set([ 'sort', 'searchTarget' ]) + constructor (options?: { startDate?: string endDate?: string @@ -37,6 +43,7 @@ export class AdvancedSearch { durationMin?: string durationMax?: string sort?: string + searchTarget?: SearchTargetType }) { if (!options) return @@ -54,6 +61,8 @@ export class AdvancedSearch { this.durationMin = parseInt(options.durationMin, 10) this.durationMax = parseInt(options.durationMax, 10) + this.searchTarget = options.searchTarget || undefined + if (isNaN(this.durationMin)) this.durationMin = undefined if (isNaN(this.durationMax)) this.durationMax = undefined @@ -61,9 +70,11 @@ export class AdvancedSearch { } containsValues () { + const exceptions = new Set([ 'sort', 'searchTarget' ]) + const obj = this.toUrlObject() for (const k of Object.keys(obj)) { - if (k === 'sort') continue // Exception + if (this.silentFilters.has(k)) continue if (obj[k] !== undefined && obj[k] !== '') return true } @@ -102,7 +113,8 @@ export class AdvancedSearch { tagsAllOf: this.tagsAllOf, durationMin: this.durationMin, durationMax: this.durationMax, - sort: this.sort + sort: this.sort, + searchTarget: this.searchTarget } } @@ -120,7 +132,8 @@ export class AdvancedSearch { tagsAllOf: this.intoArray(this.tagsAllOf), durationMin: this.durationMin, durationMax: this.durationMax, - sort: this.sort + sort: this.sort, + searchTarget: this.searchTarget } } @@ -129,7 +142,7 @@ export class AdvancedSearch { const obj = this.toUrlObject() for (const k of Object.keys(obj)) { - if (k === 'sort') continue // Exception + if (this.silentFilters.has(k)) continue if (obj[k] !== undefined && obj[k] !== '') acc++ } diff --git a/client/src/app/search/channel-lazy-load.resolver.ts b/client/src/app/search/channel-lazy-load.resolver.ts new file mode 100644 index 000000000..8be089cdd --- /dev/null +++ b/client/src/app/search/channel-lazy-load.resolver.ts @@ -0,0 +1,45 @@ +import { map } from 'rxjs/operators' +import { Injectable } from '@angular/core' +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' +import { SearchService } from './search.service' +import { RedirectService } from '@app/core' + +@Injectable() +export class ChannelLazyLoadResolver implements Resolve { + constructor ( + private router: Router, + private searchService: SearchService, + private redirectService: RedirectService + ) { } + + resolve (route: ActivatedRouteSnapshot) { + const url = route.params.url + const externalRedirect = route.params.externalRedirect + const fromPath = route.params.fromPath + + if (!url) { + console.error('Could not find url param.', { params: route.params }) + return this.router.navigateByUrl('/404') + } + + if (externalRedirect === 'true') { + window.open(url) + this.router.navigateByUrl(fromPath) + return + } + + return this.searchService.searchVideoChannels({ search: url }) + .pipe( + map(result => { + if (result.data.length !== 1) { + console.error('Cannot find result for this URL') + return this.router.navigateByUrl('/404') + } + + const channel = result.data[0] + + return this.router.navigateByUrl('/video-channels/' + channel.nameWithHost) + }) + ) + } +} diff --git a/client/src/app/search/search-filters.component.html b/client/src/app/search/search-filters.component.html index 54fc7338f..e20aef8fb 100644 --- a/client/src/app/search/search-filters.component.html +++ b/client/src/app/search/search-filters.component.html @@ -16,6 +16,25 @@ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
@@ -39,7 +58,7 @@
-
+
-
+
+
+ +
@@ -76,28 +98,6 @@
-
-
- - -
- -
- - -
- -
- - -
-
- -
- -
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
diff --git a/client/src/app/search/search-filters.component.ts b/client/src/app/search/search-filters.component.ts index 344a260df..af76260a7 100644 --- a/client/src/app/search/search-filters.component.ts +++ b/client/src/app/search/search-filters.component.ts @@ -44,7 +44,7 @@ export class SearchFiltersComponent implements OnInit { this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES this.publishedDateRanges = [ { - id: undefined, + id: 'any_published_date', label: this.i18n('Any') }, { @@ -67,7 +67,7 @@ export class SearchFiltersComponent implements OnInit { this.durationRanges = [ { - id: undefined, + id: 'any_duration', label: this.i18n('Any') }, { @@ -147,6 +147,10 @@ export class SearchFiltersComponent implements OnInit { this.originallyPublishedStartYear = this.originallyPublishedEndYear = undefined } + isSearchTargetEnabled () { + return this.serverConfig.search.searchIndex.enabled && this.serverConfig.search.searchIndex.disableLocalSearch !== true + } + private loadOriginallyPublishedAtYears () { this.originallyPublishedStartYear = this.advancedSearch.originallyPublishedStartDate ? new Date(this.advancedSearch.originallyPublishedStartDate).getFullYear().toString() diff --git a/client/src/app/search/search-routing.module.ts b/client/src/app/search/search-routing.module.ts index 0ac9e6b57..9da900e9a 100644 --- a/client/src/app/search/search-routing.module.ts +++ b/client/src/app/search/search-routing.module.ts @@ -1,7 +1,9 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { SearchComponent } from '@app/search/search.component' +import { MetaGuard } from '@ngx-meta/core' +import { VideoLazyLoadResolver } from './video-lazy-load.resolver' +import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' const searchRoutes: Routes = [ { @@ -13,6 +15,22 @@ const searchRoutes: Routes = [ title: 'Search' } } + }, + { + path: 'search/lazy-load-video', + component: SearchComponent, + canActivate: [ MetaGuard ], + resolve: { + data: VideoLazyLoadResolver + } + }, + { + path: 'search/lazy-load-channel', + component: SearchComponent, + canActivate: [ MetaGuard ], + resolve: { + data: ChannelLazyLoadResolver + } } ] diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index a4a1d41b3..3cafc676d 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html @@ -2,7 +2,11 @@
- {{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} + {{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} + + on this instance + on the vidiverse + for {{ currentSearch }} @@ -31,12 +35,12 @@
- + Avatar
- +
{{ result.displayName }}
{{ result.nameWithHost }}
@@ -44,12 +48,13 @@
{{ result.followersCount }} subscribers
- +
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index 075994dd3..d3c0761d7 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts @@ -1,16 +1,18 @@ +import { forkJoin, of, Subscription } from 'rxjs' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, Notifier } from '@app/core' -import { forkJoin, of, Subscription } from 'rxjs' +import { AuthService, Notifier, ServerService } from '@app/core' +import { HooksService } from '@app/core/plugins/hooks.service' +import { AdvancedSearch } from '@app/search/advanced-search.model' import { SearchService } from '@app/search/search.service' +import { immutableAssign } from '@app/shared/misc/utils' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { MetaService } from '@ngx-meta/core' -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' +import { MetaService } from '@ngx-meta/core' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { ServerConfig } from '@shared/models' +import { UserService } from '@app/shared' @Component({ selector: 'my-search', @@ -29,6 +31,9 @@ export class SearchComponent implements OnInit, OnDestroy { isSearchFilterCollapsed = true currentSearch: string + errorMessage: string + serverConfig: ServerConfig + private subActivatedRoute: Subscription private isInitialLoad = false // set to false to show the search filters on first arrival private firstSearch = true @@ -43,7 +48,8 @@ export class SearchComponent implements OnInit, OnDestroy { private notifier: Notifier, private searchService: SearchService, private authService: AuthService, - private hooks: HooksService + private hooks: HooksService, + private serverService: ServerService ) { } get user () { @@ -51,8 +57,11 @@ export class SearchComponent implements OnInit, OnDestroy { } ngOnInit () { + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + this.subActivatedRoute = this.route.queryParams.subscribe( - queryParams => { + async queryParams => { const querySearch = queryParams['search'] // Search updated, reset filters @@ -65,6 +74,9 @@ export class SearchComponent implements OnInit, OnDestroy { } this.advancedSearch = new AdvancedSearch(queryParams) + if (!this.advancedSearch.searchTarget) { + this.advancedSearch.searchTarget = await this.serverService.getDefaultSearchTarget() + } // Don't hide filters if we have some of them AND the user just came on the webpage this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues() @@ -99,28 +111,37 @@ export class SearchComponent implements OnInit, OnDestroy { forkJoin([ this.getVideosObs(), this.getVideoChannelObs() - ]) - .subscribe( - ([ videosResult, videoChannelsResult ]) => { - this.results = this.results - .concat(videoChannelsResult.data) - .concat(videosResult.data) - this.pagination.totalItems = videosResult.total + videoChannelsResult.total - - // Focus on channels if there are no enough videos - if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { - this.resetPagination() - this.firstSearch = false - - this.channelsPerPage = 10 - this.search() - } + ]).subscribe( + ([videosResult, videoChannelsResult]) => { + this.results = this.results + .concat(videoChannelsResult.data) + .concat(videosResult.data) + + this.pagination.totalItems = videosResult.total + videoChannelsResult.total + // Focus on channels if there are no enough videos + if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { + this.resetPagination() this.firstSearch = false - }, - err => this.notifier.error(err.message) - ) + this.channelsPerPage = 10 + this.search() + } + + this.firstSearch = false + }, + + err => { + if (this.advancedSearch.searchTarget !== 'search-index') this.notifier.error(err.message) + + this.notifier.error( + this.i18n('Search index is unavailable. Retrying with instance results instead.'), + this.i18n('Search error') + ) + this.advancedSearch.searchTarget = 'local' + this.search() + } + ) } onNearOfBottom () { @@ -146,6 +167,24 @@ export class SearchComponent implements OnInit, OnDestroy { this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) } + getChannelUrl (channel: VideoChannel) { + if (this.advancedSearch.searchTarget === 'search-index' && channel.url) { + const remoteUriConfig = this.serverConfig.search.remoteUri + + // Redirect on the external instance if not allowed to fetch remote data + const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users + const fromPath = window.location.pathname + window.location.search + + return [ '/search/lazy-load-channel', { url: channel.url, externalRedirect, fromPath } ] + } + + return [ '/video-channels', channel.nameWithHost ] + } + + hideActions () { + return this.advancedSearch.searchTarget === 'search-index' + } + private resetPagination () { this.pagination.currentPage = 1 this.pagination.totalItems = null @@ -189,7 +228,8 @@ export class SearchComponent implements OnInit, OnDestroy { const params = { search: this.currentSearch, - componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }) + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), + searchTarget: this.advancedSearch.searchTarget } return this.hooks.wrapObsFun( diff --git a/client/src/app/search/search.module.ts b/client/src/app/search/search.module.ts index 3b0fd6ee2..df5459802 100644 --- a/client/src/app/search/search.module.ts +++ b/client/src/app/search/search.module.ts @@ -1,10 +1,12 @@ -import { NgModule } from '@angular/core' import { TagInputModule } from 'ngx-chips' -import { SharedModule } from '../shared' +import { NgModule } from '@angular/core' +import { SearchFiltersComponent } from '@app/search/search-filters.component' +import { SearchRoutingModule } from '@app/search/search-routing.module' import { SearchComponent } from '@app/search/search.component' import { SearchService } from '@app/search/search.service' -import { SearchRoutingModule } from '@app/search/search-routing.module' -import { SearchFiltersComponent } from '@app/search/search-filters.component' +import { SharedModule } from '../shared' +import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' +import { VideoLazyLoadResolver } from './video-lazy-load.resolver' @NgModule({ imports: [ @@ -25,7 +27,9 @@ import { SearchFiltersComponent } from '@app/search/search-filters.component' ], providers: [ - SearchService + SearchService, + VideoLazyLoadResolver, + ChannelLazyLoadResolver ] }) export class SearchModule { } diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts index 3cad5aaa7..fdb12ea2c 100644 --- a/client/src/app/search/search.service.ts +++ b/client/src/app/search/search.service.ts @@ -1,17 +1,18 @@ +import { Observable } from 'rxjs' import { catchError, map, switchMap } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' -import { Observable } from 'rxjs' -import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model' -import { VideoService } from '@app/shared/video/video.service' -import { RestExtractor, RestService } from '@app/shared' -import { environment } from '../../environments/environment' -import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '../../../../shared' -import { Video } from '@app/shared/video/video.model' import { AdvancedSearch } from '@app/search/advanced-search.model' +import { RestExtractor, RestPagination, RestService } from '@app/shared' +import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' +import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model' import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' -import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' +import { Video } from '@app/shared/video/video.model' +import { VideoService } from '@app/shared/video/video.service' +import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '../../../../shared' +import { environment } from '../../environments/environment' +import { SearchTargetType } from '@shared/models/search/search-target-query.model' @Injectable() export class SearchService { @@ -30,21 +31,27 @@ export class SearchService { searchVideos (parameters: { search: string, - componentPagination: ComponentPaginationLight, - advancedSearch: AdvancedSearch + componentPagination?: ComponentPaginationLight, + advancedSearch?: AdvancedSearch }): Observable> { const { search, componentPagination, advancedSearch } = parameters const url = SearchService.BASE_SEARCH_URL + 'videos' - const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + let pagination: RestPagination + + if (componentPagination) { + pagination = this.restService.componentPaginationToRestPagination(componentPagination) + } let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination) if (search) params = params.append('search', search) - const advancedSearchObject = advancedSearch.toAPIObject() - params = this.restService.addObjectParams(params, advancedSearchObject) + if (advancedSearch) { + const advancedSearchObject = advancedSearch.toAPIObject() + params = this.restService.addObjectParams(params, advancedSearchObject) + } return this.authHttp .get>(url, { params }) @@ -56,17 +63,26 @@ export class SearchService { searchVideoChannels (parameters: { search: string, - componentPagination: ComponentPaginationLight + searchTarget?: SearchTargetType, + componentPagination?: ComponentPaginationLight }): Observable> { - const { search, componentPagination } = parameters + const { search, componentPagination, searchTarget } = parameters const url = SearchService.BASE_SEARCH_URL + 'video-channels' - const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + + let pagination: RestPagination + if (componentPagination) { + pagination = this.restService.componentPaginationToRestPagination(componentPagination) + } let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination) params = params.append('search', search) + if (searchTarget) { + params = params.append('searchTarget', searchTarget as string) + } + return this.authHttp .get>(url, { params }) .pipe( diff --git a/client/src/app/search/video-lazy-load.resolver.ts b/client/src/app/search/video-lazy-load.resolver.ts new file mode 100644 index 000000000..8d846d367 --- /dev/null +++ b/client/src/app/search/video-lazy-load.resolver.ts @@ -0,0 +1,43 @@ +import { map } from 'rxjs/operators' +import { Injectable } from '@angular/core' +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' +import { SearchService } from './search.service' + +@Injectable() +export class VideoLazyLoadResolver implements Resolve { + constructor ( + private router: Router, + private searchService: SearchService + ) { } + + resolve (route: ActivatedRouteSnapshot) { + const url = route.params.url + const externalRedirect = route.params.externalRedirect + const fromPath = route.params.fromPath + + if (!url) { + console.error('Could not find url param.', { params: route.params }) + return this.router.navigateByUrl('/404') + } + + if (externalRedirect === 'true') { + window.open(url) + this.router.navigateByUrl(fromPath) + return + } + + return this.searchService.searchVideos({ search: url }) + .pipe( + map(result => { + if (result.data.length !== 1) { + console.error('Cannot find result for this URL') + return this.router.navigateByUrl('/404') + } + + const video = result.data[0] + + return this.router.navigateByUrl('/videos/watch/' + video.uuid) + }) + ) + } +} -- cgit v1.2.3