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/video/formatter/video-api-format.ts | |
parent | 638a2950215af1f11be8e8bdd136ca12e5176c32 (diff) | |
download | PeerTube-d896fef7e21cacfb44967eb9059fe543a66f5dd0.tar.gz PeerTube-d896fef7e21cacfb44967eb9059fe543a66f5dd0.tar.zst PeerTube-d896fef7e21cacfb44967eb9059fe543a66f5dd0.zip |
Refactor video formatter
Diffstat (limited to 'server/models/video/formatter/video-api-format.ts')
-rw-r--r-- | server/models/video/formatter/video-api-format.ts | 304 |
1 files changed, 304 insertions, 0 deletions
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 | } | ||