]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/app/shared/shared-main/video/video.service.ts
Include video file id in API
[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
ad5db104
C
308 runTranscoding (videoIds: (number | string)[], type: 'hls' | 'webtorrent') {
309 const body: VideoTranscodingCreate = { transcodingType: type }
310
311 return from(videoIds)
312 .pipe(
313 concatMap(id => this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)),
314 toArray(),
315 catchError(err => this.restExtractor.handleError(err))
316 )
317 }
318
2de96f4d
C
319 loadCompleteDescription (descriptionPath: string) {
320 return this.authHttp
c199c427 321 .get<{ description: string }>(environment.apiUrl + descriptionPath)
db400f44 322 .pipe(
c199c427 323 map(res => res.description),
e4f0e92e 324 catchError(err => this.restExtractor.handleError(err))
db400f44 325 )
d38b8281
C
326 }
327
2e401e85 328 getSource (videoId: number) {
329 return this.authHttp
330 .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source')
331 .pipe(
332 catchError(err => {
333 if (err.status === 404) {
334 return of(undefined)
335 }
336
337 this.restExtractor.handleError(err)
338 })
339 )
340 }
341
7c07259a 342 setVideoLike (id: string) {
df98563e 343 return this.setVideoRate(id, 'like')
d38b8281
C
344 }
345
7c07259a 346 setVideoDislike (id: string) {
df98563e 347 return this.setVideoRate(id, 'dislike')
d38b8281
C
348 }
349
7c07259a 350 unsetVideoLike (id: string) {
57a49263
BB
351 return this.setVideoRate(id, 'none')
352 }
353
7c07259a 354 getUserVideoRating (id: string) {
334ddfa4 355 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
d38b8281 356
5fcbd898 357 return this.authHttp.get<UserVideoRate>(url)
e4f0e92e 358 .pipe(catchError(err => this.restExtractor.handleError(err)))
d38b8281
C
359 }
360
57c36b27 361 extractVideos (result: ResultList<VideoServerModel>) {
ba430d75 362 return this.serverService.getServerLocale()
2186386c
C
363 .pipe(
364 map(translations => {
365 const videosJson = result.data
366 const totalVideos = result.total
367 const videos: Video[] = []
368
369 for (const videoJson of videosJson) {
370 videos.push(new Video(videoJson, translations))
371 }
372
93cae479 373 return { total: totalVideos, data: videos }
2186386c
C
374 })
375 )
501bc6c2 376 }
57c36b27 377
b980bcff 378 explainedPrivacyLabels (serverPrivacies: VideoConstant<VideoPrivacy>[], defaultPrivacyId = VideoPrivacy.PUBLIC) {
d91714ca
C
379 const descriptions = {
380 [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`,
381 [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`,
382 [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`,
383 [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video`
384 }
385
386 const videoPrivacies = serverPrivacies.map(p => {
387 return {
388 ...p,
389
390 description: descriptions[p.id]
22a73cb8 391 }
d91714ca 392 })
8cd7faaa 393
29510651 394 return {
d91714ca
C
395 videoPrivacies,
396 defaultPrivacyId: serverPrivacies.find(p => p.id === defaultPrivacyId)?.id || serverPrivacies[0].id
29510651 397 }
8cd7faaa
C
398 }
399
a3f45a2a
C
400 getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) {
401 const order = [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC ]
402
403 for (const privacy of order) {
404 if (serverPrivacies.find(p => p.id === privacy)) {
405 return privacy
406 }
407 }
408
409 throw new Error('No highest privacy available')
410 }
411
5c20a455
C
412 nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) {
413 return nsfwPolicy === 'do_not_list'
414 ? 'false'
415 : 'both'
416 }
417
05ac4ac7 418 buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
33f6dce1
C
419 const {
420 params,
421 videoPagination,
422 sort,
2760b454
C
423 isLocal,
424 include,
33f6dce1
C
425 categoryOneOf,
426 languageOneOf,
527a52ac 427 privacyOneOf,
33f6dce1
C
428 skipCount,
429 nsfwPolicy,
430 isLive,
431 nsfw
432 } = options
433
434 const pagination = videoPagination
435 ? this.restService.componentToRestPagination(videoPagination)
436 : undefined
dd24f1bb 437
2b0d17cc 438 let newParams = this.restService.addRestGetParams(params, pagination, this.buildListSort(sort))
dd24f1bb 439
dd24f1bb
C
440 if (skipCount) newParams = newParams.set('skipCount', skipCount + '')
441
5079082d
C
442 if (isLocal !== undefined) newParams = newParams.set('isLocal', isLocal)
443 if (include !== undefined) newParams = newParams.set('include', include)
444 if (isLive !== undefined) newParams = newParams.set('isLive', isLive)
445 if (nsfw !== undefined) newParams = newParams.set('nsfw', nsfw)
446 if (nsfwPolicy !== undefined) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
447 if (languageOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
448 if (categoryOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
449 if (privacyOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'privacyOneOf', privacyOneOf)
dd24f1bb
C
450
451 return newParams
452 }
33f6dce1 453
2b0d17cc
C
454 private buildListSort (sortArg: VideoSortField | SortMeta) {
455 const sort = this.restService.buildSortString(sortArg)
456
457 if (typeof sort === 'string') {
458 // Silently use the best algorithm for logged in users if they chose the hot algorithm
459 if (
460 this.auth.isLoggedIn() &&
461 (sort === 'hot' || sort === '-hot')
462 ) {
463 return sort.replace('hot', 'best')
464 }
465
466 return sort
467 }
468 }
469
7c07259a 470 private setVideoRate (id: string, rateType: UserVideoRateType) {
05ac4ac7
C
471 const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate`
472 const body: UserVideoRateUpdate = {
473 rating: rateType
231ff4af
C
474 }
475
05ac4ac7
C
476 return this.authHttp
477 .put(url, body)
e8bffe96 478 .pipe(catchError(err => this.restExtractor.handleError(err)))
33f6dce1 479 }
dc8bc31b 480}