Merge branch 'develop' into cli-wrapper
[oweals/peertube.git] / server / models / video / video-format-utils.ts
1 import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
2 import { VideoModel } from './video'
3 import { VideoFileModel } from './video-file'
4 import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
5 import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers'
6 import { VideoCaptionModel } from './video-caption'
7 import {
8   getVideoCommentsActivityPubUrl,
9   getVideoDislikesActivityPubUrl,
10   getVideoLikesActivityPubUrl,
11   getVideoSharesActivityPubUrl
12 } from '../../lib/activitypub'
13
14 export type VideoFormattingJSONOptions = {
15   additionalAttributes: {
16     state?: boolean,
17     waitTranscoding?: boolean,
18     scheduledUpdate?: boolean,
19     blacklistInfo?: boolean
20   }
21 }
22 function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
23   const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
24   const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
25
26   const videoObject: Video = {
27     id: video.id,
28     uuid: video.uuid,
29     name: video.name,
30     category: {
31       id: video.category,
32       label: VideoModel.getCategoryLabel(video.category)
33     },
34     licence: {
35       id: video.licence,
36       label: VideoModel.getLicenceLabel(video.licence)
37     },
38     language: {
39       id: video.language,
40       label: VideoModel.getLanguageLabel(video.language)
41     },
42     privacy: {
43       id: video.privacy,
44       label: VideoModel.getPrivacyLabel(video.privacy)
45     },
46     nsfw: video.nsfw,
47     description: video.getTruncatedDescription(),
48     isLocal: video.isOwned(),
49     duration: video.duration,
50     views: video.views,
51     likes: video.likes,
52     dislikes: video.dislikes,
53     thumbnailPath: video.getThumbnailStaticPath(),
54     previewPath: video.getPreviewStaticPath(),
55     embedPath: video.getEmbedStaticPath(),
56     createdAt: video.createdAt,
57     updatedAt: video.updatedAt,
58     publishedAt: video.publishedAt,
59     account: {
60       id: formattedAccount.id,
61       uuid: formattedAccount.uuid,
62       name: formattedAccount.name,
63       displayName: formattedAccount.displayName,
64       url: formattedAccount.url,
65       host: formattedAccount.host,
66       avatar: formattedAccount.avatar
67     },
68     channel: {
69       id: formattedVideoChannel.id,
70       uuid: formattedVideoChannel.uuid,
71       name: formattedVideoChannel.name,
72       displayName: formattedVideoChannel.displayName,
73       url: formattedVideoChannel.url,
74       host: formattedVideoChannel.host,
75       avatar: formattedVideoChannel.avatar
76     }
77   }
78
79   if (options) {
80     if (options.additionalAttributes.state === true) {
81       videoObject.state = {
82         id: video.state,
83         label: VideoModel.getStateLabel(video.state)
84       }
85     }
86
87     if (options.additionalAttributes.waitTranscoding === true) {
88       videoObject.waitTranscoding = video.waitTranscoding
89     }
90
91     if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) {
92       videoObject.scheduledUpdate = {
93         updateAt: video.ScheduleVideoUpdate.updateAt,
94         privacy: video.ScheduleVideoUpdate.privacy || undefined
95       }
96     }
97
98     if (options.additionalAttributes.blacklistInfo === true) {
99       videoObject.blacklisted = !!video.VideoBlacklist
100       videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
101     }
102   }
103
104   return videoObject
105 }
106
107 function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
108   const formattedJson = video.toFormattedJSON({
109     additionalAttributes: {
110       scheduledUpdate: true,
111       blacklistInfo: true
112     }
113   })
114
115   const tags = video.Tags ? video.Tags.map(t => t.name) : []
116   const detailsJson = {
117     support: video.support,
118     descriptionPath: video.getDescriptionAPIPath(),
119     channel: video.VideoChannel.toFormattedJSON(),
120     account: video.VideoChannel.Account.toFormattedJSON(),
121     tags,
122     commentsEnabled: video.commentsEnabled,
123     waitTranscoding: video.waitTranscoding,
124     state: {
125       id: video.state,
126       label: VideoModel.getStateLabel(video.state)
127     },
128     files: []
129   }
130
131   // Format and sort video files
132   detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
133
134   return Object.assign(formattedJson, detailsJson)
135 }
136
137 function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] {
138   const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
139
140   return videoFiles
141     .map(videoFile => {
142       let resolutionLabel = videoFile.resolution + 'p'
143
144       return {
145         resolution: {
146           id: videoFile.resolution,
147           label: resolutionLabel
148         },
149         magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
150         size: videoFile.size,
151         fps: videoFile.fps,
152         torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp),
153         torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp),
154         fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp),
155         fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
156       } as VideoFile
157     })
158     .sort((a, b) => {
159       if (a.resolution.id < b.resolution.id) return 1
160       if (a.resolution.id === b.resolution.id) return 0
161       return -1
162     })
163 }
164
165 function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
166   const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
167   if (!video.Tags) video.Tags = []
168
169   const tag = video.Tags.map(t => ({
170     type: 'Hashtag' as 'Hashtag',
171     name: t.name
172   }))
173
174   let language
175   if (video.language) {
176     language = {
177       identifier: video.language,
178       name: VideoModel.getLanguageLabel(video.language)
179     }
180   }
181
182   let category
183   if (video.category) {
184     category = {
185       identifier: video.category + '',
186       name: VideoModel.getCategoryLabel(video.category)
187     }
188   }
189
190   let licence
191   if (video.licence) {
192     licence = {
193       identifier: video.licence + '',
194       name: VideoModel.getLicenceLabel(video.licence)
195     }
196   }
197
198   const url: ActivityUrlObject[] = []
199   for (const file of video.VideoFiles) {
200     url.push({
201       type: 'Link',
202       mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
203       href: video.getVideoFileUrl(file, baseUrlHttp),
204       height: file.resolution,
205       size: file.size,
206       fps: file.fps
207     })
208
209     url.push({
210       type: 'Link',
211       mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
212       href: video.getTorrentUrl(file, baseUrlHttp),
213       height: file.resolution
214     })
215
216     url.push({
217       type: 'Link',
218       mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
219       href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
220       height: file.resolution
221     })
222   }
223
224   // Add video url too
225   url.push({
226     type: 'Link',
227     mimeType: 'text/html',
228     href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
229   })
230
231   const subtitleLanguage = []
232   for (const caption of video.VideoCaptions) {
233     subtitleLanguage.push({
234       identifier: caption.language,
235       name: VideoCaptionModel.getLanguageLabel(caption.language)
236     })
237   }
238
239   return {
240     type: 'Video' as 'Video',
241     id: video.url,
242     name: video.name,
243     duration: getActivityStreamDuration(video.duration),
244     uuid: video.uuid,
245     tag,
246     category,
247     licence,
248     language,
249     views: video.views,
250     sensitive: video.nsfw,
251     waitTranscoding: video.waitTranscoding,
252     state: video.state,
253     commentsEnabled: video.commentsEnabled,
254     published: video.publishedAt.toISOString(),
255     updated: video.updatedAt.toISOString(),
256     mediaType: 'text/markdown',
257     content: video.getTruncatedDescription(),
258     support: video.support,
259     subtitleLanguage,
260     icon: {
261       type: 'Image',
262       url: video.getThumbnailUrl(baseUrlHttp),
263       mediaType: 'image/jpeg',
264       width: THUMBNAILS_SIZE.width,
265       height: THUMBNAILS_SIZE.height
266     },
267     url,
268     likes: getVideoLikesActivityPubUrl(video),
269     dislikes: getVideoDislikesActivityPubUrl(video),
270     shares: getVideoSharesActivityPubUrl(video),
271     comments: getVideoCommentsActivityPubUrl(video),
272     attributedTo: [
273       {
274         type: 'Person',
275         id: video.VideoChannel.Account.Actor.url
276       },
277       {
278         type: 'Group',
279         id: video.VideoChannel.Actor.url
280       }
281     ]
282   }
283 }
284
285 function getActivityStreamDuration (duration: number) {
286   // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
287   return 'PT' + duration + 'S'
288 }
289
290 export {
291   videoModelToFormattedJSON,
292   videoModelToFormattedDetailsJSON,
293   videoFilesModelToFormattedJSON,
294   videoModelToActivityPubObject,
295   getActivityStreamDuration
296 }