diff options
-rw-r--r-- | config/test.yaml | 2 | ||||
-rw-r--r-- | server/lib/client-html.ts | 3 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-file.ts | 7 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 130 | ||||
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 295 | ||||
-rw-r--r-- | server/models/video/video.ts | 419 |
7 files changed, 455 insertions, 403 deletions
diff --git a/config/test.yaml b/config/test.yaml index 16113211e..d3e0e49ac 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -32,7 +32,7 @@ redundancy: | |||
32 | - | 32 | - |
33 | size: '10MB' | 33 | size: '10MB' |
34 | strategy: 'recently-added' | 34 | strategy: 'recently-added' |
35 | minViews: 10 | 35 | minViews: 1 |
36 | 36 | ||
37 | cache: | 37 | cache: |
38 | previews: | 38 | previews: |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index a69e09c32..b1088c096 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -8,6 +8,7 @@ import { VideoModel } from '../models/video/video' | |||
8 | import * as validator from 'validator' | 8 | import * as validator from 'validator' |
9 | import { VideoPrivacy } from '../../shared/models/videos' | 9 | import { VideoPrivacy } from '../../shared/models/videos' |
10 | import { readFile } from 'fs-extra' | 10 | import { readFile } from 'fs-extra' |
11 | import { getActivityStreamDuration } from '../models/video/video-format-utils' | ||
11 | 12 | ||
12 | export class ClientHtml { | 13 | export class ClientHtml { |
13 | 14 | ||
@@ -150,7 +151,7 @@ export class ClientHtml { | |||
150 | description: videoDescriptionEscaped, | 151 | description: videoDescriptionEscaped, |
151 | thumbnailUrl: previewUrl, | 152 | thumbnailUrl: previewUrl, |
152 | uploadDate: video.createdAt.toISOString(), | 153 | uploadDate: video.createdAt.toISOString(), |
153 | duration: video.getActivityStreamDuration(), | 154 | duration: getActivityStreamDuration(video.duration), |
154 | contentUrl: videoUrl, | 155 | contentUrl: videoUrl, |
155 | embedUrl: embedUrl, | 156 | embedUrl: embedUrl, |
156 | interactionCount: video.views | 157 | interactionCount: video.views |
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index c6308f7a6..2c9ca8e12 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts | |||
@@ -8,6 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' | ||
11 | 12 | ||
12 | export type VideoFilePayload = { | 13 | export type VideoFilePayload = { |
13 | videoUUID: string | 14 | videoUUID: string |
@@ -32,7 +33,7 @@ async function processVideoFileImport (job: Bull.Job) { | |||
32 | return undefined | 33 | return undefined |
33 | } | 34 | } |
34 | 35 | ||
35 | await video.importVideoFile(payload.filePath) | 36 | await importVideoFile(video, payload.filePath) |
36 | 37 | ||
37 | await onVideoFileTranscoderOrImportSuccess(video) | 38 | await onVideoFileTranscoderOrImportSuccess(video) |
38 | return video | 39 | return video |
@@ -51,11 +52,11 @@ async function processVideoFile (job: Bull.Job) { | |||
51 | 52 | ||
52 | // Transcoding in other resolution | 53 | // Transcoding in other resolution |
53 | if (payload.resolution) { | 54 | if (payload.resolution) { |
54 | await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode || false) | 55 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) |
55 | 56 | ||
56 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) | 57 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) |
57 | } else { | 58 | } else { |
58 | await video.optimizeOriginalVideofile() | 59 | await optimizeOriginalVideofile(video) |
59 | 60 | ||
60 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) | 61 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) |
61 | } | 62 | } |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts new file mode 100644 index 000000000..bf3ff78c2 --- /dev/null +++ b/server/lib/video-transcoding.ts | |||
@@ -0,0 +1,130 @@ | |||
1 | import { CONFIG } from '../initializers' | ||
2 | import { join, extname } from 'path' | ||
3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' | ||
4 | import { copy, remove, rename, stat } from 'fs-extra' | ||
5 | import { logger } from '../helpers/logger' | ||
6 | import { VideoResolution } from '../../shared/models/videos' | ||
7 | import { VideoFileModel } from '../models/video/video-file' | ||
8 | import { VideoModel } from '../models/video/video' | ||
9 | |||
10 | async function optimizeOriginalVideofile (video: VideoModel) { | ||
11 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
12 | const newExtname = '.mp4' | ||
13 | const inputVideoFile = video.getOriginalFile() | ||
14 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) | ||
15 | const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) | ||
16 | |||
17 | const transcodeOptions = { | ||
18 | inputPath: videoInputPath, | ||
19 | outputPath: videoTranscodedPath | ||
20 | } | ||
21 | |||
22 | // Could be very long! | ||
23 | await transcode(transcodeOptions) | ||
24 | |||
25 | try { | ||
26 | await remove(videoInputPath) | ||
27 | |||
28 | // Important to do this before getVideoFilename() to take in account the new file extension | ||
29 | inputVideoFile.set('extname', newExtname) | ||
30 | |||
31 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | ||
32 | await rename(videoTranscodedPath, videoOutputPath) | ||
33 | const stats = await stat(videoOutputPath) | ||
34 | const fps = await getVideoFileFPS(videoOutputPath) | ||
35 | |||
36 | inputVideoFile.set('size', stats.size) | ||
37 | inputVideoFile.set('fps', fps) | ||
38 | |||
39 | await video.createTorrentAndSetInfoHash(inputVideoFile) | ||
40 | await inputVideoFile.save() | ||
41 | } catch (err) { | ||
42 | // Auto destruction... | ||
43 | video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) | ||
44 | |||
45 | throw err | ||
46 | } | ||
47 | } | ||
48 | |||
49 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | ||
50 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
51 | const extname = '.mp4' | ||
52 | |||
53 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | ||
54 | const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile())) | ||
55 | |||
56 | const newVideoFile = new VideoFileModel({ | ||
57 | resolution, | ||
58 | extname, | ||
59 | size: 0, | ||
60 | videoId: video.id | ||
61 | }) | ||
62 | const videoOutputPath = join(videosDirectory, video.getVideoFilename(newVideoFile)) | ||
63 | |||
64 | const transcodeOptions = { | ||
65 | inputPath: videoInputPath, | ||
66 | outputPath: videoOutputPath, | ||
67 | resolution, | ||
68 | isPortraitMode | ||
69 | } | ||
70 | |||
71 | await transcode(transcodeOptions) | ||
72 | |||
73 | const stats = await stat(videoOutputPath) | ||
74 | const fps = await getVideoFileFPS(videoOutputPath) | ||
75 | |||
76 | newVideoFile.set('size', stats.size) | ||
77 | newVideoFile.set('fps', fps) | ||
78 | |||
79 | await video.createTorrentAndSetInfoHash(newVideoFile) | ||
80 | |||
81 | await newVideoFile.save() | ||
82 | |||
83 | video.VideoFiles.push(newVideoFile) | ||
84 | } | ||
85 | |||
86 | async function importVideoFile (video: VideoModel, inputFilePath: string) { | ||
87 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | ||
88 | const { size } = await stat(inputFilePath) | ||
89 | const fps = await getVideoFileFPS(inputFilePath) | ||
90 | |||
91 | let updatedVideoFile = new VideoFileModel({ | ||
92 | resolution: videoFileResolution, | ||
93 | extname: extname(inputFilePath), | ||
94 | size, | ||
95 | fps, | ||
96 | videoId: video.id | ||
97 | }) | ||
98 | |||
99 | const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution) | ||
100 | |||
101 | if (currentVideoFile) { | ||
102 | // Remove old file and old torrent | ||
103 | await video.removeFile(currentVideoFile) | ||
104 | await video.removeTorrent(currentVideoFile) | ||
105 | // Remove the old video file from the array | ||
106 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) | ||
107 | |||
108 | // Update the database | ||
109 | currentVideoFile.set('extname', updatedVideoFile.extname) | ||
110 | currentVideoFile.set('size', updatedVideoFile.size) | ||
111 | currentVideoFile.set('fps', updatedVideoFile.fps) | ||
112 | |||
113 | updatedVideoFile = currentVideoFile | ||
114 | } | ||
115 | |||
116 | const outputPath = video.getVideoFilePath(updatedVideoFile) | ||
117 | await copy(inputFilePath, outputPath) | ||
118 | |||
119 | await video.createTorrentAndSetInfoHash(updatedVideoFile) | ||
120 | |||
121 | await updatedVideoFile.save() | ||
122 | |||
123 | video.VideoFiles.push(updatedVideoFile) | ||
124 | } | ||
125 | |||
126 | export { | ||
127 | optimizeOriginalVideofile, | ||
128 | transcodeOriginalVideofile, | ||
129 | importVideoFile | ||
130 | } | ||
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 6ae02efb9..fb07287a8 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -193,7 +193,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
193 | // On VideoModel! | 193 | // On VideoModel! |
194 | const query = { | 194 | const query = { |
195 | attributes: [ 'id', 'publishedAt' ], | 195 | attributes: [ 'id', 'publishedAt' ], |
196 | // logging: !isTestInstance(), | 196 | logging: !isTestInstance(), |
197 | limit: randomizedFactor, | 197 | limit: randomizedFactor, |
198 | order: getVideoSort('-publishedAt'), | 198 | order: getVideoSort('-publishedAt'), |
199 | where: { | 199 | where: { |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts new file mode 100644 index 000000000..fae38507b --- /dev/null +++ b/server/models/video/video-format-utils.ts | |||
@@ -0,0 +1,295 @@ | |||
1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | ||
2 | import { VideoModel } from './video' | ||
3 | import { VideoFileModel } from './video-file' | ||
4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' | ||
5 | import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' | ||
6 | import { VideoCaptionModel } from './video-caption' | ||
7 | import { | ||
8 | getVideoCommentsActivityPubUrl, | ||
9 | getVideoDislikesActivityPubUrl, | ||
10 | getVideoLikesActivityPubUrl, | ||
11 | getVideoSharesActivityPubUrl | ||
12 | } from '../../lib/activitypub' | ||
13 | |||
14 | export type VideoFormattingJSONOptions = { | ||
15 | additionalAttributes: { | ||
16 | state?: boolean, | ||
17 | waitTranscoding?: boolean, | ||
18 | scheduledUpdate?: boolean, | ||
19 | blacklistInfo?: boolean | ||
20 | } | ||
21 | } | ||
22 | function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { | ||
23 | const formattedAccount = video.VideoChannel.Account.toFormattedJSON() | ||
24 | const formattedVideoChannel = video.VideoChannel.toFormattedJSON() | ||
25 | |||
26 | const videoObject: Video = { | ||
27 | id: video.id, | ||
28 | uuid: video.uuid, | ||
29 | name: video.name, | ||
30 | category: { | ||
31 | id: video.category, | ||
32 | label: VideoModel.getCategoryLabel(video.category) | ||
33 | }, | ||
34 | licence: { | ||
35 | id: video.licence, | ||
36 | label: VideoModel.getLicenceLabel(video.licence) | ||
37 | }, | ||
38 | language: { | ||
39 | id: video.language, | ||
40 | label: VideoModel.getLanguageLabel(video.language) | ||
41 | }, | ||
42 | privacy: { | ||
43 | id: video.privacy, | ||
44 | label: VideoModel.getPrivacyLabel(video.privacy) | ||
45 | }, | ||
46 | nsfw: video.nsfw, | ||
47 | description: video.getTruncatedDescription(), | ||
48 | isLocal: video.isOwned(), | ||
49 | duration: video.duration, | ||
50 | views: video.views, | ||
51 | likes: video.likes, | ||
52 | dislikes: video.dislikes, | ||
53 | thumbnailPath: video.getThumbnailStaticPath(), | ||
54 | previewPath: video.getPreviewStaticPath(), | ||
55 | embedPath: video.getEmbedStaticPath(), | ||
56 | createdAt: video.createdAt, | ||
57 | updatedAt: video.updatedAt, | ||
58 | publishedAt: video.publishedAt, | ||
59 | account: { | ||
60 | id: formattedAccount.id, | ||
61 | uuid: formattedAccount.uuid, | ||
62 | name: formattedAccount.name, | ||
63 | displayName: formattedAccount.displayName, | ||
64 | url: formattedAccount.url, | ||
65 | host: formattedAccount.host, | ||
66 | avatar: formattedAccount.avatar | ||
67 | }, | ||
68 | channel: { | ||
69 | id: formattedVideoChannel.id, | ||
70 | uuid: formattedVideoChannel.uuid, | ||
71 | name: formattedVideoChannel.name, | ||
72 | displayName: formattedVideoChannel.displayName, | ||
73 | url: formattedVideoChannel.url, | ||
74 | host: formattedVideoChannel.host, | ||
75 | avatar: formattedVideoChannel.avatar | ||
76 | } | ||
77 | } | ||
78 | |||
79 | if (options) { | ||
80 | if (options.additionalAttributes.state === true) { | ||
81 | videoObject.state = { | ||
82 | id: video.state, | ||
83 | label: VideoModel.getStateLabel(video.state) | ||
84 | } | ||
85 | } | ||
86 | |||
87 | if (options.additionalAttributes.waitTranscoding === true) { | ||
88 | videoObject.waitTranscoding = video.waitTranscoding | ||
89 | } | ||
90 | |||
91 | if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
92 | videoObject.scheduledUpdate = { | ||
93 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
94 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
95 | } | ||
96 | } | ||
97 | |||
98 | if (options.additionalAttributes.blacklistInfo === true) { | ||
99 | videoObject.blacklisted = !!video.VideoBlacklist | ||
100 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null | ||
101 | } | ||
102 | } | ||
103 | |||
104 | return videoObject | ||
105 | } | ||
106 | |||
107 | function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | ||
108 | const formattedJson = video.toFormattedJSON({ | ||
109 | additionalAttributes: { | ||
110 | scheduledUpdate: true, | ||
111 | blacklistInfo: true | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | const detailsJson = { | ||
116 | support: video.support, | ||
117 | descriptionPath: video.getDescriptionPath(), | ||
118 | channel: video.VideoChannel.toFormattedJSON(), | ||
119 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
120 | tags: video.Tags.map(t => t.name), | ||
121 | commentsEnabled: video.commentsEnabled, | ||
122 | waitTranscoding: video.waitTranscoding, | ||
123 | state: { | ||
124 | id: video.state, | ||
125 | label: VideoModel.getStateLabel(video.state) | ||
126 | }, | ||
127 | files: [] | ||
128 | } | ||
129 | |||
130 | // Format and sort video files | ||
131 | detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
132 | |||
133 | return Object.assign(formattedJson, detailsJson) | ||
134 | } | ||
135 | |||
136 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { | ||
137 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
138 | |||
139 | return videoFiles | ||
140 | .map(videoFile => { | ||
141 | let resolutionLabel = videoFile.resolution + 'p' | ||
142 | |||
143 | return { | ||
144 | resolution: { | ||
145 | id: videoFile.resolution, | ||
146 | label: resolutionLabel | ||
147 | }, | ||
148 | magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | ||
149 | size: videoFile.size, | ||
150 | fps: videoFile.fps, | ||
151 | torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp), | ||
152 | torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp), | ||
153 | fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp), | ||
154 | fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp) | ||
155 | } as VideoFile | ||
156 | }) | ||
157 | .sort((a, b) => { | ||
158 | if (a.resolution.id < b.resolution.id) return 1 | ||
159 | if (a.resolution.id === b.resolution.id) return 0 | ||
160 | return -1 | ||
161 | }) | ||
162 | } | ||
163 | |||
164 | function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | ||
165 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
166 | if (!video.Tags) video.Tags = [] | ||
167 | |||
168 | const tag = video.Tags.map(t => ({ | ||
169 | type: 'Hashtag' as 'Hashtag', | ||
170 | name: t.name | ||
171 | })) | ||
172 | |||
173 | let language | ||
174 | if (video.language) { | ||
175 | language = { | ||
176 | identifier: video.language, | ||
177 | name: VideoModel.getLanguageLabel(video.language) | ||
178 | } | ||
179 | } | ||
180 | |||
181 | let category | ||
182 | if (video.category) { | ||
183 | category = { | ||
184 | identifier: video.category + '', | ||
185 | name: VideoModel.getCategoryLabel(video.category) | ||
186 | } | ||
187 | } | ||
188 | |||
189 | let licence | ||
190 | if (video.licence) { | ||
191 | licence = { | ||
192 | identifier: video.licence + '', | ||
193 | name: VideoModel.getLicenceLabel(video.licence) | ||
194 | } | ||
195 | } | ||
196 | |||
197 | const url: ActivityUrlObject[] = [] | ||
198 | for (const file of video.VideoFiles) { | ||
199 | url.push({ | ||
200 | type: 'Link', | ||
201 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | ||
202 | href: video.getVideoFileUrl(file, baseUrlHttp), | ||
203 | height: file.resolution, | ||
204 | size: file.size, | ||
205 | fps: file.fps | ||
206 | }) | ||
207 | |||
208 | url.push({ | ||
209 | type: 'Link', | ||
210 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
211 | href: video.getTorrentUrl(file, baseUrlHttp), | ||
212 | height: file.resolution | ||
213 | }) | ||
214 | |||
215 | url.push({ | ||
216 | type: 'Link', | ||
217 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
218 | href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | ||
219 | height: file.resolution | ||
220 | }) | ||
221 | } | ||
222 | |||
223 | // Add video url too | ||
224 | url.push({ | ||
225 | type: 'Link', | ||
226 | mimeType: 'text/html', | ||
227 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
228 | }) | ||
229 | |||
230 | const subtitleLanguage = [] | ||
231 | for (const caption of video.VideoCaptions) { | ||
232 | subtitleLanguage.push({ | ||
233 | identifier: caption.language, | ||
234 | name: VideoCaptionModel.getLanguageLabel(caption.language) | ||
235 | }) | ||
236 | } | ||
237 | |||
238 | return { | ||
239 | type: 'Video' as 'Video', | ||
240 | id: video.url, | ||
241 | name: video.name, | ||
242 | duration: getActivityStreamDuration(video.duration), | ||
243 | uuid: video.uuid, | ||
244 | tag, | ||
245 | category, | ||
246 | licence, | ||
247 | language, | ||
248 | views: video.views, | ||
249 | sensitive: video.nsfw, | ||
250 | waitTranscoding: video.waitTranscoding, | ||
251 | state: video.state, | ||
252 | commentsEnabled: video.commentsEnabled, | ||
253 | published: video.publishedAt.toISOString(), | ||
254 | updated: video.updatedAt.toISOString(), | ||
255 | mediaType: 'text/markdown', | ||
256 | content: video.getTruncatedDescription(), | ||
257 | support: video.support, | ||
258 | subtitleLanguage, | ||
259 | icon: { | ||
260 | type: 'Image', | ||
261 | url: video.getThumbnailUrl(baseUrlHttp), | ||
262 | mediaType: 'image/jpeg', | ||
263 | width: THUMBNAILS_SIZE.width, | ||
264 | height: THUMBNAILS_SIZE.height | ||
265 | }, | ||
266 | url, | ||
267 | likes: getVideoLikesActivityPubUrl(video), | ||
268 | dislikes: getVideoDislikesActivityPubUrl(video), | ||
269 | shares: getVideoSharesActivityPubUrl(video), | ||
270 | comments: getVideoCommentsActivityPubUrl(video), | ||
271 | attributedTo: [ | ||
272 | { | ||
273 | type: 'Person', | ||
274 | id: video.VideoChannel.Account.Actor.url | ||
275 | }, | ||
276 | { | ||
277 | type: 'Group', | ||
278 | id: video.VideoChannel.Actor.url | ||
279 | } | ||
280 | ] | ||
281 | } | ||
282 | } | ||
283 | |||
284 | function getActivityStreamDuration (duration: number) { | ||
285 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | ||
286 | return 'PT' + duration + 'S' | ||
287 | } | ||
288 | |||
289 | export { | ||
290 | videoModelToFormattedJSON, | ||
291 | videoModelToFormattedDetailsJSON, | ||
292 | videoFilesModelToFormattedJSON, | ||
293 | videoModelToActivityPubObject, | ||
294 | getActivityStreamDuration | ||
295 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index b7d3f184f..ce856aed2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { map, maxBy } from 'lodash' | 2 | import { maxBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as parseTorrent from 'parse-torrent' | 4 | import * as parseTorrent from 'parse-torrent' |
5 | import { extname, join } from 'path' | 5 | import { join } from 'path' |
6 | import * as Sequelize from 'sequelize' | 6 | import * as Sequelize from 'sequelize' |
7 | import { | 7 | import { |
8 | AllowNull, | 8 | AllowNull, |
@@ -27,7 +27,7 @@ import { | |||
27 | Table, | 27 | Table, |
28 | UpdatedAt | 28 | UpdatedAt |
29 | } from 'sequelize-typescript' | 29 | } from 'sequelize-typescript' |
30 | import { ActivityUrlObject, VideoPrivacy, VideoResolution, VideoState } from '../../../shared' | 30 | import { VideoPrivacy, VideoState } from '../../../shared' |
31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
@@ -45,7 +45,7 @@ import { | |||
45 | isVideoStateValid, | 45 | isVideoStateValid, |
46 | isVideoSupportValid | 46 | isVideoSupportValid |
47 | } from '../../helpers/custom-validators/videos' | 47 | } from '../../helpers/custom-validators/videos' |
48 | import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' | 48 | import { generateImageFromVideoFile, getVideoFileResolution } from '../../helpers/ffmpeg-utils' |
49 | import { logger } from '../../helpers/logger' | 49 | import { logger } from '../../helpers/logger' |
50 | import { getServerActor } from '../../helpers/utils' | 50 | import { getServerActor } from '../../helpers/utils' |
51 | import { | 51 | import { |
@@ -59,18 +59,11 @@ import { | |||
59 | STATIC_PATHS, | 59 | STATIC_PATHS, |
60 | THUMBNAILS_SIZE, | 60 | THUMBNAILS_SIZE, |
61 | VIDEO_CATEGORIES, | 61 | VIDEO_CATEGORIES, |
62 | VIDEO_EXT_MIMETYPE, | ||
63 | VIDEO_LANGUAGES, | 62 | VIDEO_LANGUAGES, |
64 | VIDEO_LICENCES, | 63 | VIDEO_LICENCES, |
65 | VIDEO_PRIVACIES, | 64 | VIDEO_PRIVACIES, |
66 | VIDEO_STATES | 65 | VIDEO_STATES |
67 | } from '../../initializers' | 66 | } from '../../initializers' |
68 | import { | ||
69 | getVideoCommentsActivityPubUrl, | ||
70 | getVideoDislikesActivityPubUrl, | ||
71 | getVideoLikesActivityPubUrl, | ||
72 | getVideoSharesActivityPubUrl | ||
73 | } from '../../lib/activitypub' | ||
74 | import { sendDeleteVideo } from '../../lib/activitypub/send' | 67 | import { sendDeleteVideo } from '../../lib/activitypub/send' |
75 | import { AccountModel } from '../account/account' | 68 | import { AccountModel } from '../account/account' |
76 | import { AccountVideoRateModel } from '../account/account-video-rate' | 69 | import { AccountVideoRateModel } from '../account/account-video-rate' |
@@ -88,9 +81,16 @@ import { VideoTagModel } from './video-tag' | |||
88 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 81 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
89 | import { VideoCaptionModel } from './video-caption' | 82 | import { VideoCaptionModel } from './video-caption' |
90 | import { VideoBlacklistModel } from './video-blacklist' | 83 | import { VideoBlacklistModel } from './video-blacklist' |
91 | import { copy, remove, rename, stat, writeFile } from 'fs-extra' | 84 | import { remove, writeFile } from 'fs-extra' |
92 | import { VideoViewModel } from './video-views' | 85 | import { VideoViewModel } from './video-views' |
93 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 86 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
87 | import { | ||
88 | videoFilesModelToFormattedJSON, | ||
89 | VideoFormattingJSONOptions, | ||
90 | videoModelToActivityPubObject, | ||
91 | videoModelToFormattedDetailsJSON, | ||
92 | videoModelToFormattedJSON | ||
93 | } from './video-format-utils' | ||
94 | 94 | ||
95 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 95 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
96 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 96 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -1257,23 +1257,23 @@ export class VideoModel extends Model<VideoModel> { | |||
1257 | } | 1257 | } |
1258 | } | 1258 | } |
1259 | 1259 | ||
1260 | private static getCategoryLabel (id: number) { | 1260 | static getCategoryLabel (id: number) { |
1261 | return VIDEO_CATEGORIES[ id ] || 'Misc' | 1261 | return VIDEO_CATEGORIES[ id ] || 'Misc' |
1262 | } | 1262 | } |
1263 | 1263 | ||
1264 | private static getLicenceLabel (id: number) { | 1264 | static getLicenceLabel (id: number) { |
1265 | return VIDEO_LICENCES[ id ] || 'Unknown' | 1265 | return VIDEO_LICENCES[ id ] || 'Unknown' |
1266 | } | 1266 | } |
1267 | 1267 | ||
1268 | private static getLanguageLabel (id: string) { | 1268 | static getLanguageLabel (id: string) { |
1269 | return VIDEO_LANGUAGES[ id ] || 'Unknown' | 1269 | return VIDEO_LANGUAGES[ id ] || 'Unknown' |
1270 | } | 1270 | } |
1271 | 1271 | ||
1272 | private static getPrivacyLabel (id: number) { | 1272 | static getPrivacyLabel (id: number) { |
1273 | return VIDEO_PRIVACIES[ id ] || 'Unknown' | 1273 | return VIDEO_PRIVACIES[ id ] || 'Unknown' |
1274 | } | 1274 | } |
1275 | 1275 | ||
1276 | private static getStateLabel (id: number) { | 1276 | static getStateLabel (id: number) { |
1277 | return VIDEO_STATES[ id ] || 'Unknown' | 1277 | return VIDEO_STATES[ id ] || 'Unknown' |
1278 | } | 1278 | } |
1279 | 1279 | ||
@@ -1369,273 +1369,20 @@ export class VideoModel extends Model<VideoModel> { | |||
1369 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) | 1369 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) |
1370 | } | 1370 | } |
1371 | 1371 | ||
1372 | toFormattedJSON (options?: { | 1372 | toFormattedJSON (options?: VideoFormattingJSONOptions): Video { |
1373 | additionalAttributes: { | 1373 | return videoModelToFormattedJSON(this, options) |
1374 | state?: boolean, | ||
1375 | waitTranscoding?: boolean, | ||
1376 | scheduledUpdate?: boolean, | ||
1377 | blacklistInfo?: boolean | ||
1378 | } | ||
1379 | }): Video { | ||
1380 | const formattedAccount = this.VideoChannel.Account.toFormattedJSON() | ||
1381 | const formattedVideoChannel = this.VideoChannel.toFormattedJSON() | ||
1382 | |||
1383 | const videoObject: Video = { | ||
1384 | id: this.id, | ||
1385 | uuid: this.uuid, | ||
1386 | name: this.name, | ||
1387 | category: { | ||
1388 | id: this.category, | ||
1389 | label: VideoModel.getCategoryLabel(this.category) | ||
1390 | }, | ||
1391 | licence: { | ||
1392 | id: this.licence, | ||
1393 | label: VideoModel.getLicenceLabel(this.licence) | ||
1394 | }, | ||
1395 | language: { | ||
1396 | id: this.language, | ||
1397 | label: VideoModel.getLanguageLabel(this.language) | ||
1398 | }, | ||
1399 | privacy: { | ||
1400 | id: this.privacy, | ||
1401 | label: VideoModel.getPrivacyLabel(this.privacy) | ||
1402 | }, | ||
1403 | nsfw: this.nsfw, | ||
1404 | description: this.getTruncatedDescription(), | ||
1405 | isLocal: this.isOwned(), | ||
1406 | duration: this.duration, | ||
1407 | views: this.views, | ||
1408 | likes: this.likes, | ||
1409 | dislikes: this.dislikes, | ||
1410 | thumbnailPath: this.getThumbnailStaticPath(), | ||
1411 | previewPath: this.getPreviewStaticPath(), | ||
1412 | embedPath: this.getEmbedStaticPath(), | ||
1413 | createdAt: this.createdAt, | ||
1414 | updatedAt: this.updatedAt, | ||
1415 | publishedAt: this.publishedAt, | ||
1416 | account: { | ||
1417 | id: formattedAccount.id, | ||
1418 | uuid: formattedAccount.uuid, | ||
1419 | name: formattedAccount.name, | ||
1420 | displayName: formattedAccount.displayName, | ||
1421 | url: formattedAccount.url, | ||
1422 | host: formattedAccount.host, | ||
1423 | avatar: formattedAccount.avatar | ||
1424 | }, | ||
1425 | channel: { | ||
1426 | id: formattedVideoChannel.id, | ||
1427 | uuid: formattedVideoChannel.uuid, | ||
1428 | name: formattedVideoChannel.name, | ||
1429 | displayName: formattedVideoChannel.displayName, | ||
1430 | url: formattedVideoChannel.url, | ||
1431 | host: formattedVideoChannel.host, | ||
1432 | avatar: formattedVideoChannel.avatar | ||
1433 | } | ||
1434 | } | ||
1435 | |||
1436 | if (options) { | ||
1437 | if (options.additionalAttributes.state === true) { | ||
1438 | videoObject.state = { | ||
1439 | id: this.state, | ||
1440 | label: VideoModel.getStateLabel(this.state) | ||
1441 | } | ||
1442 | } | ||
1443 | |||
1444 | if (options.additionalAttributes.waitTranscoding === true) { | ||
1445 | videoObject.waitTranscoding = this.waitTranscoding | ||
1446 | } | ||
1447 | |||
1448 | if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) { | ||
1449 | videoObject.scheduledUpdate = { | ||
1450 | updateAt: this.ScheduleVideoUpdate.updateAt, | ||
1451 | privacy: this.ScheduleVideoUpdate.privacy || undefined | ||
1452 | } | ||
1453 | } | ||
1454 | |||
1455 | if (options.additionalAttributes.blacklistInfo === true) { | ||
1456 | videoObject.blacklisted = !!this.VideoBlacklist | ||
1457 | videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null | ||
1458 | } | ||
1459 | } | ||
1460 | |||
1461 | return videoObject | ||
1462 | } | 1374 | } |
1463 | 1375 | ||
1464 | toFormattedDetailsJSON (): VideoDetails { | 1376 | toFormattedDetailsJSON (): VideoDetails { |
1465 | const formattedJson = this.toFormattedJSON({ | 1377 | return videoModelToFormattedDetailsJSON(this) |
1466 | additionalAttributes: { | ||
1467 | scheduledUpdate: true, | ||
1468 | blacklistInfo: true | ||
1469 | } | ||
1470 | }) | ||
1471 | |||
1472 | const detailsJson = { | ||
1473 | support: this.support, | ||
1474 | descriptionPath: this.getDescriptionPath(), | ||
1475 | channel: this.VideoChannel.toFormattedJSON(), | ||
1476 | account: this.VideoChannel.Account.toFormattedJSON(), | ||
1477 | tags: map(this.Tags, 'name'), | ||
1478 | commentsEnabled: this.commentsEnabled, | ||
1479 | waitTranscoding: this.waitTranscoding, | ||
1480 | state: { | ||
1481 | id: this.state, | ||
1482 | label: VideoModel.getStateLabel(this.state) | ||
1483 | }, | ||
1484 | files: [] | ||
1485 | } | ||
1486 | |||
1487 | // Format and sort video files | ||
1488 | detailsJson.files = this.getFormattedVideoFilesJSON() | ||
1489 | |||
1490 | return Object.assign(formattedJson, detailsJson) | ||
1491 | } | 1378 | } |
1492 | 1379 | ||
1493 | getFormattedVideoFilesJSON (): VideoFile[] { | 1380 | getFormattedVideoFilesJSON (): VideoFile[] { |
1494 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() | 1381 | return videoFilesModelToFormattedJSON(this, this.VideoFiles) |
1495 | |||
1496 | return this.VideoFiles | ||
1497 | .map(videoFile => { | ||
1498 | let resolutionLabel = videoFile.resolution + 'p' | ||
1499 | |||
1500 | return { | ||
1501 | resolution: { | ||
1502 | id: videoFile.resolution, | ||
1503 | label: resolutionLabel | ||
1504 | }, | ||
1505 | magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | ||
1506 | size: videoFile.size, | ||
1507 | fps: videoFile.fps, | ||
1508 | torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), | ||
1509 | torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp), | ||
1510 | fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp), | ||
1511 | fileDownloadUrl: this.getVideoFileDownloadUrl(videoFile, baseUrlHttp) | ||
1512 | } as VideoFile | ||
1513 | }) | ||
1514 | .sort((a, b) => { | ||
1515 | if (a.resolution.id < b.resolution.id) return 1 | ||
1516 | if (a.resolution.id === b.resolution.id) return 0 | ||
1517 | return -1 | ||
1518 | }) | ||
1519 | } | 1382 | } |
1520 | 1383 | ||
1521 | toActivityPubObject (): VideoTorrentObject { | 1384 | toActivityPubObject (): VideoTorrentObject { |
1522 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() | 1385 | return videoModelToActivityPubObject(this) |
1523 | if (!this.Tags) this.Tags = [] | ||
1524 | |||
1525 | const tag = this.Tags.map(t => ({ | ||
1526 | type: 'Hashtag' as 'Hashtag', | ||
1527 | name: t.name | ||
1528 | })) | ||
1529 | |||
1530 | let language | ||
1531 | if (this.language) { | ||
1532 | language = { | ||
1533 | identifier: this.language, | ||
1534 | name: VideoModel.getLanguageLabel(this.language) | ||
1535 | } | ||
1536 | } | ||
1537 | |||
1538 | let category | ||
1539 | if (this.category) { | ||
1540 | category = { | ||
1541 | identifier: this.category + '', | ||
1542 | name: VideoModel.getCategoryLabel(this.category) | ||
1543 | } | ||
1544 | } | ||
1545 | |||
1546 | let licence | ||
1547 | if (this.licence) { | ||
1548 | licence = { | ||
1549 | identifier: this.licence + '', | ||
1550 | name: VideoModel.getLicenceLabel(this.licence) | ||
1551 | } | ||
1552 | } | ||
1553 | |||
1554 | const url: ActivityUrlObject[] = [] | ||
1555 | for (const file of this.VideoFiles) { | ||
1556 | url.push({ | ||
1557 | type: 'Link', | ||
1558 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | ||
1559 | href: this.getVideoFileUrl(file, baseUrlHttp), | ||
1560 | height: file.resolution, | ||
1561 | size: file.size, | ||
1562 | fps: file.fps | ||
1563 | }) | ||
1564 | |||
1565 | url.push({ | ||
1566 | type: 'Link', | ||
1567 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
1568 | href: this.getTorrentUrl(file, baseUrlHttp), | ||
1569 | height: file.resolution | ||
1570 | }) | ||
1571 | |||
1572 | url.push({ | ||
1573 | type: 'Link', | ||
1574 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
1575 | href: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | ||
1576 | height: file.resolution | ||
1577 | }) | ||
1578 | } | ||
1579 | |||
1580 | // Add video url too | ||
1581 | url.push({ | ||
1582 | type: 'Link', | ||
1583 | mimeType: 'text/html', | ||
1584 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid | ||
1585 | }) | ||
1586 | |||
1587 | const subtitleLanguage = [] | ||
1588 | for (const caption of this.VideoCaptions) { | ||
1589 | subtitleLanguage.push({ | ||
1590 | identifier: caption.language, | ||
1591 | name: VideoCaptionModel.getLanguageLabel(caption.language) | ||
1592 | }) | ||
1593 | } | ||
1594 | |||
1595 | return { | ||
1596 | type: 'Video' as 'Video', | ||
1597 | id: this.url, | ||
1598 | name: this.name, | ||
1599 | duration: this.getActivityStreamDuration(), | ||
1600 | uuid: this.uuid, | ||
1601 | tag, | ||
1602 | category, | ||
1603 | licence, | ||
1604 | language, | ||
1605 | views: this.views, | ||
1606 | sensitive: this.nsfw, | ||
1607 | waitTranscoding: this.waitTranscoding, | ||
1608 | state: this.state, | ||
1609 | commentsEnabled: this.commentsEnabled, | ||
1610 | published: this.publishedAt.toISOString(), | ||
1611 | updated: this.updatedAt.toISOString(), | ||
1612 | mediaType: 'text/markdown', | ||
1613 | content: this.getTruncatedDescription(), | ||
1614 | support: this.support, | ||
1615 | subtitleLanguage, | ||
1616 | icon: { | ||
1617 | type: 'Image', | ||
1618 | url: this.getThumbnailUrl(baseUrlHttp), | ||
1619 | mediaType: 'image/jpeg', | ||
1620 | width: THUMBNAILS_SIZE.width, | ||
1621 | height: THUMBNAILS_SIZE.height | ||
1622 | }, | ||
1623 | url, | ||
1624 | likes: getVideoLikesActivityPubUrl(this), | ||
1625 | dislikes: getVideoDislikesActivityPubUrl(this), | ||
1626 | shares: getVideoSharesActivityPubUrl(this), | ||
1627 | comments: getVideoCommentsActivityPubUrl(this), | ||
1628 | attributedTo: [ | ||
1629 | { | ||
1630 | type: 'Person', | ||
1631 | id: this.VideoChannel.Account.Actor.url | ||
1632 | }, | ||
1633 | { | ||
1634 | type: 'Group', | ||
1635 | id: this.VideoChannel.Actor.url | ||
1636 | } | ||
1637 | ] | ||
1638 | } | ||
1639 | } | 1386 | } |
1640 | 1387 | ||
1641 | getTruncatedDescription () { | 1388 | getTruncatedDescription () { |
@@ -1645,123 +1392,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1645 | return peertubeTruncate(this.description, maxLength) | 1392 | return peertubeTruncate(this.description, maxLength) |
1646 | } | 1393 | } |
1647 | 1394 | ||
1648 | async optimizeOriginalVideofile () { | ||
1649 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
1650 | const newExtname = '.mp4' | ||
1651 | const inputVideoFile = this.getOriginalFile() | ||
1652 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) | ||
1653 | const videoTranscodedPath = join(videosDirectory, this.id + '-transcoded' + newExtname) | ||
1654 | |||
1655 | const transcodeOptions = { | ||
1656 | inputPath: videoInputPath, | ||
1657 | outputPath: videoTranscodedPath | ||
1658 | } | ||
1659 | |||
1660 | // Could be very long! | ||
1661 | await transcode(transcodeOptions) | ||
1662 | |||
1663 | try { | ||
1664 | await remove(videoInputPath) | ||
1665 | |||
1666 | // Important to do this before getVideoFilename() to take in account the new file extension | ||
1667 | inputVideoFile.set('extname', newExtname) | ||
1668 | |||
1669 | const videoOutputPath = this.getVideoFilePath(inputVideoFile) | ||
1670 | await rename(videoTranscodedPath, videoOutputPath) | ||
1671 | const stats = await stat(videoOutputPath) | ||
1672 | const fps = await getVideoFileFPS(videoOutputPath) | ||
1673 | |||
1674 | inputVideoFile.set('size', stats.size) | ||
1675 | inputVideoFile.set('fps', fps) | ||
1676 | |||
1677 | await this.createTorrentAndSetInfoHash(inputVideoFile) | ||
1678 | await inputVideoFile.save() | ||
1679 | |||
1680 | } catch (err) { | ||
1681 | // Auto destruction... | ||
1682 | this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) | ||
1683 | |||
1684 | throw err | ||
1685 | } | ||
1686 | } | ||
1687 | |||
1688 | async transcodeOriginalVideofile (resolution: VideoResolution, isPortraitMode: boolean) { | ||
1689 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
1690 | const extname = '.mp4' | ||
1691 | |||
1692 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | ||
1693 | const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) | ||
1694 | |||
1695 | const newVideoFile = new VideoFileModel({ | ||
1696 | resolution, | ||
1697 | extname, | ||
1698 | size: 0, | ||
1699 | videoId: this.id | ||
1700 | }) | ||
1701 | const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) | ||
1702 | |||
1703 | const transcodeOptions = { | ||
1704 | inputPath: videoInputPath, | ||
1705 | outputPath: videoOutputPath, | ||
1706 | resolution, | ||
1707 | isPortraitMode | ||
1708 | } | ||
1709 | |||
1710 | await transcode(transcodeOptions) | ||
1711 | |||
1712 | const stats = await stat(videoOutputPath) | ||
1713 | const fps = await getVideoFileFPS(videoOutputPath) | ||
1714 | |||
1715 | newVideoFile.set('size', stats.size) | ||
1716 | newVideoFile.set('fps', fps) | ||
1717 | |||
1718 | await this.createTorrentAndSetInfoHash(newVideoFile) | ||
1719 | |||
1720 | await newVideoFile.save() | ||
1721 | |||
1722 | this.VideoFiles.push(newVideoFile) | ||
1723 | } | ||
1724 | |||
1725 | async importVideoFile (inputFilePath: string) { | ||
1726 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | ||
1727 | const { size } = await stat(inputFilePath) | ||
1728 | const fps = await getVideoFileFPS(inputFilePath) | ||
1729 | |||
1730 | let updatedVideoFile = new VideoFileModel({ | ||
1731 | resolution: videoFileResolution, | ||
1732 | extname: extname(inputFilePath), | ||
1733 | size, | ||
1734 | fps, | ||
1735 | videoId: this.id | ||
1736 | }) | ||
1737 | |||
1738 | const currentVideoFile = this.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution) | ||
1739 | |||
1740 | if (currentVideoFile) { | ||
1741 | // Remove old file and old torrent | ||
1742 | await this.removeFile(currentVideoFile) | ||
1743 | await this.removeTorrent(currentVideoFile) | ||
1744 | // Remove the old video file from the array | ||
1745 | this.VideoFiles = this.VideoFiles.filter(f => f !== currentVideoFile) | ||
1746 | |||
1747 | // Update the database | ||
1748 | currentVideoFile.set('extname', updatedVideoFile.extname) | ||
1749 | currentVideoFile.set('size', updatedVideoFile.size) | ||
1750 | currentVideoFile.set('fps', updatedVideoFile.fps) | ||
1751 | |||
1752 | updatedVideoFile = currentVideoFile | ||
1753 | } | ||
1754 | |||
1755 | const outputPath = this.getVideoFilePath(updatedVideoFile) | ||
1756 | await copy(inputFilePath, outputPath) | ||
1757 | |||
1758 | await this.createTorrentAndSetInfoHash(updatedVideoFile) | ||
1759 | |||
1760 | await updatedVideoFile.save() | ||
1761 | |||
1762 | this.VideoFiles.push(updatedVideoFile) | ||
1763 | } | ||
1764 | |||
1765 | getOriginalFileResolution () { | 1395 | getOriginalFileResolution () { |
1766 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) | 1396 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) |
1767 | 1397 | ||
@@ -1796,11 +1426,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1796 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) | 1426 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) |
1797 | } | 1427 | } |
1798 | 1428 | ||
1799 | getActivityStreamDuration () { | ||
1800 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | ||
1801 | return 'PT' + this.duration + 'S' | ||
1802 | } | ||
1803 | |||
1804 | isOutdated () { | 1429 | isOutdated () { |
1805 | if (this.isOwned()) return false | 1430 | if (this.isOwned()) return false |
1806 | 1431 | ||