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