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