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