aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/shared-main/video
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared/shared-main/video')
-rw-r--r--client/src/app/shared/shared-main/video/index.ts7
-rw-r--r--client/src/app/shared/shared-main/video/redundancy.service.ts73
-rw-r--r--client/src/app/shared/shared-main/video/video-details.model.ts69
-rw-r--r--client/src/app/shared/shared-main/video/video-edit.model.ts120
-rw-r--r--client/src/app/shared/shared-main/video/video-import.service.ts100
-rw-r--r--client/src/app/shared/shared-main/video/video-ownership.service.ts64
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts188
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts380
8 files changed, 1001 insertions, 0 deletions
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts
new file mode 100644
index 000000000..3053df4ef
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/index.ts
@@ -0,0 +1,7 @@
1export * from './redundancy.service'
2export * from './video-details.model'
3export * from './video-edit.model'
4export * from './video-import.service'
5export * from './video-ownership.service'
6export * from './video.model'
7export * from './video.service'
diff --git a/client/src/app/shared/shared-main/video/redundancy.service.ts b/client/src/app/shared/shared-main/video/redundancy.service.ts
new file mode 100644
index 000000000..6e839e655
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/redundancy.service.ts
@@ -0,0 +1,73 @@
1import { SortMeta } from 'primeng/api'
2import { concat, Observable } from 'rxjs'
3import { catchError, map, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
8import { environment } from '../../../../environments/environment'
9
10@Injectable()
11export class RedundancyService {
12 static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy'
13
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) { }
19
20 updateRedundancy (host: string, redundancyAllowed: boolean) {
21 const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host
22
23 const body = { redundancyAllowed }
24
25 return this.authHttp.put(url, body)
26 .pipe(
27 map(this.restExtractor.extractDataBool),
28 catchError(err => this.restExtractor.handleError(err))
29 )
30 }
31
32 listVideoRedundancies (options: {
33 pagination: RestPagination,
34 sort: SortMeta,
35 target?: VideoRedundanciesTarget
36 }): Observable<ResultList<VideoRedundancy>> {
37 const { pagination, sort, target } = options
38
39 let params = new HttpParams()
40 params = this.restService.addRestGetParams(params, pagination, sort)
41
42 if (target) params = params.append('target', target)
43
44 return this.authHttp.get<ResultList<VideoRedundancy>>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params })
45 .pipe(
46 catchError(res => this.restExtractor.handleError(res))
47 )
48 }
49
50 addVideoRedundancy (video: Video) {
51 return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id })
52 .pipe(
53 catchError(res => this.restExtractor.handleError(res))
54 )
55 }
56
57 removeVideoRedundancies (redundancy: VideoRedundancy) {
58 const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id)
59 .concat(redundancy.redundancies.files.map(r => r.id))
60 .map(id => this.removeRedundancy(id))
61
62 return concat(...observables)
63 .pipe(toArray())
64 }
65
66 private removeRedundancy (redundancyId: number) {
67 return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId)
68 .pipe(
69 map(this.restExtractor.extractDataBool),
70 catchError(res => this.restExtractor.handleError(res))
71 )
72 }
73}
diff --git a/client/src/app/shared/shared-main/video/video-details.model.ts b/client/src/app/shared/shared-main/video/video-details.model.ts
new file mode 100644
index 000000000..a1cb051e9
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-details.model.ts
@@ -0,0 +1,69 @@
1import { Account } from '@app/shared/shared-main/account/account.model'
2import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model'
3import {
4 VideoConstant,
5 VideoDetails as VideoDetailsServerModel,
6 VideoFile,
7 VideoState,
8 VideoStreamingPlaylist,
9 VideoStreamingPlaylistType
10} from '@shared/models'
11import { Video } from './video.model'
12
13export class VideoDetails extends Video implements VideoDetailsServerModel {
14 descriptionPath: string
15 support: string
16 channel: VideoChannel
17 tags: string[]
18 files: VideoFile[]
19 account: Account
20 commentsEnabled: boolean
21 downloadEnabled: boolean
22
23 waitTranscoding: boolean
24 state: VideoConstant<VideoState>
25
26 likesPercent: number
27 dislikesPercent: number
28
29 trackerUrls: string[]
30
31 streamingPlaylists: VideoStreamingPlaylist[]
32
33 constructor (hash: VideoDetailsServerModel, translations = {}) {
34 super(hash, translations)
35
36 this.descriptionPath = hash.descriptionPath
37 this.files = hash.files
38 this.channel = new VideoChannel(hash.channel)
39 this.account = new Account(hash.account)
40 this.tags = hash.tags
41 this.support = hash.support
42 this.commentsEnabled = hash.commentsEnabled
43 this.downloadEnabled = hash.downloadEnabled
44
45 this.trackerUrls = hash.trackerUrls
46 this.streamingPlaylists = hash.streamingPlaylists
47
48 this.buildLikeAndDislikePercents()
49 }
50
51 buildLikeAndDislikePercents () {
52 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
53 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
54 }
55
56 getHlsPlaylist () {
57 return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
58 }
59
60 hasHlsPlaylist () {
61 return !!this.getHlsPlaylist()
62 }
63
64 getFiles () {
65 if (this.files.length === 0) return this.getHlsPlaylist().files
66
67 return this.files
68 }
69}
diff --git a/client/src/app/shared/shared-main/video/video-edit.model.ts b/client/src/app/shared/shared-main/video/video-edit.model.ts
new file mode 100644
index 000000000..6a529e052
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-edit.model.ts
@@ -0,0 +1,120 @@
1import { Video, VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models'
2
3export class VideoEdit implements VideoUpdate {
4 static readonly SPECIAL_SCHEDULED_PRIVACY = -1
5
6 category: number
7 licence: number
8 language: string
9 description: string
10 name: string
11 tags: string[]
12 nsfw: boolean
13 commentsEnabled: boolean
14 downloadEnabled: boolean
15 waitTranscoding: boolean
16 channelId: number
17 privacy: VideoPrivacy
18 support: string
19 thumbnailfile?: any
20 previewfile?: any
21 thumbnailUrl: string
22 previewUrl: string
23 uuid?: string
24 id?: number
25 scheduleUpdate?: VideoScheduleUpdate
26 originallyPublishedAt?: Date | string
27
28 constructor (
29 video?: Video & {
30 tags: string[],
31 commentsEnabled: boolean,
32 downloadEnabled: boolean,
33 support: string,
34 thumbnailUrl: string,
35 previewUrl: string
36 }) {
37 if (video) {
38 this.id = video.id
39 this.uuid = video.uuid
40 this.category = video.category.id
41 this.licence = video.licence.id
42 this.language = video.language.id
43 this.description = video.description
44 this.name = video.name
45 this.tags = video.tags
46 this.nsfw = video.nsfw
47 this.commentsEnabled = video.commentsEnabled
48 this.downloadEnabled = video.downloadEnabled
49 this.waitTranscoding = video.waitTranscoding
50 this.channelId = video.channel.id
51 this.privacy = video.privacy.id
52 this.support = video.support
53 this.thumbnailUrl = video.thumbnailUrl
54 this.previewUrl = video.previewUrl
55
56 this.scheduleUpdate = video.scheduledUpdate
57 this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null
58 }
59 }
60
61 patch (values: { [ id: string ]: string }) {
62 Object.keys(values).forEach((key) => {
63 this[ key ] = values[ key ]
64 })
65
66 // If schedule publication, the video is private and will be changed to public privacy
67 if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) {
68 const updateAt = new Date(values['schedulePublicationAt'])
69 updateAt.setSeconds(0)
70
71 this.privacy = VideoPrivacy.PRIVATE
72 this.scheduleUpdate = {
73 updateAt: updateAt.toISOString(),
74 privacy: VideoPrivacy.PUBLIC
75 }
76 } else {
77 this.scheduleUpdate = null
78 }
79
80 // Convert originallyPublishedAt to string so that function objectToFormData() works correctly
81 if (this.originallyPublishedAt) {
82 const originallyPublishedAt = new Date(values['originallyPublishedAt'])
83 this.originallyPublishedAt = originallyPublishedAt.toISOString()
84 }
85
86 // Use the same file than the preview for the thumbnail
87 if (this.previewfile) {
88 this.thumbnailfile = this.previewfile
89 }
90 }
91
92 toFormPatch () {
93 const json = {
94 category: this.category,
95 licence: this.licence,
96 language: this.language,
97 description: this.description,
98 support: this.support,
99 name: this.name,
100 tags: this.tags,
101 nsfw: this.nsfw,
102 commentsEnabled: this.commentsEnabled,
103 downloadEnabled: this.downloadEnabled,
104 waitTranscoding: this.waitTranscoding,
105 channelId: this.channelId,
106 privacy: this.privacy,
107 originallyPublishedAt: this.originallyPublishedAt
108 }
109
110 // Special case if we scheduled an update
111 if (this.scheduleUpdate) {
112 Object.assign(json, {
113 privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY,
114 schedulePublicationAt: new Date(this.scheduleUpdate.updateAt.toString())
115 })
116 }
117
118 return json
119 }
120}
diff --git a/client/src/app/shared/shared-main/video/video-import.service.ts b/client/src/app/shared/shared-main/video/video-import.service.ts
new file mode 100644
index 000000000..a700abacb
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-import.service.ts
@@ -0,0 +1,100 @@
1import { SortMeta } from 'primeng/api'
2import { Observable } from 'rxjs'
3import { catchError, map, switchMap } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService, ServerService, UserService } from '@app/core'
7import { objectToFormData } from '@app/helpers'
8import { peertubeTranslate, ResultList, VideoImport, VideoImportCreate, VideoUpdate } from '@shared/models'
9import { environment } from '../../../../environments/environment'
10
11@Injectable()
12export class VideoImportService {
13 private static BASE_VIDEO_IMPORT_URL = environment.apiUrl + '/api/v1/videos/imports/'
14
15 constructor (
16 private authHttp: HttpClient,
17 private restService: RestService,
18 private restExtractor: RestExtractor,
19 private serverService: ServerService
20 ) {}
21
22 importVideoUrl (targetUrl: string, video: VideoUpdate): Observable<VideoImport> {
23 const url = VideoImportService.BASE_VIDEO_IMPORT_URL
24
25 const body = this.buildImportVideoObject(video)
26 body.targetUrl = targetUrl
27
28 const data = objectToFormData(body)
29 return this.authHttp.post<VideoImport>(url, data)
30 .pipe(catchError(res => this.restExtractor.handleError(res)))
31 }
32
33 importVideoTorrent (target: string | Blob, video: VideoUpdate): Observable<VideoImport> {
34 const url = VideoImportService.BASE_VIDEO_IMPORT_URL
35 const body: VideoImportCreate = this.buildImportVideoObject(video)
36
37 if (typeof target === 'string') body.magnetUri = target
38 else body.torrentfile = target
39
40 const data = objectToFormData(body)
41 return this.authHttp.post<VideoImport>(url, data)
42 .pipe(catchError(res => this.restExtractor.handleError(res)))
43 }
44
45 getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoImport>> {
46 let params = new HttpParams()
47 params = this.restService.addRestGetParams(params, pagination, sort)
48
49 return this.authHttp
50 .get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
51 .pipe(
52 switchMap(res => this.extractVideoImports(res)),
53 map(res => this.restExtractor.convertResultListDateToHuman(res)),
54 catchError(err => this.restExtractor.handleError(err))
55 )
56 }
57
58 private buildImportVideoObject (video: VideoUpdate): VideoImportCreate {
59 const language = video.language || null
60 const licence = video.licence || null
61 const category = video.category || null
62 const description = video.description || null
63 const support = video.support || null
64 const scheduleUpdate = video.scheduleUpdate || null
65 const originallyPublishedAt = video.originallyPublishedAt || null
66
67 return {
68 name: video.name,
69 category,
70 licence,
71 language,
72 support,
73 description,
74 channelId: video.channelId,
75 privacy: video.privacy,
76 tags: video.tags,
77 nsfw: video.nsfw,
78 waitTranscoding: video.waitTranscoding,
79 commentsEnabled: video.commentsEnabled,
80 downloadEnabled: video.downloadEnabled,
81 thumbnailfile: video.thumbnailfile,
82 previewfile: video.previewfile,
83 scheduleUpdate,
84 originallyPublishedAt
85 }
86 }
87
88 private extractVideoImports (result: ResultList<VideoImport>): Observable<ResultList<VideoImport>> {
89 return this.serverService.getServerLocale()
90 .pipe(
91 map(translations => {
92 result.data.forEach(d =>
93 d.state.label = peertubeTranslate(d.state.label, translations)
94 )
95
96 return result
97 })
98 )
99 }
100}
diff --git a/client/src/app/shared/shared-main/video/video-ownership.service.ts b/client/src/app/shared/shared-main/video/video-ownership.service.ts
new file mode 100644
index 000000000..273930a6c
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-ownership.service.ts
@@ -0,0 +1,64 @@
1import { SortMeta } from 'primeng/api'
2import { Observable } from 'rxjs'
3import { catchError, map } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { ResultList, VideoChangeOwnership, VideoChangeOwnershipAccept, VideoChangeOwnershipCreate } from '@shared/models'
8import { environment } from '../../../../environments/environment'
9
10@Injectable()
11export class VideoOwnershipService {
12 private static BASE_VIDEO_CHANGE_OWNERSHIP_URL = environment.apiUrl + '/api/v1/videos/'
13
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) {
19 }
20
21 changeOwnership (id: number, username: string) {
22 const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + id + '/give-ownership'
23 const body: VideoChangeOwnershipCreate = {
24 username
25 }
26
27 return this.authHttp.post(url, body)
28 .pipe(
29 map(this.restExtractor.extractDataBool),
30 catchError(res => this.restExtractor.handleError(res))
31 )
32 }
33
34 getOwnershipChanges (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoChangeOwnership>> {
35 const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + 'ownership'
36
37 let params = new HttpParams()
38 params = this.restService.addRestGetParams(params, pagination, sort)
39
40 return this.authHttp.get<ResultList<VideoChangeOwnership>>(url, { params })
41 .pipe(
42 map(res => this.restExtractor.convertResultListDateToHuman(res)),
43 catchError(res => this.restExtractor.handleError(res))
44 )
45 }
46
47 acceptOwnership (id: number, input: VideoChangeOwnershipAccept) {
48 const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + 'ownership/' + id + '/accept'
49 return this.authHttp.post(url, input)
50 .pipe(
51 map(this.restExtractor.extractDataBool),
52 catchError(this.restExtractor.handleError)
53 )
54 }
55
56 refuseOwnership (id: number) {
57 const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + 'ownership/' + id + '/refuse'
58 return this.authHttp.post(url, {})
59 .pipe(
60 map(this.restExtractor.extractDataBool),
61 catchError(this.restExtractor.handleError)
62 )
63 }
64}
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
new file mode 100644
index 000000000..3e6d6a38d
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -0,0 +1,188 @@
1import { AuthUser } from '@app/core'
2import { User } from '@app/core/users/user.model'
3import { durationToString, getAbsoluteAPIUrl } from '@app/helpers'
4import {
5 Avatar,
6 peertubeTranslate,
7 ServerConfig,
8 UserRight,
9 Video as VideoServerModel,
10 VideoConstant,
11 VideoPrivacy,
12 VideoScheduleUpdate,
13 VideoState
14} from '@shared/models'
15import { environment } from '../../../../environments/environment'
16import { Actor } from '../account/actor.model'
17
18export class Video implements VideoServerModel {
19 byVideoChannel: string
20 byAccount: string
21
22 accountAvatarUrl: string
23 videoChannelAvatarUrl: string
24
25 createdAt: Date
26 updatedAt: Date
27 publishedAt: Date
28 originallyPublishedAt: Date | string
29 category: VideoConstant<number>
30 licence: VideoConstant<number>
31 language: VideoConstant<string>
32 privacy: VideoConstant<VideoPrivacy>
33 description: string
34 duration: number
35 durationLabel: string
36 id: number
37 uuid: string
38 isLocal: boolean
39 name: string
40 serverHost: string
41 thumbnailPath: string
42 thumbnailUrl: string
43
44 previewPath: string
45 previewUrl: string
46
47 embedPath: string
48 embedUrl: string
49
50 url?: string
51
52 views: number
53 likes: number
54 dislikes: number
55 nsfw: boolean
56
57 originInstanceUrl: string
58 originInstanceHost: string
59
60 waitTranscoding?: boolean
61 state?: VideoConstant<VideoState>
62 scheduledUpdate?: VideoScheduleUpdate
63 blacklisted?: boolean
64 blockedReason?: string
65
66 account: {
67 id: number
68 name: string
69 displayName: string
70 url: string
71 host: string
72 avatar?: Avatar
73 }
74
75 channel: {
76 id: number
77 name: string
78 displayName: string
79 url: string
80 host: string
81 avatar?: Avatar
82 }
83
84 userHistory?: {
85 currentTime: number
86 }
87
88 static buildClientUrl (videoUUID: string) {
89 return '/videos/watch/' + videoUUID
90 }
91
92 constructor (hash: VideoServerModel, translations = {}) {
93 const absoluteAPIUrl = getAbsoluteAPIUrl()
94
95 this.createdAt = new Date(hash.createdAt.toString())
96 this.publishedAt = new Date(hash.publishedAt.toString())
97 this.category = hash.category
98 this.licence = hash.licence
99 this.language = hash.language
100 this.privacy = hash.privacy
101 this.waitTranscoding = hash.waitTranscoding
102 this.state = hash.state
103 this.description = hash.description
104
105 this.duration = hash.duration
106 this.durationLabel = durationToString(hash.duration)
107
108 this.id = hash.id
109 this.uuid = hash.uuid
110
111 this.isLocal = hash.isLocal
112 this.name = hash.name
113
114 this.thumbnailPath = hash.thumbnailPath
115 this.thumbnailUrl = hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath)
116
117 this.previewPath = hash.previewPath
118 this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath)
119
120 this.embedPath = hash.embedPath
121 this.embedUrl = hash.embedUrl || (environment.embedUrl + hash.embedPath)
122
123 this.url = hash.url
124
125 this.views = hash.views
126 this.likes = hash.likes
127 this.dislikes = hash.dislikes
128
129 this.nsfw = hash.nsfw
130
131 this.account = hash.account
132 this.channel = hash.channel
133
134 this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
135 this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host)
136 this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
137 this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel)
138
139 this.category.label = peertubeTranslate(this.category.label, translations)
140 this.licence.label = peertubeTranslate(this.licence.label, translations)
141 this.language.label = peertubeTranslate(this.language.label, translations)
142 this.privacy.label = peertubeTranslate(this.privacy.label, translations)
143
144 this.scheduledUpdate = hash.scheduledUpdate
145 this.originallyPublishedAt = hash.originallyPublishedAt ? new Date(hash.originallyPublishedAt.toString()) : null
146
147 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
148
149 this.blacklisted = hash.blacklisted
150 this.blockedReason = hash.blacklistedReason
151
152 this.userHistory = hash.userHistory
153
154 this.originInstanceHost = this.account.host
155 this.originInstanceUrl = 'https://' + this.originInstanceHost
156 }
157
158 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
159 // Video is not NSFW, skip
160 if (this.nsfw === false) return false
161
162 // Return user setting if logged in
163 if (user) return user.nsfwPolicy !== 'display'
164
165 // Return default instance config
166 return serverConfig.instance.defaultNSFWPolicy !== 'display'
167 }
168
169 isRemovableBy (user: AuthUser) {
170 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
171 }
172
173 isBlockableBy (user: AuthUser) {
174 return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
175 }
176
177 isUnblockableBy (user: AuthUser) {
178 return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
179 }
180
181 isUpdatableBy (user: AuthUser) {
182 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
183 }
184
185 canBeDuplicatedBy (user: AuthUser) {
186 return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES)
187 }
188}
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
new file mode 100644
index 000000000..20d13fa10
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -0,0 +1,380 @@
1import { FfprobeData } from 'fluent-ffmpeg'
2import { Observable } from 'rxjs'
3import { catchError, map, switchMap } from 'rxjs/operators'
4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
5import { Injectable } from '@angular/core'
6import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core'
7import { objectToFormData } from '@app/helpers'
8import { I18n } from '@ngx-translate/i18n-polyfill'
9import {
10 FeedFormat,
11 NSFWPolicyType,
12 ResultList,
13 UserVideoRate,
14 UserVideoRateType,
15 UserVideoRateUpdate,
16 Video as VideoServerModel,
17 VideoConstant,
18 VideoDetails as VideoDetailsServerModel,
19 VideoFilter,
20 VideoPrivacy,
21 VideoSortField,
22 VideoUpdate
23} from '@shared/models'
24import { environment } from '../../../../environments/environment'
25import { Account, AccountService } from '../account'
26import { VideoChannel, VideoChannelService } from '../video-channel'
27import { VideoDetails } from './video-details.model'
28import { VideoEdit } from './video-edit.model'
29import { Video } from './video.model'
30
31export interface VideosProvider {
32 getVideos (parameters: {
33 videoPagination: ComponentPaginationLight,
34 sort: VideoSortField,
35 filter?: VideoFilter,
36 categoryOneOf?: number[],
37 languageOneOf?: string[]
38 nsfwPolicy: NSFWPolicyType
39 }): Observable<ResultList<Video>>
40}
41
42@Injectable()
43export class VideoService implements VideosProvider {
44 static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
45 static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.'
46
47 constructor (
48 private authHttp: HttpClient,
49 private restExtractor: RestExtractor,
50 private restService: RestService,
51 private serverService: ServerService,
52 private i18n: I18n
53 ) {}
54
55 getVideoViewUrl (uuid: string) {
56 return VideoService.BASE_VIDEO_URL + uuid + '/views'
57 }
58
59 getUserWatchingVideoUrl (uuid: string) {
60 return VideoService.BASE_VIDEO_URL + uuid + '/watching'
61 }
62
63 getVideo (options: { videoId: string }): Observable<VideoDetails> {
64 return this.serverService.getServerLocale()
65 .pipe(
66 switchMap(translations => {
67 return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + options.videoId)
68 .pipe(map(videoHash => ({ videoHash, translations })))
69 }),
70 map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
71 catchError(err => this.restExtractor.handleError(err))
72 )
73 }
74
75 updateVideo (video: VideoEdit) {
76 const language = video.language || null
77 const licence = video.licence || null
78 const category = video.category || null
79 const description = video.description || null
80 const support = video.support || null
81 const scheduleUpdate = video.scheduleUpdate || null
82 const originallyPublishedAt = video.originallyPublishedAt || null
83
84 const body: VideoUpdate = {
85 name: video.name,
86 category,
87 licence,
88 language,
89 support,
90 description,
91 channelId: video.channelId,
92 privacy: video.privacy,
93 tags: video.tags,
94 nsfw: video.nsfw,
95 waitTranscoding: video.waitTranscoding,
96 commentsEnabled: video.commentsEnabled,
97 downloadEnabled: video.downloadEnabled,
98 thumbnailfile: video.thumbnailfile,
99 previewfile: video.previewfile,
100 scheduleUpdate,
101 originallyPublishedAt
102 }
103
104 const data = objectToFormData(body)
105
106 return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, data)
107 .pipe(
108 map(this.restExtractor.extractDataBool),
109 catchError(err => this.restExtractor.handleError(err))
110 )
111 }
112
113 uploadVideo (video: FormData) {
114 const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
115
116 return this.authHttp
117 .request<{ video: { id: number, uuid: string } }>(req)
118 .pipe(catchError(err => this.restExtractor.handleError(err)))
119 }
120
121 getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable<ResultList<Video>> {
122 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
123
124 let params = new HttpParams()
125 params = this.restService.addRestGetParams(params, pagination, sort)
126 params = this.restService.addObjectParams(params, { search })
127
128 return this.authHttp
129 .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
130 .pipe(
131 switchMap(res => this.extractVideos(res)),
132 catchError(err => this.restExtractor.handleError(err))
133 )
134 }
135
136 getAccountVideos (
137 account: Account,
138 videoPagination: ComponentPaginationLight,
139 sort: VideoSortField
140 ): Observable<ResultList<Video>> {
141 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
142
143 let params = new HttpParams()
144 params = this.restService.addRestGetParams(params, pagination, sort)
145
146 return this.authHttp
147 .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
148 .pipe(
149 switchMap(res => this.extractVideos(res)),
150 catchError(err => this.restExtractor.handleError(err))
151 )
152 }
153
154 getVideoChannelVideos (
155 videoChannel: VideoChannel,
156 videoPagination: ComponentPaginationLight,
157 sort: VideoSortField,
158 nsfwPolicy?: NSFWPolicyType
159 ): Observable<ResultList<Video>> {
160 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
161
162 let params = new HttpParams()
163 params = this.restService.addRestGetParams(params, pagination, sort)
164
165 if (nsfwPolicy) {
166 params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
167 }
168
169 return this.authHttp
170 .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
171 .pipe(
172 switchMap(res => this.extractVideos(res)),
173 catchError(err => this.restExtractor.handleError(err))
174 )
175 }
176
177 getVideos (parameters: {
178 videoPagination: ComponentPaginationLight,
179 sort: VideoSortField,
180 filter?: VideoFilter,
181 categoryOneOf?: number[],
182 languageOneOf?: string[],
183 skipCount?: boolean,
184 nsfwPolicy?: NSFWPolicyType
185 }): Observable<ResultList<Video>> {
186 const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy } = parameters
187
188 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
189
190 let params = new HttpParams()
191 params = this.restService.addRestGetParams(params, pagination, sort)
192
193 if (filter) params = params.set('filter', filter)
194 if (skipCount) params = params.set('skipCount', skipCount + '')
195
196 if (nsfwPolicy) {
197 params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
198 }
199
200 if (languageOneOf) {
201 for (const l of languageOneOf) {
202 params = params.append('languageOneOf[]', l)
203 }
204 }
205
206 if (categoryOneOf) {
207 for (const c of categoryOneOf) {
208 params = params.append('categoryOneOf[]', c + '')
209 }
210 }
211
212 return this.authHttp
213 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
214 .pipe(
215 switchMap(res => this.extractVideos(res)),
216 catchError(err => this.restExtractor.handleError(err))
217 )
218 }
219
220 buildBaseFeedUrls (params: HttpParams) {
221 const feeds = [
222 {
223 format: FeedFormat.RSS,
224 label: 'rss 2.0',
225 url: VideoService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase()
226 },
227 {
228 format: FeedFormat.ATOM,
229 label: 'atom 1.0',
230 url: VideoService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase()
231 },
232 {
233 format: FeedFormat.JSON,
234 label: 'json 1.0',
235 url: VideoService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase()
236 }
237 ]
238
239 if (params && params.keys().length !== 0) {
240 for (const feed of feeds) {
241 feed.url += '?' + params.toString()
242 }
243 }
244
245 return feeds
246 }
247
248 getVideoFeedUrls (sort: VideoSortField, filter?: VideoFilter, categoryOneOf?: number[]) {
249 let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort)
250
251 if (filter) params = params.set('filter', filter)
252
253 if (categoryOneOf) {
254 for (const c of categoryOneOf) {
255 params = params.append('categoryOneOf[]', c + '')
256 }
257 }
258
259 return this.buildBaseFeedUrls(params)
260 }
261
262 getAccountFeedUrls (accountId: number) {
263 let params = this.restService.addRestGetParams(new HttpParams())
264 params = params.set('accountId', accountId.toString())
265
266 return this.buildBaseFeedUrls(params)
267 }
268
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
276 getVideoFileMetadata (metadataUrl: string) {
277 return this.authHttp
278 .get<FfprobeData>(metadataUrl)
279 .pipe(
280 catchError(err => this.restExtractor.handleError(err))
281 )
282 }
283
284 removeVideo (id: number) {
285 return this.authHttp
286 .delete(VideoService.BASE_VIDEO_URL + id)
287 .pipe(
288 map(this.restExtractor.extractDataBool),
289 catchError(err => this.restExtractor.handleError(err))
290 )
291 }
292
293 loadCompleteDescription (descriptionPath: string) {
294 return this.authHttp
295 .get<{ description: string }>(environment.apiUrl + descriptionPath)
296 .pipe(
297 map(res => res.description),
298 catchError(err => this.restExtractor.handleError(err))
299 )
300 }
301
302 setVideoLike (id: number) {
303 return this.setVideoRate(id, 'like')
304 }
305
306 setVideoDislike (id: number) {
307 return this.setVideoRate(id, 'dislike')
308 }
309
310 unsetVideoLike (id: number) {
311 return this.setVideoRate(id, 'none')
312 }
313
314 getUserVideoRating (id: number) {
315 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
316
317 return this.authHttp.get<UserVideoRate>(url)
318 .pipe(catchError(err => this.restExtractor.handleError(err)))
319 }
320
321 extractVideos (result: ResultList<VideoServerModel>) {
322 return this.serverService.getServerLocale()
323 .pipe(
324 map(translations => {
325 const videosJson = result.data
326 const totalVideos = result.total
327 const videos: Video[] = []
328
329 for (const videoJson of videosJson) {
330 videos.push(new Video(videoJson, translations))
331 }
332
333 return { total: totalVideos, data: videos }
334 })
335 )
336 }
337
338 explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[]) {
339 const base = [
340 {
341 id: VideoPrivacy.PRIVATE,
342 label: this.i18n('Only I can see this video')
343 },
344 {
345 id: VideoPrivacy.UNLISTED,
346 label: this.i18n('Only people with the private link can see this video')
347 },
348 {
349 id: VideoPrivacy.PUBLIC,
350 label: this.i18n('Anyone can see this video')
351 },
352 {
353 id: VideoPrivacy.INTERNAL,
354 label: this.i18n('Only users of this instance can see this video')
355 }
356 ]
357
358 return base.filter(o => !!privacies.find(p => p.id === o.id))
359 }
360
361 nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) {
362 return nsfwPolicy === 'do_not_list'
363 ? 'false'
364 : 'both'
365 }
366
367 private setVideoRate (id: number, rateType: UserVideoRateType) {
368 const url = VideoService.BASE_VIDEO_URL + id + '/rate'
369 const body: UserVideoRateUpdate = {
370 rating: rateType
371 }
372
373 return this.authHttp
374 .put(url, body)
375 .pipe(
376 map(this.restExtractor.extractDataBool),
377 catchError(err => this.restExtractor.handleError(err))
378 )
379 }
380}