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