aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/index.ts12
-rw-r--r--server/helpers/custom-validators/videos.ts5
-rw-r--r--server/helpers/ffmpeg-utils.ts79
-rw-r--r--server/helpers/index.ts1
-rw-r--r--server/helpers/utils.ts2
-rw-r--r--server/initializers/constants.ts11
-rw-r--r--server/initializers/migrations/0075-video-resolutions.ts43
-rw-r--r--server/middlewares/validators/videos.ts5
-rw-r--r--server/models/user/user.ts43
-rw-r--r--server/models/video/video-interface.ts2
-rw-r--r--server/models/video/video.ts207
-rw-r--r--server/tests/api/multiple-pods.ts41
-rw-r--r--server/tests/api/single-pod.ts28
-rw-r--r--server/tests/api/video-transcoder.ts2
-rw-r--r--server/tests/cli/update-host.ts4
-rw-r--r--server/tests/utils/videos.ts12
16 files changed, 264 insertions, 233 deletions
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 14c969ec3..2b7ead954 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -37,10 +37,11 @@ import {
37 retryTransactionWrapper, 37 retryTransactionWrapper,
38 generateRandomString, 38 generateRandomString,
39 getFormattedObjects, 39 getFormattedObjects,
40 renamePromise 40 renamePromise,
41 getVideoFileHeight
41} from '../../../helpers' 42} from '../../../helpers'
42import { TagInstance, VideoInstance } from '../../../models' 43import { TagInstance, VideoInstance } from '../../../models'
43import { VideoCreate, VideoUpdate, VideoResolution } from '../../../../shared' 44import { VideoCreate, VideoUpdate } from '../../../../shared'
44 45
45import { abuseVideoRouter } from './abuse' 46import { abuseVideoRouter } from './abuse'
46import { blacklistRouter } from './blacklist' 47import { blacklistRouter } from './blacklist'
@@ -192,9 +193,14 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
192 return { author, tagInstances, video } 193 return { author, tagInstances, video }
193 }) 194 })
194 .then(({ author, tagInstances, video }) => { 195 .then(({ author, tagInstances, video }) => {
196 const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
197 return getVideoFileHeight(videoFilePath)
198 .then(height => ({ author, tagInstances, video, videoFileHeight: height }))
199 })
200 .then(({ author, tagInstances, video, videoFileHeight }) => {
195 const videoFileData = { 201 const videoFileData = {
196 extname: extname(videoPhysicalFile.filename), 202 extname: extname(videoPhysicalFile.filename),
197 resolution: VideoResolution.ORIGINAL, 203 resolution: videoFileHeight,
198 size: videoPhysicalFile.size 204 size: videoPhysicalFile.size
199 } 205 }
200 206
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 2eb021ae7..a31aca019 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -8,8 +8,7 @@ import {
8 VIDEO_CATEGORIES, 8 VIDEO_CATEGORIES,
9 VIDEO_LICENCES, 9 VIDEO_LICENCES,
10 VIDEO_LANGUAGES, 10 VIDEO_LANGUAGES,
11 VIDEO_RATE_TYPES, 11 VIDEO_RATE_TYPES
12 VIDEO_FILE_RESOLUTIONS
13} from '../../initializers' 12} from '../../initializers'
14import { isUserUsernameValid } from './users' 13import { isUserUsernameValid } from './users'
15import { isArray, exists } from './misc' 14import { isArray, exists } from './misc'
@@ -128,7 +127,7 @@ function isVideoFileSizeValid (value: string) {
128} 127}
129 128
130function isVideoFileResolutionValid (value: string) { 129function isVideoFileResolutionValid (value: string) {
131 return VIDEO_FILE_RESOLUTIONS[value] !== undefined 130 return exists(value) && validator.isInt(value + '')
132} 131}
133 132
134function isVideoFileExtnameValid (value: string) { 133function isVideoFileExtnameValid (value: string) {
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
new file mode 100644
index 000000000..c35125ec1
--- /dev/null
+++ b/server/helpers/ffmpeg-utils.ts
@@ -0,0 +1,79 @@
1import * as Promise from 'bluebird'
2import * as ffmpeg from 'fluent-ffmpeg'
3
4import { CONFIG } from '../initializers'
5import { VideoResolution } from '../../shared/models/videos/video-resolution.enum'
6
7function getVideoFileHeight (path: string) {
8 return new Promise<number>((res, rej) => {
9 ffmpeg.ffprobe(path, (err, metadata) => {
10 if (err) return rej(err)
11
12 const videoStream = metadata.streams.find(s => s.codec_type === 'video')
13 return res(videoStream.height)
14 })
15 })
16}
17
18function getDurationFromVideoFile (path: string) {
19 return new Promise<number>((res, rej) => {
20 ffmpeg.ffprobe(path, (err, metadata) => {
21 if (err) return rej(err)
22
23 return res(Math.floor(metadata.format.duration))
24 })
25 })
26}
27
28function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size?: string) {
29 const options = {
30 filename: imageName,
31 count: 1,
32 folder
33 }
34
35 if (size !== undefined) {
36 options['size'] = size
37 }
38
39 return new Promise<string>((res, rej) => {
40 ffmpeg(fromPath)
41 .on('error', rej)
42 .on('end', () => res(imageName))
43 .thumbnail(options)
44 })
45}
46
47type TranscodeOptions = {
48 inputPath: string
49 outputPath: string
50 resolution?: VideoResolution
51}
52
53function transcode (options: TranscodeOptions) {
54 return new Promise<void>((res, rej) => {
55 let command = ffmpeg(options.inputPath)
56 .output(options.outputPath)
57 .videoCodec('libx264')
58 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
59 .outputOption('-movflags faststart')
60
61 if (options.resolution !== undefined) {
62 const size = `${options.resolution}x?` // '720x?' for example
63 command = command.size(size)
64 }
65
66 command.on('error', rej)
67 .on('end', res)
68 .run()
69 })
70}
71
72// ---------------------------------------------------------------------------
73
74export {
75 getVideoFileHeight,
76 getDurationFromVideoFile,
77 generateImageFromVideoFile,
78 transcode
79}
diff --git a/server/helpers/index.ts b/server/helpers/index.ts
index 78215fe10..846bd796f 100644
--- a/server/helpers/index.ts
+++ b/server/helpers/index.ts
@@ -1,6 +1,7 @@
1export * from './core-utils' 1export * from './core-utils'
2export * from './logger' 2export * from './logger'
3export * from './custom-validators' 3export * from './custom-validators'
4export * from './ffmpeg-utils'
4export * from './database-utils' 5export * from './database-utils'
5export * from './peertube-crypto' 6export * from './peertube-crypto'
6export * from './requests' 7export * from './requests'
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index b74442ab0..3317dddc3 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -61,7 +61,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
61 ] 61 ]
62 62
63 for (const resolution of resolutions) { 63 for (const resolution of resolutions) {
64 if (configResolutions[resolution.toString()] === true && videoFileHeight >= resolution) { 64 if (configResolutions[resolution.toString()] === true && videoFileHeight > resolution) {
65 resolutionsEnabled.push(resolution) 65 resolutionsEnabled.push(resolution)
66 } 66 }
67 } 67 }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index f87041a3f..b11575b34 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -189,16 +189,6 @@ const VIDEO_LANGUAGES = {
189 14: 'Italian' 189 14: 'Italian'
190} 190}
191 191
192// TODO: use VideoResolution when https://github.com/Microsoft/TypeScript/issues/13042 is fixed
193const VIDEO_FILE_RESOLUTIONS: { [ id: number ]: string } = {
194 0: 'original',
195 240: '240p',
196 360: '360p',
197 480: '480p',
198 720: '720p',
199 1080: '1080p'
200}
201
202// --------------------------------------------------------------------------- 192// ---------------------------------------------------------------------------
203 193
204// Score a pod has when we create it as a friend 194// Score a pod has when we create it as a friend
@@ -385,7 +375,6 @@ export {
385 THUMBNAILS_SIZE, 375 THUMBNAILS_SIZE,
386 USER_ROLES, 376 USER_ROLES,
387 VIDEO_CATEGORIES, 377 VIDEO_CATEGORIES,
388 VIDEO_FILE_RESOLUTIONS,
389 VIDEO_LANGUAGES, 378 VIDEO_LANGUAGES,
390 VIDEO_LICENCES, 379 VIDEO_LICENCES,
391 VIDEO_RATE_TYPES 380 VIDEO_RATE_TYPES
diff --git a/server/initializers/migrations/0075-video-resolutions.ts b/server/initializers/migrations/0075-video-resolutions.ts
index 6bc1e72ab..e1d9fdacb 100644
--- a/server/initializers/migrations/0075-video-resolutions.ts
+++ b/server/initializers/migrations/0075-video-resolutions.ts
@@ -4,6 +4,7 @@ import { join } from 'path'
4 4
5import { readdirPromise, renamePromise } from '../../helpers/core-utils' 5import { readdirPromise, renamePromise } from '../../helpers/core-utils'
6import { CONFIG } from '../../initializers/constants' 6import { CONFIG } from '../../initializers/constants'
7import { getVideoFileHeight } from '../../helpers/ffmpeg-utils'
7 8
8function up (utils: { 9function up (utils: {
9 transaction: Sequelize.Transaction, 10 transaction: Sequelize.Transaction,
@@ -14,26 +15,7 @@ function up (utils: {
14 const torrentDir = CONFIG.STORAGE.TORRENTS_DIR 15 const torrentDir = CONFIG.STORAGE.TORRENTS_DIR
15 const videoFileDir = CONFIG.STORAGE.VIDEOS_DIR 16 const videoFileDir = CONFIG.STORAGE.VIDEOS_DIR
16 17
17 return readdirPromise(torrentDir) 18 return readdirPromise(videoFileDir)
18 .then(torrentFiles => {
19 const tasks: Promise<any>[] = []
20 for (const torrentFile of torrentFiles) {
21 const matches = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.torrent/.exec(torrentFile)
22 if (matches === null) {
23 console.log('Invalid torrent file name %s.', torrentFile)
24 continue
25 }
26
27 const newTorrentName = matches[1] + '-original.torrent'
28 const p = renamePromise(join(torrentDir, torrentFile), join(torrentDir, newTorrentName))
29 tasks.push(p)
30 }
31
32 return Promise.all(tasks)
33 })
34 .then(() => {
35 return readdirPromise(videoFileDir)
36 })
37 .then(videoFiles => { 19 .then(videoFiles => {
38 const tasks: Promise<any>[] = [] 20 const tasks: Promise<any>[] = []
39 for (const videoFile of videoFiles) { 21 for (const videoFile of videoFiles) {
@@ -43,8 +25,25 @@ function up (utils: {
43 continue 25 continue
44 } 26 }
45 27
46 const newVideoFileName = matches[1] + '-original.' + matches[2] 28 const uuid = matches[1]
47 const p = renamePromise(join(videoFileDir, videoFile), join(videoFileDir, newVideoFileName)) 29 const ext = matches[2]
30
31 const p = getVideoFileHeight(join(videoFileDir, videoFile))
32 .then(height => {
33 const oldTorrentName = uuid + '.torrent'
34 const newTorrentName = uuid + '-' + height + '.torrent'
35 return renamePromise(join(torrentDir, oldTorrentName), join(torrentDir, newTorrentName)).then(() => height)
36 })
37 .then(height => {
38 const newVideoFileName = uuid + '-' + height + '.' + ext
39 return renamePromise(join(videoFileDir, videoFile), join(videoFileDir, newVideoFileName)).then(() => height)
40 })
41 .then(height => {
42 const query = 'UPDATE "VideoFiles" SET "resolution" = ' + height +
43 ' WHERE "videoId" = (SELECT "id" FROM "Videos" WHERE "uuid" = \'' + uuid + '\')'
44 return utils.sequelize.query(query)
45 })
46
48 tasks.push(p) 47 tasks.push(p)
49 } 48 }
50 49
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index bc8b7e541..5f213f974 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -19,7 +19,8 @@ import {
19 isVideoNSFWValid, 19 isVideoNSFWValid,
20 isVideoIdOrUUIDValid, 20 isVideoIdOrUUIDValid,
21 isVideoAbuseReasonValid, 21 isVideoAbuseReasonValid,
22 isVideoRatingTypeValid 22 isVideoRatingTypeValid,
23 getDurationFromVideoFile
23} from '../../helpers' 24} from '../../helpers'
24import { VideoInstance } from '../../models' 25import { VideoInstance } from '../../models'
25 26
@@ -50,7 +51,7 @@ const videosAddValidator = [
50 return undefined 51 return undefined
51 } 52 }
52 53
53 return db.Video.getDurationFromFile(videoFile.path) 54 return getDurationFromVideoFile(videoFile.path)
54 .catch(err => { 55 .catch(err => {
55 logger.error('Invalid input file in videosAddValidator.', err) 56 logger.error('Invalid input file in videosAddValidator.', err)
56 res.status(400) 57 res.status(400)
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 7a21dbefa..0dc52d3cf 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -12,7 +12,6 @@ import {
12 isUserDisplayNSFWValid, 12 isUserDisplayNSFWValid,
13 isUserVideoQuotaValid 13 isUserVideoQuotaValid
14} from '../../helpers' 14} from '../../helpers'
15import { VideoResolution } from '../../../shared'
16 15
17import { addMethodsToModel } from '../utils' 16import { addMethodsToModel } from '../utils'
18import { 17import {
@@ -243,33 +242,21 @@ loadByUsernameOrEmail = function (username: string, email: string) {
243// --------------------------------------------------------------------------- 242// ---------------------------------------------------------------------------
244 243
245function getOriginalVideoFileTotalFromUser (user: UserInstance) { 244function getOriginalVideoFileTotalFromUser (user: UserInstance) {
246 // attributes = [] because we don't want other fields than the sum 245 // Don't use sequelize because we need to use a subquery
247 const query = { 246 const query = 'SELECT SUM("size") AS "total" FROM ' +
248 where: { 247 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
249 resolution: VideoResolution.ORIGINAL 248 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
250 }, 249 'INNER JOIN "Authors" ON "Videos"."authorId" = "Authors"."id" ' +
251 include: [ 250 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' +
252 { 251 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
253 attributes: [], 252
254 model: User['sequelize'].models.Video, 253 const options = {
255 include: [ 254 bind: { userId: user.id },
256 { 255 type: Sequelize.QueryTypes.SELECT
257 attributes: [],
258 model: User['sequelize'].models.Author,
259 include: [
260 {
261 attributes: [],
262 model: User['sequelize'].models.User,
263 where: {
264 id: user.id
265 }
266 }
267 ]
268 }
269 ]
270 }
271 ]
272 } 256 }
257 return User['sequelize'].query(query, options).then(([ { total } ]) => {
258 if (total === null) return 0
273 259
274 return User['sequelize'].models.VideoFile.sum('size', query) 260 return parseInt(total, 10)
261 })
275} 262}
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index 340426f45..6a3db4f3e 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -35,7 +35,6 @@ export namespace VideoMethods {
35 35
36 // Return thumbnail name 36 // Return thumbnail name
37 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> 37 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
38 export type GetDurationFromFile = (videoPath: string) => Promise<number>
39 38
40 export type List = () => Promise<VideoInstance[]> 39 export type List = () => Promise<VideoInstance[]>
41 export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]> 40 export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]>
@@ -65,7 +64,6 @@ export namespace VideoMethods {
65 64
66export interface VideoClass { 65export interface VideoClass {
67 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 66 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
68 getDurationFromFile: VideoMethods.GetDurationFromFile
69 list: VideoMethods.List 67 list: VideoMethods.List
70 listForApi: VideoMethods.ListForApi 68 listForApi: VideoMethods.ListForApi
71 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 69 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index c376d769e..2ba6cf25f 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,12 +1,12 @@
1import * as safeBuffer from 'safe-buffer' 1import * as safeBuffer from 'safe-buffer'
2const Buffer = safeBuffer.Buffer 2const Buffer = safeBuffer.Buffer
3import * as ffmpeg from 'fluent-ffmpeg'
4import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
5import { map } from 'lodash' 4import { map } from 'lodash'
6import * as parseTorrent from 'parse-torrent' 5import * as parseTorrent from 'parse-torrent'
7import { join } from 'path' 6import { join } from 'path'
8import * as Sequelize from 'sequelize' 7import * as Sequelize from 'sequelize'
9import * as Promise from 'bluebird' 8import * as Promise from 'bluebird'
9import { maxBy } from 'lodash'
10 10
11import { TagInstance } from './tag-interface' 11import { TagInstance } from './tag-interface'
12import { 12import {
@@ -23,7 +23,10 @@ import {
23 renamePromise, 23 renamePromise,
24 writeFilePromise, 24 writeFilePromise,
25 createTorrentPromise, 25 createTorrentPromise,
26 statPromise 26 statPromise,
27 generateImageFromVideoFile,
28 transcode,
29 getVideoFileHeight
27} from '../../helpers' 30} from '../../helpers'
28import { 31import {
29 CONFIG, 32 CONFIG,
@@ -32,8 +35,7 @@ import {
32 VIDEO_CATEGORIES, 35 VIDEO_CATEGORIES,
33 VIDEO_LICENCES, 36 VIDEO_LICENCES,
34 VIDEO_LANGUAGES, 37 VIDEO_LANGUAGES,
35 THUMBNAILS_SIZE, 38 THUMBNAILS_SIZE
36 VIDEO_FILE_RESOLUTIONS
37} from '../../initializers' 39} from '../../initializers'
38import { removeVideoToFriends } from '../../lib' 40import { removeVideoToFriends } from '../../lib'
39import { VideoResolution } from '../../../shared' 41import { VideoResolution } from '../../../shared'
@@ -67,7 +69,6 @@ let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
67let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight 69let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
68 70
69let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 71let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
70let getDurationFromFile: VideoMethods.GetDurationFromFile
71let list: VideoMethods.List 72let list: VideoMethods.List
72let listForApi: VideoMethods.ListForApi 73let listForApi: VideoMethods.ListForApi
73let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 74let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
@@ -233,7 +234,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
233 associate, 234 associate,
234 235
235 generateThumbnailFromData, 236 generateThumbnailFromData,
236 getDurationFromFile,
237 list, 237 list,
238 listForApi, 238 listForApi,
239 listOwnedAndPopulateAuthorAndTags, 239 listOwnedAndPopulateAuthorAndTags,
@@ -338,11 +338,12 @@ function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.T
338getOriginalFile = function (this: VideoInstance) { 338getOriginalFile = function (this: VideoInstance) {
339 if (Array.isArray(this.VideoFiles) === false) return undefined 339 if (Array.isArray(this.VideoFiles) === false) return undefined
340 340
341 return this.VideoFiles.find(file => file.resolution === VideoResolution.ORIGINAL) 341 // The original file is the file that have the higher resolution
342 return maxBy(this.VideoFiles, file => file.resolution)
342} 343}
343 344
344getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { 345getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
345 return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname 346 return this.uuid + '-' + videoFile.resolution + videoFile.extname
346} 347}
347 348
348getThumbnailName = function (this: VideoInstance) { 349getThumbnailName = function (this: VideoInstance) {
@@ -358,7 +359,7 @@ getPreviewName = function (this: VideoInstance) {
358 359
359getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { 360getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
360 const extension = '.torrent' 361 const extension = '.torrent'
361 return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension 362 return this.uuid + '-' + videoFile.resolution + extension
362} 363}
363 364
364isOwned = function (this: VideoInstance) { 365isOwned = function (this: VideoInstance) {
@@ -366,11 +367,20 @@ isOwned = function (this: VideoInstance) {
366} 367}
367 368
368createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { 369createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
369 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null) 370 return generateImageFromVideoFile(
371 this.getVideoFilePath(videoFile),
372 CONFIG.STORAGE.PREVIEWS_DIR,
373 this.getPreviewName()
374 )
370} 375}
371 376
372createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { 377createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
373 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE) 378 return generateImageFromVideoFile(
379 this.getVideoFilePath(videoFile),
380 CONFIG.STORAGE.THUMBNAILS_DIR,
381 this.getThumbnailName(),
382 THUMBNAILS_SIZE
383 )
374} 384}
375 385
376getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { 386getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
@@ -480,8 +490,7 @@ toFormattedJSON = function (this: VideoInstance) {
480 // Format and sort video files 490 // Format and sort video files
481 json.files = this.VideoFiles 491 json.files = this.VideoFiles
482 .map(videoFile => { 492 .map(videoFile => {
483 let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution] 493 let resolutionLabel = videoFile.resolution + 'p'
484 if (!resolutionLabel) resolutionLabel = 'Unknown'
485 494
486 const videoFileJson = { 495 const videoFileJson = {
487 resolution: videoFile.resolution, 496 resolution: videoFile.resolution,
@@ -578,46 +587,42 @@ optimizeOriginalVideofile = function (this: VideoInstance) {
578 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) 587 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
579 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) 588 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
580 589
581 return new Promise<void>((res, rej) => { 590 const transcodeOptions = {
582 ffmpeg(videoInputPath) 591 inputPath: videoInputPath,
583 .output(videoOutputPath) 592 outputPath: videoOutputPath
584 .videoCodec('libx264') 593 }
585 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 594
586 .outputOption('-movflags faststart') 595 return transcode(transcodeOptions)
587 .on('error', rej) 596 .then(() => {
588 .on('end', () => { 597 return unlinkPromise(videoInputPath)
589 598 })
590 return unlinkPromise(videoInputPath) 599 .then(() => {
591 .then(() => { 600 // Important to do this before getVideoFilename() to take in account the new file extension
592 // Important to do this before getVideoFilename() to take in account the new file extension 601 inputVideoFile.set('extname', newExtname)
593 inputVideoFile.set('extname', newExtname) 602
594 603 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
595 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) 604 })
596 }) 605 .then(() => {
597 .then(() => { 606 return statPromise(this.getVideoFilePath(inputVideoFile))
598 return statPromise(this.getVideoFilePath(inputVideoFile)) 607 })
599 }) 608 .then(stats => {
600 .then(stats => { 609 return inputVideoFile.set('size', stats.size)
601 return inputVideoFile.set('size', stats.size) 610 })
602 }) 611 .then(() => {
603 .then(() => { 612 return this.createTorrentAndSetInfoHash(inputVideoFile)
604 return this.createTorrentAndSetInfoHash(inputVideoFile) 613 })
605 }) 614 .then(() => {
606 .then(() => { 615 return inputVideoFile.save()
607 return inputVideoFile.save() 616 })
608 }) 617 .then(() => {
609 .then(() => { 618 return undefined
610 return res() 619 })
611 }) 620 .catch(err => {
612 .catch(err => { 621 // Auto destruction...
613 // Auto destruction... 622 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
614 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) 623
615 624 throw err
616 return rej(err) 625 })
617 })
618 })
619 .run()
620 })
621} 626}
622 627
623transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { 628transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
@@ -634,52 +639,37 @@ transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoRes
634 videoId: this.id 639 videoId: this.id
635 }) 640 })
636 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) 641 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
637 const resolutionOption = `${resolution}x?` // '720x?' for example 642
638 643 const transcodeOptions = {
639 return new Promise<void>((res, rej) => { 644 inputPath: videoInputPath,
640 ffmpeg(videoInputPath) 645 outputPath: videoOutputPath,
641 .output(videoOutputPath) 646 resolution
642 .videoCodec('libx264') 647 }
643 .size(resolutionOption) 648 return transcode(transcodeOptions)
644 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 649 .then(() => {
645 .outputOption('-movflags faststart') 650 return statPromise(videoOutputPath)
646 .on('error', rej) 651 })
647 .on('end', () => { 652 .then(stats => {
648 return statPromise(videoOutputPath) 653 newVideoFile.set('size', stats.size)
649 .then(stats => { 654
650 newVideoFile.set('size', stats.size) 655 return undefined
651 656 })
652 return undefined 657 .then(() => {
653 }) 658 return this.createTorrentAndSetInfoHash(newVideoFile)
654 .then(() => { 659 })
655 return this.createTorrentAndSetInfoHash(newVideoFile) 660 .then(() => {
656 }) 661 return newVideoFile.save()
657 .then(() => { 662 })
658 return newVideoFile.save() 663 .then(() => {
659 }) 664 return this.VideoFiles.push(newVideoFile)
660 .then(() => { 665 })
661 return this.VideoFiles.push(newVideoFile) 666 .then(() => undefined)
662 })
663 .then(() => {
664 return res()
665 })
666 .catch(rej)
667 })
668 .run()
669 })
670} 667}
671 668
672getOriginalFileHeight = function (this: VideoInstance) { 669getOriginalFileHeight = function (this: VideoInstance) {
673 const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) 670 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
674 671
675 return new Promise<number>((res, rej) => { 672 return getVideoFileHeight(originalFilePath)
676 ffmpeg.ffprobe(originalFilePath, (err, metadata) => {
677 if (err) return rej(err)
678
679 const videoStream = metadata.streams.find(s => s.codec_type === 'video')
680 return res(videoStream.height)
681 })
682 })
683} 673}
684 674
685removeThumbnail = function (this: VideoInstance) { 675removeThumbnail = function (this: VideoInstance) {
@@ -714,16 +704,6 @@ generateThumbnailFromData = function (video: VideoInstance, thumbnailData: strin
714 }) 704 })
715} 705}
716 706
717getDurationFromFile = function (videoPath: string) {
718 return new Promise<number>((res, rej) => {
719 ffmpeg.ffprobe(videoPath, (err, metadata) => {
720 if (err) return rej(err)
721
722 return res(Math.floor(metadata.format.duration))
723 })
724 })
725}
726
727list = function () { 707list = function () {
728 const query = { 708 const query = {
729 include: [ Video['sequelize'].models.VideoFile ] 709 include: [ Video['sequelize'].models.VideoFile ]
@@ -964,22 +944,3 @@ function createBaseVideosWhere () {
964 } 944 }
965 } 945 }
966} 946}
967
968function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
969 const options = {
970 filename: imageName,
971 count: 1,
972 folder
973 }
974
975 if (size) {
976 options['size'] = size
977 }
978
979 return new Promise<string>((res, rej) => {
980 ffmpeg(videoPath)
981 .on('error', rej)
982 .on('end', () => res(imageName))
983 .thumbnail(options)
984 })
985}
diff --git a/server/tests/api/multiple-pods.ts b/server/tests/api/multiple-pods.ts
index 08fa73aa2..8b60ac0f4 100644
--- a/server/tests/api/multiple-pods.ts
+++ b/server/tests/api/multiple-pods.ts
@@ -106,8 +106,8 @@ describe('Test multiple pods', function () {
106 const file = video.files[0] 106 const file = video.files[0]
107 const magnetUri = file.magnetUri 107 const magnetUri = file.magnetUri
108 expect(file.magnetUri).to.have.lengthOf.above(2) 108 expect(file.magnetUri).to.have.lengthOf.above(2)
109 expect(file.resolution).to.equal(0) 109 expect(file.resolution).to.equal(720)
110 expect(file.resolutionLabel).to.equal('original') 110 expect(file.resolutionLabel).to.equal('720p')
111 expect(file.size).to.equal(572456) 111 expect(file.size).to.equal(572456)
112 112
113 if (server.url !== 'http://localhost:9001') { 113 if (server.url !== 'http://localhost:9001') {
@@ -172,7 +172,7 @@ describe('Test multiple pods', function () {
172 expect(dateIsValid(video.updatedAt)).to.be.true 172 expect(dateIsValid(video.updatedAt)).to.be.true
173 expect(video.author).to.equal('root') 173 expect(video.author).to.equal('root')
174 174
175 expect(video.files).to.have.lengthOf(5) 175 expect(video.files).to.have.lengthOf(4)
176 176
177 // Check common attributes 177 // Check common attributes
178 for (const file of video.files) { 178 for (const file of video.files) {
@@ -192,11 +192,6 @@ describe('Test multiple pods', function () {
192 } 192 }
193 } 193 }
194 194
195 const originalFile = video.files.find(f => f.resolution === 0)
196 expect(originalFile).not.to.be.undefined
197 expect(originalFile.resolutionLabel).to.equal('original')
198 expect(originalFile.size).to.be.above(700000).and.below(720000)
199
200 const file240p = video.files.find(f => f.resolution === 240) 195 const file240p = video.files.find(f => f.resolution === 240)
201 expect(file240p).not.to.be.undefined 196 expect(file240p).not.to.be.undefined
202 expect(file240p.resolutionLabel).to.equal('240p') 197 expect(file240p.resolutionLabel).to.equal('240p')
@@ -215,7 +210,7 @@ describe('Test multiple pods', function () {
215 const file720p = video.files.find(f => f.resolution === 720) 210 const file720p = video.files.find(f => f.resolution === 720)
216 expect(file720p).not.to.be.undefined 211 expect(file720p).not.to.be.undefined
217 expect(file720p.resolutionLabel).to.equal('720p') 212 expect(file720p.resolutionLabel).to.equal('720p')
218 expect(file720p.size).to.be.above(310000).and.below(320000) 213 expect(file720p.size).to.be.above(700000).and.below(7200000)
219 214
220 const test = await testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath) 215 const test = await testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath)
221 expect(test).to.equal(true) 216 expect(test).to.equal(true)
@@ -291,8 +286,8 @@ describe('Test multiple pods', function () {
291 286
292 const file1 = video1.files[0] 287 const file1 = video1.files[0]
293 expect(file1.magnetUri).to.have.lengthOf.above(2) 288 expect(file1.magnetUri).to.have.lengthOf.above(2)
294 expect(file1.resolution).to.equal(0) 289 expect(file1.resolution).to.equal(720)
295 expect(file1.resolutionLabel).to.equal('original') 290 expect(file1.resolutionLabel).to.equal('720p')
296 expect(file1.size).to.equal(292677) 291 expect(file1.size).to.equal(292677)
297 292
298 expect(video2.name).to.equal('my super name for pod 3-2') 293 expect(video2.name).to.equal('my super name for pod 3-2')
@@ -316,8 +311,8 @@ describe('Test multiple pods', function () {
316 const file2 = video2.files[0] 311 const file2 = video2.files[0]
317 const magnetUri2 = file2.magnetUri 312 const magnetUri2 = file2.magnetUri
318 expect(file2.magnetUri).to.have.lengthOf.above(2) 313 expect(file2.magnetUri).to.have.lengthOf.above(2)
319 expect(file2.resolution).to.equal(0) 314 expect(file2.resolution).to.equal(720)
320 expect(file2.resolutionLabel).to.equal('original') 315 expect(file2.resolutionLabel).to.equal('720p')
321 expect(file2.size).to.equal(218910) 316 expect(file2.size).to.equal(218910)
322 317
323 if (server.url !== 'http://localhost:9003') { 318 if (server.url !== 'http://localhost:9003') {
@@ -402,6 +397,22 @@ describe('Test multiple pods', function () {
402 expect(torrent.files.length).to.equal(1) 397 expect(torrent.files.length).to.equal(1)
403 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 398 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
404 }) 399 })
400
401 it('Should add the file 2 in 360p by asking pod 1', async function () {
402 // Yes, this could be long
403 this.timeout(200000)
404
405 const res = await getVideosList(servers[0].url)
406
407 const video = res.body.data.find(v => v.name === 'my super name for pod 2')
408 const file = video.files.find(f => f.resolution === 360)
409 expect(file).not.to.be.undefined
410
411 const torrent = await webtorrentAdd(file.magnetUri)
412 expect(torrent.files).to.be.an('array')
413 expect(torrent.files.length).to.equal(1)
414 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
415 })
405 }) 416 })
406 417
407 describe('Should update video views, likes and dislikes', function () { 418 describe('Should update video views, likes and dislikes', function () {
@@ -562,8 +573,8 @@ describe('Test multiple pods', function () {
562 573
563 const file = videoUpdated.files[0] 574 const file = videoUpdated.files[0]
564 expect(file.magnetUri).to.have.lengthOf.above(2) 575 expect(file.magnetUri).to.have.lengthOf.above(2)
565 expect(file.resolution).to.equal(0) 576 expect(file.resolution).to.equal(720)
566 expect(file.resolutionLabel).to.equal('original') 577 expect(file.resolutionLabel).to.equal('720p')
567 expect(file.size).to.equal(292677) 578 expect(file.size).to.equal(292677)
568 579
569 const test = await testVideoImage(server.url, 'video_short3.webm', videoUpdated.thumbnailPath) 580 const test = await testVideoImage(server.url, 'video_short3.webm', videoUpdated.thumbnailPath)
diff --git a/server/tests/api/single-pod.ts b/server/tests/api/single-pod.ts
index 83c981f9b..82bc51a3e 100644
--- a/server/tests/api/single-pod.ts
+++ b/server/tests/api/single-pod.ts
@@ -127,8 +127,8 @@ describe('Test a single pod', function () {
127 const file = video.files[0] 127 const file = video.files[0]
128 const magnetUri = file.magnetUri 128 const magnetUri = file.magnetUri
129 expect(file.magnetUri).to.have.lengthOf.above(2) 129 expect(file.magnetUri).to.have.lengthOf.above(2)
130 expect(file.resolution).to.equal(0) 130 expect(file.resolution).to.equal(720)
131 expect(file.resolutionLabel).to.equal('original') 131 expect(file.resolutionLabel).to.equal('720p')
132 expect(file.size).to.equal(218910) 132 expect(file.size).to.equal(218910)
133 133
134 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) 134 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath)
@@ -170,8 +170,8 @@ describe('Test a single pod', function () {
170 170
171 const file = video.files[0] 171 const file = video.files[0]
172 expect(file.magnetUri).to.have.lengthOf.above(2) 172 expect(file.magnetUri).to.have.lengthOf.above(2)
173 expect(file.resolution).to.equal(0) 173 expect(file.resolution).to.equal(720)
174 expect(file.resolutionLabel).to.equal('original') 174 expect(file.resolutionLabel).to.equal('720p')
175 expect(file.size).to.equal(218910) 175 expect(file.size).to.equal(218910)
176 176
177 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) 177 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath)
@@ -229,8 +229,8 @@ describe('Test a single pod', function () {
229 229
230 const file = video.files[0] 230 const file = video.files[0]
231 expect(file.magnetUri).to.have.lengthOf.above(2) 231 expect(file.magnetUri).to.have.lengthOf.above(2)
232 expect(file.resolution).to.equal(0) 232 expect(file.resolution).to.equal(720)
233 expect(file.resolutionLabel).to.equal('original') 233 expect(file.resolutionLabel).to.equal('720p')
234 expect(file.size).to.equal(218910) 234 expect(file.size).to.equal(218910)
235 235
236 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) 236 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath)
@@ -291,8 +291,8 @@ describe('Test a single pod', function () {
291 291
292 const file = video.files[0] 292 const file = video.files[0]
293 expect(file.magnetUri).to.have.lengthOf.above(2) 293 expect(file.magnetUri).to.have.lengthOf.above(2)
294 expect(file.resolution).to.equal(0) 294 expect(file.resolution).to.equal(720)
295 expect(file.resolutionLabel).to.equal('original') 295 expect(file.resolutionLabel).to.equal('720p')
296 expect(file.size).to.equal(218910) 296 expect(file.size).to.equal(218910)
297 297
298 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) 298 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath)
@@ -569,8 +569,8 @@ describe('Test a single pod', function () {
569 const file = video.files[0] 569 const file = video.files[0]
570 const magnetUri = file.magnetUri 570 const magnetUri = file.magnetUri
571 expect(file.magnetUri).to.have.lengthOf.above(2) 571 expect(file.magnetUri).to.have.lengthOf.above(2)
572 expect(file.resolution).to.equal(0) 572 expect(file.resolution).to.equal(720)
573 expect(file.resolutionLabel).to.equal('original') 573 expect(file.resolutionLabel).to.equal('720p')
574 expect(file.size).to.equal(292677) 574 expect(file.size).to.equal(292677)
575 575
576 const test = await testVideoImage(server.url, 'video_short3.webm', video.thumbnailPath) 576 const test = await testVideoImage(server.url, 'video_short3.webm', video.thumbnailPath)
@@ -612,8 +612,8 @@ describe('Test a single pod', function () {
612 612
613 const file = video.files[0] 613 const file = video.files[0]
614 expect(file.magnetUri).to.have.lengthOf.above(2) 614 expect(file.magnetUri).to.have.lengthOf.above(2)
615 expect(file.resolution).to.equal(0) 615 expect(file.resolution).to.equal(720)
616 expect(file.resolutionLabel).to.equal('original') 616 expect(file.resolutionLabel).to.equal('720p')
617 expect(file.size).to.equal(292677) 617 expect(file.size).to.equal(292677)
618 }) 618 })
619 619
@@ -647,8 +647,8 @@ describe('Test a single pod', function () {
647 647
648 const file = video.files[0] 648 const file = video.files[0]
649 expect(file.magnetUri).to.have.lengthOf.above(2) 649 expect(file.magnetUri).to.have.lengthOf.above(2)
650 expect(file.resolution).to.equal(0) 650 expect(file.resolution).to.equal(720)
651 expect(file.resolutionLabel).to.equal('original') 651 expect(file.resolutionLabel).to.equal('720p')
652 expect(file.size).to.equal(292677) 652 expect(file.size).to.equal(292677)
653 }) 653 })
654 654
diff --git a/server/tests/api/video-transcoder.ts b/server/tests/api/video-transcoder.ts
index b5d84d9e7..22d89724b 100644
--- a/server/tests/api/video-transcoder.ts
+++ b/server/tests/api/video-transcoder.ts
@@ -68,7 +68,7 @@ describe('Test video transcoding', function () {
68 const res = await getVideosList(servers[1].url) 68 const res = await getVideosList(servers[1].url)
69 69
70 const video = res.body.data[0] 70 const video = res.body.data[0]
71 expect(video.files).to.have.lengthOf(5) 71 expect(video.files).to.have.lengthOf(4)
72 72
73 const magnetUri = video.files[0].magnetUri 73 const magnetUri = video.files[0].magnetUri
74 expect(magnetUri).to.match(/\.mp4/) 74 expect(magnetUri).to.match(/\.mp4/)
diff --git a/server/tests/cli/update-host.ts b/server/tests/cli/update-host.ts
index e31a84156..7e1d3f658 100644
--- a/server/tests/cli/update-host.ts
+++ b/server/tests/cli/update-host.ts
@@ -55,13 +55,13 @@ describe('Test update host scripts', function () {
55 expect(videos).to.have.lengthOf(2) 55 expect(videos).to.have.lengthOf(2)
56 56
57 for (const video of videos) { 57 for (const video of videos) {
58 expect(video.files).to.have.lengthOf(5) 58 expect(video.files).to.have.lengthOf(4)
59 59
60 for (const file of video.files) { 60 for (const file of video.files) {
61 expect(file.magnetUri).to.contain('localhost%3A9002%2Ftracker%2Fsocket') 61 expect(file.magnetUri).to.contain('localhost%3A9002%2Ftracker%2Fsocket')
62 expect(file.magnetUri).to.contain('localhost%3A9002%2Fstatic%2Fwebseed%2F') 62 expect(file.magnetUri).to.contain('localhost%3A9002%2Fstatic%2Fwebseed%2F')
63 63
64 const torrent = await parseTorrentVideo(server, video.uuid, file.resolutionLabel) 64 const torrent = await parseTorrentVideo(server, video.uuid, file.resolution)
65 expect(torrent.announce[0]).to.equal('ws://localhost:9002/tracker/socket') 65 expect(torrent.announce[0]).to.equal('ws://localhost:9002/tracker/socket')
66 expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/webseed') 66 expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/webseed')
67 } 67 }
diff --git a/server/tests/utils/videos.ts b/server/tests/utils/videos.ts
index 7f8bd39c0..2a9a236ca 100644
--- a/server/tests/utils/videos.ts
+++ b/server/tests/utils/videos.ts
@@ -196,14 +196,14 @@ function uploadVideo (url: string, accessToken: string, videoAttributesArg: Vide
196 req.field('tags[' + i + ']', attributes.tags[i]) 196 req.field('tags[' + i + ']', attributes.tags[i])
197 } 197 }
198 198
199 let filepath = '' 199 let filePath = ''
200 if (isAbsolute(attributes.fixture)) { 200 if (isAbsolute(attributes.fixture)) {
201 filepath = attributes.fixture 201 filePath = attributes.fixture
202 } else { 202 } else {
203 filepath = join(__dirname, '..', 'api', 'fixtures', attributes.fixture) 203 filePath = join(__dirname, '..', 'api', 'fixtures', attributes.fixture)
204 } 204 }
205 205
206 return req.attach('videofile', filepath) 206 return req.attach('videofile', filePath)
207 .expect(specialStatus) 207 .expect(specialStatus)
208} 208}
209 209
@@ -238,9 +238,9 @@ function rateVideo (url: string, accessToken: string, id: number, rating: string
238 .expect(specialStatus) 238 .expect(specialStatus)
239} 239}
240 240
241function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolutionLabel: string) { 241function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
242 return new Promise<any>((res, rej) => { 242 return new Promise<any>((res, rej) => {
243 const torrentName = videoUUID + '-' + resolutionLabel + '.torrent' 243 const torrentName = videoUUID + '-' + resolution + '.torrent'
244 const torrentPath = join(__dirname, '..', '..', '..', 'test' + server.serverNumber, 'torrents', torrentName) 244 const torrentPath = join(__dirname, '..', '..', '..', 'test' + server.serverNumber, 'torrents', torrentName)
245 readFile(torrentPath, (err, data) => { 245 readFile(torrentPath, (err, data) => {
246 if (err) return rej(err) 246 if (err) return rej(err)