]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/shared/shared-main/video/video.service.ts
Fix avatar default size
[github/Chocobozzz/PeerTube.git] / client / src / app / shared / shared-main / video / video.service.ts
1 import { SortMeta } from 'primeng/api'
2 import { from, Observable } from 'rxjs'
3 import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
4 import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
5 import { Injectable } from '@angular/core'
6 import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core'
7 import { objectToFormData } from '@app/helpers'
8 import {
9 BooleanBothQuery,
10 FeedFormat,
11 NSFWPolicyType,
12 ResultList,
13 UserVideoRate,
14 UserVideoRateType,
15 UserVideoRateUpdate,
16 Video as VideoServerModel,
17 VideoChannel as VideoChannelServerModel,
18 VideoConstant,
19 VideoDetails as VideoDetailsServerModel,
20 VideoFileMetadata,
21 VideoInclude,
22 VideoPrivacy,
23 VideoSortField,
24 VideoTranscodingCreate,
25 VideoUpdate
26 } from '@shared/models'
27 import { environment } from '../../../../environments/environment'
28 import { Account } from '../account/account.model'
29 import { AccountService } from '../account/account.service'
30 import { VideoChannel, VideoChannelService } from '../video-channel'
31 import { VideoDetails } from './video-details.model'
32 import { VideoEdit } from './video-edit.model'
33 import { Video } from './video.model'
34
35 export type CommonVideoParams = {
36 videoPagination?: ComponentPaginationLight
37 sort: VideoSortField | SortMeta
38 include?: VideoInclude
39 isLocal?: boolean
40 categoryOneOf?: number[]
41 languageOneOf?: string[]
42 privacyOneOf?: VideoPrivacy[]
43 isLive?: boolean
44 skipCount?: boolean
45
46 // FIXME: remove?
47 nsfwPolicy?: NSFWPolicyType
48 nsfw?: BooleanBothQuery
49 }
50
51 @Injectable()
52 export class VideoService {
53 static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos'
54 static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.'
55 static BASE_SUBSCRIPTION_FEEDS_URL = environment.apiUrl + '/feeds/subscriptions.'
56
57 constructor (
58 private auth: AuthService,
59 private authHttp: HttpClient,
60 private restExtractor: RestExtractor,
61 private restService: RestService,
62 private serverService: ServerService
63 ) {}
64
65 getVideoViewUrl (uuid: string) {
66 return `${VideoService.BASE_VIDEO_URL}/${uuid}/views`
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 pluginData: video.pluginData,
107 scheduleUpdate,
108 originallyPublishedAt
109 }
110
111 const data = objectToFormData(body)
112
113 return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, data)
114 .pipe(catchError(err => this.restExtractor.handleError(err)))
115 }
116
117 uploadVideo (video: FormData) {
118 const req = new HttpRequest('POST', `${VideoService.BASE_VIDEO_URL}/upload`, video, { reportProgress: true })
119
120 return this.authHttp
121 .request<{ video: { id: number, uuid: string } }>(req)
122 .pipe(catchError(err => this.restExtractor.handleError(err)))
123 }
124
125 getMyVideos (options: {
126 videoPagination: ComponentPaginationLight
127 sort: VideoSortField
128 userChannels?: VideoChannelServerModel[]
129 search?: string
130 }): Observable<ResultList<Video>> {
131 const { videoPagination, sort, userChannels = [], search } = options
132
133 const pagination = this.restService.componentToRestPagination(videoPagination)
134
135 let params = new HttpParams()
136 params = this.restService.addRestGetParams(params, pagination, sort)
137
138 if (search) {
139 const filters = this.restService.parseQueryStringFilter(search, {
140 isLive: {
141 prefix: 'isLive:',
142 isBoolean: true
143 },
144 channelId: {
145 prefix: 'channel:',
146 handler: (name: string) => {
147 const channel = userChannels.find(c => c.name === name)
148
149 if (channel) return channel.id
150
151 return undefined
152 }
153 }
154 })
155
156 params = this.restService.addObjectParams(params, filters)
157 }
158
159 return this.authHttp
160 .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
161 .pipe(
162 switchMap(res => this.extractVideos(res)),
163 catchError(err => this.restExtractor.handleError(err))
164 )
165 }
166
167 getAccountVideos (parameters: CommonVideoParams & {
168 account: Pick<Account, 'nameWithHost'>
169 search?: string
170 }): Observable<ResultList<Video>> {
171 const { account, search } = parameters
172
173 let params = new HttpParams()
174 params = this.buildCommonVideosParams({ params, ...parameters })
175
176 if (search) params = params.set('search', search)
177
178 return this.authHttp
179 .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
180 .pipe(
181 switchMap(res => this.extractVideos(res)),
182 catchError(err => this.restExtractor.handleError(err))
183 )
184 }
185
186 getVideoChannelVideos (parameters: CommonVideoParams & {
187 videoChannel: Pick<VideoChannel, 'nameWithHost'>
188 }): Observable<ResultList<Video>> {
189 const { videoChannel } = parameters
190
191 let params = new HttpParams()
192 params = this.buildCommonVideosParams({ params, ...parameters })
193
194 return this.authHttp
195 .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
196 .pipe(
197 switchMap(res => this.extractVideos(res)),
198 catchError(err => this.restExtractor.handleError(err))
199 )
200 }
201
202 getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> {
203 let params = new HttpParams()
204 params = this.buildCommonVideosParams({ params, ...parameters })
205
206 return this.authHttp
207 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
208 .pipe(
209 switchMap(res => this.extractVideos(res)),
210 catchError(err => this.restExtractor.handleError(err))
211 )
212 }
213
214 buildBaseFeedUrls (params: HttpParams, base = VideoService.BASE_FEEDS_URL) {
215 const feeds = [
216 {
217 format: FeedFormat.RSS,
218 label: 'media rss 2.0',
219 url: base + FeedFormat.RSS.toLowerCase()
220 },
221 {
222 format: FeedFormat.ATOM,
223 label: 'atom 1.0',
224 url: base + FeedFormat.ATOM.toLowerCase()
225 },
226 {
227 format: FeedFormat.JSON,
228 label: 'json 1.0',
229 url: base + FeedFormat.JSON.toLowerCase()
230 }
231 ]
232
233 if (params && params.keys().length !== 0) {
234 for (const feed of feeds) {
235 feed.url += '?' + params.toString()
236 }
237 }
238
239 return feeds
240 }
241
242 getVideoFeedUrls (sort: VideoSortField, isLocal: boolean, categoryOneOf?: number[]) {
243 let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort)
244
245 if (isLocal) params = params.set('isLocal', isLocal)
246
247 if (categoryOneOf) {
248 for (const c of categoryOneOf) {
249 params = params.append('categoryOneOf[]', c + '')
250 }
251 }
252
253 return this.buildBaseFeedUrls(params)
254 }
255
256 getAccountFeedUrls (accountId: number) {
257 let params = this.restService.addRestGetParams(new HttpParams())
258 params = params.set('accountId', accountId.toString())
259
260 return this.buildBaseFeedUrls(params)
261 }
262
263 getVideoChannelFeedUrls (videoChannelId: number) {
264 let params = this.restService.addRestGetParams(new HttpParams())
265 params = params.set('videoChannelId', videoChannelId.toString())
266
267 return this.buildBaseFeedUrls(params)
268 }
269
270 getVideoSubscriptionFeedUrls (accountId: number, feedToken: string) {
271 let params = this.restService.addRestGetParams(new HttpParams())
272 params = params.set('accountId', accountId.toString())
273 params = params.set('token', feedToken)
274
275 return this.buildBaseFeedUrls(params, VideoService.BASE_SUBSCRIPTION_FEEDS_URL)
276 }
277
278 getVideoFileMetadata (metadataUrl: string) {
279 return this.authHttp
280 .get<VideoFileMetadata>(metadataUrl)
281 .pipe(
282 catchError(err => this.restExtractor.handleError(err))
283 )
284 }
285
286 removeVideo (idArg: number | number[]) {
287 const ids = Array.isArray(idArg) ? idArg : [ idArg ]
288
289 return from(ids)
290 .pipe(
291 concatMap(id => this.authHttp.delete(`${VideoService.BASE_VIDEO_URL}/${id}`)),
292 toArray(),
293 catchError(err => this.restExtractor.handleError(err))
294 )
295 }
296
297 removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'webtorrent') {
298 return from(videoIds)
299 .pipe(
300 concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + id + '/' + type)),
301 toArray(),
302 catchError(err => this.restExtractor.handleError(err))
303 )
304 }
305
306 runTranscoding (videoIds: (number | string)[], type: 'hls' | 'webtorrent') {
307 const body: VideoTranscodingCreate = { transcodingType: type }
308
309 return from(videoIds)
310 .pipe(
311 concatMap(id => this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)),
312 toArray(),
313 catchError(err => this.restExtractor.handleError(err))
314 )
315 }
316
317 loadCompleteDescription (descriptionPath: string) {
318 return this.authHttp
319 .get<{ description: string }>(environment.apiUrl + descriptionPath)
320 .pipe(
321 map(res => res.description),
322 catchError(err => this.restExtractor.handleError(err))
323 )
324 }
325
326 setVideoLike (id: number) {
327 return this.setVideoRate(id, 'like')
328 }
329
330 setVideoDislike (id: number) {
331 return this.setVideoRate(id, 'dislike')
332 }
333
334 unsetVideoLike (id: number) {
335 return this.setVideoRate(id, 'none')
336 }
337
338 getUserVideoRating (id: number) {
339 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
340
341 return this.authHttp.get<UserVideoRate>(url)
342 .pipe(catchError(err => this.restExtractor.handleError(err)))
343 }
344
345 extractVideos (result: ResultList<VideoServerModel>) {
346 return this.serverService.getServerLocale()
347 .pipe(
348 map(translations => {
349 const videosJson = result.data
350 const totalVideos = result.total
351 const videos: Video[] = []
352
353 for (const videoJson of videosJson) {
354 videos.push(new Video(videoJson, translations))
355 }
356
357 return { total: totalVideos, data: videos }
358 })
359 )
360 }
361
362 explainedPrivacyLabels (serverPrivacies: VideoConstant<VideoPrivacy>[], defaultPrivacyId = VideoPrivacy.PUBLIC) {
363 const descriptions = {
364 [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`,
365 [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`,
366 [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`,
367 [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video`
368 }
369
370 const videoPrivacies = serverPrivacies.map(p => {
371 return {
372 ...p,
373
374 description: descriptions[p.id]
375 }
376 })
377
378 return {
379 videoPrivacies,
380 defaultPrivacyId: serverPrivacies.find(p => p.id === defaultPrivacyId)?.id || serverPrivacies[0].id
381 }
382 }
383
384 getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) {
385 const order = [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC ]
386
387 for (const privacy of order) {
388 if (serverPrivacies.find(p => p.id === privacy)) {
389 return privacy
390 }
391 }
392
393 throw new Error('No highest privacy available')
394 }
395
396 nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) {
397 return nsfwPolicy === 'do_not_list'
398 ? 'false'
399 : 'both'
400 }
401
402 buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
403 const {
404 params,
405 videoPagination,
406 sort,
407 isLocal,
408 include,
409 categoryOneOf,
410 languageOneOf,
411 privacyOneOf,
412 skipCount,
413 nsfwPolicy,
414 isLive,
415 nsfw
416 } = options
417
418 const pagination = videoPagination
419 ? this.restService.componentToRestPagination(videoPagination)
420 : undefined
421
422 let newParams = this.restService.addRestGetParams(params, pagination, this.buildListSort(sort))
423
424 if (skipCount) newParams = newParams.set('skipCount', skipCount + '')
425
426 if (isLocal) newParams = newParams.set('isLocal', isLocal)
427 if (include) newParams = newParams.set('include', include)
428 if (isLive) newParams = newParams.set('isLive', isLive)
429 if (nsfw) newParams = newParams.set('nsfw', nsfw)
430 if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
431 if (languageOneOf) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
432 if (categoryOneOf) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
433 if (privacyOneOf) newParams = this.restService.addArrayParams(newParams, 'privacyOneOf', privacyOneOf)
434
435 return newParams
436 }
437
438 private buildListSort (sortArg: VideoSortField | SortMeta) {
439 const sort = this.restService.buildSortString(sortArg)
440
441 if (typeof sort === 'string') {
442 // Silently use the best algorithm for logged in users if they chose the hot algorithm
443 if (
444 this.auth.isLoggedIn() &&
445 (sort === 'hot' || sort === '-hot')
446 ) {
447 return sort.replace('hot', 'best')
448 }
449
450 return sort
451 }
452 }
453
454 private setVideoRate (id: number, rateType: UserVideoRateType) {
455 const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate`
456 const body: UserVideoRateUpdate = {
457 rating: rateType
458 }
459
460 return this.authHttp
461 .put(url, body)
462 .pipe(catchError(err => this.restExtractor.handleError(err)))
463 }
464 }