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