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