]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video.ts
Handle blacklist (#84)
[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, options: { transaction: Sequelize.Transaction }) {
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, options.transaction)
318 )
319
320 // Remove physical files and torrents
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 logger.info('Creating torrent %s.', filePath)
382
383 return writeFilePromise(filePath, torrent).then(() => torrent)
384 })
385 .then(torrent => {
386 const parsedTorrent = parseTorrent(torrent)
387
388 videoFile.infoHash = parsedTorrent.infoHash
389 })
390 }
391
392 generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) {
393 let baseUrlHttp
394 let baseUrlWs
395
396 if (this.isOwned()) {
397 baseUrlHttp = CONFIG.WEBSERVER.URL
398 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
399 } else {
400 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
401 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
402 }
403
404 const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
405 const announce = [ baseUrlWs + '/tracker/socket' ]
406 const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
407
408 const magnetHash = {
409 xs,
410 announce,
411 urlList,
412 infoHash: videoFile.infoHash,
413 name: this.name
414 }
415
416 return magnetUtil.encode(magnetHash)
417 }
418
419 toFormattedJSON = function (this: VideoInstance) {
420 let podHost
421
422 if (this.Author.Pod) {
423 podHost = this.Author.Pod.host
424 } else {
425 // It means it's our video
426 podHost = CONFIG.WEBSERVER.HOST
427 }
428
429 // Maybe our pod is not up to date and there are new categories since our version
430 let categoryLabel = VIDEO_CATEGORIES[this.category]
431 if (!categoryLabel) categoryLabel = 'Misc'
432
433 // Maybe our pod is not up to date and there are new licences since our version
434 let licenceLabel = VIDEO_LICENCES[this.licence]
435 if (!licenceLabel) licenceLabel = 'Unknown'
436
437 // Language is an optional attribute
438 let languageLabel = VIDEO_LANGUAGES[this.language]
439 if (!languageLabel) languageLabel = 'Unknown'
440
441 const json = {
442 id: this.id,
443 uuid: this.uuid,
444 name: this.name,
445 category: this.category,
446 categoryLabel,
447 licence: this.licence,
448 licenceLabel,
449 language: this.language,
450 languageLabel,
451 nsfw: this.nsfw,
452 description: this.description,
453 podHost,
454 isLocal: this.isOwned(),
455 author: this.Author.name,
456 duration: this.duration,
457 views: this.views,
458 likes: this.likes,
459 dislikes: this.dislikes,
460 tags: map<TagInstance, string>(this.Tags, 'name'),
461 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
462 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
463 createdAt: this.createdAt,
464 updatedAt: this.updatedAt,
465 files: []
466 }
467
468 this.VideoFiles.forEach(videoFile => {
469 let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
470 if (!resolutionLabel) resolutionLabel = 'Unknown'
471
472 const videoFileJson = {
473 resolution: videoFile.resolution,
474 resolutionLabel,
475 magnetUri: this.generateMagnetUri(videoFile),
476 size: videoFile.size
477 }
478
479 json.files.push(videoFileJson)
480 })
481
482 return json
483 }
484
485 toAddRemoteJSON = function (this: VideoInstance) {
486 // Get thumbnail data to send to the other pod
487 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
488
489 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
490 const remoteVideo = {
491 uuid: this.uuid,
492 name: this.name,
493 category: this.category,
494 licence: this.licence,
495 language: this.language,
496 nsfw: this.nsfw,
497 description: this.description,
498 author: this.Author.name,
499 duration: this.duration,
500 thumbnailData: thumbnailData.toString('binary'),
501 tags: map<TagInstance, string>(this.Tags, 'name'),
502 createdAt: this.createdAt,
503 updatedAt: this.updatedAt,
504 views: this.views,
505 likes: this.likes,
506 dislikes: this.dislikes,
507 files: []
508 }
509
510 this.VideoFiles.forEach(videoFile => {
511 remoteVideo.files.push({
512 infoHash: videoFile.infoHash,
513 resolution: videoFile.resolution,
514 extname: videoFile.extname,
515 size: videoFile.size
516 })
517 })
518
519 return remoteVideo
520 })
521 }
522
523 toUpdateRemoteJSON = function (this: VideoInstance) {
524 const json = {
525 uuid: this.uuid,
526 name: this.name,
527 category: this.category,
528 licence: this.licence,
529 language: this.language,
530 nsfw: this.nsfw,
531 description: this.description,
532 author: this.Author.name,
533 duration: this.duration,
534 tags: map<TagInstance, string>(this.Tags, 'name'),
535 createdAt: this.createdAt,
536 updatedAt: this.updatedAt,
537 views: this.views,
538 likes: this.likes,
539 dislikes: this.dislikes,
540 files: []
541 }
542
543 this.VideoFiles.forEach(videoFile => {
544 json.files.push({
545 infoHash: videoFile.infoHash,
546 resolution: videoFile.resolution,
547 extname: videoFile.extname,
548 size: videoFile.size
549 })
550 })
551
552 return json
553 }
554
555 transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
556 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
557 const newExtname = '.mp4'
558 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
559 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
560
561 return new Promise<void>((res, rej) => {
562 ffmpeg(videoInputPath)
563 .output(videoOutputPath)
564 .videoCodec('libx264')
565 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
566 .outputOption('-movflags faststart')
567 .on('error', rej)
568 .on('end', () => {
569
570 return unlinkPromise(videoInputPath)
571 .then(() => {
572 // Important to do this before getVideoFilename() to take in account the new file extension
573 inputVideoFile.set('extname', newExtname)
574
575 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
576 })
577 .then(() => {
578 return this.createTorrentAndSetInfoHash(inputVideoFile)
579 })
580 .then(() => {
581 return inputVideoFile.save()
582 })
583 .then(() => {
584 return res()
585 })
586 .catch(err => {
587 // Auto destruction...
588 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
589
590 return rej(err)
591 })
592 })
593 .run()
594 })
595 }
596
597 removeThumbnail = function (this: VideoInstance) {
598 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
599 return unlinkPromise(thumbnailPath)
600 }
601
602 removePreview = function (this: VideoInstance) {
603 // Same name than video thumbnail
604 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
605 }
606
607 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
608 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
609 return unlinkPromise(filePath)
610 }
611
612 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
613 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
614 return unlinkPromise(torrentPath)
615 }
616
617 // ------------------------------ STATICS ------------------------------
618
619 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
620 // Creating the thumbnail for a remote video
621
622 const thumbnailName = video.getThumbnailName()
623 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
624 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
625 return thumbnailName
626 })
627 }
628
629 getDurationFromFile = function (videoPath: string) {
630 return new Promise<number>((res, rej) => {
631 ffmpeg.ffprobe(videoPath, (err, metadata) => {
632 if (err) return rej(err)
633
634 return res(Math.floor(metadata.format.duration))
635 })
636 })
637 }
638
639 list = function () {
640 const query = {
641 include: [ Video['sequelize'].models.VideoFile ]
642 }
643
644 return Video.findAll(query)
645 }
646
647 listForApi = function (start: number, count: number, sort: string) {
648 // Exclude blacklisted videos from the list
649 const query = {
650 distinct: true,
651 offset: start,
652 limit: count,
653 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
654 include: [
655 {
656 model: Video['sequelize'].models.Author,
657 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
658 },
659 Video['sequelize'].models.Tag,
660 Video['sequelize'].models.VideoFile
661 ],
662 where: createBaseVideosWhere()
663 }
664
665 return Video.findAndCountAll(query).then(({ rows, count }) => {
666 return {
667 data: rows,
668 total: count
669 }
670 })
671 }
672
673 loadByHostAndUUID = function (fromHost: string, uuid: string) {
674 const query = {
675 where: {
676 uuid
677 },
678 include: [
679 {
680 model: Video['sequelize'].models.VideoFile
681 },
682 {
683 model: Video['sequelize'].models.Author,
684 include: [
685 {
686 model: Video['sequelize'].models.Pod,
687 required: true,
688 where: {
689 host: fromHost
690 }
691 }
692 ]
693 }
694 ]
695 }
696
697 return Video.findOne(query)
698 }
699
700 listOwnedAndPopulateAuthorAndTags = function () {
701 const query = {
702 where: {
703 remote: false
704 },
705 include: [
706 Video['sequelize'].models.VideoFile,
707 Video['sequelize'].models.Author,
708 Video['sequelize'].models.Tag
709 ]
710 }
711
712 return Video.findAll(query)
713 }
714
715 listOwnedByAuthor = function (author: string) {
716 const query = {
717 where: {
718 remote: false
719 },
720 include: [
721 {
722 model: Video['sequelize'].models.VideoFile
723 },
724 {
725 model: Video['sequelize'].models.Author,
726 where: {
727 name: author
728 }
729 }
730 ]
731 }
732
733 return Video.findAll(query)
734 }
735
736 load = function (id: number) {
737 return Video.findById(id)
738 }
739
740 loadByUUID = function (uuid: string) {
741 const query = {
742 where: {
743 uuid
744 },
745 include: [ Video['sequelize'].models.VideoFile ]
746 }
747 return Video.findOne(query)
748 }
749
750 loadAndPopulateAuthor = function (id: number) {
751 const options = {
752 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
753 }
754
755 return Video.findById(id, options)
756 }
757
758 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
759 const options = {
760 include: [
761 {
762 model: Video['sequelize'].models.Author,
763 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
764 },
765 Video['sequelize'].models.Tag,
766 Video['sequelize'].models.VideoFile
767 ]
768 }
769
770 return Video.findById(id, options)
771 }
772
773 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
774 const options = {
775 where: {
776 uuid
777 },
778 include: [
779 {
780 model: Video['sequelize'].models.Author,
781 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
782 },
783 Video['sequelize'].models.Tag,
784 Video['sequelize'].models.VideoFile
785 ]
786 }
787
788 return Video.findOne(options)
789 }
790
791 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
792 const podInclude: Sequelize.IncludeOptions = {
793 model: Video['sequelize'].models.Pod,
794 required: false
795 }
796
797 const authorInclude: Sequelize.IncludeOptions = {
798 model: Video['sequelize'].models.Author,
799 include: [
800 podInclude
801 ]
802 }
803
804 const tagInclude: Sequelize.IncludeOptions = {
805 model: Video['sequelize'].models.Tag
806 }
807
808 const videoFileInclude: Sequelize.IncludeOptions = {
809 model: Video['sequelize'].models.VideoFile
810 }
811
812 const query: Sequelize.FindOptions<VideoAttributes> = {
813 distinct: true,
814 where: createBaseVideosWhere(),
815 offset: start,
816 limit: count,
817 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
818 }
819
820 // Make an exact search with the magnet
821 if (field === 'magnetUri') {
822 videoFileInclude.where = {
823 infoHash: magnetUtil.decode(value).infoHash
824 }
825 } else if (field === 'tags') {
826 const escapedValue = Video['sequelize'].escape('%' + value + '%')
827 query.where['id'].$in = Video['sequelize'].literal(
828 `(SELECT "VideoTags"."videoId"
829 FROM "Tags"
830 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
831 WHERE name ILIKE ${escapedValue}
832 )`
833 )
834 } else if (field === 'host') {
835 // FIXME: Include our pod? (not stored in the database)
836 podInclude.where = {
837 host: {
838 $iLike: '%' + value + '%'
839 }
840 }
841 podInclude.required = true
842 } else if (field === 'author') {
843 authorInclude.where = {
844 name: {
845 $iLike: '%' + value + '%'
846 }
847 }
848
849 // authorInclude.or = true
850 } else {
851 query.where[field] = {
852 $iLike: '%' + value + '%'
853 }
854 }
855
856 query.include = [
857 authorInclude, tagInclude, videoFileInclude
858 ]
859
860 return Video.findAndCountAll(query).then(({ rows, count }) => {
861 return {
862 data: rows,
863 total: count
864 }
865 })
866 }
867
868 // ---------------------------------------------------------------------------
869
870 function createBaseVideosWhere () {
871 return {
872 id: {
873 $notIn: Video['sequelize'].literal(
874 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
875 )
876 }
877 }
878 }
879
880 function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
881 const options = {
882 filename: imageName,
883 count: 1,
884 folder
885 }
886
887 if (size) {
888 options['size'] = size
889 }
890
891 return new Promise<string>((res, rej) => {
892 ffmpeg(videoPath)
893 .on('error', rej)
894 .on('end', () => res(imageName))
895 .thumbnail(options)
896 })
897 }