1 import { forkJoin, of, Subscription } from 'rxjs'
2 import { Component, OnDestroy, OnInit } from '@angular/core'
3 import { ActivatedRoute, Router } from '@angular/router'
4 import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
5 import { immutableAssign } from '@app/helpers'
6 import { Video, VideoChannel } from '@app/shared/shared-main'
7 import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
8 import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature'
9 import { HTMLServerConfig, SearchTargetType } from '@shared/models'
12 selector: 'my-search',
13 styleUrls: [ './search.component.scss' ],
14 templateUrl: './search.component.html'
16 export class SearchComponent implements OnInit, OnDestroy {
17 results: (Video | VideoChannel)[] = []
19 pagination: ComponentPagination = {
21 itemsPerPage: 10, // Only for videos, use another variable for channels
24 advancedSearch: AdvancedSearch = new AdvancedSearch()
25 isSearchFilterCollapsed = true
28 videoDisplayOptions: MiniatureDisplayOptions = {
43 private subActivatedRoute: Subscription
44 private isInitialLoad = false // set to false to show the search filters on first arrival
45 private firstSearch = true
47 private channelsPerPage = 2
49 private lastSearchTarget: SearchTargetType
51 private serverConfig: HTMLServerConfig
54 private route: ActivatedRoute,
55 private router: Router,
56 private metaService: MetaService,
57 private notifier: Notifier,
58 private searchService: SearchService,
59 private authService: AuthService,
60 private userService: UserService,
61 private hooks: HooksService,
62 private serverService: ServerService
66 this.serverConfig = this.serverService.getHTMLConfig()
68 this.subActivatedRoute = this.route.queryParams.subscribe(
69 async queryParams => {
70 const querySearch = queryParams['search']
71 const searchTarget = queryParams['searchTarget']
73 // Search updated, reset filters
74 if (this.currentSearch !== querySearch || searchTarget !== this.advancedSearch.searchTarget) {
75 this.resetPagination()
76 this.advancedSearch.reset()
78 this.currentSearch = querySearch || undefined
82 this.advancedSearch = new AdvancedSearch(queryParams)
83 if (!this.advancedSearch.searchTarget) {
84 this.advancedSearch.searchTarget = this.getDefaultSearchTarget()
87 // Don't hide filters if we have some of them AND the user just came on the webpage
88 this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues()
89 this.isInitialLoad = false
94 err => this.notifier.error(err.text)
97 this.userService.getAnonymousOrLoggedUser()
98 .subscribe(user => this.userMiniature = user)
100 this.hooks.runAction('action:search.init', 'search')
104 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
107 isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
108 return d instanceof VideoChannel
111 isVideo (v: VideoChannel | Video): v is Video {
112 return v instanceof Video
116 return this.authService.isLoggedIn()
119 getVideoLinkType (): VideoLinkType {
120 if (this.advancedSearch.searchTarget === 'search-index') {
121 const remoteUriConfig = this.serverConfig.search.remoteUri
123 // Redirect on the external instance if not allowed to fetch remote data
124 if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) {
137 this.getVideoChannelObs()
139 ([videosResult, videoChannelsResult]) => {
140 this.results = this.results
141 .concat(videoChannelsResult.data)
142 .concat(videosResult.data)
144 this.pagination.totalItems = videosResult.total + videoChannelsResult.total
145 this.lastSearchTarget = this.advancedSearch.searchTarget
147 // Focus on channels if there are no enough videos
148 if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) {
149 this.resetPagination()
150 this.firstSearch = false
152 this.channelsPerPage = 10
156 this.firstSearch = false
160 if (this.advancedSearch.searchTarget !== 'search-index') {
161 this.notifier.error(err.message)
166 $localize`Search index is unavailable. Retrying with instance results instead.`,
167 $localize`Search error`
169 this.advancedSearch.searchTarget = 'local'
177 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
179 this.pagination.currentPage += 1
184 this.resetPagination()
186 this.updateUrlFromAdvancedSearch()
190 return this.advancedSearch.size()
193 // Add VideoChannel for typings, but the template already checks "video" argument is a video
194 removeVideoFromArray (video: Video | VideoChannel) {
195 this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
198 isExternalChannelUrl () {
199 return this.getVideoLinkType() === 'external'
202 getExternalChannelUrl (channel: VideoChannel) {
203 // Same algorithm than videos
204 if (this.getVideoLinkType() === 'external') {
208 // lazy-load or internal
212 getInternalChannelUrl (channel: VideoChannel) {
213 const linkType = this.getVideoLinkType()
215 if (linkType === 'internal') {
216 return [ '/c', channel.nameWithHost ]
219 if (linkType === 'lazy-load') {
220 return [ '/search/lazy-load-channel', { url: channel.url } ]
228 return this.lastSearchTarget === 'search-index'
231 private resetPagination () {
232 this.pagination.currentPage = 1
233 this.pagination.totalItems = null
234 this.channelsPerPage = 2
239 private updateTitle () {
240 const suffix = this.currentSearch
241 ? ' ' + this.currentSearch
244 this.metaService.setTitle($localize`Search` + suffix)
247 private updateUrlFromAdvancedSearch () {
248 const search = this.currentSearch || undefined
250 this.router.navigate([], {
251 relativeTo: this.route,
252 queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search })
256 private getVideosObs () {
258 search: this.currentSearch,
259 componentPagination: this.pagination,
260 advancedSearch: this.advancedSearch
263 return this.hooks.wrapObsFun(
264 this.searchService.searchVideos.bind(this.searchService),
267 'filter:api.search.videos.list.params',
268 'filter:api.search.videos.list.result'
272 private getVideoChannelObs () {
273 if (!this.currentSearch) return of({ data: [], total: 0 })
276 search: this.currentSearch,
277 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }),
278 searchTarget: this.advancedSearch.searchTarget
281 return this.hooks.wrapObsFun(
282 this.searchService.searchVideoChannels.bind(this.searchService),
285 'filter:api.search.video-channels.list.params',
286 'filter:api.search.video-channels.list.result'
290 private getDefaultSearchTarget (): SearchTargetType {
291 const searchIndexConfig = this.serverConfig.search.searchIndex
293 if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) {
294 return 'search-index'