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