diff options
Diffstat (limited to 'client/src/app/shared')
4 files changed, 271 insertions, 0 deletions
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts new file mode 100644 index 000000000..516854a8c --- /dev/null +++ b/client/src/app/shared/shared-search/advanced-search.model.ts | |||
@@ -0,0 +1,160 @@ | |||
1 | import { NSFWQuery, SearchTargetType } from '@shared/models' | ||
2 | |||
3 | export class AdvancedSearch { | ||
4 | startDate: string // ISO 8601 | ||
5 | endDate: string // ISO 8601 | ||
6 | |||
7 | originallyPublishedStartDate: string // ISO 8601 | ||
8 | originallyPublishedEndDate: string // ISO 8601 | ||
9 | |||
10 | nsfw: NSFWQuery | ||
11 | |||
12 | categoryOneOf: string | ||
13 | |||
14 | licenceOneOf: string | ||
15 | |||
16 | languageOneOf: string | ||
17 | |||
18 | tagsOneOf: string | ||
19 | tagsAllOf: string | ||
20 | |||
21 | durationMin: number // seconds | ||
22 | durationMax: number // seconds | ||
23 | |||
24 | sort: string | ||
25 | |||
26 | searchTarget: SearchTargetType | ||
27 | |||
28 | // Filters we don't want to count, because they are mandatory | ||
29 | private silentFilters = new Set([ 'sort', 'searchTarget' ]) | ||
30 | |||
31 | constructor (options?: { | ||
32 | startDate?: string | ||
33 | endDate?: string | ||
34 | originallyPublishedStartDate?: string | ||
35 | originallyPublishedEndDate?: string | ||
36 | nsfw?: NSFWQuery | ||
37 | categoryOneOf?: string | ||
38 | licenceOneOf?: string | ||
39 | languageOneOf?: string | ||
40 | tagsOneOf?: string | ||
41 | tagsAllOf?: string | ||
42 | durationMin?: string | ||
43 | durationMax?: string | ||
44 | sort?: string | ||
45 | searchTarget?: SearchTargetType | ||
46 | }) { | ||
47 | if (!options) return | ||
48 | |||
49 | this.startDate = options.startDate || undefined | ||
50 | this.endDate = options.endDate || undefined | ||
51 | this.originallyPublishedStartDate = options.originallyPublishedStartDate || undefined | ||
52 | this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined | ||
53 | |||
54 | this.nsfw = options.nsfw || undefined | ||
55 | this.categoryOneOf = options.categoryOneOf || undefined | ||
56 | this.licenceOneOf = options.licenceOneOf || undefined | ||
57 | this.languageOneOf = options.languageOneOf || undefined | ||
58 | this.tagsOneOf = options.tagsOneOf || undefined | ||
59 | this.tagsAllOf = options.tagsAllOf || undefined | ||
60 | this.durationMin = parseInt(options.durationMin, 10) | ||
61 | this.durationMax = parseInt(options.durationMax, 10) | ||
62 | |||
63 | this.searchTarget = options.searchTarget || undefined | ||
64 | |||
65 | if (isNaN(this.durationMin)) this.durationMin = undefined | ||
66 | if (isNaN(this.durationMax)) this.durationMax = undefined | ||
67 | |||
68 | this.sort = options.sort || '-match' | ||
69 | } | ||
70 | |||
71 | containsValues () { | ||
72 | const exceptions = new Set([ 'sort', 'searchTarget' ]) | ||
73 | |||
74 | const obj = this.toUrlObject() | ||
75 | for (const k of Object.keys(obj)) { | ||
76 | if (this.silentFilters.has(k)) continue | ||
77 | |||
78 | if (obj[k] !== undefined && obj[k] !== '') return true | ||
79 | } | ||
80 | |||
81 | return false | ||
82 | } | ||
83 | |||
84 | reset () { | ||
85 | this.startDate = undefined | ||
86 | this.endDate = undefined | ||
87 | this.originallyPublishedStartDate = undefined | ||
88 | this.originallyPublishedEndDate = undefined | ||
89 | this.nsfw = undefined | ||
90 | this.categoryOneOf = undefined | ||
91 | this.licenceOneOf = undefined | ||
92 | this.languageOneOf = undefined | ||
93 | this.tagsOneOf = undefined | ||
94 | this.tagsAllOf = undefined | ||
95 | this.durationMin = undefined | ||
96 | this.durationMax = undefined | ||
97 | |||
98 | this.sort = '-match' | ||
99 | } | ||
100 | |||
101 | toUrlObject () { | ||
102 | return { | ||
103 | startDate: this.startDate, | ||
104 | endDate: this.endDate, | ||
105 | originallyPublishedStartDate: this.originallyPublishedStartDate, | ||
106 | originallyPublishedEndDate: this.originallyPublishedEndDate, | ||
107 | nsfw: this.nsfw, | ||
108 | categoryOneOf: this.categoryOneOf, | ||
109 | licenceOneOf: this.licenceOneOf, | ||
110 | languageOneOf: this.languageOneOf, | ||
111 | tagsOneOf: this.tagsOneOf, | ||
112 | tagsAllOf: this.tagsAllOf, | ||
113 | durationMin: this.durationMin, | ||
114 | durationMax: this.durationMax, | ||
115 | sort: this.sort, | ||
116 | searchTarget: this.searchTarget | ||
117 | } | ||
118 | } | ||
119 | |||
120 | toAPIObject () { | ||
121 | return { | ||
122 | startDate: this.startDate, | ||
123 | endDate: this.endDate, | ||
124 | originallyPublishedStartDate: this.originallyPublishedStartDate, | ||
125 | originallyPublishedEndDate: this.originallyPublishedEndDate, | ||
126 | nsfw: this.nsfw, | ||
127 | categoryOneOf: this.intoArray(this.categoryOneOf), | ||
128 | licenceOneOf: this.intoArray(this.licenceOneOf), | ||
129 | languageOneOf: this.intoArray(this.languageOneOf), | ||
130 | tagsOneOf: this.intoArray(this.tagsOneOf), | ||
131 | tagsAllOf: this.intoArray(this.tagsAllOf), | ||
132 | durationMin: this.durationMin, | ||
133 | durationMax: this.durationMax, | ||
134 | sort: this.sort, | ||
135 | searchTarget: this.searchTarget | ||
136 | } | ||
137 | } | ||
138 | |||
139 | size () { | ||
140 | let acc = 0 | ||
141 | |||
142 | const obj = this.toUrlObject() | ||
143 | for (const k of Object.keys(obj)) { | ||
144 | if (this.silentFilters.has(k)) continue | ||
145 | |||
146 | if (obj[k] !== undefined && obj[k] !== '') acc++ | ||
147 | } | ||
148 | |||
149 | return acc | ||
150 | } | ||
151 | |||
152 | private intoArray (value: any) { | ||
153 | if (!value) return undefined | ||
154 | if (Array.isArray(value)) return value | ||
155 | |||
156 | if (typeof value === 'string') return value.split(',') | ||
157 | |||
158 | return [ value ] | ||
159 | } | ||
160 | } | ||
diff --git a/client/src/app/shared/shared-search/index.ts b/client/src/app/shared/shared-search/index.ts new file mode 100644 index 000000000..f687f6767 --- /dev/null +++ b/client/src/app/shared/shared-search/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './advanced-search.model' | ||
2 | export * from './search.service' | ||
3 | 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 new file mode 100644 index 000000000..96b954c99 --- /dev/null +++ b/client/src/app/shared/shared-search/search.service.ts | |||
@@ -0,0 +1,88 @@ | |||
1 | import { Observable } from 'rxjs' | ||
2 | import { catchError, map, switchMap } from 'rxjs/operators' | ||
3 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core' | ||
6 | import { peertubeLocalStorage } from '@app/helpers' | ||
7 | import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' | ||
8 | import { ResultList, SearchTargetType, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '@shared/models' | ||
9 | import { environment } from '../../../environments/environment' | ||
10 | import { AdvancedSearch } from './advanced-search.model' | ||
11 | |||
12 | @Injectable() | ||
13 | export class SearchService { | ||
14 | static BASE_SEARCH_URL = environment.apiUrl + '/api/v1/search/' | ||
15 | |||
16 | constructor ( | ||
17 | private authHttp: HttpClient, | ||
18 | private restExtractor: RestExtractor, | ||
19 | private restService: RestService, | ||
20 | private videoService: VideoService | ||
21 | ) { | ||
22 | // Add ability to override search endpoint if the user updated this local storage key | ||
23 | const searchUrl = peertubeLocalStorage.getItem('search-url') | ||
24 | if (searchUrl) SearchService.BASE_SEARCH_URL = searchUrl | ||
25 | } | ||
26 | |||
27 | searchVideos (parameters: { | ||
28 | search: string, | ||
29 | componentPagination?: ComponentPaginationLight, | ||
30 | advancedSearch?: AdvancedSearch | ||
31 | }): Observable<ResultList<Video>> { | ||
32 | const { search, componentPagination, advancedSearch } = parameters | ||
33 | |||
34 | const url = SearchService.BASE_SEARCH_URL + 'videos' | ||
35 | let pagination: RestPagination | ||
36 | |||
37 | if (componentPagination) { | ||
38 | pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
39 | } | ||
40 | |||
41 | let params = new HttpParams() | ||
42 | params = this.restService.addRestGetParams(params, pagination) | ||
43 | |||
44 | if (search) params = params.append('search', search) | ||
45 | |||
46 | if (advancedSearch) { | ||
47 | const advancedSearchObject = advancedSearch.toAPIObject() | ||
48 | params = this.restService.addObjectParams(params, advancedSearchObject) | ||
49 | } | ||
50 | |||
51 | return this.authHttp | ||
52 | .get<ResultList<VideoServerModel>>(url, { params }) | ||
53 | .pipe( | ||
54 | switchMap(res => this.videoService.extractVideos(res)), | ||
55 | catchError(err => this.restExtractor.handleError(err)) | ||
56 | ) | ||
57 | } | ||
58 | |||
59 | searchVideoChannels (parameters: { | ||
60 | search: string, | ||
61 | searchTarget?: SearchTargetType, | ||
62 | componentPagination?: ComponentPaginationLight | ||
63 | }): Observable<ResultList<VideoChannel>> { | ||
64 | const { search, componentPagination, searchTarget } = parameters | ||
65 | |||
66 | const url = SearchService.BASE_SEARCH_URL + 'video-channels' | ||
67 | |||
68 | let pagination: RestPagination | ||
69 | if (componentPagination) { | ||
70 | pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
71 | } | ||
72 | |||
73 | let params = new HttpParams() | ||
74 | params = this.restService.addRestGetParams(params, pagination) | ||
75 | params = params.append('search', search) | ||
76 | |||
77 | if (searchTarget) { | ||
78 | params = params.append('searchTarget', searchTarget as string) | ||
79 | } | ||
80 | |||
81 | return this.authHttp | ||
82 | .get<ResultList<VideoChannelServerModel>>(url, { params }) | ||
83 | .pipe( | ||
84 | map(res => VideoChannelService.extractVideoChannels(res)), | ||
85 | catchError(err => this.restExtractor.handleError(err)) | ||
86 | ) | ||
87 | } | ||
88 | } | ||
diff --git a/client/src/app/shared/shared-search/shared-search.module.ts b/client/src/app/shared/shared-search/shared-search.module.ts new file mode 100644 index 000000000..134300d88 --- /dev/null +++ b/client/src/app/shared/shared-search/shared-search.module.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedMainModule } from '../shared-main' | ||
3 | import { SearchService } from './search.service' | ||
4 | |||
5 | @NgModule({ | ||
6 | imports: [ | ||
7 | SharedMainModule | ||
8 | ], | ||
9 | |||
10 | declarations: [ | ||
11 | ], | ||
12 | |||
13 | exports: [ | ||
14 | ], | ||
15 | |||
16 | providers: [ | ||
17 | SearchService | ||
18 | ] | ||
19 | }) | ||
20 | export class SharedSearchModule { } | ||