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