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