aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/formatter/index.ts2
-rw-r--r--server/models/video/formatter/shared/index.ts1
-rw-r--r--server/models/video/formatter/shared/video-format-utils.ts7
-rw-r--r--server/models/video/formatter/video-activity-pub-format.ts295
-rw-r--r--server/models/video/formatter/video-api-format.ts304
-rw-r--r--server/models/video/formatter/video-format-utils.ts543
-rw-r--r--server/models/video/sql/video/shared/abstract-video-query-builder.ts4
-rw-r--r--server/models/video/sql/video/shared/video-file-query-builder.ts12
-rw-r--r--server/models/video/sql/video/shared/video-model-builder.ts20
-rw-r--r--server/models/video/sql/video/shared/video-table-attributes.ts1
-rw-r--r--server/models/video/sql/video/video-model-get-query-builder.ts12
-rw-r--r--server/models/video/sql/video/videos-id-list-query-builder.ts16
-rw-r--r--server/models/video/sql/video/videos-model-list-query-builder.ts10
-rw-r--r--server/models/video/storyboard.ts169
-rw-r--r--server/models/video/thumbnail.ts12
-rw-r--r--server/models/video/video-caption.ts8
-rw-r--r--server/models/video/video-change-ownership.ts2
-rw-r--r--server/models/video/video-file.ts34
-rw-r--r--server/models/video/video-password.ts137
-rw-r--r--server/models/video/video-playlist-element.ts5
-rw-r--r--server/models/video/video-playlist.ts6
-rw-r--r--server/models/video/video.ts97
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 @@
1export * from './video-activity-pub-format'
2export * 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 @@
1import { MVideoFile } from '@server/types/models'
2
3export 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
2import { isArray } from 'lodash'
3import { generateMagnetUri } from '@server/helpers/webtorrent'
4import { getActivityStreamDuration } from '@server/lib/activitypub/activity'
5import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
6import {
7 ActivityIconObject,
8 ActivityPlaylistUrlObject,
9 ActivityPubStoryboard,
10 ActivityTagObject,
11 ActivityTrackerUrlObject,
12 ActivityUrlObject,
13 VideoObject
14} from '@shared/models'
15import { MIMETYPES, WEBSERVER } from '../../../initializers/constants'
16import {
17 getLocalVideoCommentsActivityPubUrl,
18 getLocalVideoDislikesActivityPubUrl,
19 getLocalVideoLikesActivityPubUrl,
20 getLocalVideoSharesActivityPubUrl
21} from '../../../lib/activitypub/url'
22import { MStreamingPlaylistFiles, MUserId, MVideo, MVideoAP, MVideoFile } from '../../../types/models'
23import { VideoCaptionModel } from '../video-caption'
24import { sortByResolutionDesc } from './shared'
25import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format'
26
27export 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
117function 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
135function 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
162function 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
219function 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
231function 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
248function 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
266function 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
275function 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
286function 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 @@
1import { generateMagnetUri } from '@server/helpers/webtorrent'
2import { tracer } from '@server/lib/opentelemetry/tracing'
3import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
4import { VideoViewsManager } from '@server/lib/views/video-views-manager'
5import { uuidToShort } from '@shared/extra-utils'
6import {
7 Video,
8 VideoAdditionalAttributes,
9 VideoDetails,
10 VideoFile,
11 VideoInclude,
12 VideosCommonQueryAfterSanitize,
13 VideoStreamingPlaylist
14} from '@shared/models'
15import { isArray } from '../../../helpers/custom-validators/misc'
16import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, VIDEO_STATES } from '../../../initializers/constants'
17import { MServer, MStreamingPlaylistRedundanciesOpt, MVideoFormattable, MVideoFormattableDetails } from '../../../types/models'
18import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file'
19import { sortByResolutionDesc } from './shared'
20
21export 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
34export 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
51export 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
125export 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
165export 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
187export 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
236export function getCategoryLabel (id: number) {
237 return VIDEO_CATEGORIES[id] || 'Unknown'
238}
239
240export function getLicenceLabel (id: number) {
241 return VIDEO_LICENCES[id] || 'Unknown'
242}
243
244export function getLanguageLabel (id: string) {
245 return VIDEO_LANGUAGES[id] || 'Unknown'
246}
247
248export function getPrivacyLabel (id: number) {
249 return VIDEO_PRIVACIES[id] || 'Unknown'
250}
251
252export function getStateLabel (id: number) {
253 return VIDEO_STATES[id] || 'Unknown'
254}
255
256// ---------------------------------------------------------------------------
257// Private
258// ---------------------------------------------------------------------------
259
260function 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 @@
1import { generateMagnetUri } from '@server/helpers/webtorrent'
2import { getActivityStreamDuration } from '@server/lib/activitypub/activity'
3import { tracer } from '@server/lib/opentelemetry/tracing'
4import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
5import { VideoViewsManager } from '@server/lib/views/video-views-manager'
6import { uuidToShort } from '@shared/extra-utils'
7import {
8 ActivityTagObject,
9 ActivityUrlObject,
10 Video,
11 VideoDetails,
12 VideoFile,
13 VideoInclude,
14 VideoObject,
15 VideosCommonQueryAfterSanitize,
16 VideoStreamingPlaylist
17} from '@shared/models'
18import { isArray } from '../../../helpers/custom-validators/misc'
19import {
20 MIMETYPES,
21 VIDEO_CATEGORIES,
22 VIDEO_LANGUAGES,
23 VIDEO_LICENCES,
24 VIDEO_PRIVACIES,
25 VIDEO_STATES,
26 WEBSERVER
27} from '../../../initializers/constants'
28import {
29 getLocalVideoCommentsActivityPubUrl,
30 getLocalVideoDislikesActivityPubUrl,
31 getLocalVideoLikesActivityPubUrl,
32 getLocalVideoSharesActivityPubUrl
33} from '../../../lib/activitypub/url'
34import {
35 MServer,
36 MStreamingPlaylistRedundanciesOpt,
37 MUserId,
38 MVideo,
39 MVideoAP,
40 MVideoFile,
41 MVideoFormattable,
42 MVideoFormattableDetails
43} from '../../../types/models'
44import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file'
45import { VideoCaptionModel } from '../video-caption'
46
47export 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
60function 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
75function 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
181function 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
217function 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
242function 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
248function 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
291function 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
342function 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
490function getCategoryLabel (id: number) {
491 return VIDEO_CATEGORIES[id] || 'Unknown'
492}
493
494function getLicenceLabel (id: number) {
495 return VIDEO_LICENCES[id] || 'Unknown'
496}
497
498function getLanguageLabel (id: string) {
499 return VIDEO_LANGUAGES[id] || 'Unknown'
500}
501
502function getPrivacyLabel (id: number) {
503 return VIDEO_PRIVACIES[id] || 'Unknown'
504}
505
506function getStateLabel (id: number) {
507 return VIDEO_STATES[id] || 'Unknown'
508}
509
510export {
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
527function 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
36export class VideoModelGetQueryBuilder { 36export 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 {
92export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { 92export 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 @@
1import { remove } from 'fs-extra'
2import { join } from 'path'
3import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { CONFIG } from '@server/initializers/config'
5import { MStoryboard, MStoryboardVideo, MVideo } from '@server/types/models'
6import { Storyboard } from '@shared/models'
7import { AttributesOnly } from '@shared/typescript-utils'
8import { logger } from '../../helpers/logger'
9import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
10import { VideoModel } from './video'
11import { 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})
26export 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'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
23import { CONFIG } from '../../initializers/config' 23import { CONFIG } from '../../initializers/config'
24import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' 24import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
25import { VideoModel } from './video' 25import { VideoModel } from './video'
26import { VideoPlaylistModel } from './video-playlist' 26import { 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'
18import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' 18import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionLanguageUrl, MVideoCaptionVideo } from '@server/types/models'
19import { buildUUID } from '@shared/extra-utils' 19import { buildUUID } from '@shared/extra-utils'
20import { AttributesOnly } from '@shared/typescript-utils' 20import { AttributesOnly } from '@shared/typescript-utils'
21import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 21import { 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'
26import { 26import {
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'
32import { getFSTorrentFilePath } from '@server/lib/paths' 32import { getFSTorrentFilePath } from '@server/lib/paths'
33import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' 33import { 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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from './video'
3import { AttributesOnly } from '@shared/typescript-utils'
4import { ResultList, VideoPassword } from '@shared/models'
5import { getSort, throwIfNotValid } from '../shared'
6import { FindOptions, Transaction } from 'sequelize'
7import { MVideoPassword } from '@server/types/models'
8import { isPasswordValid } from '@server/helpers/custom-validators/videos'
9import { 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})
28export 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 {
32import { 32import {
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 {
29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
30import { InternalEventEmitter } from '@server/lib/internal-event-emitter' 30import { InternalEventEmitter } from '@server/lib/internal-event-emitter'
31import { LiveManager } from '@server/lib/live/live-manager' 31import { LiveManager } from '@server/lib/live/live-manager'
32import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' 32import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebVideoObjectStorage } from '@server/lib/object-storage'
33import { tracer } from '@server/lib/opentelemetry/tracing' 33import { tracer } from '@server/lib/opentelemetry/tracing'
34import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' 34import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths'
35import { Hooks } from '@server/lib/plugins/hooks' 35import { Hooks } from '@server/lib/plugins/hooks'
@@ -58,7 +58,7 @@ import {
58import { AttributesOnly } from '@shared/typescript-utils' 58import { AttributesOnly } from '@shared/typescript-utils'
59import { peertubeTruncate } from '../../helpers/core-utils' 59import { peertubeTruncate } from '../../helpers/core-utils'
60import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 60import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
61import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' 61import { exists, isArray, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc'
62import { 62import {
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,
111import { UserModel } from '../user/user' 114import { UserModel } from '../user/user'
112import { UserVideoHistoryModel } from '../user/user-video-history' 115import { UserVideoHistoryModel } from '../user/user-video-history'
113import { VideoViewModel } from '../view/video-view' 116import { VideoViewModel } from '../view/video-view'
117import { videoModelToActivityPubObject } from './formatter/video-activity-pub-format'
114import { 118import {
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'
121import { ScheduleVideoUpdateModel } from './schedule-video-update' 124import { ScheduleVideoUpdateModel } from './schedule-video-update'
122import { 125import {
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'
132import { StoryboardModel } from './storyboard'
129import { TagModel } from './tag' 133import { TagModel } from './tag'
130import { ThumbnailModel } from './thumbnail' 134import { ThumbnailModel } from './thumbnail'
131import { VideoBlacklistModel } from './video-blacklist' 135import { VideoBlacklistModel } from './video-blacklist'
@@ -136,6 +140,7 @@ import { VideoFileModel } from './video-file'
136import { VideoImportModel } from './video-import' 140import { VideoImportModel } from './video-import'
137import { VideoJobInfoModel } from './video-job-info' 141import { VideoJobInfoModel } from './video-job-info'
138import { VideoLiveModel } from './video-live' 142import { VideoLiveModel } from './video-live'
143import { VideoPasswordModel } from './video-password'
139import { VideoPlaylistElementModel } from './video-playlist-element' 144import { VideoPlaylistElementModel } from './video-playlist-element'
140import { VideoShareModel } from './video-share' 145import { VideoShareModel } from './video-share'
141import { VideoSourceModel } from './video-source' 146import { 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 }