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