]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video.ts
Add ability to download a video from direct link or torrent file
[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 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 getPreviewName,
257 getPreviewPath,
258 getThumbnailName,
259 getThumbnailPath,
260 getTorrentFileName,
261 getVideoFilename,
262 getVideoFilePath,
263 getOriginalFile,
264 isOwned,
265 removeFile,
266 removePreview,
267 removeThumbnail,
268 removeTorrent,
269 toAddRemoteJSON,
270 toFormattedJSON,
271 toUpdateRemoteJSON,
272 optimizeOriginalVideofile,
273 transcodeOriginalVideofile,
274 getOriginalFileHeight,
275 getEmbedPath
276 ]
277 addMethodsToModel(Video, classMethods, instanceMethods)
278
279 return Video
280 }
281
282 // ------------------------------ METHODS ------------------------------
283
284 function associate (models) {
285 Video.belongsTo(models.Author, {
286 foreignKey: {
287 name: 'authorId',
288 allowNull: false
289 },
290 onDelete: 'cascade'
291 })
292
293 Video.belongsToMany(models.Tag, {
294 foreignKey: 'videoId',
295 through: models.VideoTag,
296 onDelete: 'cascade'
297 })
298
299 Video.hasMany(models.VideoAbuse, {
300 foreignKey: {
301 name: 'videoId',
302 allowNull: false
303 },
304 onDelete: 'cascade'
305 })
306
307 Video.hasMany(models.VideoFile, {
308 foreignKey: {
309 name: 'videoId',
310 allowNull: false
311 },
312 onDelete: 'cascade'
313 })
314 }
315
316 function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
317 const tasks = []
318
319 tasks.push(
320 video.removeThumbnail()
321 )
322
323 if (video.isOwned()) {
324 const removeVideoToFriendsParams = {
325 uuid: video.uuid
326 }
327
328 tasks.push(
329 video.removePreview(),
330 removeVideoToFriends(removeVideoToFriendsParams, options.transaction)
331 )
332
333 // Remove physical files and torrents
334 video.VideoFiles.forEach(file => {
335 tasks.push(video.removeFile(file))
336 tasks.push(video.removeTorrent(file))
337 })
338 }
339
340 return Promise.all(tasks)
341 .catch(err => {
342 logger.error('Some errors when removing files of video %d in after destroy hook.', video.uuid, err)
343 })
344 }
345
346 getOriginalFile = function (this: VideoInstance) {
347 if (Array.isArray(this.VideoFiles) === false) return undefined
348
349 // The original file is the file that have the higher resolution
350 return maxBy(this.VideoFiles, file => file.resolution)
351 }
352
353 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
354 return this.uuid + '-' + videoFile.resolution + videoFile.extname
355 }
356
357 getThumbnailName = function (this: VideoInstance) {
358 // We always have a copy of the thumbnail
359 const extension = '.jpg'
360 return this.uuid + extension
361 }
362
363 getPreviewName = function (this: VideoInstance) {
364 const extension = '.jpg'
365 return this.uuid + extension
366 }
367
368 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
369 const extension = '.torrent'
370 return this.uuid + '-' + videoFile.resolution + extension
371 }
372
373 isOwned = function (this: VideoInstance) {
374 return this.remote === false
375 }
376
377 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
378 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
379
380 return generateImageFromVideoFile(
381 this.getVideoFilePath(videoFile),
382 CONFIG.STORAGE.PREVIEWS_DIR,
383 this.getPreviewName(),
384 imageSize
385 )
386 }
387
388 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
389 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
390
391 return generateImageFromVideoFile(
392 this.getVideoFilePath(videoFile),
393 CONFIG.STORAGE.THUMBNAILS_DIR,
394 this.getThumbnailName(),
395 imageSize
396 )
397 }
398
399 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
400 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
401 }
402
403 createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
404 const options = {
405 announceList: [
406 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
407 ],
408 urlList: [
409 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
410 ]
411 }
412
413 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
414 .then(torrent => {
415 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
416 logger.info('Creating torrent %s.', filePath)
417
418 return writeFilePromise(filePath, torrent).then(() => torrent)
419 })
420 .then(torrent => {
421 const parsedTorrent = parseTorrent(torrent)
422
423 videoFile.infoHash = parsedTorrent.infoHash
424 })
425 }
426
427 getEmbedPath = function (this: VideoInstance) {
428 return '/videos/embed/' + this.uuid
429 }
430
431 getThumbnailPath = function (this: VideoInstance) {
432 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
433 }
434
435 getPreviewPath = function (this: VideoInstance) {
436 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
437 }
438
439 toFormattedJSON = function (this: VideoInstance) {
440 let podHost
441
442 if (this.Author.Pod) {
443 podHost = this.Author.Pod.host
444 } else {
445 // It means it's our video
446 podHost = CONFIG.WEBSERVER.HOST
447 }
448
449 // Maybe our pod is not up to date and there are new categories since our version
450 let categoryLabel = VIDEO_CATEGORIES[this.category]
451 if (!categoryLabel) categoryLabel = 'Misc'
452
453 // Maybe our pod is not up to date and there are new licences since our version
454 let licenceLabel = VIDEO_LICENCES[this.licence]
455 if (!licenceLabel) licenceLabel = 'Unknown'
456
457 // Language is an optional attribute
458 let languageLabel = VIDEO_LANGUAGES[this.language]
459 if (!languageLabel) languageLabel = 'Unknown'
460
461 const json = {
462 id: this.id,
463 uuid: this.uuid,
464 name: this.name,
465 category: this.category,
466 categoryLabel,
467 licence: this.licence,
468 licenceLabel,
469 language: this.language,
470 languageLabel,
471 nsfw: this.nsfw,
472 description: this.description,
473 podHost,
474 isLocal: this.isOwned(),
475 author: this.Author.name,
476 duration: this.duration,
477 views: this.views,
478 likes: this.likes,
479 dislikes: this.dislikes,
480 tags: map<TagInstance, string>(this.Tags, 'name'),
481 thumbnailPath: this.getThumbnailPath(),
482 previewPath: this.getPreviewPath(),
483 embedPath: this.getEmbedPath(),
484 createdAt: this.createdAt,
485 updatedAt: this.updatedAt,
486 files: []
487 }
488
489 // Format and sort video files
490 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
491 json.files = this.VideoFiles
492 .map(videoFile => {
493 let resolutionLabel = videoFile.resolution + 'p'
494
495 const videoFileJson = {
496 resolution: videoFile.resolution,
497 resolutionLabel,
498 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
499 size: videoFile.size,
500 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
501 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
502 }
503
504 return videoFileJson
505 })
506 .sort((a, b) => {
507 if (a.resolution < b.resolution) return 1
508 if (a.resolution === b.resolution) return 0
509 return -1
510 })
511
512 return json
513 }
514
515 toAddRemoteJSON = function (this: VideoInstance) {
516 // Get thumbnail data to send to the other pod
517 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
518
519 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
520 const remoteVideo = {
521 uuid: this.uuid,
522 name: this.name,
523 category: this.category,
524 licence: this.licence,
525 language: this.language,
526 nsfw: this.nsfw,
527 description: this.description,
528 author: this.Author.name,
529 duration: this.duration,
530 thumbnailData: thumbnailData.toString('binary'),
531 tags: map<TagInstance, string>(this.Tags, 'name'),
532 createdAt: this.createdAt,
533 updatedAt: this.updatedAt,
534 views: this.views,
535 likes: this.likes,
536 dislikes: this.dislikes,
537 files: []
538 }
539
540 this.VideoFiles.forEach(videoFile => {
541 remoteVideo.files.push({
542 infoHash: videoFile.infoHash,
543 resolution: videoFile.resolution,
544 extname: videoFile.extname,
545 size: videoFile.size
546 })
547 })
548
549 return remoteVideo
550 })
551 }
552
553 toUpdateRemoteJSON = function (this: VideoInstance) {
554 const json = {
555 uuid: this.uuid,
556 name: this.name,
557 category: this.category,
558 licence: this.licence,
559 language: this.language,
560 nsfw: this.nsfw,
561 description: this.description,
562 author: this.Author.name,
563 duration: this.duration,
564 tags: map<TagInstance, string>(this.Tags, 'name'),
565 createdAt: this.createdAt,
566 updatedAt: this.updatedAt,
567 views: this.views,
568 likes: this.likes,
569 dislikes: this.dislikes,
570 files: []
571 }
572
573 this.VideoFiles.forEach(videoFile => {
574 json.files.push({
575 infoHash: videoFile.infoHash,
576 resolution: videoFile.resolution,
577 extname: videoFile.extname,
578 size: videoFile.size
579 })
580 })
581
582 return json
583 }
584
585 optimizeOriginalVideofile = function (this: VideoInstance) {
586 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
587 const newExtname = '.mp4'
588 const inputVideoFile = this.getOriginalFile()
589 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
590 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
591
592 const transcodeOptions = {
593 inputPath: videoInputPath,
594 outputPath: videoOutputPath
595 }
596
597 return transcode(transcodeOptions)
598 .then(() => {
599 return unlinkPromise(videoInputPath)
600 })
601 .then(() => {
602 // Important to do this before getVideoFilename() to take in account the new file extension
603 inputVideoFile.set('extname', newExtname)
604
605 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
606 })
607 .then(() => {
608 return statPromise(this.getVideoFilePath(inputVideoFile))
609 })
610 .then(stats => {
611 return inputVideoFile.set('size', stats.size)
612 })
613 .then(() => {
614 return this.createTorrentAndSetInfoHash(inputVideoFile)
615 })
616 .then(() => {
617 return inputVideoFile.save()
618 })
619 .then(() => {
620 return undefined
621 })
622 .catch(err => {
623 // Auto destruction...
624 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
625
626 throw err
627 })
628 }
629
630 transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
631 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
632 const extname = '.mp4'
633
634 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
635 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
636
637 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
638 resolution,
639 extname,
640 size: 0,
641 videoId: this.id
642 })
643 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
644
645 const transcodeOptions = {
646 inputPath: videoInputPath,
647 outputPath: videoOutputPath,
648 resolution
649 }
650 return transcode(transcodeOptions)
651 .then(() => {
652 return statPromise(videoOutputPath)
653 })
654 .then(stats => {
655 newVideoFile.set('size', stats.size)
656
657 return undefined
658 })
659 .then(() => {
660 return this.createTorrentAndSetInfoHash(newVideoFile)
661 })
662 .then(() => {
663 return newVideoFile.save()
664 })
665 .then(() => {
666 return this.VideoFiles.push(newVideoFile)
667 })
668 .then(() => undefined)
669 }
670
671 getOriginalFileHeight = function (this: VideoInstance) {
672 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
673
674 return getVideoFileHeight(originalFilePath)
675 }
676
677 removeThumbnail = function (this: VideoInstance) {
678 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
679 return unlinkPromise(thumbnailPath)
680 }
681
682 removePreview = function (this: VideoInstance) {
683 // Same name than video thumbnail
684 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
685 }
686
687 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
688 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
689 return unlinkPromise(filePath)
690 }
691
692 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
693 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
694 return unlinkPromise(torrentPath)
695 }
696
697 // ------------------------------ STATICS ------------------------------
698
699 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
700 // Creating the thumbnail for a remote video
701
702 const thumbnailName = video.getThumbnailName()
703 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
704 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
705 return thumbnailName
706 })
707 }
708
709 list = function () {
710 const query = {
711 include: [ Video['sequelize'].models.VideoFile ]
712 }
713
714 return Video.findAll(query)
715 }
716
717 listForApi = function (start: number, count: number, sort: string) {
718 // Exclude blacklisted videos from the list
719 const query = {
720 distinct: true,
721 offset: start,
722 limit: count,
723 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
724 include: [
725 {
726 model: Video['sequelize'].models.Author,
727 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
728 },
729 Video['sequelize'].models.Tag,
730 Video['sequelize'].models.VideoFile
731 ],
732 where: createBaseVideosWhere()
733 }
734
735 return Video.findAndCountAll(query).then(({ rows, count }) => {
736 return {
737 data: rows,
738 total: count
739 }
740 })
741 }
742
743 loadByHostAndUUID = function (fromHost: string, uuid: string) {
744 const query = {
745 where: {
746 uuid
747 },
748 include: [
749 {
750 model: Video['sequelize'].models.VideoFile
751 },
752 {
753 model: Video['sequelize'].models.Author,
754 include: [
755 {
756 model: Video['sequelize'].models.Pod,
757 required: true,
758 where: {
759 host: fromHost
760 }
761 }
762 ]
763 }
764 ]
765 }
766
767 return Video.findOne(query)
768 }
769
770 listOwnedAndPopulateAuthorAndTags = function () {
771 const query = {
772 where: {
773 remote: false
774 },
775 include: [
776 Video['sequelize'].models.VideoFile,
777 Video['sequelize'].models.Author,
778 Video['sequelize'].models.Tag
779 ]
780 }
781
782 return Video.findAll(query)
783 }
784
785 listOwnedByAuthor = function (author: string) {
786 const query = {
787 where: {
788 remote: false
789 },
790 include: [
791 {
792 model: Video['sequelize'].models.VideoFile
793 },
794 {
795 model: Video['sequelize'].models.Author,
796 where: {
797 name: author
798 }
799 }
800 ]
801 }
802
803 return Video.findAll(query)
804 }
805
806 load = function (id: number) {
807 return Video.findById(id)
808 }
809
810 loadByUUID = function (uuid: string) {
811 const query = {
812 where: {
813 uuid
814 },
815 include: [ Video['sequelize'].models.VideoFile ]
816 }
817 return Video.findOne(query)
818 }
819
820 loadAndPopulateAuthor = function (id: number) {
821 const options = {
822 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
823 }
824
825 return Video.findById(id, options)
826 }
827
828 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
829 const options = {
830 include: [
831 {
832 model: Video['sequelize'].models.Author,
833 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
834 },
835 Video['sequelize'].models.Tag,
836 Video['sequelize'].models.VideoFile
837 ]
838 }
839
840 return Video.findById(id, options)
841 }
842
843 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
844 const options = {
845 where: {
846 uuid
847 },
848 include: [
849 {
850 model: Video['sequelize'].models.Author,
851 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
852 },
853 Video['sequelize'].models.Tag,
854 Video['sequelize'].models.VideoFile
855 ]
856 }
857
858 return Video.findOne(options)
859 }
860
861 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
862 const podInclude: Sequelize.IncludeOptions = {
863 model: Video['sequelize'].models.Pod,
864 required: false
865 }
866
867 const authorInclude: Sequelize.IncludeOptions = {
868 model: Video['sequelize'].models.Author,
869 include: [
870 podInclude
871 ]
872 }
873
874 const tagInclude: Sequelize.IncludeOptions = {
875 model: Video['sequelize'].models.Tag
876 }
877
878 const videoFileInclude: Sequelize.IncludeOptions = {
879 model: Video['sequelize'].models.VideoFile
880 }
881
882 const query: Sequelize.FindOptions<VideoAttributes> = {
883 distinct: true,
884 where: createBaseVideosWhere(),
885 offset: start,
886 limit: count,
887 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
888 }
889
890 // Make an exact search with the magnet
891 if (field === 'magnetUri') {
892 videoFileInclude.where = {
893 infoHash: magnetUtil.decode(value).infoHash
894 }
895 } else if (field === 'tags') {
896 const escapedValue = Video['sequelize'].escape('%' + value + '%')
897 query.where['id'].$in = Video['sequelize'].literal(
898 `(SELECT "VideoTags"."videoId"
899 FROM "Tags"
900 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
901 WHERE name ILIKE ${escapedValue}
902 )`
903 )
904 } else if (field === 'host') {
905 // FIXME: Include our pod? (not stored in the database)
906 podInclude.where = {
907 host: {
908 $iLike: '%' + value + '%'
909 }
910 }
911 podInclude.required = true
912 } else if (field === 'author') {
913 authorInclude.where = {
914 name: {
915 $iLike: '%' + value + '%'
916 }
917 }
918
919 // authorInclude.or = true
920 } else {
921 query.where[field] = {
922 $iLike: '%' + value + '%'
923 }
924 }
925
926 query.include = [
927 authorInclude, tagInclude, videoFileInclude
928 ]
929
930 return Video.findAndCountAll(query).then(({ rows, count }) => {
931 return {
932 data: rows,
933 total: count
934 }
935 })
936 }
937
938 // ---------------------------------------------------------------------------
939
940 function createBaseVideosWhere () {
941 return {
942 id: {
943 $notIn: Video['sequelize'].literal(
944 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
945 )
946 }
947 }
948 }
949
950 function getBaseUrls (video: VideoInstance) {
951 let baseUrlHttp
952 let baseUrlWs
953
954 if (video.isOwned()) {
955 baseUrlHttp = CONFIG.WEBSERVER.URL
956 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
957 } else {
958 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.Author.Pod.host
959 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.Author.Pod.host
960 }
961
962 return { baseUrlHttp, baseUrlWs }
963 }
964
965 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
966 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
967 }
968
969 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
970 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
971 }
972
973 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
974 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
975 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
976 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
977
978 const magnetHash = {
979 xs,
980 announce,
981 urlList,
982 infoHash: videoFile.infoHash,
983 name: video.name
984 }
985
986 return magnetUtil.encode(magnetHash)
987 }