1 import { map, maxBy, truncate } from 'lodash'
2 import * as magnetUtil from 'magnet-uri'
3 import * as parseTorrent from 'parse-torrent'
4 import { join } from 'path'
5 import * as safeBuffer from 'safe-buffer'
6 import * as Sequelize from 'sequelize'
7 import { VideoPrivacy, VideoResolution } from '../../../shared'
8 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
11 generateImageFromVideoFile,
15 isVideoDescriptionValid,
28 } from '../../helpers'
41 } from '../../initializers'
43 import { addMethodsToModel, getSort } from '../utils'
45 import { TagInstance } from './tag-interface'
46 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
47 import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface'
49 const Buffer = safeBuffer.Buffer
51 let Video: Sequelize.Model<VideoInstance, VideoAttributes>
52 let getOriginalFile: VideoMethods.GetOriginalFile
53 let getVideoFilename: VideoMethods.GetVideoFilename
54 let getThumbnailName: VideoMethods.GetThumbnailName
55 let getThumbnailPath: VideoMethods.GetThumbnailPath
56 let getPreviewName: VideoMethods.GetPreviewName
57 let getPreviewPath: VideoMethods.GetPreviewPath
58 let getTorrentFileName: VideoMethods.GetTorrentFileName
59 let isOwned: VideoMethods.IsOwned
60 let toFormattedJSON: VideoMethods.ToFormattedJSON
61 let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
62 let toActivityPubObject: VideoMethods.ToActivityPubObject
63 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
64 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
65 let createPreview: VideoMethods.CreatePreview
66 let createThumbnail: VideoMethods.CreateThumbnail
67 let getVideoFilePath: VideoMethods.GetVideoFilePath
68 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
69 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
70 let getEmbedPath: VideoMethods.GetEmbedPath
71 let getDescriptionPath: VideoMethods.GetDescriptionPath
72 let getTruncatedDescription: VideoMethods.GetTruncatedDescription
73 let getCategoryLabel: VideoMethods.GetCategoryLabel
74 let getLicenceLabel: VideoMethods.GetLicenceLabel
75 let getLanguageLabel: VideoMethods.GetLanguageLabel
77 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
78 let list: VideoMethods.List
79 let listForApi: VideoMethods.ListForApi
80 let listUserVideosForApi: VideoMethods.ListUserVideosForApi
81 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
82 let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
83 let listOwnedByAccount: VideoMethods.ListOwnedByAccount
84 let load: VideoMethods.Load
85 let loadByUUID: VideoMethods.LoadByUUID
86 let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
87 let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
88 let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
89 let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
90 let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
91 let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
92 let removeThumbnail: VideoMethods.RemoveThumbnail
93 let removePreview: VideoMethods.RemovePreview
94 let removeFile: VideoMethods.RemoveFile
95 let removeTorrent: VideoMethods.RemoveTorrent
97 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
98 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
101 type: DataTypes.UUID,
102 defaultValue: DataTypes.UUIDV4,
109 type: DataTypes.STRING,
112 nameValid: value => {
113 const res = isVideoNameValid(value)
114 if (res === false) throw new Error('Video name is not valid.')
119 type: DataTypes.INTEGER,
122 categoryValid: value => {
123 const res = isVideoCategoryValid(value)
124 if (res === false) throw new Error('Video category is not valid.')
129 type: DataTypes.INTEGER,
133 licenceValid: value => {
134 const res = isVideoLicenceValid(value)
135 if (res === false) throw new Error('Video licence is not valid.')
140 type: DataTypes.INTEGER,
143 languageValid: value => {
144 const res = isVideoLanguageValid(value)
145 if (res === false) throw new Error('Video language is not valid.')
150 type: DataTypes.INTEGER,
153 privacyValid: value => {
154 const res = isVideoPrivacyValid(value)
155 if (res === false) throw new Error('Video privacy is not valid.')
160 type: DataTypes.BOOLEAN,
163 nsfwValid: value => {
164 const res = isVideoNSFWValid(value)
165 if (res === false) throw new Error('Video nsfw attribute is not valid.')
170 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
173 descriptionValid: value => {
174 const res = isVideoDescriptionValid(value)
175 if (res === false) throw new Error('Video description is not valid.')
180 type: DataTypes.INTEGER,
183 durationValid: value => {
184 const res = isVideoDurationValid(value)
185 if (res === false) throw new Error('Video duration is not valid.')
190 type: DataTypes.INTEGER,
199 type: DataTypes.INTEGER,
208 type: DataTypes.INTEGER,
217 type: DataTypes.BOOLEAN,
222 type: DataTypes.STRING,
235 fields: [ 'createdAt' ]
238 fields: [ 'duration' ]
250 fields: [ 'channelId' ]
253 fields: [ 'parentId' ]
262 const classMethods = [
265 generateThumbnailFromData,
268 listUserVideosForApi,
269 listOwnedAndPopulateAccountAndTags,
272 loadAndPopulateAccount,
273 loadAndPopulateAccountAndPodAndTags,
277 loadLocalVideoByUUID,
278 loadByUUIDAndPopulateAccountAndPodAndTags,
279 searchAndPopulateAccountAndPodAndTags
281 const instanceMethods = [
284 createTorrentAndSetInfoHash,
300 toFormattedDetailsJSON,
301 optimizeOriginalVideofile,
302 transcodeOriginalVideofile,
303 getOriginalFileHeight,
305 getTruncatedDescription,
311 addMethodsToModel(Video, classMethods, instanceMethods)
316 // ------------------------------ METHODS ------------------------------
318 function associate (models) {
319 Video.belongsTo(models.VideoChannel, {
327 Video.belongsTo(models.VideoChannel, {
335 Video.belongsToMany(models.Tag, {
336 foreignKey: 'videoId',
337 through: models.VideoTag,
341 Video.hasMany(models.VideoAbuse, {
349 Video.hasMany(models.VideoFile, {
358 function afterDestroy (video: VideoInstance) {
362 video.removeThumbnail()
365 if (video.isOwned()) {
366 const removeVideoToFriendsParams = {
371 video.removePreview()
372 // FIXME: remove video for followers
375 // Remove physical files and torrents
376 video.VideoFiles.forEach(file => {
377 tasks.push(video.removeFile(file))
378 tasks.push(video.removeTorrent(file))
382 return Promise.all(tasks)
384 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
388 getOriginalFile = function (this: VideoInstance) {
389 if (Array.isArray(this.VideoFiles) === false) return undefined
391 // The original file is the file that have the higher resolution
392 return maxBy(this.VideoFiles, file => file.resolution)
395 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
396 return this.uuid + '-' + videoFile.resolution + videoFile.extname
399 getThumbnailName = function (this: VideoInstance) {
400 // We always have a copy of the thumbnail
401 const extension = '.jpg'
402 return this.uuid + extension
405 getPreviewName = function (this: VideoInstance) {
406 const extension = '.jpg'
407 return this.uuid + extension
410 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
411 const extension = '.torrent'
412 return this.uuid + '-' + videoFile.resolution + extension
415 isOwned = function (this: VideoInstance) {
416 return this.remote === false
419 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
420 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
422 return generateImageFromVideoFile(
423 this.getVideoFilePath(videoFile),
424 CONFIG.STORAGE.PREVIEWS_DIR,
425 this.getPreviewName(),
430 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
431 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
433 return generateImageFromVideoFile(
434 this.getVideoFilePath(videoFile),
435 CONFIG.STORAGE.THUMBNAILS_DIR,
436 this.getThumbnailName(),
441 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
442 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
445 createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
448 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
451 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
455 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
457 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
458 logger.info('Creating torrent %s.', filePath)
460 await writeFilePromise(filePath, torrent)
462 const parsedTorrent = parseTorrent(torrent)
463 videoFile.infoHash = parsedTorrent.infoHash
466 getEmbedPath = function (this: VideoInstance) {
467 return '/videos/embed/' + this.uuid
470 getThumbnailPath = function (this: VideoInstance) {
471 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
474 getPreviewPath = function (this: VideoInstance) {
475 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
478 toFormattedJSON = function (this: VideoInstance) {
481 if (this.VideoChannel.Account.Pod) {
482 podHost = this.VideoChannel.Account.Pod.host
484 // It means it's our video
485 podHost = CONFIG.WEBSERVER.HOST
492 category: this.category,
493 categoryLabel: this.getCategoryLabel(),
494 licence: this.licence,
495 licenceLabel: this.getLicenceLabel(),
496 language: this.language,
497 languageLabel: this.getLanguageLabel(),
499 description: this.getTruncatedDescription(),
501 isLocal: this.isOwned(),
502 account: this.VideoChannel.Account.name,
503 duration: this.duration,
506 dislikes: this.dislikes,
507 tags: map<TagInstance, string>(this.Tags, 'name'),
508 thumbnailPath: this.getThumbnailPath(),
509 previewPath: this.getPreviewPath(),
510 embedPath: this.getEmbedPath(),
511 createdAt: this.createdAt,
512 updatedAt: this.updatedAt
518 toFormattedDetailsJSON = function (this: VideoInstance) {
519 const formattedJson = this.toFormattedJSON()
521 // Maybe our pod is not up to date and there are new privacy settings since our version
522 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
523 if (!privacyLabel) privacyLabel = 'Unknown'
525 const detailsJson = {
527 privacy: this.privacy,
528 descriptionPath: this.getDescriptionPath(),
529 channel: this.VideoChannel.toFormattedJSON(),
533 // Format and sort video files
534 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
535 detailsJson.files = this.VideoFiles
537 let resolutionLabel = videoFile.resolution + 'p'
539 const videoFileJson = {
540 resolution: videoFile.resolution,
542 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
543 size: videoFile.size,
544 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
545 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
551 if (a.resolution < b.resolution) return 1
552 if (a.resolution === b.resolution) return 0
556 return Object.assign(formattedJson, detailsJson)
559 toActivityPubObject = function (this: VideoInstance) {
560 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
562 const tag = this.Tags.map(t => ({
563 type: 'Hashtag' as 'Hashtag',
568 for (const file of this.VideoFiles) {
571 mimeType: 'video/' + file.extname,
572 url: getVideoFileUrl(this, file, baseUrlHttp),
573 width: file.resolution,
579 mimeType: 'application/x-bittorrent',
580 url: getTorrentUrl(this, file, baseUrlHttp),
581 width: file.resolution
586 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
587 url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
588 width: file.resolution
592 const videoObject: VideoTorrentObject = {
593 type: 'Video' as 'Video',
594 id: getActivityPubUrl('video', this.uuid),
596 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
597 duration: 'PT' + this.duration + 'S',
601 identifier: this.category + '',
602 name: this.getCategoryLabel()
605 identifier: this.licence + '',
606 name: this.getLicenceLabel()
609 identifier: this.language + '',
610 name: this.getLanguageLabel()
614 published: this.createdAt,
615 updated: this.updatedAt,
616 mediaType: 'text/markdown',
617 content: this.getTruncatedDescription(),
620 url: getThumbnailUrl(this, baseUrlHttp),
621 mediaType: 'image/jpeg',
622 width: THUMBNAILS_SIZE.width,
623 height: THUMBNAILS_SIZE.height
631 getTruncatedDescription = function (this: VideoInstance) {
633 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
636 return truncate(this.description, options)
639 optimizeOriginalVideofile = async function (this: VideoInstance) {
640 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
641 const newExtname = '.mp4'
642 const inputVideoFile = this.getOriginalFile()
643 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
644 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
646 const transcodeOptions = {
647 inputPath: videoInputPath,
648 outputPath: videoOutputPath
652 // Could be very long!
653 await transcode(transcodeOptions)
655 await unlinkPromise(videoInputPath)
657 // Important to do this before getVideoFilename() to take in account the new file extension
658 inputVideoFile.set('extname', newExtname)
660 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
661 const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
663 inputVideoFile.set('size', stats.size)
665 await this.createTorrentAndSetInfoHash(inputVideoFile)
666 await inputVideoFile.save()
669 // Auto destruction...
670 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
676 transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) {
677 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
678 const extname = '.mp4'
680 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
681 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
683 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
689 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
691 const transcodeOptions = {
692 inputPath: videoInputPath,
693 outputPath: videoOutputPath,
697 await transcode(transcodeOptions)
699 const stats = await statPromise(videoOutputPath)
701 newVideoFile.set('size', stats.size)
703 await this.createTorrentAndSetInfoHash(newVideoFile)
705 await newVideoFile.save()
707 this.VideoFiles.push(newVideoFile)
710 getOriginalFileHeight = function (this: VideoInstance) {
711 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
713 return getVideoFileHeight(originalFilePath)
716 getDescriptionPath = function (this: VideoInstance) {
717 return `/api/${API_VERSION}/videos/${this.uuid}/description`
720 getCategoryLabel = function (this: VideoInstance) {
721 let categoryLabel = VIDEO_CATEGORIES[this.category]
723 // Maybe our pod is not up to date and there are new categories since our version
724 if (!categoryLabel) categoryLabel = 'Misc'
729 getLicenceLabel = function (this: VideoInstance) {
730 let licenceLabel = VIDEO_LICENCES[this.licence]
732 // Maybe our pod is not up to date and there are new licences since our version
733 if (!licenceLabel) licenceLabel = 'Unknown'
738 getLanguageLabel = function (this: VideoInstance) {
739 // Language is an optional attribute
740 let languageLabel = VIDEO_LANGUAGES[this.language]
741 if (!languageLabel) languageLabel = 'Unknown'
746 removeThumbnail = function (this: VideoInstance) {
747 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
748 return unlinkPromise(thumbnailPath)
751 removePreview = function (this: VideoInstance) {
752 // Same name than video thumbnail
753 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
756 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
757 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
758 return unlinkPromise(filePath)
761 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
762 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
763 return unlinkPromise(torrentPath)
766 // ------------------------------ STATICS ------------------------------
768 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
769 // Creating the thumbnail for a remote video
771 const thumbnailName = video.getThumbnailName()
772 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
773 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
780 include: [ Video['sequelize'].models.VideoFile ]
783 return Video.findAll(query)
786 listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
791 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
794 model: Video['sequelize'].models.VideoChannel,
798 model: Video['sequelize'].models.Account,
806 Video['sequelize'].models.Tag
810 return Video.findAndCountAll(query).then(({ rows, count }) => {
818 listForApi = function (start: number, count: number, sort: string) {
823 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
826 model: Video['sequelize'].models.VideoChannel,
829 model: Video['sequelize'].models.Account,
832 model: Video['sequelize'].models.Pod,
839 Video['sequelize'].models.Tag
841 where: createBaseVideosWhere()
844 return Video.findAndCountAll(query).then(({ rows, count }) => {
852 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
853 const query: Sequelize.FindOptions<VideoAttributes> = {
859 model: Video['sequelize'].models.VideoFile
862 model: Video['sequelize'].models.VideoChannel,
865 model: Video['sequelize'].models.Account,
868 model: Video['sequelize'].models.Pod,
881 if (t !== undefined) query.transaction = t
883 return Video.findOne(query)
886 listOwnedAndPopulateAccountAndTags = function () {
892 Video['sequelize'].models.VideoFile,
894 model: Video['sequelize'].models.VideoChannel,
895 include: [ Video['sequelize'].models.Account ]
897 Video['sequelize'].models.Tag
901 return Video.findAll(query)
904 listOwnedByAccount = function (account: string) {
911 model: Video['sequelize'].models.VideoFile
914 model: Video['sequelize'].models.VideoChannel,
917 model: Video['sequelize'].models.Account,
927 return Video.findAll(query)
930 load = function (id: number) {
931 return Video.findById(id)
934 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
935 const query: Sequelize.FindOptions<VideoAttributes> = {
939 include: [ Video['sequelize'].models.VideoFile ]
942 if (t !== undefined) query.transaction = t
944 return Video.findOne(query)
947 loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) {
948 const query: Sequelize.FindOptions<VideoAttributes> = {
955 include: [ Video['sequelize'].models.VideoFile ]
958 if (t !== undefined) query.transaction = t
960 return Video.findOne(query)
963 loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
964 const query: Sequelize.FindOptions<VideoAttributes> = {
969 include: [ Video['sequelize'].models.VideoFile ]
972 if (t !== undefined) query.transaction = t
974 return Video.findOne(query)
977 loadAndPopulateAccount = function (id: number) {
980 Video['sequelize'].models.VideoFile,
982 model: Video['sequelize'].models.VideoChannel,
983 include: [ Video['sequelize'].models.Account ]
988 return Video.findById(id, options)
991 loadAndPopulateAccountAndPodAndTags = function (id: number) {
995 model: Video['sequelize'].models.VideoChannel,
998 model: Video['sequelize'].models.Account,
999 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
1003 Video['sequelize'].models.Tag,
1004 Video['sequelize'].models.VideoFile
1008 return Video.findById(id, options)
1011 loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) {
1018 model: Video['sequelize'].models.VideoChannel,
1021 model: Video['sequelize'].models.Account,
1022 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
1026 Video['sequelize'].models.Tag,
1027 Video['sequelize'].models.VideoFile
1031 return Video.findOne(options)
1034 searchAndPopulateAccountAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
1035 const podInclude: Sequelize.IncludeOptions = {
1036 model: Video['sequelize'].models.Pod,
1040 const accountInclude: Sequelize.IncludeOptions = {
1041 model: Video['sequelize'].models.Account,
1042 include: [ podInclude ]
1045 const videoChannelInclude: Sequelize.IncludeOptions = {
1046 model: Video['sequelize'].models.VideoChannel,
1047 include: [ accountInclude ],
1051 const tagInclude: Sequelize.IncludeOptions = {
1052 model: Video['sequelize'].models.Tag
1055 const query: Sequelize.FindOptions<VideoAttributes> = {
1057 where: createBaseVideosWhere(),
1060 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
1063 if (field === 'tags') {
1064 const escapedValue = Video['sequelize'].escape('%' + value + '%')
1065 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
1066 `(SELECT "VideoTags"."videoId"
1068 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1069 WHERE name ILIKE ${escapedValue}
1072 } else if (field === 'host') {
1073 // FIXME: Include our pod? (not stored in the database)
1074 podInclude.where = {
1076 [Sequelize.Op.iLike]: '%' + value + '%'
1079 podInclude.required = true
1080 } else if (field === 'account') {
1081 accountInclude.where = {
1083 [Sequelize.Op.iLike]: '%' + value + '%'
1087 query.where[field] = {
1088 [Sequelize.Op.iLike]: '%' + value + '%'
1093 videoChannelInclude, tagInclude
1096 return Video.findAndCountAll(query).then(({ rows, count }) => {
1104 // ---------------------------------------------------------------------------
1106 function createBaseVideosWhere () {
1109 [Sequelize.Op.notIn]: Video['sequelize'].literal(
1110 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1113 privacy: VideoPrivacy.PUBLIC
1117 function getBaseUrls (video: VideoInstance) {
1121 if (video.isOwned()) {
1122 baseUrlHttp = CONFIG.WEBSERVER.URL
1123 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1125 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Pod.host
1126 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Pod.host
1129 return { baseUrlHttp, baseUrlWs }
1132 function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
1133 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
1136 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1137 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1140 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1141 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1144 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1145 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1146 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1147 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1149 const magnetHash = {
1153 infoHash: videoFile.infoHash,
1157 return magnetUtil.encode(magnetHash)