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