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