diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/models/video/formatter | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/models/video/formatter')
-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 | 296 | ||||
-rw-r--r-- | server/models/video/formatter/video-api-format.ts | 305 |
5 files changed, 0 insertions, 611 deletions
diff --git a/server/models/video/formatter/index.ts b/server/models/video/formatter/index.ts deleted file mode 100644 index 77b406559..000000000 --- a/server/models/video/formatter/index.ts +++ /dev/null | |||
@@ -1,2 +0,0 @@ | |||
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 deleted file mode 100644 index d558fa7d6..000000000 --- a/server/models/video/formatter/shared/index.ts +++ /dev/null | |||
@@ -1 +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 deleted file mode 100644 index df3bbdf1c..000000000 --- a/server/models/video/formatter/shared/video-format-utils.ts +++ /dev/null | |||
@@ -1,7 +0,0 @@ | |||
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 deleted file mode 100644 index 694c66c33..000000000 --- a/server/models/video/formatter/video-activity-pub-format.ts +++ /dev/null | |||
@@ -1,296 +0,0 @@ | |||
1 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
2 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
3 | import { getActivityStreamDuration } from '@server/lib/activitypub/activity' | ||
4 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
5 | import { | ||
6 | ActivityIconObject, | ||
7 | ActivityPlaylistUrlObject, | ||
8 | ActivityPubStoryboard, | ||
9 | ActivityTagObject, | ||
10 | ActivityTrackerUrlObject, | ||
11 | ActivityUrlObject, | ||
12 | VideoObject | ||
13 | } from '@shared/models' | ||
14 | import { MIMETYPES, WEBSERVER } from '../../../initializers/constants' | ||
15 | import { | ||
16 | getLocalVideoCommentsActivityPubUrl, | ||
17 | getLocalVideoDislikesActivityPubUrl, | ||
18 | getLocalVideoLikesActivityPubUrl, | ||
19 | getLocalVideoSharesActivityPubUrl | ||
20 | } from '../../../lib/activitypub/url' | ||
21 | import { MStreamingPlaylistFiles, MUserId, MVideo, MVideoAP, MVideoFile } from '../../../types/models' | ||
22 | import { VideoCaptionModel } from '../video-caption' | ||
23 | import { sortByResolutionDesc } from './shared' | ||
24 | import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format' | ||
25 | |||
26 | export function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
27 | const language = video.language | ||
28 | ? { identifier: video.language, name: getLanguageLabel(video.language) } | ||
29 | : undefined | ||
30 | |||
31 | const category = video.category | ||
32 | ? { identifier: video.category + '', name: getCategoryLabel(video.category) } | ||
33 | : undefined | ||
34 | |||
35 | const licence = video.licence | ||
36 | ? { identifier: video.licence + '', name: getLicenceLabel(video.licence) } | ||
37 | : undefined | ||
38 | |||
39 | const url: ActivityUrlObject[] = [ | ||
40 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
41 | { | ||
42 | type: 'Link', | ||
43 | mediaType: 'text/html', | ||
44 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
45 | } as ActivityUrlObject, | ||
46 | |||
47 | ...buildVideoFileUrls({ video, files: video.VideoFiles }), | ||
48 | |||
49 | ...buildStreamingPlaylistUrls(video), | ||
50 | |||
51 | ...buildTrackerUrls(video) | ||
52 | ] | ||
53 | |||
54 | return { | ||
55 | type: 'Video' as 'Video', | ||
56 | id: video.url, | ||
57 | name: video.name, | ||
58 | duration: getActivityStreamDuration(video.duration), | ||
59 | uuid: video.uuid, | ||
60 | category, | ||
61 | licence, | ||
62 | language, | ||
63 | views: video.views, | ||
64 | sensitive: video.nsfw, | ||
65 | waitTranscoding: video.waitTranscoding, | ||
66 | |||
67 | state: video.state, | ||
68 | commentsEnabled: video.commentsEnabled, | ||
69 | downloadEnabled: video.downloadEnabled, | ||
70 | published: video.publishedAt.toISOString(), | ||
71 | |||
72 | originallyPublishedAt: video.originallyPublishedAt | ||
73 | ? video.originallyPublishedAt.toISOString() | ||
74 | : null, | ||
75 | |||
76 | updated: video.updatedAt.toISOString(), | ||
77 | |||
78 | uploadDate: video.inputFileUpdatedAt?.toISOString(), | ||
79 | |||
80 | tag: buildTags(video), | ||
81 | |||
82 | mediaType: 'text/markdown', | ||
83 | content: video.description, | ||
84 | support: video.support, | ||
85 | |||
86 | subtitleLanguage: buildSubtitleLanguage(video), | ||
87 | |||
88 | icon: buildIcon(video), | ||
89 | |||
90 | preview: buildPreviewAPAttribute(video), | ||
91 | |||
92 | url, | ||
93 | |||
94 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
95 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
96 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
97 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
98 | |||
99 | attributedTo: [ | ||
100 | { | ||
101 | type: 'Person', | ||
102 | id: video.VideoChannel.Account.Actor.url | ||
103 | }, | ||
104 | { | ||
105 | type: 'Group', | ||
106 | id: video.VideoChannel.Actor.url | ||
107 | } | ||
108 | ], | ||
109 | |||
110 | ...buildLiveAPAttributes(video) | ||
111 | } | ||
112 | } | ||
113 | |||
114 | // --------------------------------------------------------------------------- | ||
115 | // Private | ||
116 | // --------------------------------------------------------------------------- | ||
117 | |||
118 | function buildLiveAPAttributes (video: MVideoAP) { | ||
119 | if (!video.isLive) { | ||
120 | return { | ||
121 | isLiveBroadcast: false, | ||
122 | liveSaveReplay: null, | ||
123 | permanentLive: null, | ||
124 | latencyMode: null | ||
125 | } | ||
126 | } | ||
127 | |||
128 | return { | ||
129 | isLiveBroadcast: true, | ||
130 | liveSaveReplay: video.VideoLive.saveReplay, | ||
131 | permanentLive: video.VideoLive.permanentLive, | ||
132 | latencyMode: video.VideoLive.latencyMode | ||
133 | } | ||
134 | } | ||
135 | |||
136 | function buildPreviewAPAttribute (video: MVideoAP): ActivityPubStoryboard[] { | ||
137 | if (!video.Storyboard) return undefined | ||
138 | |||
139 | const storyboard = video.Storyboard | ||
140 | |||
141 | return [ | ||
142 | { | ||
143 | type: 'Image', | ||
144 | rel: [ 'storyboard' ], | ||
145 | url: [ | ||
146 | { | ||
147 | mediaType: 'image/jpeg', | ||
148 | |||
149 | href: storyboard.getOriginFileUrl(video), | ||
150 | |||
151 | width: storyboard.totalWidth, | ||
152 | height: storyboard.totalHeight, | ||
153 | |||
154 | tileWidth: storyboard.spriteWidth, | ||
155 | tileHeight: storyboard.spriteHeight, | ||
156 | tileDuration: getActivityStreamDuration(storyboard.spriteDuration) | ||
157 | } | ||
158 | ] | ||
159 | } | ||
160 | ] | ||
161 | } | ||
162 | |||
163 | function buildVideoFileUrls (options: { | ||
164 | video: MVideo | ||
165 | files: MVideoFile[] | ||
166 | user?: MUserId | ||
167 | }): ActivityUrlObject[] { | ||
168 | const { video, files } = options | ||
169 | |||
170 | if (!isArray(files)) return [] | ||
171 | |||
172 | const urls: ActivityUrlObject[] = [] | ||
173 | |||
174 | const trackerUrls = video.getTrackerUrls() | ||
175 | const sortedFiles = files | ||
176 | .filter(f => !f.isLive()) | ||
177 | .sort(sortByResolutionDesc) | ||
178 | |||
179 | for (const file of sortedFiles) { | ||
180 | urls.push({ | ||
181 | type: 'Link', | ||
182 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
183 | href: file.getFileUrl(video), | ||
184 | height: file.resolution, | ||
185 | size: file.size, | ||
186 | fps: file.fps | ||
187 | }) | ||
188 | |||
189 | urls.push({ | ||
190 | type: 'Link', | ||
191 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
192 | mediaType: 'application/json' as 'application/json', | ||
193 | href: getLocalVideoFileMetadataUrl(video, file), | ||
194 | height: file.resolution, | ||
195 | fps: file.fps | ||
196 | }) | ||
197 | |||
198 | if (file.hasTorrent()) { | ||
199 | urls.push({ | ||
200 | type: 'Link', | ||
201 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
202 | href: file.getTorrentUrl(), | ||
203 | height: file.resolution | ||
204 | }) | ||
205 | |||
206 | urls.push({ | ||
207 | type: 'Link', | ||
208 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
209 | href: generateMagnetUri(video, file, trackerUrls), | ||
210 | height: file.resolution | ||
211 | }) | ||
212 | } | ||
213 | } | ||
214 | |||
215 | return urls | ||
216 | } | ||
217 | |||
218 | // --------------------------------------------------------------------------- | ||
219 | |||
220 | function buildStreamingPlaylistUrls (video: MVideoAP): ActivityPlaylistUrlObject[] { | ||
221 | if (!isArray(video.VideoStreamingPlaylists)) return [] | ||
222 | |||
223 | return video.VideoStreamingPlaylists | ||
224 | .map(playlist => ({ | ||
225 | type: 'Link', | ||
226 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
227 | href: playlist.getMasterPlaylistUrl(video), | ||
228 | tag: buildStreamingPlaylistTags(video, playlist) | ||
229 | })) | ||
230 | } | ||
231 | |||
232 | function buildStreamingPlaylistTags (video: MVideoAP, playlist: MStreamingPlaylistFiles) { | ||
233 | return [ | ||
234 | ...playlist.p2pMediaLoaderInfohashes.map(i => ({ type: 'Infohash' as 'Infohash', name: i })), | ||
235 | |||
236 | { | ||
237 | type: 'Link', | ||
238 | name: 'sha256', | ||
239 | mediaType: 'application/json' as 'application/json', | ||
240 | href: playlist.getSha256SegmentsUrl(video) | ||
241 | }, | ||
242 | |||
243 | ...buildVideoFileUrls({ video, files: playlist.VideoFiles }) | ||
244 | ] as ActivityTagObject[] | ||
245 | } | ||
246 | |||
247 | // --------------------------------------------------------------------------- | ||
248 | |||
249 | function buildTrackerUrls (video: MVideoAP): ActivityTrackerUrlObject[] { | ||
250 | return video.getTrackerUrls() | ||
251 | .map(trackerUrl => { | ||
252 | const rel2 = trackerUrl.startsWith('http') | ||
253 | ? 'http' | ||
254 | : 'websocket' | ||
255 | |||
256 | return { | ||
257 | type: 'Link', | ||
258 | name: `tracker-${rel2}`, | ||
259 | rel: [ 'tracker', rel2 ], | ||
260 | href: trackerUrl | ||
261 | } | ||
262 | }) | ||
263 | } | ||
264 | |||
265 | // --------------------------------------------------------------------------- | ||
266 | |||
267 | function buildTags (video: MVideoAP) { | ||
268 | if (!isArray(video.Tags)) return [] | ||
269 | |||
270 | return video.Tags.map(t => ({ | ||
271 | type: 'Hashtag' as 'Hashtag', | ||
272 | name: t.name | ||
273 | })) | ||
274 | } | ||
275 | |||
276 | function buildIcon (video: MVideoAP): ActivityIconObject[] { | ||
277 | return [ video.getMiniature(), video.getPreview() ] | ||
278 | .map(i => ({ | ||
279 | type: 'Image', | ||
280 | url: i.getOriginFileUrl(video), | ||
281 | mediaType: 'image/jpeg', | ||
282 | width: i.width, | ||
283 | height: i.height | ||
284 | })) | ||
285 | } | ||
286 | |||
287 | function buildSubtitleLanguage (video: MVideoAP) { | ||
288 | if (!isArray(video.VideoCaptions)) return [] | ||
289 | |||
290 | return video.VideoCaptions | ||
291 | .map(caption => ({ | ||
292 | identifier: caption.language, | ||
293 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
294 | url: caption.getFileUrl(video) | ||
295 | })) | ||
296 | } | ||
diff --git a/server/models/video/formatter/video-api-format.ts b/server/models/video/formatter/video-api-format.ts deleted file mode 100644 index 7a58f5d3c..000000000 --- a/server/models/video/formatter/video-api-format.ts +++ /dev/null | |||
@@ -1,305 +0,0 @@ | |||
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 | inputFileUpdatedAt: video.inputFileUpdatedAt, | ||
153 | state: { | ||
154 | id: video.state, | ||
155 | label: getStateLabel(video.state) | ||
156 | }, | ||
157 | |||
158 | trackerUrls: video.getTrackerUrls() | ||
159 | } | ||
160 | |||
161 | span.end() | ||
162 | |||
163 | return detailsJSON | ||
164 | } | ||
165 | |||
166 | export function streamingPlaylistsModelToFormattedJSON ( | ||
167 | video: MVideoFormattable, | ||
168 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
169 | ): VideoStreamingPlaylist[] { | ||
170 | if (isArray(playlists) === false) return [] | ||
171 | |||
172 | return playlists | ||
173 | .map(playlist => ({ | ||
174 | id: playlist.id, | ||
175 | type: playlist.type, | ||
176 | |||
177 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
178 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
179 | |||
180 | redundancies: isArray(playlist.RedundancyVideos) | ||
181 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
182 | : [], | ||
183 | |||
184 | files: videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
185 | })) | ||
186 | } | ||
187 | |||
188 | export function videoFilesModelToFormattedJSON ( | ||
189 | video: MVideoFormattable, | ||
190 | videoFiles: MVideoFileRedundanciesOpt[], | ||
191 | options: { | ||
192 | includeMagnet?: boolean // default true | ||
193 | } = {} | ||
194 | ): VideoFile[] { | ||
195 | const { includeMagnet = true } = options | ||
196 | |||
197 | if (isArray(videoFiles) === false) return [] | ||
198 | |||
199 | const trackerUrls = includeMagnet | ||
200 | ? video.getTrackerUrls() | ||
201 | : [] | ||
202 | |||
203 | return videoFiles | ||
204 | .filter(f => !f.isLive()) | ||
205 | .sort(sortByResolutionDesc) | ||
206 | .map(videoFile => { | ||
207 | return { | ||
208 | id: videoFile.id, | ||
209 | |||
210 | resolution: { | ||
211 | id: videoFile.resolution, | ||
212 | label: videoFile.resolution === 0 | ||
213 | ? 'Audio' | ||
214 | : `${videoFile.resolution}p` | ||
215 | }, | ||
216 | |||
217 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
218 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
219 | : undefined, | ||
220 | |||
221 | size: videoFile.size, | ||
222 | fps: videoFile.fps, | ||
223 | |||
224 | torrentUrl: videoFile.getTorrentUrl(), | ||
225 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
226 | |||
227 | fileUrl: videoFile.getFileUrl(video), | ||
228 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
229 | |||
230 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
231 | } | ||
232 | }) | ||
233 | } | ||
234 | |||
235 | // --------------------------------------------------------------------------- | ||
236 | |||
237 | export function getCategoryLabel (id: number) { | ||
238 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
239 | } | ||
240 | |||
241 | export function getLicenceLabel (id: number) { | ||
242 | return VIDEO_LICENCES[id] || 'Unknown' | ||
243 | } | ||
244 | |||
245 | export function getLanguageLabel (id: string) { | ||
246 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
247 | } | ||
248 | |||
249 | export function getPrivacyLabel (id: number) { | ||
250 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
251 | } | ||
252 | |||
253 | export function getStateLabel (id: number) { | ||
254 | return VIDEO_STATES[id] || 'Unknown' | ||
255 | } | ||
256 | |||
257 | // --------------------------------------------------------------------------- | ||
258 | // Private | ||
259 | // --------------------------------------------------------------------------- | ||
260 | |||
261 | function buildAdditionalAttributes (video: MVideoFormattable, options: VideoFormattingJSONOptions) { | ||
262 | const add = options.additionalAttributes | ||
263 | |||
264 | const result: Partial<VideoAdditionalAttributes> = {} | ||
265 | |||
266 | if (add?.state === true) { | ||
267 | result.state = { | ||
268 | id: video.state, | ||
269 | label: getStateLabel(video.state) | ||
270 | } | ||
271 | } | ||
272 | |||
273 | if (add?.waitTranscoding === true) { | ||
274 | result.waitTranscoding = video.waitTranscoding | ||
275 | } | ||
276 | |||
277 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
278 | result.scheduledUpdate = { | ||
279 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
280 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
281 | } | ||
282 | } | ||
283 | |||
284 | if (add?.blacklistInfo === true) { | ||
285 | result.blacklisted = !!video.VideoBlacklist | ||
286 | result.blacklistedReason = | ||
287 | video.VideoBlacklist | ||
288 | ? video.VideoBlacklist.reason | ||
289 | : null | ||
290 | } | ||
291 | |||
292 | if (add?.blockedOwner === true) { | ||
293 | result.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
294 | |||
295 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
296 | result.blockedServer = !!(server?.isBlocked()) | ||
297 | } | ||
298 | |||
299 | if (add?.files === true) { | ||
300 | result.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
301 | result.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
302 | } | ||
303 | |||
304 | return result | ||
305 | } | ||