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