]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/app/shared/shared-main/video/video.service.ts
Fix select in share modal
[github/Chocobozzz/PeerTube.git] / client / src / app / shared / shared-main / video / video.service.ts
CommitLineData
33f6dce1
C
1import { SortMeta } from 'primeng/api'
2import { from, Observable } from 'rxjs'
3import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
29510651 4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
202f6b6c 5import { Injectable } from '@angular/core'
33f6dce1 6import { ComponentPaginationLight, RestExtractor, RestPagination, RestService, ServerService, UserService } from '@app/core'
67ed6552 7import { objectToFormData } from '@app/helpers'
231ff4af 8import { AdvancedInputFilter } from '@app/shared/shared-forms'
8cd7faaa 9import {
dd24f1bb 10 BooleanBothQuery,
67ed6552
C
11 FeedFormat,
12 NSFWPolicyType,
13 ResultList,
8cd7faaa 14 UserVideoRate,
5c6d985f 15 UserVideoRateType,
8cd7faaa 16 UserVideoRateUpdate,
67ed6552 17 Video as VideoServerModel,
978c87e7 18 VideoChannel as VideoChannelServerModel,
8cd7faaa 19 VideoConstant,
67ed6552 20 VideoDetails as VideoDetailsServerModel,
66357162 21 VideoFileMetadata,
2760b454 22 VideoInclude,
8cd7faaa 23 VideoPrivacy,
67ed6552 24 VideoSortField,
5beb89f2 25 VideoUpdate
67ed6552
C
26} from '@shared/models'
27import { environment } from '../../../../environments/environment'
b4c3c51d
C
28import { Account } from '../account/account.model'
29import { AccountService } from '../account/account.service'
67ed6552 30import { VideoChannel, VideoChannelService } from '../video-channel'
404b54e1
C
31import { VideoDetails } from './video-details.model'
32import { VideoEdit } from './video-edit.model'
202f6b6c 33import { Video } from './video.model'
dc8bc31b 34
dd24f1bb 35export type CommonVideoParams = {
33f6dce1
C
36 videoPagination?: ComponentPaginationLight
37 sort: VideoSortField | SortMeta
2760b454
C
38 include?: VideoInclude
39 isLocal?: boolean
dd24f1bb
C
40 categoryOneOf?: number[]
41 languageOneOf?: string[]
42 isLive?: boolean
43 skipCount?: boolean
2760b454 44
dd24f1bb
C
45 // FIXME: remove?
46 nsfwPolicy?: NSFWPolicyType
47 nsfw?: BooleanBothQuery
7f5f4152
BJ
48}
49
dc8bc31b 50@Injectable()
dd24f1bb 51export class VideoService {
7e7d8e48 52 static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos'
40e87e9e 53 static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.'
5beb89f2 54 static BASE_SUBSCRIPTION_FEEDS_URL = environment.apiUrl + '/feeds/subscriptions.'
dc8bc31b 55
df98563e 56 constructor (
d592e0a9 57 private authHttp: HttpClient,
de59c48f 58 private restExtractor: RestExtractor,
7ce44a74 59 private restService: RestService,
5beb89f2 60 private serverService: ServerService
4fd8aa32
C
61 ) {}
62
8cac1b64 63 getVideoViewUrl (uuid: string) {
231ff4af 64 return `${VideoService.BASE_VIDEO_URL}/${uuid}/views`
8cac1b64
C
65 }
66
6e46de09 67 getUserWatchingVideoUrl (uuid: string) {
231ff4af 68 return `${VideoService.BASE_VIDEO_URL}/${uuid}/watching`
6e46de09
C
69 }
70
93cae479 71 getVideo (options: { videoId: string }): Observable<VideoDetails> {
ba430d75 72 return this.serverService.getServerLocale()
db400f44 73 .pipe(
7ce44a74 74 switchMap(translations => {
231ff4af 75 return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`)
74b7c6d4 76 .pipe(map(videoHash => ({ videoHash, translations })))
7ce44a74
C
77 }),
78 map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
e4f0e92e 79 catchError(err => this.restExtractor.handleError(err))
db400f44 80 )
4fd8aa32 81 }
dc8bc31b 82
404b54e1 83 updateVideo (video: VideoEdit) {
360329cc
C
84 const language = video.language || null
85 const licence = video.licence || null
86 const category = video.category || null
87 const description = video.description || null
88 const support = video.support || null
e94fc297 89 const scheduleUpdate = video.scheduleUpdate || null
1e74f19a 90 const originallyPublishedAt = video.originallyPublishedAt || null
c24ac1c1 91
4771e000 92 const body: VideoUpdate = {
d8e689b8 93 name: video.name,
cadb46d8
C
94 category,
95 licence,
c24ac1c1 96 language,
3bf1ec2e 97 support,
cadb46d8 98 description,
0f320037 99 channelId: video.channelId,
fd45e8f4 100 privacy: video.privacy,
4771e000 101 tags: video.tags,
47564bbe 102 nsfw: video.nsfw,
2186386c 103 waitTranscoding: video.waitTranscoding,
6de36768 104 commentsEnabled: video.commentsEnabled,
7f2cfe3a 105 downloadEnabled: video.downloadEnabled,
6de36768 106 thumbnailfile: video.thumbnailfile,
bbe0f064 107 previewfile: video.previewfile,
7294aab0 108 pluginData: video.pluginData,
1e74f19a 109 scheduleUpdate,
110 originallyPublishedAt
df98563e 111 }
c24ac1c1 112
6de36768
C
113 const data = objectToFormData(body)
114
231ff4af 115 return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, data)
db400f44
C
116 .pipe(
117 map(this.restExtractor.extractDataBool),
e4f0e92e 118 catchError(err => this.restExtractor.handleError(err))
db400f44 119 )
d8e689b8
C
120 }
121
db7af09b 122 uploadVideo (video: FormData) {
231ff4af 123 const req = new HttpRequest('POST', `${VideoService.BASE_VIDEO_URL}/upload`, video, { reportProgress: true })
bfb3a98f 124
fd45e8f4 125 return this.authHttp
2186386c 126 .request<{ video: { id: number, uuid: string } }>(req)
e4f0e92e 127 .pipe(catchError(err => this.restExtractor.handleError(err)))
bfb3a98f
C
128 }
129
978c87e7
C
130 getMyVideos (options: {
131 videoPagination: ComponentPaginationLight
132 sort: VideoSortField
133 userChannels?: VideoChannelServerModel[]
134 search?: string
135 }): Observable<ResultList<Video>> {
136 const { videoPagination, sort, userChannels = [], search } = options
137
4beda9e1 138 const pagination = this.restService.componentToRestPagination(videoPagination)
cf20596c 139
d592e0a9
C
140 let params = new HttpParams()
141 params = this.restService.addRestGetParams(params, pagination, sort)
1fd61899
C
142
143 if (search) {
144 const filters = this.restService.parseQueryStringFilter(search, {
145 isLive: {
146 prefix: 'isLive:',
1a7d0887 147 isBoolean: true
978c87e7
C
148 },
149 channelId: {
150 prefix: 'channel:',
151 handler: (name: string) => {
152 const channel = userChannels.find(c => c.name === name)
153
154 if (channel) return channel.id
155
156 return undefined
157 }
1fd61899
C
158 }
159 })
160
161 params = this.restService.addObjectParams(params, filters)
162 }
dc8bc31b 163
7ce44a74 164 return this.authHttp
bf64ed41 165 .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
db400f44 166 .pipe(
7ce44a74 167 switchMap(res => this.extractVideos(res)),
e4f0e92e 168 catchError(err => this.restExtractor.handleError(err))
db400f44 169 )
dc8bc31b
C
170 }
171
dd24f1bb 172 getAccountVideos (parameters: CommonVideoParams & {
9df52d66 173 account: Pick<Account, 'nameWithHost'>
37024082 174 search?: string
0aa52e17 175 }): Observable<ResultList<Video>> {
dd24f1bb 176 const { account, search } = parameters
0626e7af
C
177
178 let params = new HttpParams()
dd24f1bb 179 params = this.buildCommonVideosParams({ params, ...parameters })
0aa52e17 180
dd24f1bb 181 if (search) params = params.set('search', search)
37024082 182
0626e7af 183 return this.authHttp
7ce44a74 184 .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
db400f44 185 .pipe(
7ce44a74 186 switchMap(res => this.extractVideos(res)),
e4f0e92e 187 catchError(err => this.restExtractor.handleError(err))
db400f44 188 )
0626e7af
C
189 }
190
dd24f1bb 191 getVideoChannelVideos (parameters: CommonVideoParams & {
9df52d66 192 videoChannel: Pick<VideoChannel, 'nameWithHost'>
0aa52e17 193 }): Observable<ResultList<Video>> {
dd24f1bb 194 const { videoChannel } = parameters
170726f5
C
195
196 let params = new HttpParams()
dd24f1bb 197 params = this.buildCommonVideosParams({ params, ...parameters })
0aa52e17 198
170726f5 199 return this.authHttp
f5b0af50 200 .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
db400f44 201 .pipe(
7ce44a74 202 switchMap(res => this.extractVideos(res)),
22a16e36
C
203 catchError(err => this.restExtractor.handleError(err))
204 )
205 }
206
33f6dce1 207 getAdminVideos (
231ff4af 208 options: CommonVideoParams & { pagination: RestPagination, search?: string }
33f6dce1 209 ): Observable<ResultList<Video>> {
231ff4af 210 const { pagination, search } = options
2760b454 211
33f6dce1 212 let params = new HttpParams()
231ff4af 213 params = this.buildCommonVideosParams({ params, ...options })
33f6dce1
C
214
215 params = params.set('start', pagination.start.toString())
216 .set('count', pagination.count.toString())
217
231ff4af 218 params = this.buildAdminParamsFromSearch(search, params)
33f6dce1 219
33f6dce1
C
220 return this.authHttp
221 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
222 .pipe(
223 switchMap(res => this.extractVideos(res)),
224 catchError(err => this.restExtractor.handleError(err))
225 )
226 }
227
dd24f1bb 228 getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> {
fd45e8f4 229 let params = new HttpParams()
dd24f1bb 230 params = this.buildCommonVideosParams({ params, ...parameters })
5c20a455 231
fd45e8f4 232 return this.authHttp
7ce44a74 233 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
db400f44 234 .pipe(
7ce44a74 235 switchMap(res => this.extractVideos(res)),
e4f0e92e 236 catchError(err => this.restExtractor.handleError(err))
db400f44 237 )
fd45e8f4
C
238 }
239
5beb89f2 240 buildBaseFeedUrls (params: HttpParams, base = VideoService.BASE_FEEDS_URL) {
cc1561f9
C
241 const feeds = [
242 {
39ba2e8e 243 format: FeedFormat.RSS,
2d011d94 244 label: 'media rss 2.0',
5beb89f2 245 url: base + FeedFormat.RSS.toLowerCase()
cc1561f9
C
246 },
247 {
39ba2e8e 248 format: FeedFormat.ATOM,
cc1561f9 249 label: 'atom 1.0',
5beb89f2 250 url: base + FeedFormat.ATOM.toLowerCase()
cc1561f9
C
251 },
252 {
39ba2e8e 253 format: FeedFormat.JSON,
cc1561f9 254 label: 'json 1.0',
5beb89f2 255 url: base + FeedFormat.JSON.toLowerCase()
cc1561f9
C
256 }
257 ]
258
7b87d2d5
C
259 if (params && params.keys().length !== 0) {
260 for (const feed of feeds) {
261 feed.url += '?' + params.toString()
262 }
263 }
264
cc1561f9 265 return feeds
244e76a5
RK
266 }
267
2760b454 268 getVideoFeedUrls (sort: VideoSortField, isLocal: boolean, categoryOneOf?: number[]) {
7b87d2d5 269 let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort)
244e76a5 270
2760b454 271 if (isLocal) params = params.set('isLocal', isLocal)
cc1561f9 272
5c20a455
C
273 if (categoryOneOf) {
274 for (const c of categoryOneOf) {
275 params = params.append('categoryOneOf[]', c + '')
276 }
277 }
61b909b9 278
7b87d2d5 279 return this.buildBaseFeedUrls(params)
244e76a5
RK
280 }
281
cc1561f9 282 getAccountFeedUrls (accountId: number) {
244e76a5 283 let params = this.restService.addRestGetParams(new HttpParams())
244e76a5 284 params = params.set('accountId', accountId.toString())
cc1561f9 285
7b87d2d5 286 return this.buildBaseFeedUrls(params)
244e76a5
RK
287 }
288
170726f5
C
289 getVideoChannelFeedUrls (videoChannelId: number) {
290 let params = this.restService.addRestGetParams(new HttpParams())
291 params = params.set('videoChannelId', videoChannelId.toString())
292
293 return this.buildBaseFeedUrls(params)
294 }
295
5beb89f2 296 getVideoSubscriptionFeedUrls (accountId: number, feedToken: string) {
afff310e
RK
297 let params = this.restService.addRestGetParams(new HttpParams())
298 params = params.set('accountId', accountId.toString())
afff310e
RK
299 params = params.set('token', feedToken)
300
5beb89f2 301 return this.buildBaseFeedUrls(params, VideoService.BASE_SUBSCRIPTION_FEEDS_URL)
afff310e
RK
302 }
303
8319d6ae
RK
304 getVideoFileMetadata (metadataUrl: string) {
305 return this.authHttp
583eb04b 306 .get<VideoFileMetadata>(metadataUrl)
8319d6ae
RK
307 .pipe(
308 catchError(err => this.restExtractor.handleError(err))
309 )
310 }
311
33f6dce1
C
312 removeVideo (idArg: number | number[]) {
313 const ids = Array.isArray(idArg) ? idArg : [ idArg ]
314
315 return from(ids)
316 .pipe(
231ff4af 317 concatMap(id => this.authHttp.delete(`${VideoService.BASE_VIDEO_URL}/${id}`)),
33f6dce1
C
318 toArray(),
319 catchError(err => this.restExtractor.handleError(err))
320 )
4fd8aa32
C
321 }
322
2de96f4d
C
323 loadCompleteDescription (descriptionPath: string) {
324 return this.authHttp
c199c427 325 .get<{ description: string }>(environment.apiUrl + descriptionPath)
db400f44 326 .pipe(
c199c427 327 map(res => res.description),
e4f0e92e 328 catchError(err => this.restExtractor.handleError(err))
db400f44 329 )
d38b8281
C
330 }
331
0a6658fd 332 setVideoLike (id: number) {
df98563e 333 return this.setVideoRate(id, 'like')
d38b8281
C
334 }
335
0a6658fd 336 setVideoDislike (id: number) {
df98563e 337 return this.setVideoRate(id, 'dislike')
d38b8281
C
338 }
339
57a49263
BB
340 unsetVideoLike (id: number) {
341 return this.setVideoRate(id, 'none')
342 }
343
5fcbd898 344 getUserVideoRating (id: number) {
334ddfa4 345 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
d38b8281 346
5fcbd898 347 return this.authHttp.get<UserVideoRate>(url)
e4f0e92e 348 .pipe(catchError(err => this.restExtractor.handleError(err)))
d38b8281
C
349 }
350
57c36b27 351 extractVideos (result: ResultList<VideoServerModel>) {
ba430d75 352 return this.serverService.getServerLocale()
2186386c
C
353 .pipe(
354 map(translations => {
355 const videosJson = result.data
356 const totalVideos = result.total
357 const videos: Video[] = []
358
359 for (const videoJson of videosJson) {
360 videos.push(new Video(videoJson, translations))
361 }
362
93cae479 363 return { total: totalVideos, data: videos }
2186386c
C
364 })
365 )
501bc6c2 366 }
57c36b27 367
b980bcff 368 explainedPrivacyLabels (serverPrivacies: VideoConstant<VideoPrivacy>[], defaultPrivacyId = VideoPrivacy.PUBLIC) {
d91714ca
C
369 const descriptions = {
370 [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`,
371 [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`,
372 [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`,
373 [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video`
374 }
375
376 const videoPrivacies = serverPrivacies.map(p => {
377 return {
378 ...p,
379
380 description: descriptions[p.id]
22a73cb8 381 }
d91714ca 382 })
8cd7faaa 383
29510651 384 return {
d91714ca
C
385 videoPrivacies,
386 defaultPrivacyId: serverPrivacies.find(p => p.id === defaultPrivacyId)?.id || serverPrivacies[0].id
29510651 387 }
8cd7faaa
C
388 }
389
a3f45a2a
C
390 getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) {
391 const order = [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC ]
392
393 for (const privacy of order) {
394 if (serverPrivacies.find(p => p.id === privacy)) {
395 return privacy
396 }
397 }
398
399 throw new Error('No highest privacy available')
400 }
401
5c20a455
C
402 nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) {
403 return nsfwPolicy === 'do_not_list'
404 ? 'false'
405 : 'both'
406 }
407
5c6d985f 408 private setVideoRate (id: number, rateType: UserVideoRateType) {
231ff4af 409 const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate`
57c36b27
C
410 const body: UserVideoRateUpdate = {
411 rating: rateType
412 }
413
414 return this.authHttp
415 .put(url, body)
416 .pipe(
417 map(this.restExtractor.extractDataBool),
418 catchError(err => this.restExtractor.handleError(err))
419 )
420 }
dd24f1bb
C
421
422 private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
33f6dce1
C
423 const {
424 params,
425 videoPagination,
426 sort,
2760b454
C
427 isLocal,
428 include,
33f6dce1
C
429 categoryOneOf,
430 languageOneOf,
431 skipCount,
432 nsfwPolicy,
433 isLive,
434 nsfw
435 } = options
436
437 const pagination = videoPagination
438 ? this.restService.componentToRestPagination(videoPagination)
439 : undefined
dd24f1bb 440
dd24f1bb
C
441 let newParams = this.restService.addRestGetParams(params, pagination, sort)
442
dd24f1bb
C
443 if (skipCount) newParams = newParams.set('skipCount', skipCount + '')
444
2760b454
C
445 if (isLocal) newParams = newParams.set('isLocal', isLocal)
446 if (include) newParams = newParams.set('include', include)
dd24f1bb
C
447 if (isLive) newParams = newParams.set('isLive', isLive)
448 if (nsfw) newParams = newParams.set('nsfw', nsfw)
449 if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
450 if (languageOneOf) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
451 if (categoryOneOf) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
452
453 return newParams
454 }
33f6dce1 455
231ff4af
C
456 buildAdminInputFilter (): AdvancedInputFilter[] {
457 return [
458 {
459 title: $localize`Videos scope`,
460 children: [
461 {
462 queryParams: { search: 'isLocal:false' },
463 label: $localize`Remote videos`
464 },
465 {
466 queryParams: { search: 'isLocal:true' },
467 label: $localize`Local videos`
468 }
469 ]
470 },
471
472 {
473 title: $localize`Include/Exclude`,
474 children: [
475 {
476 queryParams: { search: 'excludeMuted' },
477 label: $localize`Exclude muted accounts`
478 }
479 ]
480 }
481 ]
482 }
483
33f6dce1 484 private buildAdminParamsFromSearch (search: string, params: HttpParams) {
231ff4af
C
485 let include = VideoInclude.BLACKLISTED |
486 VideoInclude.BLOCKED_OWNER |
487 VideoInclude.HIDDEN_PRIVACY |
488 VideoInclude.NOT_PUBLISHED_STATE |
489 VideoInclude.FILES
490
491 if (!search) return this.restService.addObjectParams(params, { include })
492
33f6dce1 493 const filters = this.restService.parseQueryStringFilter(search, {
2760b454
C
494 isLocal: {
495 prefix: 'isLocal:',
496 isBoolean: true
231ff4af
C
497 },
498 excludeMuted: {
499 prefix: 'excludeMuted',
500 handler: () => true
33f6dce1
C
501 }
502 })
503
231ff4af
C
504 if (filters.excludeMuted) {
505 include &= ~VideoInclude.BLOCKED_OWNER
506
507 filters.excludeMuted = undefined
508 }
509
510 return this.restService.addObjectParams(params, { ...filters, include })
33f6dce1 511 }
dc8bc31b 512}