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