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