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