]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video.js
Add link to wiki for production installation
[github/Chocobozzz/PeerTube.git] / server / models / video.js
CommitLineData
aaf61f38
C
1'use strict'
2
4d324488 3const Buffer = require('safe-buffer').Buffer
052937db 4const createTorrent = require('create-torrent')
aaf61f38
C
5const ffmpeg = require('fluent-ffmpeg')
6const fs = require('fs')
f285faa0 7const magnetUtil = require('magnet-uri')
7920c273 8const map = require('lodash/map')
1a42c9e2 9const parallel = require('async/parallel')
052937db 10const parseTorrent = require('parse-torrent')
aaf61f38 11const pathUtils = require('path')
67bf9b96 12const values = require('lodash/values')
aaf61f38
C
13
14const constants = require('../initializers/constants')
aaf61f38 15const logger = require('../helpers/logger')
98ac898a 16const friends = require('../lib/friends')
0ff21c1c 17const modelUtils = require('./utils')
67bf9b96 18const customVideosValidators = require('../helpers/custom-validators').videos
aaf61f38 19
aaf61f38
C
20// ---------------------------------------------------------------------------
21
feb4bdfd 22module.exports = function (sequelize, DataTypes) {
feb4bdfd
C
23 const Video = sequelize.define('Video',
24 {
25 id: {
26 type: DataTypes.UUID,
27 defaultValue: DataTypes.UUIDV4,
67bf9b96
C
28 primaryKey: true,
29 validate: {
30 isUUID: 4
31 }
aaf61f38 32 },
feb4bdfd 33 name: {
67bf9b96
C
34 type: DataTypes.STRING,
35 allowNull: false,
36 validate: {
37 nameValid: function (value) {
38 const res = customVideosValidators.isVideoNameValid(value)
39 if (res === false) throw new Error('Video name is not valid.')
40 }
41 }
6a94a109 42 },
feb4bdfd 43 extname: {
67bf9b96
C
44 type: DataTypes.ENUM(values(constants.CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
45 allowNull: false
feb4bdfd
C
46 },
47 remoteId: {
67bf9b96
C
48 type: DataTypes.UUID,
49 allowNull: true,
50 validate: {
51 isUUID: 4
52 }
feb4bdfd 53 },
6e07c3de
C
54 category: {
55 type: DataTypes.INTEGER,
56 allowNull: false,
57 validate: {
58 categoryValid: function (value) {
59 const res = customVideosValidators.isVideoCategoryValid(value)
60 if (res === false) throw new Error('Video category is not valid.')
61 }
62 }
63 },
6f0c39e2
C
64 licence: {
65 type: DataTypes.INTEGER,
66 allowNull: false,
67 validate: {
68 licenceValid: function (value) {
69 const res = customVideosValidators.isVideoLicenceValid(value)
70 if (res === false) throw new Error('Video licence is not valid.')
71 }
72 }
73 },
31b59b47
C
74 nsfw: {
75 type: DataTypes.BOOLEAN,
76 allowNull: false,
77 validate: {
78 nsfwValid: function (value) {
79 const res = customVideosValidators.isVideoNSFWValid(value)
80 if (res === false) throw new Error('Video nsfw attribute is not valid.')
81 }
82 }
83 },
feb4bdfd 84 description: {
67bf9b96
C
85 type: DataTypes.STRING,
86 allowNull: false,
87 validate: {
88 descriptionValid: function (value) {
89 const res = customVideosValidators.isVideoDescriptionValid(value)
90 if (res === false) throw new Error('Video description is not valid.')
91 }
92 }
feb4bdfd
C
93 },
94 infoHash: {
67bf9b96
C
95 type: DataTypes.STRING,
96 allowNull: false,
97 validate: {
98 infoHashValid: function (value) {
99 const res = customVideosValidators.isVideoInfoHashValid(value)
100 if (res === false) throw new Error('Video info hash is not valid.')
101 }
102 }
feb4bdfd
C
103 },
104 duration: {
67bf9b96
C
105 type: DataTypes.INTEGER,
106 allowNull: false,
107 validate: {
108 durationValid: function (value) {
109 const res = customVideosValidators.isVideoDurationValid(value)
110 if (res === false) throw new Error('Video duration is not valid.')
111 }
112 }
9e167724
C
113 },
114 views: {
115 type: DataTypes.INTEGER,
116 allowNull: false,
117 defaultValue: 0,
118 validate: {
119 min: 0,
120 isInt: true
121 }
d38b8281
C
122 },
123 likes: {
124 type: DataTypes.INTEGER,
125 allowNull: false,
126 defaultValue: 0,
127 validate: {
128 min: 0,
129 isInt: true
130 }
131 },
132 dislikes: {
133 type: DataTypes.INTEGER,
134 allowNull: false,
135 defaultValue: 0,
136 validate: {
137 min: 0,
138 isInt: true
139 }
aaf61f38 140 }
feb4bdfd
C
141 },
142 {
319d072e
C
143 indexes: [
144 {
145 fields: [ 'authorId' ]
146 },
147 {
148 fields: [ 'remoteId' ]
149 },
150 {
151 fields: [ 'name' ]
152 },
153 {
154 fields: [ 'createdAt' ]
155 },
156 {
157 fields: [ 'duration' ]
158 },
159 {
160 fields: [ 'infoHash' ]
9e167724
C
161 },
162 {
163 fields: [ 'views' ]
d38b8281
C
164 },
165 {
166 fields: [ 'likes' ]
319d072e
C
167 }
168 ],
feb4bdfd
C
169 classMethods: {
170 associate,
171
4d324488 172 generateThumbnailFromData,
feb4bdfd 173 getDurationFromFile,
b769007f 174 list,
feb4bdfd 175 listForApi,
7920c273 176 listOwnedAndPopulateAuthorAndTags,
feb4bdfd
C
177 listOwnedByAuthor,
178 load,
3d118fb5 179 loadByHostAndRemoteId,
feb4bdfd 180 loadAndPopulateAuthor,
7920c273
C
181 loadAndPopulateAuthorAndPodAndTags,
182 searchAndPopulateAuthorAndPodAndTags
feb4bdfd
C
183 },
184 instanceMethods: {
185 generateMagnetUri,
186 getVideoFilename,
187 getThumbnailName,
188 getPreviewName,
189 getTorrentName,
190 isOwned,
191 toFormatedJSON,
7b1f49de
C
192 toAddRemoteJSON,
193 toUpdateRemoteJSON
feb4bdfd
C
194 },
195 hooks: {
67bf9b96 196 beforeValidate,
feb4bdfd
C
197 beforeCreate,
198 afterDestroy
199 }
200 }
201 )
aaf61f38 202
feb4bdfd
C
203 return Video
204}
aaf61f38 205
67bf9b96 206function beforeValidate (video, options, next) {
7f4e7c36
C
207 // Put a fake infoHash if it does not exists yet
208 if (video.isOwned() && !video.infoHash) {
67bf9b96
C
209 // 40 hexa length
210 video.infoHash = '0123456789abcdef0123456789abcdef01234567'
211 }
212
213 return next(null)
214}
feb4bdfd
C
215
216function beforeCreate (video, options, next) {
aaf61f38
C
217 const tasks = []
218
219 if (video.isOwned()) {
f285faa0 220 const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
aaf61f38
C
221
222 tasks.push(
15103f11 223 function createVideoTorrent (callback) {
25cad919
C
224 const options = {
225 announceList: [
3737bbaf 226 [ constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
25cad919
C
227 ],
228 urlList: [
f285faa0 229 constants.CONFIG.WEBSERVER.URL + constants.STATIC_PATHS.WEBSEED + video.getVideoFilename()
25cad919
C
230 ]
231 }
232
233 createTorrent(videoPath, options, function (err, torrent) {
052937db
C
234 if (err) return callback(err)
235
15103f11
C
236 const filePath = pathUtils.join(constants.CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
237 fs.writeFile(filePath, torrent, function (err) {
052937db
C
238 if (err) return callback(err)
239
240 const parsedTorrent = parseTorrent(torrent)
67bf9b96
C
241 video.set('infoHash', parsedTorrent.infoHash)
242 video.validate().asCallback(callback)
052937db
C
243 })
244 })
aaf61f38 245 },
15103f11
C
246
247 function createVideoThumbnail (callback) {
558d7c23 248 createThumbnail(video, videoPath, callback)
6a94a109 249 },
15103f11
C
250
251 function createVIdeoPreview (callback) {
558d7c23 252 createPreview(video, videoPath, callback)
aaf61f38
C
253 }
254 )
255
c77fa067 256 return parallel(tasks, next)
aaf61f38 257 }
c77fa067
C
258
259 return next()
feb4bdfd 260}
aaf61f38 261
feb4bdfd
C
262function afterDestroy (video, options, next) {
263 const tasks = []
264
265 tasks.push(
266 function (callback) {
267 removeThumbnail(video, callback)
268 }
269 )
270
271 if (video.isOwned()) {
272 tasks.push(
15103f11 273 function removeVideoFile (callback) {
feb4bdfd
C
274 removeFile(video, callback)
275 },
98ac898a 276
15103f11 277 function removeVideoTorrent (callback) {
feb4bdfd
C
278 removeTorrent(video, callback)
279 },
98ac898a 280
15103f11 281 function removeVideoPreview (callback) {
feb4bdfd 282 removePreview(video, callback)
98ac898a
C
283 },
284
15103f11 285 function removeVideoToFriends (callback) {
98ac898a 286 const params = {
98ac898a
C
287 remoteId: video.id
288 }
289
290 friends.removeVideoToFriends(params)
291
292 return callback()
feb4bdfd
C
293 }
294 )
295 }
296
297 parallel(tasks, next)
298}
aaf61f38
C
299
300// ------------------------------ METHODS ------------------------------
301
feb4bdfd
C
302function associate (models) {
303 this.belongsTo(models.Author, {
304 foreignKey: {
305 name: 'authorId',
306 allowNull: false
307 },
308 onDelete: 'cascade'
309 })
7920c273
C
310
311 this.belongsToMany(models.Tag, {
312 foreignKey: 'videoId',
313 through: models.VideoTag,
314 onDelete: 'cascade'
315 })
55fa55a9
C
316
317 this.hasMany(models.VideoAbuse, {
318 foreignKey: {
319 name: 'videoId',
320 allowNull: false
321 },
322 onDelete: 'cascade'
323 })
feb4bdfd
C
324}
325
f285faa0
C
326function generateMagnetUri () {
327 let baseUrlHttp, baseUrlWs
328
329 if (this.isOwned()) {
330 baseUrlHttp = constants.CONFIG.WEBSERVER.URL
331 baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT
332 } else {
feb4bdfd
C
333 baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
334 baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
f285faa0
C
335 }
336
337 const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName()
338 const announce = baseUrlWs + '/tracker/socket'
339 const urlList = [ baseUrlHttp + constants.STATIC_PATHS.WEBSEED + this.getVideoFilename() ]
340
341 const magnetHash = {
342 xs,
343 announce,
344 urlList,
feb4bdfd 345 infoHash: this.infoHash,
f285faa0
C
346 name: this.name
347 }
348
349 return magnetUtil.encode(magnetHash)
558d7c23
C
350}
351
f285faa0 352function getVideoFilename () {
feb4bdfd 353 if (this.isOwned()) return this.id + this.extname
f285faa0
C
354
355 return this.remoteId + this.extname
356}
357
358function getThumbnailName () {
359 // We always have a copy of the thumbnail
feb4bdfd 360 return this.id + '.jpg'
558d7c23
C
361}
362
f285faa0
C
363function getPreviewName () {
364 const extension = '.jpg'
365
feb4bdfd 366 if (this.isOwned()) return this.id + extension
f285faa0
C
367
368 return this.remoteId + extension
369}
370
558d7c23 371function getTorrentName () {
f285faa0
C
372 const extension = '.torrent'
373
feb4bdfd 374 if (this.isOwned()) return this.id + extension
f285faa0
C
375
376 return this.remoteId + extension
558d7c23
C
377}
378
aaf61f38 379function isOwned () {
558d7c23 380 return this.remoteId === null
aaf61f38
C
381}
382
383function toFormatedJSON () {
feb4bdfd
C
384 let podHost
385
386 if (this.Author.Pod) {
387 podHost = this.Author.Pod.host
388 } else {
389 // It means it's our video
390 podHost = constants.CONFIG.WEBSERVER.HOST
391 }
392
6e07c3de
C
393 // Maybe our pod is not up to date and there are new categories since our version
394 let categoryLabel = constants.VIDEO_CATEGORIES[this.category]
395 if (!categoryLabel) categoryLabel = 'Misc'
396
6f0c39e2
C
397 // Maybe our pod is not up to date and there are new licences since our version
398 let licenceLabel = constants.VIDEO_LICENCES[this.licence]
399 if (!licenceLabel) licenceLabel = 'Unknown'
400
aaf61f38 401 const json = {
feb4bdfd 402 id: this.id,
aaf61f38 403 name: this.name,
6e07c3de
C
404 category: this.category,
405 categoryLabel,
6f0c39e2
C
406 licence: this.licence,
407 licenceLabel,
31b59b47 408 nsfw: this.nsfw,
aaf61f38 409 description: this.description,
feb4bdfd 410 podHost,
aaf61f38 411 isLocal: this.isOwned(),
f285faa0 412 magnetUri: this.generateMagnetUri(),
feb4bdfd 413 author: this.Author.name,
aaf61f38 414 duration: this.duration,
9e167724 415 views: this.views,
d38b8281
C
416 likes: this.likes,
417 dislikes: this.dislikes,
7920c273 418 tags: map(this.Tags, 'name'),
91cc839a 419 thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
79066fdf
C
420 createdAt: this.createdAt,
421 updatedAt: this.updatedAt
aaf61f38
C
422 }
423
424 return json
425}
426
7b1f49de 427function toAddRemoteJSON (callback) {
aaf61f38
C
428 const self = this
429
4d324488 430 // Get thumbnail data to send to the other pod
f285faa0 431 const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
558d7c23 432 fs.readFile(thumbnailPath, function (err, thumbnailData) {
aaf61f38
C
433 if (err) {
434 logger.error('Cannot read the thumbnail of the video')
435 return callback(err)
436 }
437
438 const remoteVideo = {
439 name: self.name,
6e07c3de 440 category: self.category,
6f0c39e2 441 licence: self.licence,
31b59b47 442 nsfw: self.nsfw,
aaf61f38 443 description: self.description,
feb4bdfd
C
444 infoHash: self.infoHash,
445 remoteId: self.id,
446 author: self.Author.name,
aaf61f38 447 duration: self.duration,
4d324488 448 thumbnailData: thumbnailData.toString('binary'),
7920c273 449 tags: map(self.Tags, 'name'),
feb4bdfd 450 createdAt: self.createdAt,
79066fdf 451 updatedAt: self.updatedAt,
e3d156b3 452 extname: self.extname,
d38b8281
C
453 views: self.views,
454 likes: self.likes,
455 dislikes: self.dislikes
aaf61f38
C
456 }
457
458 return callback(null, remoteVideo)
459 })
460}
461
7b1f49de
C
462function toUpdateRemoteJSON (callback) {
463 const json = {
464 name: this.name,
6e07c3de 465 category: this.category,
6f0c39e2 466 licence: this.licence,
31b59b47 467 nsfw: this.nsfw,
7b1f49de
C
468 description: this.description,
469 infoHash: this.infoHash,
470 remoteId: this.id,
471 author: this.Author.name,
472 duration: this.duration,
473 tags: map(this.Tags, 'name'),
474 createdAt: this.createdAt,
79066fdf 475 updatedAt: this.updatedAt,
e3d156b3 476 extname: this.extname,
d38b8281
C
477 views: this.views,
478 likes: this.likes,
479 dislikes: this.dislikes
7b1f49de
C
480 }
481
482 return json
483}
484
aaf61f38
C
485// ------------------------------ STATICS ------------------------------
486
4d324488 487function generateThumbnailFromData (video, thumbnailData, callback) {
c77fa067
C
488 // Creating the thumbnail for a remote video
489
490 const thumbnailName = video.getThumbnailName()
15103f11 491 const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
4d324488 492 fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) {
c77fa067
C
493 if (err) return callback(err)
494
495 return callback(null, thumbnailName)
496 })
497}
498
aaf61f38
C
499function getDurationFromFile (videoPath, callback) {
500 ffmpeg.ffprobe(videoPath, function (err, metadata) {
501 if (err) return callback(err)
502
503 return callback(null, Math.floor(metadata.format.duration))
504 })
505}
506
b769007f 507function list (callback) {
9cc99d7b 508 return this.findAll().asCallback(callback)
b769007f
C
509}
510
0ff21c1c 511function listForApi (start, count, sort, callback) {
feb4bdfd
C
512 const query = {
513 offset: start,
514 limit: count,
7920c273 515 distinct: true, // For the count, a video can have many tags
178edb20 516 order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ],
feb4bdfd
C
517 include: [
518 {
519 model: this.sequelize.models.Author,
7920c273
C
520 include: [ { model: this.sequelize.models.Pod, required: false } ]
521 },
522
523 this.sequelize.models.Tag
feb4bdfd
C
524 ]
525 }
526
527 return this.findAndCountAll(query).asCallback(function (err, result) {
528 if (err) return callback(err)
529
530 return callback(null, result.rows, result.count)
531 })
aaf61f38
C
532}
533
3d118fb5 534function loadByHostAndRemoteId (fromHost, remoteId, callback) {
feb4bdfd
C
535 const query = {
536 where: {
537 remoteId: remoteId
538 },
539 include: [
540 {
541 model: this.sequelize.models.Author,
542 include: [
543 {
544 model: this.sequelize.models.Pod,
7920c273 545 required: true,
feb4bdfd
C
546 where: {
547 host: fromHost
548 }
549 }
550 ]
551 }
552 ]
553 }
aaf61f38 554
3d118fb5 555 return this.findOne(query).asCallback(callback)
aaf61f38
C
556}
557
7920c273 558function listOwnedAndPopulateAuthorAndTags (callback) {
558d7c23 559 // If remoteId is null this is *our* video
feb4bdfd
C
560 const query = {
561 where: {
562 remoteId: null
563 },
7920c273 564 include: [ this.sequelize.models.Author, this.sequelize.models.Tag ]
feb4bdfd
C
565 }
566
567 return this.findAll(query).asCallback(callback)
aaf61f38
C
568}
569
9bd26629 570function listOwnedByAuthor (author, callback) {
feb4bdfd
C
571 const query = {
572 where: {
573 remoteId: null
574 },
575 include: [
576 {
577 model: this.sequelize.models.Author,
578 where: {
579 name: author
580 }
581 }
582 ]
583 }
9bd26629 584
feb4bdfd 585 return this.findAll(query).asCallback(callback)
aaf61f38
C
586}
587
588function load (id, callback) {
feb4bdfd
C
589 return this.findById(id).asCallback(callback)
590}
591
592function loadAndPopulateAuthor (id, callback) {
593 const options = {
594 include: [ this.sequelize.models.Author ]
595 }
596
597 return this.findById(id, options).asCallback(callback)
598}
599
7920c273 600function loadAndPopulateAuthorAndPodAndTags (id, callback) {
feb4bdfd
C
601 const options = {
602 include: [
603 {
604 model: this.sequelize.models.Author,
7920c273
C
605 include: [ { model: this.sequelize.models.Pod, required: false } ]
606 },
607 this.sequelize.models.Tag
feb4bdfd
C
608 ]
609 }
610
611 return this.findById(id, options).asCallback(callback)
aaf61f38
C
612}
613
7920c273 614function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, callback) {
feb4bdfd 615 const podInclude = {
7920c273
C
616 model: this.sequelize.models.Pod,
617 required: false
feb4bdfd 618 }
7920c273 619
feb4bdfd
C
620 const authorInclude = {
621 model: this.sequelize.models.Author,
622 include: [
623 podInclude
624 ]
625 }
626
7920c273
C
627 const tagInclude = {
628 model: this.sequelize.models.Tag
629 }
630
feb4bdfd
C
631 const query = {
632 where: {},
feb4bdfd
C
633 offset: start,
634 limit: count,
7920c273 635 distinct: true, // For the count, a video can have many tags
178edb20 636 order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ]
feb4bdfd
C
637 }
638
aaf61f38 639 // Make an exact search with the magnet
55723d16
C
640 if (field === 'magnetUri') {
641 const infoHash = magnetUtil.decode(value).infoHash
feb4bdfd 642 query.where.infoHash = infoHash
55723d16 643 } else if (field === 'tags') {
7920c273
C
644 const escapedValue = this.sequelize.escape('%' + value + '%')
645 query.where = {
646 id: {
647 $in: this.sequelize.literal(
648 '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
649 )
feb4bdfd
C
650 }
651 }
7920c273
C
652 } else if (field === 'host') {
653 // FIXME: Include our pod? (not stored in the database)
654 podInclude.where = {
655 host: {
656 $like: '%' + value + '%'
feb4bdfd 657 }
feb4bdfd 658 }
7920c273 659 podInclude.required = true
feb4bdfd 660 } else if (field === 'author') {
7920c273
C
661 authorInclude.where = {
662 name: {
feb4bdfd
C
663 $like: '%' + value + '%'
664 }
665 }
7920c273
C
666
667 // authorInclude.or = true
aaf61f38 668 } else {
feb4bdfd
C
669 query.where[field] = {
670 $like: '%' + value + '%'
671 }
aaf61f38
C
672 }
673
7920c273
C
674 query.include = [
675 authorInclude, tagInclude
676 ]
677
678 if (tagInclude.where) {
679 // query.include.push([ this.sequelize.models.Tag ])
680 }
681
feb4bdfd
C
682 return this.findAndCountAll(query).asCallback(function (err, result) {
683 if (err) return callback(err)
684
685 return callback(null, result.rows, result.count)
686 })
aaf61f38
C
687}
688
aaf61f38
C
689// ---------------------------------------------------------------------------
690
aaf61f38 691function removeThumbnail (video, callback) {
15103f11
C
692 const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
693 fs.unlink(thumbnailPath, callback)
aaf61f38
C
694}
695
696function removeFile (video, callback) {
15103f11
C
697 const filePath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
698 fs.unlink(filePath, callback)
aaf61f38
C
699}
700
aaf61f38 701function removeTorrent (video, callback) {
15103f11
C
702 const torrenPath = pathUtils.join(constants.CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
703 fs.unlink(torrenPath, callback)
aaf61f38
C
704}
705
6a94a109
C
706function removePreview (video, callback) {
707 // Same name than video thumnail
f285faa0 708 fs.unlink(constants.CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback)
6a94a109
C
709}
710
558d7c23 711function createPreview (video, videoPath, callback) {
f285faa0 712 generateImage(video, videoPath, constants.CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), callback)
6a94a109
C
713}
714
558d7c23 715function createThumbnail (video, videoPath, callback) {
f285faa0 716 generateImage(video, videoPath, constants.CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), constants.THUMBNAILS_SIZE, callback)
aaf61f38
C
717}
718
f285faa0 719function generateImage (video, videoPath, folder, imageName, size, callback) {
558d7c23 720 const options = {
f285faa0 721 filename: imageName,
558d7c23
C
722 count: 1,
723 folder
724 }
aaf61f38 725
558d7c23
C
726 if (!callback) {
727 callback = size
728 } else {
729 options.size = size
730 }
731
732 ffmpeg(videoPath)
733 .on('error', callback)
734 .on('end', function () {
f285faa0 735 callback(null, imageName)
aaf61f38 736 })
558d7c23 737 .thumbnail(options)
aaf61f38 738}