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