diff options
Diffstat (limited to 'server/models/video')
22 files changed, 1072 insertions, 625 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 f2001e432..000000000 --- a/server/models/video/formatter/video-format-utils.ts +++ /dev/null | |||
@@ -1,543 +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 | ActivityTagObject, | ||
9 | ActivityUrlObject, | ||
10 | Video, | ||
11 | VideoDetails, | ||
12 | VideoFile, | ||
13 | VideoInclude, | ||
14 | VideoObject, | ||
15 | VideosCommonQueryAfterSanitize, | ||
16 | VideoStreamingPlaylist | ||
17 | } from '@shared/models' | ||
18 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
19 | import { | ||
20 | MIMETYPES, | ||
21 | VIDEO_CATEGORIES, | ||
22 | VIDEO_LANGUAGES, | ||
23 | VIDEO_LICENCES, | ||
24 | VIDEO_PRIVACIES, | ||
25 | VIDEO_STATES, | ||
26 | WEBSERVER | ||
27 | } from '../../../initializers/constants' | ||
28 | import { | ||
29 | getLocalVideoCommentsActivityPubUrl, | ||
30 | getLocalVideoDislikesActivityPubUrl, | ||
31 | getLocalVideoLikesActivityPubUrl, | ||
32 | getLocalVideoSharesActivityPubUrl | ||
33 | } from '../../../lib/activitypub/url' | ||
34 | import { | ||
35 | MServer, | ||
36 | MStreamingPlaylistRedundanciesOpt, | ||
37 | MUserId, | ||
38 | MVideo, | ||
39 | MVideoAP, | ||
40 | MVideoFile, | ||
41 | MVideoFormattable, | ||
42 | MVideoFormattableDetails | ||
43 | } from '../../../types/models' | ||
44 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' | ||
45 | import { VideoCaptionModel } from '../video-caption' | ||
46 | |||
47 | export type VideoFormattingJSONOptions = { | ||
48 | completeDescription?: boolean | ||
49 | |||
50 | additionalAttributes?: { | ||
51 | state?: boolean | ||
52 | waitTranscoding?: boolean | ||
53 | scheduledUpdate?: boolean | ||
54 | blacklistInfo?: boolean | ||
55 | files?: boolean | ||
56 | blockedOwner?: boolean | ||
57 | } | ||
58 | } | ||
59 | |||
60 | function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { | ||
61 | if (!query?.include) return {} | ||
62 | |||
63 | return { | ||
64 | additionalAttributes: { | ||
65 | state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
66 | waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
67 | scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
68 | blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), | ||
69 | files: !!(query.include & VideoInclude.FILES), | ||
70 | blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | |||
75 | function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video { | ||
76 | const span = tracer.startSpan('peertube.VideoModel.toFormattedJSON') | ||
77 | |||
78 | const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined | ||
79 | |||
80 | const videoObject: Video = { | ||
81 | id: video.id, | ||
82 | uuid: video.uuid, | ||
83 | shortUUID: uuidToShort(video.uuid), | ||
84 | |||
85 | url: video.url, | ||
86 | |||
87 | name: video.name, | ||
88 | category: { | ||
89 | id: video.category, | ||
90 | label: getCategoryLabel(video.category) | ||
91 | }, | ||
92 | licence: { | ||
93 | id: video.licence, | ||
94 | label: getLicenceLabel(video.licence) | ||
95 | }, | ||
96 | language: { | ||
97 | id: video.language, | ||
98 | label: getLanguageLabel(video.language) | ||
99 | }, | ||
100 | privacy: { | ||
101 | id: video.privacy, | ||
102 | label: getPrivacyLabel(video.privacy) | ||
103 | }, | ||
104 | nsfw: video.nsfw, | ||
105 | |||
106 | truncatedDescription: video.getTruncatedDescription(), | ||
107 | description: options && options.completeDescription === true | ||
108 | ? video.description | ||
109 | : video.getTruncatedDescription(), | ||
110 | |||
111 | isLocal: video.isOwned(), | ||
112 | duration: video.duration, | ||
113 | |||
114 | views: video.views, | ||
115 | viewers: VideoViewsManager.Instance.getViewers(video), | ||
116 | |||
117 | likes: video.likes, | ||
118 | dislikes: video.dislikes, | ||
119 | thumbnailPath: video.getMiniatureStaticPath(), | ||
120 | previewPath: video.getPreviewStaticPath(), | ||
121 | embedPath: video.getEmbedStaticPath(), | ||
122 | createdAt: video.createdAt, | ||
123 | updatedAt: video.updatedAt, | ||
124 | publishedAt: video.publishedAt, | ||
125 | originallyPublishedAt: video.originallyPublishedAt, | ||
126 | |||
127 | isLive: video.isLive, | ||
128 | |||
129 | account: video.VideoChannel.Account.toFormattedSummaryJSON(), | ||
130 | channel: video.VideoChannel.toFormattedSummaryJSON(), | ||
131 | |||
132 | userHistory: userHistory | ||
133 | ? { currentTime: userHistory.currentTime } | ||
134 | : undefined, | ||
135 | |||
136 | // Can be added by external plugins | ||
137 | pluginData: (video as any).pluginData | ||
138 | } | ||
139 | |||
140 | const add = options.additionalAttributes | ||
141 | if (add?.state === true) { | ||
142 | videoObject.state = { | ||
143 | id: video.state, | ||
144 | label: getStateLabel(video.state) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | if (add?.waitTranscoding === true) { | ||
149 | videoObject.waitTranscoding = video.waitTranscoding | ||
150 | } | ||
151 | |||
152 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
153 | videoObject.scheduledUpdate = { | ||
154 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
155 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
156 | } | ||
157 | } | ||
158 | |||
159 | if (add?.blacklistInfo === true) { | ||
160 | videoObject.blacklisted = !!video.VideoBlacklist | ||
161 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null | ||
162 | } | ||
163 | |||
164 | if (add?.blockedOwner === true) { | ||
165 | videoObject.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
166 | |||
167 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
168 | videoObject.blockedServer = !!(server?.isBlocked()) | ||
169 | } | ||
170 | |||
171 | if (add?.files === true) { | ||
172 | videoObject.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
173 | videoObject.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
174 | } | ||
175 | |||
176 | span.end() | ||
177 | |||
178 | return videoObject | ||
179 | } | ||
180 | |||
181 | function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { | ||
182 | const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') | ||
183 | |||
184 | const videoJSON = video.toFormattedJSON({ | ||
185 | completeDescription: true, | ||
186 | additionalAttributes: { | ||
187 | scheduledUpdate: true, | ||
188 | blacklistInfo: true, | ||
189 | files: true | ||
190 | } | ||
191 | }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists'>> | ||
192 | |||
193 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | ||
194 | |||
195 | const detailsJSON = { | ||
196 | support: video.support, | ||
197 | descriptionPath: video.getDescriptionAPIPath(), | ||
198 | channel: video.VideoChannel.toFormattedJSON(), | ||
199 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
200 | tags, | ||
201 | commentsEnabled: video.commentsEnabled, | ||
202 | downloadEnabled: video.downloadEnabled, | ||
203 | waitTranscoding: video.waitTranscoding, | ||
204 | state: { | ||
205 | id: video.state, | ||
206 | label: getStateLabel(video.state) | ||
207 | }, | ||
208 | |||
209 | trackerUrls: video.getTrackerUrls() | ||
210 | } | ||
211 | |||
212 | span.end() | ||
213 | |||
214 | return Object.assign(videoJSON, detailsJSON) | ||
215 | } | ||
216 | |||
217 | function streamingPlaylistsModelToFormattedJSON ( | ||
218 | video: MVideoFormattable, | ||
219 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
220 | ): VideoStreamingPlaylist[] { | ||
221 | if (isArray(playlists) === false) return [] | ||
222 | |||
223 | return playlists | ||
224 | .map(playlist => { | ||
225 | const redundancies = isArray(playlist.RedundancyVideos) | ||
226 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
227 | : [] | ||
228 | |||
229 | const files = videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
230 | |||
231 | return { | ||
232 | id: playlist.id, | ||
233 | type: playlist.type, | ||
234 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
235 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
236 | redundancies, | ||
237 | files | ||
238 | } | ||
239 | }) | ||
240 | } | ||
241 | |||
242 | function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | ||
243 | if (fileA.resolution < fileB.resolution) return 1 | ||
244 | if (fileA.resolution === fileB.resolution) return 0 | ||
245 | return -1 | ||
246 | } | ||
247 | |||
248 | function videoFilesModelToFormattedJSON ( | ||
249 | video: MVideoFormattable, | ||
250 | videoFiles: MVideoFileRedundanciesOpt[], | ||
251 | options: { | ||
252 | includeMagnet?: boolean // default true | ||
253 | } = {} | ||
254 | ): VideoFile[] { | ||
255 | const { includeMagnet = true } = options | ||
256 | |||
257 | const trackerUrls = includeMagnet | ||
258 | ? video.getTrackerUrls() | ||
259 | : [] | ||
260 | |||
261 | return (videoFiles || []) | ||
262 | .filter(f => !f.isLive()) | ||
263 | .sort(sortByResolutionDesc) | ||
264 | .map(videoFile => { | ||
265 | return { | ||
266 | id: videoFile.id, | ||
267 | |||
268 | resolution: { | ||
269 | id: videoFile.resolution, | ||
270 | label: videoFile.resolution === 0 ? 'Audio' : `${videoFile.resolution}p` | ||
271 | }, | ||
272 | |||
273 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
274 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
275 | : undefined, | ||
276 | |||
277 | size: videoFile.size, | ||
278 | fps: videoFile.fps, | ||
279 | |||
280 | torrentUrl: videoFile.getTorrentUrl(), | ||
281 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
282 | |||
283 | fileUrl: videoFile.getFileUrl(video), | ||
284 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
285 | |||
286 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
287 | } as VideoFile | ||
288 | }) | ||
289 | } | ||
290 | |||
291 | function addVideoFilesInAPAcc (options: { | ||
292 | acc: ActivityUrlObject[] | ActivityTagObject[] | ||
293 | video: MVideo | ||
294 | files: MVideoFile[] | ||
295 | user?: MUserId | ||
296 | }) { | ||
297 | const { acc, video, files } = options | ||
298 | |||
299 | const trackerUrls = video.getTrackerUrls() | ||
300 | |||
301 | const sortedFiles = (files || []) | ||
302 | .filter(f => !f.isLive()) | ||
303 | .sort(sortByResolutionDesc) | ||
304 | |||
305 | for (const file of sortedFiles) { | ||
306 | acc.push({ | ||
307 | type: 'Link', | ||
308 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
309 | href: file.getFileUrl(video), | ||
310 | height: file.resolution, | ||
311 | size: file.size, | ||
312 | fps: file.fps | ||
313 | }) | ||
314 | |||
315 | acc.push({ | ||
316 | type: 'Link', | ||
317 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
318 | mediaType: 'application/json' as 'application/json', | ||
319 | href: getLocalVideoFileMetadataUrl(video, file), | ||
320 | height: file.resolution, | ||
321 | fps: file.fps | ||
322 | }) | ||
323 | |||
324 | if (file.hasTorrent()) { | ||
325 | acc.push({ | ||
326 | type: 'Link', | ||
327 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
328 | href: file.getTorrentUrl(), | ||
329 | height: file.resolution | ||
330 | }) | ||
331 | |||
332 | acc.push({ | ||
333 | type: 'Link', | ||
334 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
335 | href: generateMagnetUri(video, file, trackerUrls), | ||
336 | height: file.resolution | ||
337 | }) | ||
338 | } | ||
339 | } | ||
340 | } | ||
341 | |||
342 | function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
343 | if (!video.Tags) video.Tags = [] | ||
344 | |||
345 | const tag = video.Tags.map(t => ({ | ||
346 | type: 'Hashtag' as 'Hashtag', | ||
347 | name: t.name | ||
348 | })) | ||
349 | |||
350 | let language | ||
351 | if (video.language) { | ||
352 | language = { | ||
353 | identifier: video.language, | ||
354 | name: getLanguageLabel(video.language) | ||
355 | } | ||
356 | } | ||
357 | |||
358 | let category | ||
359 | if (video.category) { | ||
360 | category = { | ||
361 | identifier: video.category + '', | ||
362 | name: getCategoryLabel(video.category) | ||
363 | } | ||
364 | } | ||
365 | |||
366 | let licence | ||
367 | if (video.licence) { | ||
368 | licence = { | ||
369 | identifier: video.licence + '', | ||
370 | name: getLicenceLabel(video.licence) | ||
371 | } | ||
372 | } | ||
373 | |||
374 | const url: ActivityUrlObject[] = [ | ||
375 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
376 | { | ||
377 | type: 'Link', | ||
378 | mediaType: 'text/html', | ||
379 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
380 | } | ||
381 | ] | ||
382 | |||
383 | addVideoFilesInAPAcc({ acc: url, video, files: video.VideoFiles || [] }) | ||
384 | |||
385 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | ||
386 | const tag = playlist.p2pMediaLoaderInfohashes | ||
387 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) as ActivityTagObject[] | ||
388 | tag.push({ | ||
389 | type: 'Link', | ||
390 | name: 'sha256', | ||
391 | mediaType: 'application/json' as 'application/json', | ||
392 | href: playlist.getSha256SegmentsUrl(video) | ||
393 | }) | ||
394 | |||
395 | addVideoFilesInAPAcc({ acc: tag, video, files: playlist.VideoFiles || [] }) | ||
396 | |||
397 | url.push({ | ||
398 | type: 'Link', | ||
399 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
400 | href: playlist.getMasterPlaylistUrl(video), | ||
401 | tag | ||
402 | }) | ||
403 | } | ||
404 | |||
405 | for (const trackerUrl of video.getTrackerUrls()) { | ||
406 | const rel2 = trackerUrl.startsWith('http') | ||
407 | ? 'http' | ||
408 | : 'websocket' | ||
409 | |||
410 | url.push({ | ||
411 | type: 'Link', | ||
412 | name: `tracker-${rel2}`, | ||
413 | rel: [ 'tracker', rel2 ], | ||
414 | href: trackerUrl | ||
415 | }) | ||
416 | } | ||
417 | |||
418 | const subtitleLanguage = [] | ||
419 | for (const caption of video.VideoCaptions) { | ||
420 | subtitleLanguage.push({ | ||
421 | identifier: caption.language, | ||
422 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
423 | url: caption.getFileUrl(video) | ||
424 | }) | ||
425 | } | ||
426 | |||
427 | const icons = [ video.getMiniature(), video.getPreview() ] | ||
428 | |||
429 | return { | ||
430 | type: 'Video' as 'Video', | ||
431 | id: video.url, | ||
432 | name: video.name, | ||
433 | duration: getActivityStreamDuration(video.duration), | ||
434 | uuid: video.uuid, | ||
435 | tag, | ||
436 | category, | ||
437 | licence, | ||
438 | language, | ||
439 | views: video.views, | ||
440 | sensitive: video.nsfw, | ||
441 | waitTranscoding: video.waitTranscoding, | ||
442 | |||
443 | state: video.state, | ||
444 | commentsEnabled: video.commentsEnabled, | ||
445 | downloadEnabled: video.downloadEnabled, | ||
446 | published: video.publishedAt.toISOString(), | ||
447 | |||
448 | originallyPublishedAt: video.originallyPublishedAt | ||
449 | ? video.originallyPublishedAt.toISOString() | ||
450 | : null, | ||
451 | |||
452 | updated: video.updatedAt.toISOString(), | ||
453 | |||
454 | mediaType: 'text/markdown', | ||
455 | content: video.description, | ||
456 | support: video.support, | ||
457 | |||
458 | subtitleLanguage, | ||
459 | |||
460 | icon: icons.map(i => ({ | ||
461 | type: 'Image', | ||
462 | url: i.getOriginFileUrl(video), | ||
463 | mediaType: 'image/jpeg', | ||
464 | width: i.width, | ||
465 | height: i.height | ||
466 | })), | ||
467 | |||
468 | url, | ||
469 | |||
470 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
471 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
472 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
473 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
474 | |||
475 | attributedTo: [ | ||
476 | { | ||
477 | type: 'Person', | ||
478 | id: video.VideoChannel.Account.Actor.url | ||
479 | }, | ||
480 | { | ||
481 | type: 'Group', | ||
482 | id: video.VideoChannel.Actor.url | ||
483 | } | ||
484 | ], | ||
485 | |||
486 | ...buildLiveAPAttributes(video) | ||
487 | } | ||
488 | } | ||
489 | |||
490 | function getCategoryLabel (id: number) { | ||
491 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
492 | } | ||
493 | |||
494 | function getLicenceLabel (id: number) { | ||
495 | return VIDEO_LICENCES[id] || 'Unknown' | ||
496 | } | ||
497 | |||
498 | function getLanguageLabel (id: string) { | ||
499 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
500 | } | ||
501 | |||
502 | function getPrivacyLabel (id: number) { | ||
503 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
504 | } | ||
505 | |||
506 | function getStateLabel (id: number) { | ||
507 | return VIDEO_STATES[id] || 'Unknown' | ||
508 | } | ||
509 | |||
510 | export { | ||
511 | videoModelToFormattedJSON, | ||
512 | videoModelToFormattedDetailsJSON, | ||
513 | videoFilesModelToFormattedJSON, | ||
514 | videoModelToActivityPubObject, | ||
515 | |||
516 | guessAdditionalAttributesFromQuery, | ||
517 | |||
518 | getCategoryLabel, | ||
519 | getLicenceLabel, | ||
520 | getLanguageLabel, | ||
521 | getPrivacyLabel, | ||
522 | getStateLabel | ||
523 | } | ||
524 | |||
525 | // --------------------------------------------------------------------------- | ||
526 | |||
527 | function buildLiveAPAttributes (video: MVideoAP) { | ||
528 | if (!video.isLive) { | ||
529 | return { | ||
530 | isLiveBroadcast: false, | ||
531 | liveSaveReplay: null, | ||
532 | permanentLive: null, | ||
533 | latencyMode: null | ||
534 | } | ||
535 | } | ||
536 | |||
537 | return { | ||
538 | isLiveBroadcast: true, | ||
539 | liveSaveReplay: video.VideoLive.saveReplay, | ||
540 | permanentLive: video.VideoLive.permanentLive, | ||
541 | latencyMode: video.VideoLive.latencyMode | ||
542 | } | ||
543 | } | ||
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index cbd57ad8c..56a00aa0c 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -111,7 +111,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
111 | } | 111 | } |
112 | } | 112 | } |
113 | 113 | ||
114 | protected includeWebtorrentFiles () { | 114 | protected includeWebVideoFiles () { |
115 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') | 115 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') |
116 | 116 | ||
117 | this.attributes = { | 117 | this.attributes = { |
@@ -263,7 +263,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
263 | } | 263 | } |
264 | } | 264 | } |
265 | 265 | ||
266 | protected includeWebTorrentRedundancies () { | 266 | protected includeWebVideoRedundancies () { |
267 | this.addJoin( | 267 | this.addJoin( |
268 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + | 268 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + |
269 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' | 269 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' |
diff --git a/server/models/video/sql/video/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index cc53a4860..196b72b43 100644 --- a/server/models/video/sql/video/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
@@ -14,7 +14,7 @@ export type FileQueryOptions = { | |||
14 | 14 | ||
15 | /** | 15 | /** |
16 | * | 16 | * |
17 | * Fetch files (webtorrent and streaming playlist) according to a video | 17 | * Fetch files (web videos and streaming playlist) according to a video |
18 | * | 18 | * |
19 | */ | 19 | */ |
20 | 20 | ||
@@ -25,8 +25,8 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
25 | super(sequelize, 'get') | 25 | super(sequelize, 'get') |
26 | } | 26 | } |
27 | 27 | ||
28 | queryWebTorrentVideos (options: FileQueryOptions) { | 28 | queryWebVideos (options: FileQueryOptions) { |
29 | this.buildWebtorrentFilesQuery(options) | 29 | this.buildWebVideoFilesQuery(options) |
30 | 30 | ||
31 | return this.runQuery(options) | 31 | return this.runQuery(options) |
32 | } | 32 | } |
@@ -37,15 +37,15 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
37 | return this.runQuery(options) | 37 | return this.runQuery(options) |
38 | } | 38 | } |
39 | 39 | ||
40 | private buildWebtorrentFilesQuery (options: FileQueryOptions) { | 40 | private buildWebVideoFilesQuery (options: FileQueryOptions) { |
41 | this.attributes = { | 41 | this.attributes = { |
42 | '"video"."id"': '' | 42 | '"video"."id"': '' |
43 | } | 43 | } |
44 | 44 | ||
45 | this.includeWebtorrentFiles() | 45 | this.includeWebVideoFiles() |
46 | 46 | ||
47 | if (options.includeRedundancy) { | 47 | if (options.includeRedundancy) { |
48 | this.includeWebTorrentRedundancies() | 48 | this.includeWebVideoRedundancies() |
49 | } | 49 | } |
50 | 50 | ||
51 | this.whereId(options) | 51 | this.whereId(options) |
diff --git a/server/models/video/sql/video/shared/video-model-builder.ts b/server/models/video/sql/video/shared/video-model-builder.ts index 0a2beb7db..740aa842f 100644 --- a/server/models/video/sql/video/shared/video-model-builder.ts +++ b/server/models/video/sql/video/shared/video-model-builder.ts | |||
@@ -60,10 +60,10 @@ export class VideoModelBuilder { | |||
60 | buildVideosFromRows (options: { | 60 | buildVideosFromRows (options: { |
61 | rows: SQLRow[] | 61 | rows: SQLRow[] |
62 | include?: VideoInclude | 62 | include?: VideoInclude |
63 | rowsWebTorrentFiles?: SQLRow[] | 63 | rowsWebVideoFiles?: SQLRow[] |
64 | rowsStreamingPlaylist?: SQLRow[] | 64 | rowsStreamingPlaylist?: SQLRow[] |
65 | }) { | 65 | }) { |
66 | const { rows, rowsWebTorrentFiles, rowsStreamingPlaylist, include } = options | 66 | const { rows, rowsWebVideoFiles, rowsStreamingPlaylist, include } = options |
67 | 67 | ||
68 | this.reinit() | 68 | this.reinit() |
69 | 69 | ||
@@ -85,8 +85,8 @@ export class VideoModelBuilder { | |||
85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) | 85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) |
86 | } | 86 | } |
87 | 87 | ||
88 | if (!rowsWebTorrentFiles) { | 88 | if (!rowsWebVideoFiles) { |
89 | this.addWebTorrentFile(row, videoModel) | 89 | this.addWebVideoFile(row, videoModel) |
90 | } | 90 | } |
91 | 91 | ||
92 | if (!rowsStreamingPlaylist) { | 92 | if (!rowsStreamingPlaylist) { |
@@ -112,7 +112,7 @@ export class VideoModelBuilder { | |||
112 | } | 112 | } |
113 | } | 113 | } |
114 | 114 | ||
115 | this.grabSeparateWebTorrentFiles(rowsWebTorrentFiles) | 115 | this.grabSeparateWebVideoFiles(rowsWebVideoFiles) |
116 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) | 116 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) |
117 | 117 | ||
118 | return this.videos | 118 | return this.videos |
@@ -140,15 +140,15 @@ export class VideoModelBuilder { | |||
140 | this.videos = [] | 140 | this.videos = [] |
141 | } | 141 | } |
142 | 142 | ||
143 | private grabSeparateWebTorrentFiles (rowsWebTorrentFiles?: SQLRow[]) { | 143 | private grabSeparateWebVideoFiles (rowsWebVideoFiles?: SQLRow[]) { |
144 | if (!rowsWebTorrentFiles) return | 144 | if (!rowsWebVideoFiles) return |
145 | 145 | ||
146 | for (const row of rowsWebTorrentFiles) { | 146 | for (const row of rowsWebVideoFiles) { |
147 | const id = row['VideoFiles.id'] | 147 | const id = row['VideoFiles.id'] |
148 | if (!id) continue | 148 | if (!id) continue |
149 | 149 | ||
150 | const videoModel = this.videosMemo[row.id] | 150 | const videoModel = this.videosMemo[row.id] |
151 | this.addWebTorrentFile(row, videoModel) | 151 | this.addWebVideoFile(row, videoModel) |
152 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) | 152 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) |
153 | } | 153 | } |
154 | } | 154 | } |
@@ -258,7 +258,7 @@ export class VideoModelBuilder { | |||
258 | this.thumbnailsDone.add(id) | 258 | this.thumbnailsDone.add(id) |
259 | } | 259 | } |
260 | 260 | ||
261 | private addWebTorrentFile (row: SQLRow, videoModel: VideoModel) { | 261 | private addWebVideoFile (row: SQLRow, videoModel: VideoModel) { |
262 | const id = row['VideoFiles.id'] | 262 | const id = row['VideoFiles.id'] |
263 | if (!id || this.videoFileMemo[id]) return | 263 | if (!id || this.videoFileMemo[id]) return |
264 | 264 | ||
diff --git a/server/models/video/sql/video/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts index 34967cd20..e0fa9d7c1 100644 --- a/server/models/video/sql/video/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts | |||
@@ -60,6 +60,7 @@ export class VideoTableAttributes { | |||
60 | 'height', | 60 | 'height', |
61 | 'width', | 61 | 'width', |
62 | 'fileUrl', | 62 | 'fileUrl', |
63 | 'onDisk', | ||
63 | 'automaticallyGenerated', | 64 | 'automaticallyGenerated', |
64 | 'videoId', | 65 | 'videoId', |
65 | 'videoPlaylistId', | 66 | 'videoPlaylistId', |
diff --git a/server/models/video/sql/video/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index 8e90ff641..3f43d4d92 100644 --- a/server/models/video/sql/video/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
@@ -35,7 +35,7 @@ export type BuildVideoGetQueryOptions = { | |||
35 | 35 | ||
36 | export class VideoModelGetQueryBuilder { | 36 | export class VideoModelGetQueryBuilder { |
37 | videoQueryBuilder: VideosModelGetQuerySubBuilder | 37 | videoQueryBuilder: VideosModelGetQuerySubBuilder |
38 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | 38 | webVideoFilesQueryBuilder: VideoFileQueryBuilder |
39 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | 39 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder |
40 | 40 | ||
41 | private readonly videoModelBuilder: VideoModelBuilder | 41 | private readonly videoModelBuilder: VideoModelBuilder |
@@ -44,7 +44,7 @@ export class VideoModelGetQueryBuilder { | |||
44 | 44 | ||
45 | constructor (protected readonly sequelize: Sequelize) { | 45 | constructor (protected readonly sequelize: Sequelize) { |
46 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) | 46 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) |
47 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 47 | this.webVideoFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
48 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 48 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
49 | 49 | ||
50 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTableAttributes('get')) | 50 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTableAttributes('get')) |
@@ -57,11 +57,11 @@ export class VideoModelGetQueryBuilder { | |||
57 | includeRedundancy: this.shouldIncludeRedundancies(options) | 57 | includeRedundancy: this.shouldIncludeRedundancies(options) |
58 | } | 58 | } |
59 | 59 | ||
60 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ | 60 | const [ videoRows, webVideoFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ |
61 | this.videoQueryBuilder.queryVideos(options), | 61 | this.videoQueryBuilder.queryVideos(options), |
62 | 62 | ||
63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
64 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions) | 64 | ? this.webVideoFilesQueryBuilder.queryWebVideos(fileQueryOptions) |
65 | : Promise.resolve(undefined), | 65 | : Promise.resolve(undefined), |
66 | 66 | ||
67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
@@ -71,7 +71,7 @@ export class VideoModelGetQueryBuilder { | |||
71 | 71 | ||
72 | const videos = this.videoModelBuilder.buildVideosFromRows({ | 72 | const videos = this.videoModelBuilder.buildVideosFromRows({ |
73 | rows: videoRows, | 73 | rows: videoRows, |
74 | rowsWebTorrentFiles: webtorrentFilesRows, | 74 | rowsWebVideoFiles: webVideoFilesRows, |
75 | rowsStreamingPlaylist: streamingPlaylistFilesRows | 75 | rowsStreamingPlaylist: streamingPlaylistFilesRows |
76 | }) | 76 | }) |
77 | 77 | ||
@@ -92,7 +92,7 @@ export class VideoModelGetQueryBuilder { | |||
92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { | 92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { |
93 | protected attributes: { [key: string]: string } | 93 | protected attributes: { [key: string]: string } |
94 | 94 | ||
95 | protected webtorrentFilesQuery: string | 95 | protected webVideoFilesQuery: string |
96 | protected streamingPlaylistFilesQuery: string | 96 | protected streamingPlaylistFilesQuery: string |
97 | 97 | ||
98 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) | 98 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) |
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index cba77c1d1..7f2376102 100644 --- a/server/models/video/sql/video/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
@@ -48,7 +48,9 @@ export type BuildVideosListQueryOptions = { | |||
48 | 48 | ||
49 | hasFiles?: boolean | 49 | hasFiles?: boolean |
50 | hasHLSFiles?: boolean | 50 | hasHLSFiles?: boolean |
51 | hasWebtorrentFiles?: boolean | 51 | |
52 | hasWebVideoFiles?: boolean | ||
53 | hasWebtorrentFiles?: boolean // TODO: Remove in v7 | ||
52 | 54 | ||
53 | accountId?: number | 55 | accountId?: number |
54 | videoChannelId?: number | 56 | videoChannelId?: number |
@@ -175,7 +177,9 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
175 | } | 177 | } |
176 | 178 | ||
177 | if (exists(options.hasWebtorrentFiles)) { | 179 | if (exists(options.hasWebtorrentFiles)) { |
178 | this.whereWebTorrentFileExists(options.hasWebtorrentFiles) | 180 | this.whereWebVideoFileExists(options.hasWebtorrentFiles) |
181 | } else if (exists(options.hasWebVideoFiles)) { | ||
182 | this.whereWebVideoFileExists(options.hasWebVideoFiles) | ||
179 | } | 183 | } |
180 | 184 | ||
181 | if (exists(options.hasHLSFiles)) { | 185 | if (exists(options.hasHLSFiles)) { |
@@ -400,18 +404,18 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
400 | } | 404 | } |
401 | 405 | ||
402 | private whereFileExists () { | 406 | private whereFileExists () { |
403 | this.and.push(`(${this.buildWebTorrentFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`) | 407 | this.and.push(`(${this.buildWebVideoFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`) |
404 | } | 408 | } |
405 | 409 | ||
406 | private whereWebTorrentFileExists (exists: boolean) { | 410 | private whereWebVideoFileExists (exists: boolean) { |
407 | this.and.push(this.buildWebTorrentFileExistsQuery(exists)) | 411 | this.and.push(this.buildWebVideoFileExistsQuery(exists)) |
408 | } | 412 | } |
409 | 413 | ||
410 | private whereHLSFileExists (exists: boolean) { | 414 | private whereHLSFileExists (exists: boolean) { |
411 | this.and.push(this.buildHLSFileExistsQuery(exists)) | 415 | this.and.push(this.buildHLSFileExistsQuery(exists)) |
412 | } | 416 | } |
413 | 417 | ||
414 | private buildWebTorrentFileExistsQuery (exists: boolean) { | 418 | private buildWebVideoFileExistsQuery (exists: boolean) { |
415 | const prefix = exists ? '' : 'NOT ' | 419 | const prefix = exists ? '' : 'NOT ' |
416 | 420 | ||
417 | return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' | 421 | return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' |
diff --git a/server/models/video/sql/video/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index 3fdac4ed3..b73dc28cd 100644 --- a/server/models/video/sql/video/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
@@ -18,7 +18,7 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
18 | private innerQuery: string | 18 | private innerQuery: string |
19 | private innerSort: string | 19 | private innerSort: string |
20 | 20 | ||
21 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | 21 | webVideoFilesQueryBuilder: VideoFileQueryBuilder |
22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | 22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder |
23 | 23 | ||
24 | private readonly videoModelBuilder: VideoModelBuilder | 24 | private readonly videoModelBuilder: VideoModelBuilder |
@@ -27,7 +27,7 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
27 | super(sequelize, 'list') | 27 | super(sequelize, 'list') |
28 | 28 | ||
29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) | 29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) |
30 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 30 | this.webVideoFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
32 | } | 32 | } |
33 | 33 | ||
@@ -48,12 +48,12 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
48 | includeRedundancy: false | 48 | includeRedundancy: false |
49 | } | 49 | } |
50 | 50 | ||
51 | const [ rowsWebTorrentFiles, rowsStreamingPlaylist ] = await Promise.all([ | 51 | const [ rowsWebVideoFiles, rowsStreamingPlaylist ] = await Promise.all([ |
52 | this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions), | 52 | this.webVideoFilesQueryBuilder.queryWebVideos(fileQueryOptions), |
53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) | 53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) |
54 | ]) | 54 | ]) |
55 | 55 | ||
56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebTorrentFiles }) | 56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebVideoFiles }) |
57 | } | 57 | } |
58 | } | 58 | } |
59 | 59 | ||
diff --git a/server/models/video/storyboard.ts b/server/models/video/storyboard.ts new file mode 100644 index 000000000..65a044c98 --- /dev/null +++ b/server/models/video/storyboard.ts | |||
@@ -0,0 +1,169 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { MStoryboard, MStoryboardVideo, MVideo } from '@server/types/models' | ||
6 | import { Storyboard } from '@shared/models' | ||
7 | import { AttributesOnly } from '@shared/typescript-utils' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | ||
10 | import { VideoModel } from './video' | ||
11 | import { Transaction } from 'sequelize' | ||
12 | |||
13 | @Table({ | ||
14 | tableName: 'storyboard', | ||
15 | indexes: [ | ||
16 | { | ||
17 | fields: [ 'videoId' ], | ||
18 | unique: true | ||
19 | }, | ||
20 | { | ||
21 | fields: [ 'filename' ], | ||
22 | unique: true | ||
23 | } | ||
24 | ] | ||
25 | }) | ||
26 | export class StoryboardModel extends Model<Partial<AttributesOnly<StoryboardModel>>> { | ||
27 | |||
28 | @AllowNull(false) | ||
29 | @Column | ||
30 | filename: string | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Column | ||
34 | totalHeight: number | ||
35 | |||
36 | @AllowNull(false) | ||
37 | @Column | ||
38 | totalWidth: number | ||
39 | |||
40 | @AllowNull(false) | ||
41 | @Column | ||
42 | spriteHeight: number | ||
43 | |||
44 | @AllowNull(false) | ||
45 | @Column | ||
46 | spriteWidth: number | ||
47 | |||
48 | @AllowNull(false) | ||
49 | @Column | ||
50 | spriteDuration: number | ||
51 | |||
52 | @AllowNull(true) | ||
53 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max)) | ||
54 | fileUrl: string | ||
55 | |||
56 | @ForeignKey(() => VideoModel) | ||
57 | @Column | ||
58 | videoId: number | ||
59 | |||
60 | @BelongsTo(() => VideoModel, { | ||
61 | foreignKey: { | ||
62 | allowNull: true | ||
63 | }, | ||
64 | onDelete: 'CASCADE' | ||
65 | }) | ||
66 | Video: VideoModel | ||
67 | |||
68 | @CreatedAt | ||
69 | createdAt: Date | ||
70 | |||
71 | @UpdatedAt | ||
72 | updatedAt: Date | ||
73 | |||
74 | @AfterDestroy | ||
75 | static removeInstanceFile (instance: StoryboardModel) { | ||
76 | logger.info('Removing storyboard file %s.', instance.filename) | ||
77 | |||
78 | // Don't block the transaction | ||
79 | instance.removeFile() | ||
80 | .catch(err => logger.error('Cannot remove storyboard file %s.', instance.filename, { err })) | ||
81 | } | ||
82 | |||
83 | static loadByVideo (videoId: number, transaction?: Transaction): Promise<MStoryboard> { | ||
84 | const query = { | ||
85 | where: { | ||
86 | videoId | ||
87 | }, | ||
88 | transaction | ||
89 | } | ||
90 | |||
91 | return StoryboardModel.findOne(query) | ||
92 | } | ||
93 | |||
94 | static loadByFilename (filename: string): Promise<MStoryboard> { | ||
95 | const query = { | ||
96 | where: { | ||
97 | filename | ||
98 | } | ||
99 | } | ||
100 | |||
101 | return StoryboardModel.findOne(query) | ||
102 | } | ||
103 | |||
104 | static loadWithVideoByFilename (filename: string): Promise<MStoryboardVideo> { | ||
105 | const query = { | ||
106 | where: { | ||
107 | filename | ||
108 | }, | ||
109 | include: [ | ||
110 | { | ||
111 | model: VideoModel.unscoped(), | ||
112 | required: true | ||
113 | } | ||
114 | ] | ||
115 | } | ||
116 | |||
117 | return StoryboardModel.findOne(query) | ||
118 | } | ||
119 | |||
120 | // --------------------------------------------------------------------------- | ||
121 | |||
122 | static async listStoryboardsOf (video: MVideo): Promise<MStoryboardVideo[]> { | ||
123 | const query = { | ||
124 | where: { | ||
125 | videoId: video.id | ||
126 | } | ||
127 | } | ||
128 | |||
129 | const storyboards = await StoryboardModel.findAll<MStoryboard>(query) | ||
130 | |||
131 | return storyboards.map(s => Object.assign(s, { Video: video })) | ||
132 | } | ||
133 | |||
134 | // --------------------------------------------------------------------------- | ||
135 | |||
136 | getOriginFileUrl (video: MVideo) { | ||
137 | if (video.isOwned()) { | ||
138 | return WEBSERVER.URL + this.getLocalStaticPath() | ||
139 | } | ||
140 | |||
141 | return this.fileUrl | ||
142 | } | ||
143 | |||
144 | getLocalStaticPath () { | ||
145 | return LAZY_STATIC_PATHS.STORYBOARDS + this.filename | ||
146 | } | ||
147 | |||
148 | getPath () { | ||
149 | return join(CONFIG.STORAGE.STORYBOARDS_DIR, this.filename) | ||
150 | } | ||
151 | |||
152 | removeFile () { | ||
153 | return remove(this.getPath()) | ||
154 | } | ||
155 | |||
156 | toFormattedJSON (this: MStoryboardVideo): Storyboard { | ||
157 | return { | ||
158 | storyboardPath: this.getLocalStaticPath(), | ||
159 | |||
160 | totalHeight: this.totalHeight, | ||
161 | totalWidth: this.totalWidth, | ||
162 | |||
163 | spriteWidth: this.spriteWidth, | ||
164 | spriteHeight: this.spriteHeight, | ||
165 | |||
166 | spriteDuration: this.spriteDuration | ||
167 | } | ||
168 | } | ||
169 | } | ||
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index a4ac581e5..1722acdb4 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -21,7 +21,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
22 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
23 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
24 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | 24 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants' |
25 | import { VideoModel } from './video' | 25 | import { VideoModel } from './video' |
26 | import { VideoPlaylistModel } from './video-playlist' | 26 | import { VideoPlaylistModel } from './video-playlist' |
27 | 27 | ||
@@ -69,6 +69,10 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
69 | @Column | 69 | @Column |
70 | automaticallyGenerated: boolean | 70 | automaticallyGenerated: boolean |
71 | 71 | ||
72 | @AllowNull(false) | ||
73 | @Column | ||
74 | onDisk: boolean | ||
75 | |||
72 | @ForeignKey(() => VideoModel) | 76 | @ForeignKey(() => VideoModel) |
73 | @Column | 77 | @Column |
74 | videoId: number | 78 | videoId: number |
@@ -106,7 +110,7 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
106 | [ThumbnailType.MINIATURE]: { | 110 | [ThumbnailType.MINIATURE]: { |
107 | label: 'miniature', | 111 | label: 'miniature', |
108 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, | 112 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, |
109 | staticPath: STATIC_PATHS.THUMBNAILS | 113 | staticPath: LAZY_STATIC_PATHS.THUMBNAILS |
110 | }, | 114 | }, |
111 | [ThumbnailType.PREVIEW]: { | 115 | [ThumbnailType.PREVIEW]: { |
112 | label: 'preview', | 116 | label: 'preview', |
@@ -197,4 +201,8 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
197 | 201 | ||
198 | this.previousThumbnailFilename = undefined | 202 | this.previousThumbnailFilename = undefined |
199 | } | 203 | } |
204 | |||
205 | isOwned () { | ||
206 | return !this.fileUrl | ||
207 | } | ||
200 | } | 208 | } |
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index 1fb1cae82..dd4cefd65 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | Table, | 15 | Table, |
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' | 18 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionLanguageUrl, MVideoCaptionVideo } from '@server/types/models' |
19 | import { buildUUID } from '@shared/extra-utils' | 19 | import { buildUUID } from '@shared/extra-utils' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | 20 | import { AttributesOnly } from '@shared/typescript-utils' |
21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' | 21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' |
@@ -225,7 +225,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption | |||
225 | } | 225 | } |
226 | } | 226 | } |
227 | 227 | ||
228 | getCaptionStaticPath (this: MVideoCaption) { | 228 | getCaptionStaticPath (this: MVideoCaptionLanguageUrl) { |
229 | return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename) | 229 | return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename) |
230 | } | 230 | } |
231 | 231 | ||
@@ -233,9 +233,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption | |||
233 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) | 233 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) |
234 | } | 234 | } |
235 | 235 | ||
236 | getFileUrl (video: MVideo) { | 236 | getFileUrl (this: MVideoCaptionLanguageUrl, video: MVideo) { |
237 | if (!this.Video) this.Video = video as VideoModel | ||
238 | |||
239 | if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() | 237 | if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() |
240 | 238 | ||
241 | return this.fileUrl | 239 | return this.fileUrl |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 2db4b523a..26f072f4f 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -45,7 +45,7 @@ enum ScopeNames { | |||
45 | { | 45 | { |
46 | model: VideoModel.scope([ | 46 | model: VideoModel.scope([ |
47 | VideoScopeNames.WITH_THUMBNAILS, | 47 | VideoScopeNames.WITH_THUMBNAILS, |
48 | VideoScopeNames.WITH_WEBTORRENT_FILES, | 48 | VideoScopeNames.WITH_WEB_VIDEO_FILES, |
49 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, | 49 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, |
50 | VideoScopeNames.WITH_ACCOUNT_DETAILS | 50 | VideoScopeNames.WITH_ACCOUNT_DETAILS |
51 | ]), | 51 | ]), |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 07bc13de1..ee34ad2ff 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -26,8 +26,8 @@ import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | |||
26 | import { | 26 | import { |
27 | getHLSPrivateFileUrl, | 27 | getHLSPrivateFileUrl, |
28 | getHLSPublicFileUrl, | 28 | getHLSPublicFileUrl, |
29 | getWebTorrentPrivateFileUrl, | 29 | getWebVideoPrivateFileUrl, |
30 | getWebTorrentPublicFileUrl | 30 | getWebVideoPublicFileUrl |
31 | } from '@server/lib/object-storage' | 31 | } from '@server/lib/object-storage' |
32 | import { getFSTorrentFilePath } from '@server/lib/paths' | 32 | import { getFSTorrentFilePath } from '@server/lib/paths' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | 33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' |
@@ -276,15 +276,15 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
276 | 276 | ||
277 | static async doesOwnedTorrentFileExist (filename: string) { | 277 | static async doesOwnedTorrentFileExist (filename: string) { |
278 | const query = 'SELECT 1 FROM "videoFile" ' + | 278 | const query = 'SELECT 1 FROM "videoFile" ' + |
279 | 'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' + | 279 | 'LEFT JOIN "video" "webvideo" ON "webvideo"."id" = "videoFile"."videoId" AND "webvideo"."remote" IS FALSE ' + |
280 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + | 280 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + |
281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | 281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + |
282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | 282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webvideo"."id" IS NOT NULL) LIMIT 1' |
283 | 283 | ||
284 | return doesExist(this.sequelize, query, { filename }) | 284 | return doesExist(this.sequelize, query, { filename }) |
285 | } | 285 | } |
286 | 286 | ||
287 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | 287 | static async doesOwnedWebVideoFileExist (filename: string) { |
288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | 288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + |
289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
290 | 290 | ||
@@ -378,7 +378,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
378 | } | 378 | } |
379 | 379 | ||
380 | static getStats () { | 380 | static getStats () { |
381 | const webtorrentFilesQuery: FindOptions = { | 381 | const webVideoFilesQuery: FindOptions = { |
382 | include: [ | 382 | include: [ |
383 | { | 383 | { |
384 | attributes: [], | 384 | attributes: [], |
@@ -412,10 +412,10 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
412 | } | 412 | } |
413 | 413 | ||
414 | return Promise.all([ | 414 | return Promise.all([ |
415 | VideoFileModel.aggregate('size', 'SUM', webtorrentFilesQuery), | 415 | VideoFileModel.aggregate('size', 'SUM', webVideoFilesQuery), |
416 | VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery) | 416 | VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery) |
417 | ]).then(([ webtorrentResult, hlsResult ]) => ({ | 417 | ]).then(([ webVideoResult, hlsResult ]) => ({ |
418 | totalLocalVideoFilesSize: parseAggregateResult(webtorrentResult) + parseAggregateResult(hlsResult) | 418 | totalLocalVideoFilesSize: parseAggregateResult(webVideoResult) + parseAggregateResult(hlsResult) |
419 | })) | 419 | })) |
420 | } | 420 | } |
421 | 421 | ||
@@ -433,7 +433,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
433 | 433 | ||
434 | const element = mode === 'streaming-playlist' | 434 | const element = mode === 'streaming-playlist' |
435 | ? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId }) | 435 | ? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId }) |
436 | : await VideoFileModel.loadWebTorrentFile({ ...baseFind, videoId: videoFile.videoId }) | 436 | : await VideoFileModel.loadWebVideoFile({ ...baseFind, videoId: videoFile.videoId }) |
437 | 437 | ||
438 | if (!element) return videoFile.save({ transaction }) | 438 | if (!element) return videoFile.save({ transaction }) |
439 | 439 | ||
@@ -444,7 +444,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
444 | return element.save({ transaction }) | 444 | return element.save({ transaction }) |
445 | } | 445 | } |
446 | 446 | ||
447 | static async loadWebTorrentFile (options: { | 447 | static async loadWebVideoFile (options: { |
448 | videoId: number | 448 | videoId: number |
449 | fps: number | 449 | fps: number |
450 | resolution: number | 450 | resolution: number |
@@ -523,7 +523,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
523 | return getHLSPrivateFileUrl(video, this.filename) | 523 | return getHLSPrivateFileUrl(video, this.filename) |
524 | } | 524 | } |
525 | 525 | ||
526 | return getWebTorrentPrivateFileUrl(this.filename) | 526 | return getWebVideoPrivateFileUrl(this.filename) |
527 | } | 527 | } |
528 | 528 | ||
529 | private getPublicObjectStorageUrl () { | 529 | private getPublicObjectStorageUrl () { |
@@ -531,7 +531,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
531 | return getHLSPublicFileUrl(this.fileUrl) | 531 | return getHLSPublicFileUrl(this.fileUrl) |
532 | } | 532 | } |
533 | 533 | ||
534 | return getWebTorrentPublicFileUrl(this.fileUrl) | 534 | return getWebVideoPublicFileUrl(this.fileUrl) |
535 | } | 535 | } |
536 | 536 | ||
537 | // --------------------------------------------------------------------------- | 537 | // --------------------------------------------------------------------------- |
@@ -553,15 +553,15 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
553 | getFileStaticPath (video: MVideo) { | 553 | getFileStaticPath (video: MVideo) { |
554 | if (this.isHLS()) return this.getHLSFileStaticPath(video) | 554 | if (this.isHLS()) return this.getHLSFileStaticPath(video) |
555 | 555 | ||
556 | return this.getWebTorrentFileStaticPath(video) | 556 | return this.getWebVideoFileStaticPath(video) |
557 | } | 557 | } |
558 | 558 | ||
559 | private getWebTorrentFileStaticPath (video: MVideo) { | 559 | private getWebVideoFileStaticPath (video: MVideo) { |
560 | if (isVideoInPrivateDirectory(video.privacy)) { | 560 | if (isVideoInPrivateDirectory(video.privacy)) { |
561 | return join(STATIC_PATHS.PRIVATE_WEBSEED, this.filename) | 561 | return join(STATIC_PATHS.PRIVATE_WEB_VIDEOS, this.filename) |
562 | } | 562 | } |
563 | 563 | ||
564 | return join(STATIC_PATHS.WEBSEED, this.filename) | 564 | return join(STATIC_PATHS.WEB_VIDEOS, this.filename) |
565 | } | 565 | } |
566 | 566 | ||
567 | private getHLSFileStaticPath (video: MVideo) { | 567 | private getHLSFileStaticPath (video: MVideo) { |
diff --git a/server/models/video/video-password.ts b/server/models/video/video-password.ts new file mode 100644 index 000000000..648366c3b --- /dev/null +++ b/server/models/video/video-password.ts | |||
@@ -0,0 +1,137 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoModel } from './video' | ||
3 | import { AttributesOnly } from '@shared/typescript-utils' | ||
4 | import { ResultList, VideoPassword } from '@shared/models' | ||
5 | import { getSort, throwIfNotValid } from '../shared' | ||
6 | import { FindOptions, Transaction } from 'sequelize' | ||
7 | import { MVideoPassword } from '@server/types/models' | ||
8 | import { isPasswordValid } from '@server/helpers/custom-validators/videos' | ||
9 | import { pick } from '@shared/core-utils' | ||
10 | |||
11 | @DefaultScope(() => ({ | ||
12 | include: [ | ||
13 | { | ||
14 | model: VideoModel.unscoped(), | ||
15 | required: true | ||
16 | } | ||
17 | ] | ||
18 | })) | ||
19 | @Table({ | ||
20 | tableName: 'videoPassword', | ||
21 | indexes: [ | ||
22 | { | ||
23 | fields: [ 'videoId', 'password' ], | ||
24 | unique: true | ||
25 | } | ||
26 | ] | ||
27 | }) | ||
28 | export class VideoPasswordModel extends Model<Partial<AttributesOnly<VideoPasswordModel>>> { | ||
29 | |||
30 | @AllowNull(false) | ||
31 | @Is('VideoPassword', value => throwIfNotValid(value, isPasswordValid, 'videoPassword')) | ||
32 | @Column | ||
33 | password: string | ||
34 | |||
35 | @CreatedAt | ||
36 | createdAt: Date | ||
37 | |||
38 | @UpdatedAt | ||
39 | updatedAt: Date | ||
40 | |||
41 | @ForeignKey(() => VideoModel) | ||
42 | @Column | ||
43 | videoId: number | ||
44 | |||
45 | @BelongsTo(() => VideoModel, { | ||
46 | foreignKey: { | ||
47 | allowNull: false | ||
48 | }, | ||
49 | onDelete: 'cascade' | ||
50 | }) | ||
51 | Video: VideoModel | ||
52 | |||
53 | static async countByVideoId (videoId: number, t?: Transaction) { | ||
54 | const query: FindOptions = { | ||
55 | where: { | ||
56 | videoId | ||
57 | }, | ||
58 | transaction: t | ||
59 | } | ||
60 | |||
61 | return VideoPasswordModel.count(query) | ||
62 | } | ||
63 | |||
64 | static async loadByIdAndVideo (options: { id: number, videoId: number, t?: Transaction }): Promise<MVideoPassword> { | ||
65 | const { id, videoId, t } = options | ||
66 | const query: FindOptions = { | ||
67 | where: { | ||
68 | id, | ||
69 | videoId | ||
70 | }, | ||
71 | transaction: t | ||
72 | } | ||
73 | |||
74 | return VideoPasswordModel.findOne(query) | ||
75 | } | ||
76 | |||
77 | static async listPasswords (options: { | ||
78 | start: number | ||
79 | count: number | ||
80 | sort: string | ||
81 | videoId: number | ||
82 | }): Promise<ResultList<MVideoPassword>> { | ||
83 | const { start, count, sort, videoId } = options | ||
84 | |||
85 | const { count: total, rows: data } = await VideoPasswordModel.findAndCountAll({ | ||
86 | where: { videoId }, | ||
87 | order: getSort(sort), | ||
88 | offset: start, | ||
89 | limit: count | ||
90 | }) | ||
91 | |||
92 | return { total, data } | ||
93 | } | ||
94 | |||
95 | static async addPasswords (passwords: string[], videoId: number, transaction?: Transaction): Promise<void> { | ||
96 | for (const password of passwords) { | ||
97 | await VideoPasswordModel.create({ | ||
98 | password, | ||
99 | videoId | ||
100 | }, { transaction }) | ||
101 | } | ||
102 | } | ||
103 | |||
104 | static async deleteAllPasswords (videoId: number, transaction?: Transaction) { | ||
105 | await VideoPasswordModel.destroy({ | ||
106 | where: { videoId }, | ||
107 | transaction | ||
108 | }) | ||
109 | } | ||
110 | |||
111 | static async deletePassword (passwordId: number, transaction?: Transaction) { | ||
112 | await VideoPasswordModel.destroy({ | ||
113 | where: { id: passwordId }, | ||
114 | transaction | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | static async isACorrectPassword (options: { | ||
119 | videoId: number | ||
120 | password: string | ||
121 | }) { | ||
122 | const query = { | ||
123 | where: pick(options, [ 'videoId', 'password' ]) | ||
124 | } | ||
125 | return VideoPasswordModel.findOne(query) | ||
126 | } | ||
127 | |||
128 | toFormattedJSON (): VideoPassword { | ||
129 | return { | ||
130 | id: this.id, | ||
131 | password: this.password, | ||
132 | videoId: this.videoId, | ||
133 | createdAt: this.createdAt, | ||
134 | updatedAt: this.updatedAt | ||
135 | } | ||
136 | } | ||
137 | } | ||
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index b832f9768..61ae6b9fe 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -336,7 +336,10 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
336 | // Internal video? | 336 | // Internal video? |
337 | if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR | 337 | if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR |
338 | 338 | ||
339 | if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE | 339 | // Private, internal and password protected videos cannot be read without appropriate access (ownership, internal) |
340 | if (new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ]).has(video.privacy)) { | ||
341 | return VideoPlaylistElementType.PRIVATE | ||
342 | } | ||
340 | 343 | ||
341 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE | 344 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE |
342 | 345 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index faf4bea78..15999d409 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -32,7 +32,7 @@ import { | |||
32 | import { | 32 | import { |
33 | ACTIVITY_PUB, | 33 | ACTIVITY_PUB, |
34 | CONSTRAINTS_FIELDS, | 34 | CONSTRAINTS_FIELDS, |
35 | STATIC_PATHS, | 35 | LAZY_STATIC_PATHS, |
36 | THUMBNAILS_SIZE, | 36 | THUMBNAILS_SIZE, |
37 | VIDEO_PLAYLIST_PRIVACIES, | 37 | VIDEO_PLAYLIST_PRIVACIES, |
38 | VIDEO_PLAYLIST_TYPES, | 38 | VIDEO_PLAYLIST_TYPES, |
@@ -592,13 +592,13 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
592 | getThumbnailUrl () { | 592 | getThumbnailUrl () { |
593 | if (!this.hasThumbnail()) return null | 593 | if (!this.hasThumbnail()) return null |
594 | 594 | ||
595 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename | 595 | return WEBSERVER.URL + LAZY_STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename |
596 | } | 596 | } |
597 | 597 | ||
598 | getThumbnailStaticPath () { | 598 | getThumbnailStaticPath () { |
599 | if (!this.hasThumbnail()) return null | 599 | if (!this.hasThumbnail()) return null |
600 | 600 | ||
601 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) | 601 | return join(LAZY_STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) |
602 | } | 602 | } |
603 | 603 | ||
604 | getWatchStaticPath () { | 604 | getWatchStaticPath () { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 8e3af62a4..4c6297243 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -29,7 +29,7 @@ import { | |||
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { InternalEventEmitter } from '@server/lib/internal-event-emitter' | 30 | import { InternalEventEmitter } from '@server/lib/internal-event-emitter' |
31 | import { LiveManager } from '@server/lib/live/live-manager' | 31 | import { LiveManager } from '@server/lib/live/live-manager' |
32 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' | 32 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebVideoObjectStorage } from '@server/lib/object-storage' |
33 | import { tracer } from '@server/lib/opentelemetry/tracing' | 33 | import { tracer } from '@server/lib/opentelemetry/tracing' |
34 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 34 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
35 | import { Hooks } from '@server/lib/plugins/hooks' | 35 | import { Hooks } from '@server/lib/plugins/hooks' |
@@ -58,7 +58,7 @@ import { | |||
58 | import { AttributesOnly } from '@shared/typescript-utils' | 58 | import { AttributesOnly } from '@shared/typescript-utils' |
59 | import { peertubeTruncate } from '../../helpers/core-utils' | 59 | import { peertubeTruncate } from '../../helpers/core-utils' |
60 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 60 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
61 | import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' | 61 | import { exists, isArray, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' |
62 | import { | 62 | import { |
63 | isVideoDescriptionValid, | 63 | isVideoDescriptionValid, |
64 | isVideoDurationValid, | 64 | isVideoDurationValid, |
@@ -75,6 +75,7 @@ import { | |||
75 | MChannel, | 75 | MChannel, |
76 | MChannelAccountDefault, | 76 | MChannelAccountDefault, |
77 | MChannelId, | 77 | MChannelId, |
78 | MStoryboard, | ||
78 | MStreamingPlaylist, | 79 | MStreamingPlaylist, |
79 | MStreamingPlaylistFilesVideo, | 80 | MStreamingPlaylistFilesVideo, |
80 | MUserAccountId, | 81 | MUserAccountId, |
@@ -83,6 +84,8 @@ import { | |||
83 | MVideoAccountLight, | 84 | MVideoAccountLight, |
84 | MVideoAccountLightBlacklistAllFiles, | 85 | MVideoAccountLightBlacklistAllFiles, |
85 | MVideoAP, | 86 | MVideoAP, |
87 | MVideoAPLight, | ||
88 | MVideoCaptionLanguageUrl, | ||
86 | MVideoDetails, | 89 | MVideoDetails, |
87 | MVideoFileVideo, | 90 | MVideoFileVideo, |
88 | MVideoFormattable, | 91 | MVideoFormattable, |
@@ -111,13 +114,13 @@ import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, | |||
111 | import { UserModel } from '../user/user' | 114 | import { UserModel } from '../user/user' |
112 | import { UserVideoHistoryModel } from '../user/user-video-history' | 115 | import { UserVideoHistoryModel } from '../user/user-video-history' |
113 | import { VideoViewModel } from '../view/video-view' | 116 | import { VideoViewModel } from '../view/video-view' |
117 | import { videoModelToActivityPubObject } from './formatter/video-activity-pub-format' | ||
114 | import { | 118 | import { |
115 | videoFilesModelToFormattedJSON, | 119 | videoFilesModelToFormattedJSON, |
116 | VideoFormattingJSONOptions, | 120 | VideoFormattingJSONOptions, |
117 | videoModelToActivityPubObject, | ||
118 | videoModelToFormattedDetailsJSON, | 121 | videoModelToFormattedDetailsJSON, |
119 | videoModelToFormattedJSON | 122 | videoModelToFormattedJSON |
120 | } from './formatter/video-format-utils' | 123 | } from './formatter/video-api-format' |
121 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 124 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
122 | import { | 125 | import { |
123 | BuildVideosListQueryOptions, | 126 | BuildVideosListQueryOptions, |
@@ -126,6 +129,7 @@ import { | |||
126 | VideosIdListQueryBuilder, | 129 | VideosIdListQueryBuilder, |
127 | VideosModelListQueryBuilder | 130 | VideosModelListQueryBuilder |
128 | } from './sql/video' | 131 | } from './sql/video' |
132 | import { StoryboardModel } from './storyboard' | ||
129 | import { TagModel } from './tag' | 133 | import { TagModel } from './tag' |
130 | import { ThumbnailModel } from './thumbnail' | 134 | import { ThumbnailModel } from './thumbnail' |
131 | import { VideoBlacklistModel } from './video-blacklist' | 135 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -136,6 +140,7 @@ import { VideoFileModel } from './video-file' | |||
136 | import { VideoImportModel } from './video-import' | 140 | import { VideoImportModel } from './video-import' |
137 | import { VideoJobInfoModel } from './video-job-info' | 141 | import { VideoJobInfoModel } from './video-job-info' |
138 | import { VideoLiveModel } from './video-live' | 142 | import { VideoLiveModel } from './video-live' |
143 | import { VideoPasswordModel } from './video-password' | ||
139 | import { VideoPlaylistElementModel } from './video-playlist-element' | 144 | import { VideoPlaylistElementModel } from './video-playlist-element' |
140 | import { VideoShareModel } from './video-share' | 145 | import { VideoShareModel } from './video-share' |
141 | import { VideoSourceModel } from './video-source' | 146 | import { VideoSourceModel } from './video-source' |
@@ -146,7 +151,7 @@ export enum ScopeNames { | |||
146 | FOR_API = 'FOR_API', | 151 | FOR_API = 'FOR_API', |
147 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', | 152 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', |
148 | WITH_TAGS = 'WITH_TAGS', | 153 | WITH_TAGS = 'WITH_TAGS', |
149 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', | 154 | WITH_WEB_VIDEO_FILES = 'WITH_WEB_VIDEO_FILES', |
150 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 155 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
151 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 156 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
152 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 157 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
@@ -285,7 +290,7 @@ export type ForAPIOptions = { | |||
285 | } | 290 | } |
286 | ] | 291 | ] |
287 | }, | 292 | }, |
288 | [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => { | 293 | [ScopeNames.WITH_WEB_VIDEO_FILES]: (withRedundancies = false) => { |
289 | let subInclude: any[] = [] | 294 | let subInclude: any[] = [] |
290 | 295 | ||
291 | if (withRedundancies === true) { | 296 | if (withRedundancies === true) { |
@@ -734,6 +739,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
734 | }) | 739 | }) |
735 | VideoCaptions: VideoCaptionModel[] | 740 | VideoCaptions: VideoCaptionModel[] |
736 | 741 | ||
742 | @HasMany(() => VideoPasswordModel, { | ||
743 | foreignKey: { | ||
744 | name: 'videoId', | ||
745 | allowNull: false | ||
746 | }, | ||
747 | onDelete: 'cascade' | ||
748 | }) | ||
749 | VideoPasswords: VideoPasswordModel[] | ||
750 | |||
737 | @HasOne(() => VideoJobInfoModel, { | 751 | @HasOne(() => VideoJobInfoModel, { |
738 | foreignKey: { | 752 | foreignKey: { |
739 | name: 'videoId', | 753 | name: 'videoId', |
@@ -743,6 +757,16 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
743 | }) | 757 | }) |
744 | VideoJobInfo: VideoJobInfoModel | 758 | VideoJobInfo: VideoJobInfoModel |
745 | 759 | ||
760 | @HasOne(() => StoryboardModel, { | ||
761 | foreignKey: { | ||
762 | name: 'videoId', | ||
763 | allowNull: false | ||
764 | }, | ||
765 | onDelete: 'cascade', | ||
766 | hooks: true | ||
767 | }) | ||
768 | Storyboard: StoryboardModel | ||
769 | |||
746 | @AfterCreate | 770 | @AfterCreate |
747 | static notifyCreate (video: MVideo) { | 771 | static notifyCreate (video: MVideo) { |
748 | InternalEventEmitter.Instance.emit('video-created', { video }) | 772 | InternalEventEmitter.Instance.emit('video-created', { video }) |
@@ -789,7 +813,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
789 | 813 | ||
790 | // Remove physical files and torrents | 814 | // Remove physical files and torrents |
791 | instance.VideoFiles.forEach(file => { | 815 | instance.VideoFiles.forEach(file => { |
792 | tasks.push(instance.removeWebTorrentFile(file)) | 816 | tasks.push(instance.removeWebVideoFile(file)) |
793 | }) | 817 | }) |
794 | 818 | ||
795 | // Remove playlists file | 819 | // Remove playlists file |
@@ -894,6 +918,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
894 | required: false | 918 | required: false |
895 | }, | 919 | }, |
896 | { | 920 | { |
921 | model: StoryboardModel.unscoped(), | ||
922 | required: false | ||
923 | }, | ||
924 | { | ||
897 | attributes: [ 'id', 'url' ], | 925 | attributes: [ 'id', 'url' ], |
898 | model: VideoShareModel.unscoped(), | 926 | model: VideoShareModel.unscoped(), |
899 | required: false, | 927 | required: false, |
@@ -1079,7 +1107,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1079 | include?: VideoInclude | 1107 | include?: VideoInclude |
1080 | 1108 | ||
1081 | hasFiles?: boolean // default false | 1109 | hasFiles?: boolean // default false |
1082 | hasWebtorrentFiles?: boolean | 1110 | |
1111 | hasWebtorrentFiles?: boolean // TODO: remove in v7 | ||
1112 | hasWebVideoFiles?: boolean | ||
1113 | |||
1083 | hasHLSFiles?: boolean | 1114 | hasHLSFiles?: boolean |
1084 | 1115 | ||
1085 | categoryOneOf?: number[] | 1116 | categoryOneOf?: number[] |
@@ -1144,6 +1175,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1144 | 'historyOfUser', | 1175 | 'historyOfUser', |
1145 | 'hasHLSFiles', | 1176 | 'hasHLSFiles', |
1146 | 'hasWebtorrentFiles', | 1177 | 'hasWebtorrentFiles', |
1178 | 'hasWebVideoFiles', | ||
1147 | 'search', | 1179 | 'search', |
1148 | 'excludeAlreadyWatched' | 1180 | 'excludeAlreadyWatched' |
1149 | ]), | 1181 | ]), |
@@ -1177,7 +1209,9 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1177 | 1209 | ||
1178 | user?: MUserAccountId | 1210 | user?: MUserAccountId |
1179 | 1211 | ||
1180 | hasWebtorrentFiles?: boolean | 1212 | hasWebtorrentFiles?: boolean // TODO: remove in v7 |
1213 | hasWebVideoFiles?: boolean | ||
1214 | |||
1181 | hasHLSFiles?: boolean | 1215 | hasHLSFiles?: boolean |
1182 | 1216 | ||
1183 | search?: string | 1217 | search?: string |
@@ -1224,6 +1258,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1224 | 'durationMax', | 1258 | 'durationMax', |
1225 | 'hasHLSFiles', | 1259 | 'hasHLSFiles', |
1226 | 'hasWebtorrentFiles', | 1260 | 'hasWebtorrentFiles', |
1261 | 'hasWebVideoFiles', | ||
1227 | 'uuids', | 1262 | 'uuids', |
1228 | 'search', | 1263 | 'search', |
1229 | 'displayOnlyForFollower', | 1264 | 'displayOnlyForFollower', |
@@ -1648,7 +1683,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1648 | return this.getQualityFileBy(minBy) | 1683 | return this.getQualityFileBy(minBy) |
1649 | } | 1684 | } |
1650 | 1685 | ||
1651 | getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { | 1686 | getWebVideoFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { |
1652 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1687 | if (Array.isArray(this.VideoFiles) === false) return undefined |
1653 | 1688 | ||
1654 | const file = this.VideoFiles.find(f => f.resolution === resolution) | 1689 | const file = this.VideoFiles.find(f => f.resolution === resolution) |
@@ -1657,7 +1692,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1657 | return Object.assign(file, { Video: this }) | 1692 | return Object.assign(file, { Video: this }) |
1658 | } | 1693 | } |
1659 | 1694 | ||
1660 | hasWebTorrentFiles () { | 1695 | hasWebVideoFiles () { |
1661 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 | 1696 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 |
1662 | } | 1697 | } |
1663 | 1698 | ||
@@ -1758,6 +1793,32 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1758 | ) | 1793 | ) |
1759 | } | 1794 | } |
1760 | 1795 | ||
1796 | async lightAPToFullAP (this: MVideoAPLight, transaction: Transaction): Promise<MVideoAP> { | ||
1797 | const videoAP = this as MVideoAP | ||
1798 | |||
1799 | const getCaptions = () => { | ||
1800 | if (isArray(videoAP.VideoCaptions)) return videoAP.VideoCaptions | ||
1801 | |||
1802 | return this.$get('VideoCaptions', { | ||
1803 | attributes: [ 'filename', 'language', 'fileUrl' ], | ||
1804 | transaction | ||
1805 | }) as Promise<MVideoCaptionLanguageUrl[]> | ||
1806 | } | ||
1807 | |||
1808 | const getStoryboard = () => { | ||
1809 | if (videoAP.Storyboard) return videoAP.Storyboard | ||
1810 | |||
1811 | return this.$get('Storyboard', { transaction }) as Promise<MStoryboard> | ||
1812 | } | ||
1813 | |||
1814 | const [ captions, storyboard ] = await Promise.all([ getCaptions(), getStoryboard() ]) | ||
1815 | |||
1816 | return Object.assign(this, { | ||
1817 | VideoCaptions: captions, | ||
1818 | Storyboard: storyboard | ||
1819 | }) | ||
1820 | } | ||
1821 | |||
1761 | getTruncatedDescription () { | 1822 | getTruncatedDescription () { |
1762 | if (!this.description) return null | 1823 | if (!this.description) return null |
1763 | 1824 | ||
@@ -1830,7 +1891,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1830 | .concat(toAdd) | 1891 | .concat(toAdd) |
1831 | } | 1892 | } |
1832 | 1893 | ||
1833 | removeWebTorrentFile (videoFile: MVideoFile, isRedundancy = false) { | 1894 | removeWebVideoFile (videoFile: MVideoFile, isRedundancy = false) { |
1834 | const filePath = isRedundancy | 1895 | const filePath = isRedundancy |
1835 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) | 1896 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) |
1836 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) | 1897 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) |
@@ -1839,7 +1900,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1839 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) | 1900 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) |
1840 | 1901 | ||
1841 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 1902 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
1842 | promises.push(removeWebTorrentObjectStorage(videoFile)) | 1903 | promises.push(removeWebVideoObjectStorage(videoFile)) |
1843 | } | 1904 | } |
1844 | 1905 | ||
1845 | return Promise.all(promises) | 1906 | return Promise.all(promises) |
@@ -1918,7 +1979,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1918 | 1979 | ||
1919 | // --------------------------------------------------------------------------- | 1980 | // --------------------------------------------------------------------------- |
1920 | 1981 | ||
1921 | requiresAuth (options: { | 1982 | requiresUserAuth (options: { |
1922 | urlParamId: string | 1983 | urlParamId: string |
1923 | checkBlacklist: boolean | 1984 | checkBlacklist: boolean |
1924 | }) { | 1985 | }) { |
@@ -1936,11 +1997,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1936 | 1997 | ||
1937 | if (checkBlacklist && this.VideoBlacklist) return true | 1998 | if (checkBlacklist && this.VideoBlacklist) return true |
1938 | 1999 | ||
1939 | if (this.privacy !== VideoPrivacy.PUBLIC) { | 2000 | if (this.privacy === VideoPrivacy.PUBLIC || this.privacy === VideoPrivacy.PASSWORD_PROTECTED) { |
1940 | throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) | 2001 | return false |
1941 | } | 2002 | } |
1942 | 2003 | ||
1943 | return false | 2004 | throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) |
1944 | } | 2005 | } |
1945 | 2006 | ||
1946 | hasPrivateStaticPath () { | 2007 | hasPrivateStaticPath () { |
@@ -1962,7 +2023,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1962 | } | 2023 | } |
1963 | 2024 | ||
1964 | getBandwidthBits (this: MVideo, videoFile: MVideoFile) { | 2025 | getBandwidthBits (this: MVideo, videoFile: MVideoFile) { |
1965 | if (!this.duration) throw new Error(`Cannot get bandwidth bits because video ${this.url} has duration of 0`) | 2026 | if (!this.duration) return videoFile.size |
1966 | 2027 | ||
1967 | return Math.ceil((videoFile.size * 8) / this.duration) | 2028 | return Math.ceil((videoFile.size * 8) / this.duration) |
1968 | } | 2029 | } |