diff options
author | Chocobozzz <me@florianbigard.com> | 2023-06-05 13:22:16 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-06-29 10:18:00 +0200 |
commit | d896fef7e21cacfb44967eb9059fe543a66f5dd0 (patch) | |
tree | 153f0810cb4137b8df04bd94053f6a4f0db24b56 /server/models | |
parent | 638a2950215af1f11be8e8bdd136ca12e5176c32 (diff) | |
download | PeerTube-d896fef7e21cacfb44967eb9059fe543a66f5dd0.tar.gz PeerTube-d896fef7e21cacfb44967eb9059fe543a66f5dd0.tar.zst PeerTube-d896fef7e21cacfb44967eb9059fe543a66f5dd0.zip |
Refactor video formatter
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/video/formatter/index.ts | 2 | ||||
-rw-r--r-- | server/models/video/formatter/shared/index.ts | 1 | ||||
-rw-r--r-- | server/models/video/formatter/shared/video-format-utils.ts | 7 | ||||
-rw-r--r-- | server/models/video/formatter/video-activity-pub-format.ts | 295 | ||||
-rw-r--r-- | server/models/video/formatter/video-api-format.ts | 304 | ||||
-rw-r--r-- | server/models/video/formatter/video-format-utils.ts | 561 | ||||
-rw-r--r-- | server/models/video/video.ts | 4 |
7 files changed, 611 insertions, 563 deletions
diff --git a/server/models/video/formatter/index.ts b/server/models/video/formatter/index.ts new file mode 100644 index 000000000..77b406559 --- /dev/null +++ b/server/models/video/formatter/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './video-activity-pub-format' | ||
2 | export * from './video-api-format' | ||
diff --git a/server/models/video/formatter/shared/index.ts b/server/models/video/formatter/shared/index.ts new file mode 100644 index 000000000..d558fa7d6 --- /dev/null +++ b/server/models/video/formatter/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-format-utils' | |||
diff --git a/server/models/video/formatter/shared/video-format-utils.ts b/server/models/video/formatter/shared/video-format-utils.ts new file mode 100644 index 000000000..df3bbdf1c --- /dev/null +++ b/server/models/video/formatter/shared/video-format-utils.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import { MVideoFile } from '@server/types/models' | ||
2 | |||
3 | export function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | ||
4 | if (fileA.resolution < fileB.resolution) return 1 | ||
5 | if (fileA.resolution === fileB.resolution) return 0 | ||
6 | return -1 | ||
7 | } | ||
diff --git a/server/models/video/formatter/video-activity-pub-format.ts b/server/models/video/formatter/video-activity-pub-format.ts new file mode 100644 index 000000000..c0d3d5f3e --- /dev/null +++ b/server/models/video/formatter/video-activity-pub-format.ts | |||
@@ -0,0 +1,295 @@ | |||
1 | |||
2 | import { isArray } from 'lodash' | ||
3 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
4 | import { getActivityStreamDuration } from '@server/lib/activitypub/activity' | ||
5 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
6 | import { | ||
7 | ActivityIconObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityPubStoryboard, | ||
10 | ActivityTagObject, | ||
11 | ActivityTrackerUrlObject, | ||
12 | ActivityUrlObject, | ||
13 | VideoObject | ||
14 | } from '@shared/models' | ||
15 | import { MIMETYPES, WEBSERVER } from '../../../initializers/constants' | ||
16 | import { | ||
17 | getLocalVideoCommentsActivityPubUrl, | ||
18 | getLocalVideoDislikesActivityPubUrl, | ||
19 | getLocalVideoLikesActivityPubUrl, | ||
20 | getLocalVideoSharesActivityPubUrl | ||
21 | } from '../../../lib/activitypub/url' | ||
22 | import { MStreamingPlaylistFiles, MUserId, MVideo, MVideoAP, MVideoFile } from '../../../types/models' | ||
23 | import { VideoCaptionModel } from '../video-caption' | ||
24 | import { sortByResolutionDesc } from './shared' | ||
25 | import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format' | ||
26 | |||
27 | export function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
28 | const language = video.language | ||
29 | ? { identifier: video.language, name: getLanguageLabel(video.language) } | ||
30 | : undefined | ||
31 | |||
32 | const category = video.category | ||
33 | ? { identifier: video.category + '', name: getCategoryLabel(video.category) } | ||
34 | : undefined | ||
35 | |||
36 | const licence = video.licence | ||
37 | ? { identifier: video.licence + '', name: getLicenceLabel(video.licence) } | ||
38 | : undefined | ||
39 | |||
40 | const url: ActivityUrlObject[] = [ | ||
41 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
42 | { | ||
43 | type: 'Link', | ||
44 | mediaType: 'text/html', | ||
45 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
46 | } as ActivityUrlObject, | ||
47 | |||
48 | ...buildVideoFileUrls({ video, files: video.VideoFiles }), | ||
49 | |||
50 | ...buildStreamingPlaylistUrls(video), | ||
51 | |||
52 | ...buildTrackerUrls(video) | ||
53 | ] | ||
54 | |||
55 | return { | ||
56 | type: 'Video' as 'Video', | ||
57 | id: video.url, | ||
58 | name: video.name, | ||
59 | duration: getActivityStreamDuration(video.duration), | ||
60 | uuid: video.uuid, | ||
61 | category, | ||
62 | licence, | ||
63 | language, | ||
64 | views: video.views, | ||
65 | sensitive: video.nsfw, | ||
66 | waitTranscoding: video.waitTranscoding, | ||
67 | |||
68 | state: video.state, | ||
69 | commentsEnabled: video.commentsEnabled, | ||
70 | downloadEnabled: video.downloadEnabled, | ||
71 | published: video.publishedAt.toISOString(), | ||
72 | |||
73 | originallyPublishedAt: video.originallyPublishedAt | ||
74 | ? video.originallyPublishedAt.toISOString() | ||
75 | : null, | ||
76 | |||
77 | updated: video.updatedAt.toISOString(), | ||
78 | |||
79 | tag: buildTags(video), | ||
80 | |||
81 | mediaType: 'text/markdown', | ||
82 | content: video.description, | ||
83 | support: video.support, | ||
84 | |||
85 | subtitleLanguage: buildSubtitleLanguage(video), | ||
86 | |||
87 | icon: buildIcon(video), | ||
88 | |||
89 | preview: buildPreviewAPAttribute(video), | ||
90 | |||
91 | url, | ||
92 | |||
93 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
94 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
95 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
96 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
97 | |||
98 | attributedTo: [ | ||
99 | { | ||
100 | type: 'Person', | ||
101 | id: video.VideoChannel.Account.Actor.url | ||
102 | }, | ||
103 | { | ||
104 | type: 'Group', | ||
105 | id: video.VideoChannel.Actor.url | ||
106 | } | ||
107 | ], | ||
108 | |||
109 | ...buildLiveAPAttributes(video) | ||
110 | } | ||
111 | } | ||
112 | |||
113 | // --------------------------------------------------------------------------- | ||
114 | // Private | ||
115 | // --------------------------------------------------------------------------- | ||
116 | |||
117 | function buildLiveAPAttributes (video: MVideoAP) { | ||
118 | if (!video.isLive) { | ||
119 | return { | ||
120 | isLiveBroadcast: false, | ||
121 | liveSaveReplay: null, | ||
122 | permanentLive: null, | ||
123 | latencyMode: null | ||
124 | } | ||
125 | } | ||
126 | |||
127 | return { | ||
128 | isLiveBroadcast: true, | ||
129 | liveSaveReplay: video.VideoLive.saveReplay, | ||
130 | permanentLive: video.VideoLive.permanentLive, | ||
131 | latencyMode: video.VideoLive.latencyMode | ||
132 | } | ||
133 | } | ||
134 | |||
135 | function buildPreviewAPAttribute (video: MVideoAP): ActivityPubStoryboard[] { | ||
136 | if (!video.Storyboard) return undefined | ||
137 | |||
138 | const storyboard = video.Storyboard | ||
139 | |||
140 | return [ | ||
141 | { | ||
142 | type: 'Image', | ||
143 | rel: [ 'storyboard' ], | ||
144 | url: [ | ||
145 | { | ||
146 | mediaType: 'image/jpeg', | ||
147 | |||
148 | href: storyboard.getOriginFileUrl(video), | ||
149 | |||
150 | width: storyboard.totalWidth, | ||
151 | height: storyboard.totalHeight, | ||
152 | |||
153 | tileWidth: storyboard.spriteWidth, | ||
154 | tileHeight: storyboard.spriteHeight, | ||
155 | tileDuration: getActivityStreamDuration(storyboard.spriteDuration) | ||
156 | } | ||
157 | ] | ||
158 | } | ||
159 | ] | ||
160 | } | ||
161 | |||
162 | function buildVideoFileUrls (options: { | ||
163 | video: MVideo | ||
164 | files: MVideoFile[] | ||
165 | user?: MUserId | ||
166 | }): ActivityUrlObject[] { | ||
167 | const { video, files } = options | ||
168 | |||
169 | if (!isArray(files)) return [] | ||
170 | |||
171 | const urls: ActivityUrlObject[] = [] | ||
172 | |||
173 | const trackerUrls = video.getTrackerUrls() | ||
174 | const sortedFiles = files | ||
175 | .filter(f => !f.isLive()) | ||
176 | .sort(sortByResolutionDesc) | ||
177 | |||
178 | for (const file of sortedFiles) { | ||
179 | urls.push({ | ||
180 | type: 'Link', | ||
181 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
182 | href: file.getFileUrl(video), | ||
183 | height: file.resolution, | ||
184 | size: file.size, | ||
185 | fps: file.fps | ||
186 | }) | ||
187 | |||
188 | urls.push({ | ||
189 | type: 'Link', | ||
190 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
191 | mediaType: 'application/json' as 'application/json', | ||
192 | href: getLocalVideoFileMetadataUrl(video, file), | ||
193 | height: file.resolution, | ||
194 | fps: file.fps | ||
195 | }) | ||
196 | |||
197 | if (file.hasTorrent()) { | ||
198 | urls.push({ | ||
199 | type: 'Link', | ||
200 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
201 | href: file.getTorrentUrl(), | ||
202 | height: file.resolution | ||
203 | }) | ||
204 | |||
205 | urls.push({ | ||
206 | type: 'Link', | ||
207 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
208 | href: generateMagnetUri(video, file, trackerUrls), | ||
209 | height: file.resolution | ||
210 | }) | ||
211 | } | ||
212 | } | ||
213 | |||
214 | return urls | ||
215 | } | ||
216 | |||
217 | // --------------------------------------------------------------------------- | ||
218 | |||
219 | function buildStreamingPlaylistUrls (video: MVideoAP): ActivityPlaylistUrlObject[] { | ||
220 | if (!isArray(video.VideoStreamingPlaylists)) return [] | ||
221 | |||
222 | return video.VideoStreamingPlaylists | ||
223 | .map(playlist => ({ | ||
224 | type: 'Link', | ||
225 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
226 | href: playlist.getMasterPlaylistUrl(video), | ||
227 | tag: buildStreamingPlaylistTags(video, playlist) | ||
228 | })) | ||
229 | } | ||
230 | |||
231 | function buildStreamingPlaylistTags (video: MVideoAP, playlist: MStreamingPlaylistFiles) { | ||
232 | return [ | ||
233 | ...playlist.p2pMediaLoaderInfohashes.map(i => ({ type: 'Infohash' as 'Infohash', name: i })), | ||
234 | |||
235 | { | ||
236 | type: 'Link', | ||
237 | name: 'sha256', | ||
238 | mediaType: 'application/json' as 'application/json', | ||
239 | href: playlist.getSha256SegmentsUrl(video) | ||
240 | }, | ||
241 | |||
242 | ...buildVideoFileUrls({ video, files: playlist.VideoFiles }) | ||
243 | ] as ActivityTagObject[] | ||
244 | } | ||
245 | |||
246 | // --------------------------------------------------------------------------- | ||
247 | |||
248 | function buildTrackerUrls (video: MVideoAP): ActivityTrackerUrlObject[] { | ||
249 | return video.getTrackerUrls() | ||
250 | .map(trackerUrl => { | ||
251 | const rel2 = trackerUrl.startsWith('http') | ||
252 | ? 'http' | ||
253 | : 'websocket' | ||
254 | |||
255 | return { | ||
256 | type: 'Link', | ||
257 | name: `tracker-${rel2}`, | ||
258 | rel: [ 'tracker', rel2 ], | ||
259 | href: trackerUrl | ||
260 | } | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | // --------------------------------------------------------------------------- | ||
265 | |||
266 | function buildTags (video: MVideoAP) { | ||
267 | if (!isArray(video.Tags)) return [] | ||
268 | |||
269 | return video.Tags.map(t => ({ | ||
270 | type: 'Hashtag' as 'Hashtag', | ||
271 | name: t.name | ||
272 | })) | ||
273 | } | ||
274 | |||
275 | function buildIcon (video: MVideoAP): ActivityIconObject[] { | ||
276 | return [ video.getMiniature(), video.getPreview() ] | ||
277 | .map(i => ({ | ||
278 | type: 'Image', | ||
279 | url: i.getOriginFileUrl(video), | ||
280 | mediaType: 'image/jpeg', | ||
281 | width: i.width, | ||
282 | height: i.height | ||
283 | })) | ||
284 | } | ||
285 | |||
286 | function buildSubtitleLanguage (video: MVideoAP) { | ||
287 | if (!isArray(video.VideoCaptions)) return [] | ||
288 | |||
289 | return video.VideoCaptions | ||
290 | .map(caption => ({ | ||
291 | identifier: caption.language, | ||
292 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
293 | url: caption.getFileUrl(video) | ||
294 | })) | ||
295 | } | ||
diff --git a/server/models/video/formatter/video-api-format.ts b/server/models/video/formatter/video-api-format.ts new file mode 100644 index 000000000..1af51d132 --- /dev/null +++ b/server/models/video/formatter/video-api-format.ts | |||
@@ -0,0 +1,304 @@ | |||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
2 | import { tracer } from '@server/lib/opentelemetry/tracing' | ||
3 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
4 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
5 | import { uuidToShort } from '@shared/extra-utils' | ||
6 | import { | ||
7 | Video, | ||
8 | VideoAdditionalAttributes, | ||
9 | VideoDetails, | ||
10 | VideoFile, | ||
11 | VideoInclude, | ||
12 | VideosCommonQueryAfterSanitize, | ||
13 | VideoStreamingPlaylist | ||
14 | } from '@shared/models' | ||
15 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
16 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, VIDEO_STATES } from '../../../initializers/constants' | ||
17 | import { MServer, MStreamingPlaylistRedundanciesOpt, MVideoFormattable, MVideoFormattableDetails } from '../../../types/models' | ||
18 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' | ||
19 | import { sortByResolutionDesc } from './shared' | ||
20 | |||
21 | export type VideoFormattingJSONOptions = { | ||
22 | completeDescription?: boolean | ||
23 | |||
24 | additionalAttributes?: { | ||
25 | state?: boolean | ||
26 | waitTranscoding?: boolean | ||
27 | scheduledUpdate?: boolean | ||
28 | blacklistInfo?: boolean | ||
29 | files?: boolean | ||
30 | blockedOwner?: boolean | ||
31 | } | ||
32 | } | ||
33 | |||
34 | export function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { | ||
35 | if (!query?.include) return {} | ||
36 | |||
37 | return { | ||
38 | additionalAttributes: { | ||
39 | state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
40 | waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
41 | scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
42 | blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), | ||
43 | files: !!(query.include & VideoInclude.FILES), | ||
44 | blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video { | ||
52 | const span = tracer.startSpan('peertube.VideoModel.toFormattedJSON') | ||
53 | |||
54 | const userHistory = isArray(video.UserVideoHistories) | ||
55 | ? video.UserVideoHistories[0] | ||
56 | : undefined | ||
57 | |||
58 | const videoObject: Video = { | ||
59 | id: video.id, | ||
60 | uuid: video.uuid, | ||
61 | shortUUID: uuidToShort(video.uuid), | ||
62 | |||
63 | url: video.url, | ||
64 | |||
65 | name: video.name, | ||
66 | category: { | ||
67 | id: video.category, | ||
68 | label: getCategoryLabel(video.category) | ||
69 | }, | ||
70 | licence: { | ||
71 | id: video.licence, | ||
72 | label: getLicenceLabel(video.licence) | ||
73 | }, | ||
74 | language: { | ||
75 | id: video.language, | ||
76 | label: getLanguageLabel(video.language) | ||
77 | }, | ||
78 | privacy: { | ||
79 | id: video.privacy, | ||
80 | label: getPrivacyLabel(video.privacy) | ||
81 | }, | ||
82 | nsfw: video.nsfw, | ||
83 | |||
84 | truncatedDescription: video.getTruncatedDescription(), | ||
85 | description: options && options.completeDescription === true | ||
86 | ? video.description | ||
87 | : video.getTruncatedDescription(), | ||
88 | |||
89 | isLocal: video.isOwned(), | ||
90 | duration: video.duration, | ||
91 | |||
92 | views: video.views, | ||
93 | viewers: VideoViewsManager.Instance.getViewers(video), | ||
94 | |||
95 | likes: video.likes, | ||
96 | dislikes: video.dislikes, | ||
97 | thumbnailPath: video.getMiniatureStaticPath(), | ||
98 | previewPath: video.getPreviewStaticPath(), | ||
99 | embedPath: video.getEmbedStaticPath(), | ||
100 | createdAt: video.createdAt, | ||
101 | updatedAt: video.updatedAt, | ||
102 | publishedAt: video.publishedAt, | ||
103 | originallyPublishedAt: video.originallyPublishedAt, | ||
104 | |||
105 | isLive: video.isLive, | ||
106 | |||
107 | account: video.VideoChannel.Account.toFormattedSummaryJSON(), | ||
108 | channel: video.VideoChannel.toFormattedSummaryJSON(), | ||
109 | |||
110 | userHistory: userHistory | ||
111 | ? { currentTime: userHistory.currentTime } | ||
112 | : undefined, | ||
113 | |||
114 | // Can be added by external plugins | ||
115 | pluginData: (video as any).pluginData, | ||
116 | |||
117 | ...buildAdditionalAttributes(video, options) | ||
118 | } | ||
119 | |||
120 | span.end() | ||
121 | |||
122 | return videoObject | ||
123 | } | ||
124 | |||
125 | export function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { | ||
126 | const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') | ||
127 | |||
128 | const videoJSON = video.toFormattedJSON({ | ||
129 | completeDescription: true, | ||
130 | additionalAttributes: { | ||
131 | scheduledUpdate: true, | ||
132 | blacklistInfo: true, | ||
133 | files: true | ||
134 | } | ||
135 | }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists' | 'scheduledUpdate' | 'blacklisted' | 'blacklistedReason'>> | ||
136 | |||
137 | const tags = video.Tags | ||
138 | ? video.Tags.map(t => t.name) | ||
139 | : [] | ||
140 | |||
141 | const detailsJSON = { | ||
142 | ...videoJSON, | ||
143 | |||
144 | support: video.support, | ||
145 | descriptionPath: video.getDescriptionAPIPath(), | ||
146 | channel: video.VideoChannel.toFormattedJSON(), | ||
147 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
148 | tags, | ||
149 | commentsEnabled: video.commentsEnabled, | ||
150 | downloadEnabled: video.downloadEnabled, | ||
151 | waitTranscoding: video.waitTranscoding, | ||
152 | state: { | ||
153 | id: video.state, | ||
154 | label: getStateLabel(video.state) | ||
155 | }, | ||
156 | |||
157 | trackerUrls: video.getTrackerUrls() | ||
158 | } | ||
159 | |||
160 | span.end() | ||
161 | |||
162 | return detailsJSON | ||
163 | } | ||
164 | |||
165 | export function streamingPlaylistsModelToFormattedJSON ( | ||
166 | video: MVideoFormattable, | ||
167 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
168 | ): VideoStreamingPlaylist[] { | ||
169 | if (isArray(playlists) === false) return [] | ||
170 | |||
171 | return playlists | ||
172 | .map(playlist => ({ | ||
173 | id: playlist.id, | ||
174 | type: playlist.type, | ||
175 | |||
176 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
177 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
178 | |||
179 | redundancies: isArray(playlist.RedundancyVideos) | ||
180 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
181 | : [], | ||
182 | |||
183 | files: videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
184 | })) | ||
185 | } | ||
186 | |||
187 | export function videoFilesModelToFormattedJSON ( | ||
188 | video: MVideoFormattable, | ||
189 | videoFiles: MVideoFileRedundanciesOpt[], | ||
190 | options: { | ||
191 | includeMagnet?: boolean // default true | ||
192 | } = {} | ||
193 | ): VideoFile[] { | ||
194 | const { includeMagnet = true } = options | ||
195 | |||
196 | if (isArray(videoFiles) === false) return [] | ||
197 | |||
198 | const trackerUrls = includeMagnet | ||
199 | ? video.getTrackerUrls() | ||
200 | : [] | ||
201 | |||
202 | return videoFiles | ||
203 | .filter(f => !f.isLive()) | ||
204 | .sort(sortByResolutionDesc) | ||
205 | .map(videoFile => { | ||
206 | return { | ||
207 | id: videoFile.id, | ||
208 | |||
209 | resolution: { | ||
210 | id: videoFile.resolution, | ||
211 | label: videoFile.resolution === 0 | ||
212 | ? 'Audio' | ||
213 | : `${videoFile.resolution}p` | ||
214 | }, | ||
215 | |||
216 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
217 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
218 | : undefined, | ||
219 | |||
220 | size: videoFile.size, | ||
221 | fps: videoFile.fps, | ||
222 | |||
223 | torrentUrl: videoFile.getTorrentUrl(), | ||
224 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
225 | |||
226 | fileUrl: videoFile.getFileUrl(video), | ||
227 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
228 | |||
229 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
230 | } | ||
231 | }) | ||
232 | } | ||
233 | |||
234 | // --------------------------------------------------------------------------- | ||
235 | |||
236 | export function getCategoryLabel (id: number) { | ||
237 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
238 | } | ||
239 | |||
240 | export function getLicenceLabel (id: number) { | ||
241 | return VIDEO_LICENCES[id] || 'Unknown' | ||
242 | } | ||
243 | |||
244 | export function getLanguageLabel (id: string) { | ||
245 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
246 | } | ||
247 | |||
248 | export function getPrivacyLabel (id: number) { | ||
249 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
250 | } | ||
251 | |||
252 | export function getStateLabel (id: number) { | ||
253 | return VIDEO_STATES[id] || 'Unknown' | ||
254 | } | ||
255 | |||
256 | // --------------------------------------------------------------------------- | ||
257 | // Private | ||
258 | // --------------------------------------------------------------------------- | ||
259 | |||
260 | function buildAdditionalAttributes (video: MVideoFormattable, options: VideoFormattingJSONOptions) { | ||
261 | const add = options.additionalAttributes | ||
262 | |||
263 | const result: Partial<VideoAdditionalAttributes> = {} | ||
264 | |||
265 | if (add?.state === true) { | ||
266 | result.state = { | ||
267 | id: video.state, | ||
268 | label: getStateLabel(video.state) | ||
269 | } | ||
270 | } | ||
271 | |||
272 | if (add?.waitTranscoding === true) { | ||
273 | result.waitTranscoding = video.waitTranscoding | ||
274 | } | ||
275 | |||
276 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
277 | result.scheduledUpdate = { | ||
278 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
279 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
280 | } | ||
281 | } | ||
282 | |||
283 | if (add?.blacklistInfo === true) { | ||
284 | result.blacklisted = !!video.VideoBlacklist | ||
285 | result.blacklistedReason = | ||
286 | video.VideoBlacklist | ||
287 | ? video.VideoBlacklist.reason | ||
288 | : null | ||
289 | } | ||
290 | |||
291 | if (add?.blockedOwner === true) { | ||
292 | result.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
293 | |||
294 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
295 | result.blockedServer = !!(server?.isBlocked()) | ||
296 | } | ||
297 | |||
298 | if (add?.files === true) { | ||
299 | result.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
300 | result.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
301 | } | ||
302 | |||
303 | return result | ||
304 | } | ||
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts deleted file mode 100644 index 4179545b8..000000000 --- a/server/models/video/formatter/video-format-utils.ts +++ /dev/null | |||
@@ -1,561 +0,0 @@ | |||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
2 | import { getActivityStreamDuration } from '@server/lib/activitypub/activity' | ||
3 | import { tracer } from '@server/lib/opentelemetry/tracing' | ||
4 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
5 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
6 | import { uuidToShort } from '@shared/extra-utils' | ||
7 | import { | ||
8 | ActivityPubStoryboard, | ||
9 | ActivityTagObject, | ||
10 | ActivityUrlObject, | ||
11 | Video, | ||
12 | VideoDetails, | ||
13 | VideoFile, | ||
14 | VideoInclude, | ||
15 | VideoObject, | ||
16 | VideosCommonQueryAfterSanitize, | ||
17 | VideoStreamingPlaylist | ||
18 | } from '@shared/models' | ||
19 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
20 | import { | ||
21 | MIMETYPES, | ||
22 | VIDEO_CATEGORIES, | ||
23 | VIDEO_LANGUAGES, | ||
24 | VIDEO_LICENCES, | ||
25 | VIDEO_PRIVACIES, | ||
26 | VIDEO_STATES, | ||
27 | WEBSERVER | ||
28 | } from '../../../initializers/constants' | ||
29 | import { | ||
30 | getLocalVideoCommentsActivityPubUrl, | ||
31 | getLocalVideoDislikesActivityPubUrl, | ||
32 | getLocalVideoLikesActivityPubUrl, | ||
33 | getLocalVideoSharesActivityPubUrl | ||
34 | } from '../../../lib/activitypub/url' | ||
35 | import { | ||
36 | MServer, | ||
37 | MStreamingPlaylistRedundanciesOpt, | ||
38 | MUserId, | ||
39 | MVideo, | ||
40 | MVideoAP, | ||
41 | MVideoFile, | ||
42 | MVideoFormattable, | ||
43 | MVideoFormattableDetails | ||
44 | } from '../../../types/models' | ||
45 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' | ||
46 | import { VideoCaptionModel } from '../video-caption' | ||
47 | |||
48 | export type VideoFormattingJSONOptions = { | ||
49 | completeDescription?: boolean | ||
50 | |||
51 | additionalAttributes?: { | ||
52 | state?: boolean | ||
53 | waitTranscoding?: boolean | ||
54 | scheduledUpdate?: boolean | ||
55 | blacklistInfo?: boolean | ||
56 | files?: boolean | ||
57 | blockedOwner?: boolean | ||
58 | } | ||
59 | } | ||
60 | |||
61 | function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { | ||
62 | if (!query?.include) return {} | ||
63 | |||
64 | return { | ||
65 | additionalAttributes: { | ||
66 | state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
67 | waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
68 | scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
69 | blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), | ||
70 | files: !!(query.include & VideoInclude.FILES), | ||
71 | blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | |||
76 | function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video { | ||
77 | const span = tracer.startSpan('peertube.VideoModel.toFormattedJSON') | ||
78 | |||
79 | const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined | ||
80 | |||
81 | const videoObject: Video = { | ||
82 | id: video.id, | ||
83 | uuid: video.uuid, | ||
84 | shortUUID: uuidToShort(video.uuid), | ||
85 | |||
86 | url: video.url, | ||
87 | |||
88 | name: video.name, | ||
89 | category: { | ||
90 | id: video.category, | ||
91 | label: getCategoryLabel(video.category) | ||
92 | }, | ||
93 | licence: { | ||
94 | id: video.licence, | ||
95 | label: getLicenceLabel(video.licence) | ||
96 | }, | ||
97 | language: { | ||
98 | id: video.language, | ||
99 | label: getLanguageLabel(video.language) | ||
100 | }, | ||
101 | privacy: { | ||
102 | id: video.privacy, | ||
103 | label: getPrivacyLabel(video.privacy) | ||
104 | }, | ||
105 | nsfw: video.nsfw, | ||
106 | |||
107 | truncatedDescription: video.getTruncatedDescription(), | ||
108 | description: options && options.completeDescription === true | ||
109 | ? video.description | ||
110 | : video.getTruncatedDescription(), | ||
111 | |||
112 | isLocal: video.isOwned(), | ||
113 | duration: video.duration, | ||
114 | |||
115 | views: video.views, | ||
116 | viewers: VideoViewsManager.Instance.getViewers(video), | ||
117 | |||
118 | likes: video.likes, | ||
119 | dislikes: video.dislikes, | ||
120 | thumbnailPath: video.getMiniatureStaticPath(), | ||
121 | previewPath: video.getPreviewStaticPath(), | ||
122 | embedPath: video.getEmbedStaticPath(), | ||
123 | createdAt: video.createdAt, | ||
124 | updatedAt: video.updatedAt, | ||
125 | publishedAt: video.publishedAt, | ||
126 | originallyPublishedAt: video.originallyPublishedAt, | ||
127 | |||
128 | isLive: video.isLive, | ||
129 | |||
130 | account: video.VideoChannel.Account.toFormattedSummaryJSON(), | ||
131 | channel: video.VideoChannel.toFormattedSummaryJSON(), | ||
132 | |||
133 | userHistory: userHistory | ||
134 | ? { currentTime: userHistory.currentTime } | ||
135 | : undefined, | ||
136 | |||
137 | // Can be added by external plugins | ||
138 | pluginData: (video as any).pluginData | ||
139 | } | ||
140 | |||
141 | const add = options.additionalAttributes | ||
142 | if (add?.state === true) { | ||
143 | videoObject.state = { | ||
144 | id: video.state, | ||
145 | label: getStateLabel(video.state) | ||
146 | } | ||
147 | } | ||
148 | |||
149 | if (add?.waitTranscoding === true) { | ||
150 | videoObject.waitTranscoding = video.waitTranscoding | ||
151 | } | ||
152 | |||
153 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
154 | videoObject.scheduledUpdate = { | ||
155 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
156 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
157 | } | ||
158 | } | ||
159 | |||
160 | if (add?.blacklistInfo === true) { | ||
161 | videoObject.blacklisted = !!video.VideoBlacklist | ||
162 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null | ||
163 | } | ||
164 | |||
165 | if (add?.blockedOwner === true) { | ||
166 | videoObject.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
167 | |||
168 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
169 | videoObject.blockedServer = !!(server?.isBlocked()) | ||
170 | } | ||
171 | |||
172 | if (add?.files === true) { | ||
173 | videoObject.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
174 | videoObject.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
175 | } | ||
176 | |||
177 | span.end() | ||
178 | |||
179 | return videoObject | ||
180 | } | ||
181 | |||
182 | function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { | ||
183 | const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') | ||
184 | |||
185 | const videoJSON = video.toFormattedJSON({ | ||
186 | completeDescription: true, | ||
187 | additionalAttributes: { | ||
188 | scheduledUpdate: true, | ||
189 | blacklistInfo: true, | ||
190 | files: true | ||
191 | } | ||
192 | }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists'>> | ||
193 | |||
194 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | ||
195 | |||
196 | const detailsJSON = { | ||
197 | support: video.support, | ||
198 | descriptionPath: video.getDescriptionAPIPath(), | ||
199 | channel: video.VideoChannel.toFormattedJSON(), | ||
200 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
201 | tags, | ||
202 | commentsEnabled: video.commentsEnabled, | ||
203 | downloadEnabled: video.downloadEnabled, | ||
204 | waitTranscoding: video.waitTranscoding, | ||
205 | state: { | ||
206 | id: video.state, | ||
207 | label: getStateLabel(video.state) | ||
208 | }, | ||
209 | |||
210 | trackerUrls: video.getTrackerUrls() | ||
211 | } | ||
212 | |||
213 | span.end() | ||
214 | |||
215 | return Object.assign(videoJSON, detailsJSON) | ||
216 | } | ||
217 | |||
218 | function streamingPlaylistsModelToFormattedJSON ( | ||
219 | video: MVideoFormattable, | ||
220 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
221 | ): VideoStreamingPlaylist[] { | ||
222 | if (isArray(playlists) === false) return [] | ||
223 | |||
224 | return playlists | ||
225 | .map(playlist => { | ||
226 | const redundancies = isArray(playlist.RedundancyVideos) | ||
227 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
228 | : [] | ||
229 | |||
230 | const files = videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
231 | |||
232 | return { | ||
233 | id: playlist.id, | ||
234 | type: playlist.type, | ||
235 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
236 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
237 | redundancies, | ||
238 | files | ||
239 | } | ||
240 | }) | ||
241 | } | ||
242 | |||
243 | function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | ||
244 | if (fileA.resolution < fileB.resolution) return 1 | ||
245 | if (fileA.resolution === fileB.resolution) return 0 | ||
246 | return -1 | ||
247 | } | ||
248 | |||
249 | function videoFilesModelToFormattedJSON ( | ||
250 | video: MVideoFormattable, | ||
251 | videoFiles: MVideoFileRedundanciesOpt[], | ||
252 | options: { | ||
253 | includeMagnet?: boolean // default true | ||
254 | } = {} | ||
255 | ): VideoFile[] { | ||
256 | const { includeMagnet = true } = options | ||
257 | |||
258 | const trackerUrls = includeMagnet | ||
259 | ? video.getTrackerUrls() | ||
260 | : [] | ||
261 | |||
262 | return (videoFiles || []) | ||
263 | .filter(f => !f.isLive()) | ||
264 | .sort(sortByResolutionDesc) | ||
265 | .map(videoFile => { | ||
266 | return { | ||
267 | id: videoFile.id, | ||
268 | |||
269 | resolution: { | ||
270 | id: videoFile.resolution, | ||
271 | label: videoFile.resolution === 0 ? 'Audio' : `${videoFile.resolution}p` | ||
272 | }, | ||
273 | |||
274 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
275 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
276 | : undefined, | ||
277 | |||
278 | size: videoFile.size, | ||
279 | fps: videoFile.fps, | ||
280 | |||
281 | torrentUrl: videoFile.getTorrentUrl(), | ||
282 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
283 | |||
284 | fileUrl: videoFile.getFileUrl(video), | ||
285 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
286 | |||
287 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
288 | } as VideoFile | ||
289 | }) | ||
290 | } | ||
291 | |||
292 | function addVideoFilesInAPAcc (options: { | ||
293 | acc: ActivityUrlObject[] | ActivityTagObject[] | ||
294 | video: MVideo | ||
295 | files: MVideoFile[] | ||
296 | user?: MUserId | ||
297 | }) { | ||
298 | const { acc, video, files } = options | ||
299 | |||
300 | const trackerUrls = video.getTrackerUrls() | ||
301 | |||
302 | const sortedFiles = (files || []) | ||
303 | .filter(f => !f.isLive()) | ||
304 | .sort(sortByResolutionDesc) | ||
305 | |||
306 | for (const file of sortedFiles) { | ||
307 | acc.push({ | ||
308 | type: 'Link', | ||
309 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
310 | href: file.getFileUrl(video), | ||
311 | height: file.resolution, | ||
312 | size: file.size, | ||
313 | fps: file.fps | ||
314 | }) | ||
315 | |||
316 | acc.push({ | ||
317 | type: 'Link', | ||
318 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
319 | mediaType: 'application/json' as 'application/json', | ||
320 | href: getLocalVideoFileMetadataUrl(video, file), | ||
321 | height: file.resolution, | ||
322 | fps: file.fps | ||
323 | }) | ||
324 | |||
325 | if (file.hasTorrent()) { | ||
326 | acc.push({ | ||
327 | type: 'Link', | ||
328 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
329 | href: file.getTorrentUrl(), | ||
330 | height: file.resolution | ||
331 | }) | ||
332 | |||
333 | acc.push({ | ||
334 | type: 'Link', | ||
335 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
336 | href: generateMagnetUri(video, file, trackerUrls), | ||
337 | height: file.resolution | ||
338 | }) | ||
339 | } | ||
340 | } | ||
341 | } | ||
342 | |||
343 | function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
344 | if (!video.Tags) video.Tags = [] | ||
345 | |||
346 | const tag = video.Tags.map(t => ({ | ||
347 | type: 'Hashtag' as 'Hashtag', | ||
348 | name: t.name | ||
349 | })) | ||
350 | |||
351 | const language = video.language | ||
352 | ? { identifier: video.language, name: getLanguageLabel(video.language) } | ||
353 | : undefined | ||
354 | |||
355 | const category = video.category | ||
356 | ? { identifier: video.category + '', name: getCategoryLabel(video.category) } | ||
357 | : undefined | ||
358 | |||
359 | const licence = video.licence | ||
360 | ? { identifier: video.licence + '', name: getLicenceLabel(video.licence) } | ||
361 | : undefined | ||
362 | |||
363 | const url: ActivityUrlObject[] = [ | ||
364 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
365 | { | ||
366 | type: 'Link', | ||
367 | mediaType: 'text/html', | ||
368 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
369 | } | ||
370 | ] | ||
371 | |||
372 | addVideoFilesInAPAcc({ acc: url, video, files: video.VideoFiles || [] }) | ||
373 | |||
374 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | ||
375 | const tag = playlist.p2pMediaLoaderInfohashes | ||
376 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) as ActivityTagObject[] | ||
377 | tag.push({ | ||
378 | type: 'Link', | ||
379 | name: 'sha256', | ||
380 | mediaType: 'application/json' as 'application/json', | ||
381 | href: playlist.getSha256SegmentsUrl(video) | ||
382 | }) | ||
383 | |||
384 | addVideoFilesInAPAcc({ acc: tag, video, files: playlist.VideoFiles || [] }) | ||
385 | |||
386 | url.push({ | ||
387 | type: 'Link', | ||
388 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
389 | href: playlist.getMasterPlaylistUrl(video), | ||
390 | tag | ||
391 | }) | ||
392 | } | ||
393 | |||
394 | for (const trackerUrl of video.getTrackerUrls()) { | ||
395 | const rel2 = trackerUrl.startsWith('http') | ||
396 | ? 'http' | ||
397 | : 'websocket' | ||
398 | |||
399 | url.push({ | ||
400 | type: 'Link', | ||
401 | name: `tracker-${rel2}`, | ||
402 | rel: [ 'tracker', rel2 ], | ||
403 | href: trackerUrl | ||
404 | }) | ||
405 | } | ||
406 | |||
407 | const subtitleLanguage = [] | ||
408 | for (const caption of video.VideoCaptions) { | ||
409 | subtitleLanguage.push({ | ||
410 | identifier: caption.language, | ||
411 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
412 | url: caption.getFileUrl(video) | ||
413 | }) | ||
414 | } | ||
415 | |||
416 | const icons = [ video.getMiniature(), video.getPreview() ] | ||
417 | |||
418 | return { | ||
419 | type: 'Video' as 'Video', | ||
420 | id: video.url, | ||
421 | name: video.name, | ||
422 | duration: getActivityStreamDuration(video.duration), | ||
423 | uuid: video.uuid, | ||
424 | tag, | ||
425 | category, | ||
426 | licence, | ||
427 | language, | ||
428 | views: video.views, | ||
429 | sensitive: video.nsfw, | ||
430 | waitTranscoding: video.waitTranscoding, | ||
431 | |||
432 | state: video.state, | ||
433 | commentsEnabled: video.commentsEnabled, | ||
434 | downloadEnabled: video.downloadEnabled, | ||
435 | published: video.publishedAt.toISOString(), | ||
436 | |||
437 | originallyPublishedAt: video.originallyPublishedAt | ||
438 | ? video.originallyPublishedAt.toISOString() | ||
439 | : null, | ||
440 | |||
441 | updated: video.updatedAt.toISOString(), | ||
442 | |||
443 | mediaType: 'text/markdown', | ||
444 | content: video.description, | ||
445 | support: video.support, | ||
446 | |||
447 | subtitleLanguage, | ||
448 | |||
449 | icon: icons.map(i => ({ | ||
450 | type: 'Image', | ||
451 | url: i.getOriginFileUrl(video), | ||
452 | mediaType: 'image/jpeg', | ||
453 | width: i.width, | ||
454 | height: i.height | ||
455 | })), | ||
456 | |||
457 | preview: buildPreviewAPAttribute(video), | ||
458 | |||
459 | url, | ||
460 | |||
461 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
462 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
463 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
464 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
465 | |||
466 | attributedTo: [ | ||
467 | { | ||
468 | type: 'Person', | ||
469 | id: video.VideoChannel.Account.Actor.url | ||
470 | }, | ||
471 | { | ||
472 | type: 'Group', | ||
473 | id: video.VideoChannel.Actor.url | ||
474 | } | ||
475 | ], | ||
476 | |||
477 | ...buildLiveAPAttributes(video) | ||
478 | } | ||
479 | } | ||
480 | |||
481 | function getCategoryLabel (id: number) { | ||
482 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
483 | } | ||
484 | |||
485 | function getLicenceLabel (id: number) { | ||
486 | return VIDEO_LICENCES[id] || 'Unknown' | ||
487 | } | ||
488 | |||
489 | function getLanguageLabel (id: string) { | ||
490 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
491 | } | ||
492 | |||
493 | function getPrivacyLabel (id: number) { | ||
494 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
495 | } | ||
496 | |||
497 | function getStateLabel (id: number) { | ||
498 | return VIDEO_STATES[id] || 'Unknown' | ||
499 | } | ||
500 | |||
501 | export { | ||
502 | videoModelToFormattedJSON, | ||
503 | videoModelToFormattedDetailsJSON, | ||
504 | videoFilesModelToFormattedJSON, | ||
505 | videoModelToActivityPubObject, | ||
506 | |||
507 | guessAdditionalAttributesFromQuery, | ||
508 | |||
509 | getCategoryLabel, | ||
510 | getLicenceLabel, | ||
511 | getLanguageLabel, | ||
512 | getPrivacyLabel, | ||
513 | getStateLabel | ||
514 | } | ||
515 | |||
516 | // --------------------------------------------------------------------------- | ||
517 | |||
518 | function buildLiveAPAttributes (video: MVideoAP) { | ||
519 | if (!video.isLive) { | ||
520 | return { | ||
521 | isLiveBroadcast: false, | ||
522 | liveSaveReplay: null, | ||
523 | permanentLive: null, | ||
524 | latencyMode: null | ||
525 | } | ||
526 | } | ||
527 | |||
528 | return { | ||
529 | isLiveBroadcast: true, | ||
530 | liveSaveReplay: video.VideoLive.saveReplay, | ||
531 | permanentLive: video.VideoLive.permanentLive, | ||
532 | latencyMode: video.VideoLive.latencyMode | ||
533 | } | ||
534 | } | ||
535 | |||
536 | function buildPreviewAPAttribute (video: MVideoAP): ActivityPubStoryboard[] { | ||
537 | if (!video.Storyboard) return undefined | ||
538 | |||
539 | const storyboard = video.Storyboard | ||
540 | |||
541 | return [ | ||
542 | { | ||
543 | type: 'Image', | ||
544 | rel: [ 'storyboard' ], | ||
545 | url: [ | ||
546 | { | ||
547 | mediaType: 'image/jpeg', | ||
548 | |||
549 | href: storyboard.getOriginFileUrl(video), | ||
550 | |||
551 | width: storyboard.totalWidth, | ||
552 | height: storyboard.totalHeight, | ||
553 | |||
554 | tileWidth: storyboard.spriteWidth, | ||
555 | tileHeight: storyboard.spriteHeight, | ||
556 | tileDuration: getActivityStreamDuration(storyboard.spriteDuration) | ||
557 | } | ||
558 | ] | ||
559 | } | ||
560 | ] | ||
561 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index fd56d2423..06aec1308 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -114,13 +114,13 @@ import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, | |||
114 | import { UserModel } from '../user/user' | 114 | import { UserModel } from '../user/user' |
115 | import { UserVideoHistoryModel } from '../user/user-video-history' | 115 | import { UserVideoHistoryModel } from '../user/user-video-history' |
116 | import { VideoViewModel } from '../view/video-view' | 116 | import { VideoViewModel } from '../view/video-view' |
117 | import { videoModelToActivityPubObject } from './formatter/video-activity-pub-format' | ||
117 | import { | 118 | import { |
118 | videoFilesModelToFormattedJSON, | 119 | videoFilesModelToFormattedJSON, |
119 | VideoFormattingJSONOptions, | 120 | VideoFormattingJSONOptions, |
120 | videoModelToActivityPubObject, | ||
121 | videoModelToFormattedDetailsJSON, | 121 | videoModelToFormattedDetailsJSON, |
122 | videoModelToFormattedJSON | 122 | videoModelToFormattedJSON |
123 | } from './formatter/video-format-utils' | 123 | } from './formatter/video-api-format' |
124 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 124 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
125 | import { | 125 | import { |
126 | BuildVideosListQueryOptions, | 126 | BuildVideosListQueryOptions, |