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