Add ability to list all local videos
[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       href: video.getVideoFileUrl(file, baseUrlHttp),
212       height: file.resolution,
213       size: file.size,
214       fps: file.fps
215     })
216
217     url.push({
218       type: 'Link',
219       mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
220       href: video.getTorrentUrl(file, baseUrlHttp),
221       height: file.resolution
222     })
223
224     url.push({
225       type: 'Link',
226       mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
227       href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
228       height: file.resolution
229     })
230   }
231
232   // Add video url too
233   url.push({
234     type: 'Link',
235     mimeType: 'text/html',
236     href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
237   })
238
239   const subtitleLanguage = []
240   for (const caption of video.VideoCaptions) {
241     subtitleLanguage.push({
242       identifier: caption.language,
243       name: VideoCaptionModel.getLanguageLabel(caption.language)
244     })
245   }
246
247   return {
248     type: 'Video' as 'Video',
249     id: video.url,
250     name: video.name,
251     duration: getActivityStreamDuration(video.duration),
252     uuid: video.uuid,
253     tag,
254     category,
255     licence,
256     language,
257     views: video.views,
258     sensitive: video.nsfw,
259     waitTranscoding: video.waitTranscoding,
260     state: video.state,
261     commentsEnabled: video.commentsEnabled,
262     published: video.publishedAt.toISOString(),
263     updated: video.updatedAt.toISOString(),
264     mediaType: 'text/markdown',
265     content: video.getTruncatedDescription(),
266     support: video.support,
267     subtitleLanguage,
268     icon: {
269       type: 'Image',
270       url: video.getThumbnailUrl(baseUrlHttp),
271       mediaType: 'image/jpeg',
272       width: THUMBNAILS_SIZE.width,
273       height: THUMBNAILS_SIZE.height
274     },
275     url,
276     likes: getVideoLikesActivityPubUrl(video),
277     dislikes: getVideoDislikesActivityPubUrl(video),
278     shares: getVideoSharesActivityPubUrl(video),
279     comments: getVideoCommentsActivityPubUrl(video),
280     attributedTo: [
281       {
282         type: 'Person',
283         id: video.VideoChannel.Account.Actor.url
284       },
285       {
286         type: 'Group',
287         id: video.VideoChannel.Actor.url
288       }
289     ]
290   }
291 }
292
293 function getActivityStreamDuration (duration: number) {
294   // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
295   return 'PT' + duration + 'S'
296 }
297
298 export {
299   videoModelToFormattedJSON,
300   videoModelToFormattedDetailsJSON,
301   videoFilesModelToFormattedJSON,
302   videoModelToActivityPubObject,
303   getActivityStreamDuration
304 }