Add visitor settings, rework logged-in dropdown (#2514)
[oweals/peertube.git] / client / src / app / shared / video / video.service.ts
1 import { catchError, map, switchMap } from 'rxjs/operators'
2 import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
3 import { Injectable } from '@angular/core'
4 import { Observable } from 'rxjs'
5 import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared'
6 import { ResultList } from '../../../../../shared/models/result-list.model'
7 import {
8   UserVideoRate,
9   UserVideoRateType,
10   UserVideoRateUpdate,
11   VideoConstant,
12   VideoFilter,
13   VideoPrivacy,
14   VideoUpdate
15 } from '../../../../../shared/models/videos'
16 import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
17 import { environment } from '../../../environments/environment'
18 import { ComponentPaginationLight } from '../rest/component-pagination.model'
19 import { RestExtractor } from '../rest/rest-extractor.service'
20 import { RestService } from '../rest/rest.service'
21 import { UserService } from '../users/user.service'
22 import { VideoSortField } from './sort-field.type'
23 import { VideoDetails } from './video-details.model'
24 import { VideoEdit } from './video-edit.model'
25 import { Video } from './video.model'
26 import { objectToFormData } from '@app/shared/misc/utils'
27 import { Account } from '@app/shared/account/account.model'
28 import { AccountService } from '@app/shared/account/account.service'
29 import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
30 import { ServerService, AuthService } from '@app/core'
31 import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
32 import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
33 import { I18n } from '@ngx-translate/i18n-polyfill'
34 import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
35
36 export interface VideosProvider {
37   getVideos (parameters: {
38     videoPagination: ComponentPaginationLight,
39     sort: VideoSortField,
40     filter?: VideoFilter,
41     categoryOneOf?: number,
42     languageOneOf?: string[]
43   }): Observable<ResultList<Video>>
44 }
45
46 @Injectable()
47 export class VideoService implements VideosProvider {
48   static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
49   static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.'
50
51   constructor (
52     private authHttp: HttpClient,
53     private authService: AuthService,
54     private userService: UserService,
55     private restExtractor: RestExtractor,
56     private restService: RestService,
57     private serverService: ServerService,
58     private i18n: I18n
59   ) {}
60
61   getVideoViewUrl (uuid: string) {
62     return VideoService.BASE_VIDEO_URL + uuid + '/views'
63   }
64
65   getUserWatchingVideoUrl (uuid: string) {
66     return VideoService.BASE_VIDEO_URL + uuid + '/watching'
67   }
68
69   getVideo (options: { videoId: string }): Observable<VideoDetails> {
70     return this.serverService.getServerLocale()
71                .pipe(
72                  switchMap(translations => {
73                    return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + options.videoId)
74                               .pipe(map(videoHash => ({ videoHash, translations })))
75                  }),
76                  map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
77                  catchError(err => this.restExtractor.handleError(err))
78                )
79   }
80
81   updateVideo (video: VideoEdit) {
82     const language = video.language || null
83     const licence = video.licence || null
84     const category = video.category || null
85     const description = video.description || null
86     const support = video.support || null
87     const scheduleUpdate = video.scheduleUpdate || null
88     const originallyPublishedAt = video.originallyPublishedAt || null
89
90     const body: VideoUpdate = {
91       name: video.name,
92       category,
93       licence,
94       language,
95       support,
96       description,
97       channelId: video.channelId,
98       privacy: video.privacy,
99       tags: video.tags,
100       nsfw: video.nsfw,
101       waitTranscoding: video.waitTranscoding,
102       commentsEnabled: video.commentsEnabled,
103       downloadEnabled: video.downloadEnabled,
104       thumbnailfile: video.thumbnailfile,
105       previewfile: video.previewfile,
106       scheduleUpdate,
107       originallyPublishedAt
108     }
109
110     const data = objectToFormData(body)
111
112     return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, data)
113                .pipe(
114                  map(this.restExtractor.extractDataBool),
115                  catchError(err => this.restExtractor.handleError(err))
116                )
117   }
118
119   uploadVideo (video: FormData) {
120     const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
121
122     return this.authHttp
123                .request<{ video: { id: number, uuid: string } }>(req)
124                .pipe(catchError(err => this.restExtractor.handleError(err)))
125   }
126
127   getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable<ResultList<Video>> {
128     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
129
130     let params = new HttpParams()
131     params = this.restService.addRestGetParams(params, pagination, sort)
132     params = this.restService.addObjectParams(params, { search })
133
134     return this.authHttp
135                .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
136                .pipe(
137                  switchMap(res => this.extractVideos(res)),
138                  catchError(err => this.restExtractor.handleError(err))
139                )
140   }
141
142   getAccountVideos (
143     account: Account,
144     videoPagination: ComponentPaginationLight,
145     sort: VideoSortField
146   ): Observable<ResultList<Video>> {
147     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
148
149     let params = new HttpParams()
150     params = this.restService.addRestGetParams(params, pagination, sort)
151
152     return this.authHttp
153                .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
154                .pipe(
155                  switchMap(res => this.extractVideos(res)),
156                  catchError(err => this.restExtractor.handleError(err))
157                )
158   }
159
160   getVideoChannelVideos (
161     videoChannel: VideoChannel,
162     videoPagination: ComponentPaginationLight,
163     sort: VideoSortField
164   ): Observable<ResultList<Video>> {
165     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
166
167     let params = new HttpParams()
168     params = this.restService.addRestGetParams(params, pagination, sort)
169
170     return this.authHttp
171                .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
172                .pipe(
173                  switchMap(res => this.extractVideos(res)),
174                  catchError(err => this.restExtractor.handleError(err))
175                )
176   }
177
178   getUserSubscriptionVideos (parameters: {
179     videoPagination: ComponentPaginationLight,
180     sort: VideoSortField,
181     skipCount?: boolean
182   }): Observable<ResultList<Video>> {
183     const { videoPagination, sort, skipCount } = parameters
184     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
185
186     let params = new HttpParams()
187     params = this.restService.addRestGetParams(params, pagination, sort)
188
189     if (skipCount) params = params.set('skipCount', skipCount + '')
190
191     return this.authHttp
192                .get<ResultList<Video>>(UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/videos', { params })
193                .pipe(
194                  switchMap(res => this.extractVideos(res)),
195                  catchError(err => this.restExtractor.handleError(err))
196                )
197   }
198
199   getVideos (parameters: {
200     videoPagination: ComponentPaginationLight,
201     sort: VideoSortField,
202     filter?: VideoFilter,
203     categoryOneOf?: number,
204     languageOneOf?: string[],
205     skipCount?: boolean,
206     nsfw?: boolean
207   }): Observable<ResultList<Video>> {
208     const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfw } = parameters
209
210     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
211
212     let params = new HttpParams()
213     params = this.restService.addRestGetParams(params, pagination, sort)
214
215     if (filter) params = params.set('filter', filter)
216     if (categoryOneOf) params = params.set('categoryOneOf', categoryOneOf + '')
217     if (skipCount) params = params.set('skipCount', skipCount + '')
218
219     if (nsfw) {
220       params = params.set('nsfw', nsfw + '')
221     } else {
222       const nsfwPolicy = this.authService.isLoggedIn()
223         ? this.authService.getUser().nsfwPolicy
224         : this.userService.getAnonymousUser().nsfwPolicy
225       if (this.nsfwPolicyToFilter(nsfwPolicy)) params.set('nsfw', 'false')
226     }
227
228     if (languageOneOf) {
229       for (const l of languageOneOf) {
230         params = params.append('languageOneOf[]', l)
231       }
232     }
233
234     return this.authHttp
235                .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
236                .pipe(
237                  switchMap(res => this.extractVideos(res)),
238                  catchError(err => this.restExtractor.handleError(err))
239                )
240   }
241
242   buildBaseFeedUrls (params: HttpParams) {
243     const feeds = [
244       {
245         format: FeedFormat.RSS,
246         label: 'rss 2.0',
247         url: VideoService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase()
248       },
249       {
250         format: FeedFormat.ATOM,
251         label: 'atom 1.0',
252         url: VideoService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase()
253       },
254       {
255         format: FeedFormat.JSON,
256         label: 'json 1.0',
257         url: VideoService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase()
258       }
259     ]
260
261     if (params && params.keys().length !== 0) {
262       for (const feed of feeds) {
263         feed.url += '?' + params.toString()
264       }
265     }
266
267     return feeds
268   }
269
270   getVideoFeedUrls (sort: VideoSortField, filter?: VideoFilter, categoryOneOf?: number) {
271     let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort)
272
273     if (filter) params = params.set('filter', filter)
274
275     if (categoryOneOf) params = params.set('categoryOneOf', categoryOneOf + '')
276
277     return this.buildBaseFeedUrls(params)
278   }
279
280   getAccountFeedUrls (accountId: number) {
281     let params = this.restService.addRestGetParams(new HttpParams())
282     params = params.set('accountId', accountId.toString())
283
284     return this.buildBaseFeedUrls(params)
285   }
286
287   getVideoChannelFeedUrls (videoChannelId: number) {
288     let params = this.restService.addRestGetParams(new HttpParams())
289     params = params.set('videoChannelId', videoChannelId.toString())
290
291     return this.buildBaseFeedUrls(params)
292   }
293
294   removeVideo (id: number) {
295     return this.authHttp
296                .delete(VideoService.BASE_VIDEO_URL + id)
297                .pipe(
298                  map(this.restExtractor.extractDataBool),
299                  catchError(err => this.restExtractor.handleError(err))
300                )
301   }
302
303   loadCompleteDescription (descriptionPath: string) {
304     return this.authHttp
305                .get<{ description: string }>(environment.apiUrl + descriptionPath)
306                .pipe(
307                  map(res => res.description),
308                  catchError(err => this.restExtractor.handleError(err))
309                )
310   }
311
312   setVideoLike (id: number) {
313     return this.setVideoRate(id, 'like')
314   }
315
316   setVideoDislike (id: number) {
317     return this.setVideoRate(id, 'dislike')
318   }
319
320   unsetVideoLike (id: number) {
321     return this.setVideoRate(id, 'none')
322   }
323
324   getUserVideoRating (id: number) {
325     const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
326
327     return this.authHttp.get<UserVideoRate>(url)
328                .pipe(catchError(err => this.restExtractor.handleError(err)))
329   }
330
331   extractVideos (result: ResultList<VideoServerModel>) {
332     return this.serverService.getServerLocale()
333                .pipe(
334                  map(translations => {
335                    const videosJson = result.data
336                    const totalVideos = result.total
337                    const videos: Video[] = []
338
339                    for (const videoJson of videosJson) {
340                      videos.push(new Video(videoJson, translations))
341                    }
342
343                    return { total: totalVideos, data: videos }
344                  })
345                )
346   }
347
348   explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[]) {
349     const base = [
350       {
351         id: VideoPrivacy.PRIVATE,
352         label: this.i18n('Only I can see this video')
353       },
354       {
355         id: VideoPrivacy.UNLISTED,
356         label: this.i18n('Only people with the private link can see this video')
357       },
358       {
359         id: VideoPrivacy.PUBLIC,
360         label: this.i18n('Anyone can see this video')
361       },
362       {
363         id: VideoPrivacy.INTERNAL,
364         label: this.i18n('Only users of this instance can see this video')
365       }
366     ]
367
368     return base.filter(o => !!privacies.find(p => p.id === o.id))
369   }
370
371   private setVideoRate (id: number, rateType: UserVideoRateType) {
372     const url = VideoService.BASE_VIDEO_URL + id + '/rate'
373     const body: UserVideoRateUpdate = {
374       rating: rateType
375     }
376
377     return this.authHttp
378                .put(url, body)
379                .pipe(
380                  map(this.restExtractor.extractDataBool),
381                  catchError(err => this.restExtractor.handleError(err))
382                )
383   }
384
385   private nsfwPolicyToFilter (policy: NSFWPolicyType) {
386     return policy === 'do_not_list'
387   }
388 }