1 import * as safeBuffer from 'safe-buffer'
2 const Buffer = safeBuffer.Buffer
3 import * as magnetUtil from 'magnet-uri'
4 import { map, maxBy, truncate } from 'lodash'
5 import * as parseTorrent from 'parse-torrent'
6 import { join } from 'path'
7 import * as Sequelize from 'sequelize'
9 import { TagInstance } from './tag-interface'
17 isVideoDescriptionValid,
20 readFileBufferPromise,
26 generateImageFromVideoFile,
30 } from '../../helpers'
43 } from '../../initializers'
44 import { removeVideoToFriends } from '../../lib'
45 import { VideoResolution, VideoPrivacy } from '../../../shared'
46 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
48 import { addMethodsToModel, getSort } from '../utils'
54 } from './video-interface'
55 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
57 let Video: Sequelize.Model<VideoInstance, VideoAttributes>
58 let getOriginalFile: VideoMethods.GetOriginalFile
59 let getVideoFilename: VideoMethods.GetVideoFilename
60 let getThumbnailName: VideoMethods.GetThumbnailName
61 let getThumbnailPath: VideoMethods.GetThumbnailPath
62 let getPreviewName: VideoMethods.GetPreviewName
63 let getPreviewPath: VideoMethods.GetPreviewPath
64 let getTorrentFileName: VideoMethods.GetTorrentFileName
65 let isOwned: VideoMethods.IsOwned
66 let toFormattedJSON: VideoMethods.ToFormattedJSON
67 let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
68 let toActivityPubObject: VideoMethods.ToActivityPubObject
69 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
70 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
71 let createPreview: VideoMethods.CreatePreview
72 let createThumbnail: VideoMethods.CreateThumbnail
73 let getVideoFilePath: VideoMethods.GetVideoFilePath
74 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
75 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
76 let getEmbedPath: VideoMethods.GetEmbedPath
77 let getDescriptionPath: VideoMethods.GetDescriptionPath
78 let getTruncatedDescription: VideoMethods.GetTruncatedDescription
79 let getCategoryLabel: VideoMethods.GetCategoryLabel
80 let getLicenceLabel: VideoMethods.GetLicenceLabel
81 let getLanguageLabel: VideoMethods.GetLanguageLabel
83 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
84 let list: VideoMethods.List
85 let listForApi: VideoMethods.ListForApi
86 let listUserVideosForApi: VideoMethods.ListUserVideosForApi
87 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
88 let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
89 let listOwnedByAccount: VideoMethods.ListOwnedByAccount
90 let load: VideoMethods.Load
91 let loadByUUID: VideoMethods.LoadByUUID
92 let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
93 let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
94 let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
95 let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
96 let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
97 let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
98 let removeThumbnail: VideoMethods.RemoveThumbnail
99 let removePreview: VideoMethods.RemovePreview
100 let removeFile: VideoMethods.RemoveFile
101 let removeTorrent: VideoMethods.RemoveTorrent
103 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
104 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
107 type: DataTypes.UUID,
108 defaultValue: DataTypes.UUIDV4,
115 type: DataTypes.STRING,
118 nameValid: value => {
119 const res = isVideoNameValid(value)
120 if (res === false) throw new Error('Video name is not valid.')
125 type: DataTypes.INTEGER,
128 categoryValid: value => {
129 const res = isVideoCategoryValid(value)
130 if (res === false) throw new Error('Video category is not valid.')
135 type: DataTypes.INTEGER,
139 licenceValid: value => {
140 const res = isVideoLicenceValid(value)
141 if (res === false) throw new Error('Video licence is not valid.')
146 type: DataTypes.INTEGER,
149 languageValid: value => {
150 const res = isVideoLanguageValid(value)
151 if (res === false) throw new Error('Video language is not valid.')
156 type: DataTypes.INTEGER,
159 privacyValid: value => {
160 const res = isVideoPrivacyValid(value)
161 if (res === false) throw new Error('Video privacy is not valid.')
166 type: DataTypes.BOOLEAN,
169 nsfwValid: value => {
170 const res = isVideoNSFWValid(value)
171 if (res === false) throw new Error('Video nsfw attribute is not valid.')
176 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
179 descriptionValid: value => {
180 const res = isVideoDescriptionValid(value)
181 if (res === false) throw new Error('Video description is not valid.')
186 type: DataTypes.INTEGER,
189 durationValid: value => {
190 const res = isVideoDurationValid(value)
191 if (res === false) throw new Error('Video duration is not valid.')
196 type: DataTypes.INTEGER,
205 type: DataTypes.INTEGER,
214 type: DataTypes.INTEGER,
223 type: DataTypes.BOOLEAN,
228 type: DataTypes.STRING,
241 fields: [ 'createdAt' ]
244 fields: [ 'duration' ]
256 fields: [ 'channelId' ]
259 fields: [ 'parentId' ]
268 const classMethods = [
271 generateThumbnailFromData,
274 listUserVideosForApi,
275 listOwnedAndPopulateAccountAndTags,
278 loadAndPopulateAccount,
279 loadAndPopulateAccountAndPodAndTags,
283 loadLocalVideoByUUID,
284 loadByUUIDAndPopulateAccountAndPodAndTags,
285 searchAndPopulateAccountAndPodAndTags
287 const instanceMethods = [
290 createTorrentAndSetInfoHash,
306 toFormattedDetailsJSON,
307 optimizeOriginalVideofile,
308 transcodeOriginalVideofile,
309 getOriginalFileHeight,
311 getTruncatedDescription,
317 addMethodsToModel(Video, classMethods, instanceMethods)
322 // ------------------------------ METHODS ------------------------------
324 function associate (models) {
325 Video.belongsTo(models.VideoChannel, {
333 Video.belongsTo(models.VideoChannel, {
341 Video.belongsToMany(models.Tag, {
342 foreignKey: 'videoId',
343 through: models.VideoTag,
347 Video.hasMany(models.VideoAbuse, {
355 Video.hasMany(models.VideoFile, {
364 function afterDestroy (video: VideoInstance) {
368 video.removeThumbnail()
371 if (video.isOwned()) {
372 const removeVideoToFriendsParams = {
377 video.removePreview(),
378 removeVideoToFriends(removeVideoToFriendsParams)
381 // Remove physical files and torrents
382 video.VideoFiles.forEach(file => {
383 tasks.push(video.removeFile(file))
384 tasks.push(video.removeTorrent(file))
388 return Promise.all(tasks)
390 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
394 getOriginalFile = function (this: VideoInstance) {
395 if (Array.isArray(this.VideoFiles) === false) return undefined
397 // The original file is the file that have the higher resolution
398 return maxBy(this.VideoFiles, file => file.resolution)
401 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
402 return this.uuid + '-' + videoFile.resolution + videoFile.extname
405 getThumbnailName = function (this: VideoInstance) {
406 // We always have a copy of the thumbnail
407 const extension = '.jpg'
408 return this.uuid + extension
411 getPreviewName = function (this: VideoInstance) {
412 const extension = '.jpg'
413 return this.uuid + extension
416 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
417 const extension = '.torrent'
418 return this.uuid + '-' + videoFile.resolution + extension
421 isOwned = function (this: VideoInstance) {
422 return this.remote === false
425 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
426 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
428 return generateImageFromVideoFile(
429 this.getVideoFilePath(videoFile),
430 CONFIG.STORAGE.PREVIEWS_DIR,
431 this.getPreviewName(),
436 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
437 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
439 return generateImageFromVideoFile(
440 this.getVideoFilePath(videoFile),
441 CONFIG.STORAGE.THUMBNAILS_DIR,
442 this.getThumbnailName(),
447 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
448 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
451 createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
454 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
457 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
461 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
463 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
464 logger.info('Creating torrent %s.', filePath)
466 await writeFilePromise(filePath, torrent)
468 const parsedTorrent = parseTorrent(torrent)
469 videoFile.infoHash = parsedTorrent.infoHash
472 getEmbedPath = function (this: VideoInstance) {
473 return '/videos/embed/' + this.uuid
476 getThumbnailPath = function (this: VideoInstance) {
477 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
480 getPreviewPath = function (this: VideoInstance) {
481 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
484 toFormattedJSON = function (this: VideoInstance) {
487 if (this.VideoChannel.Account.Pod) {
488 podHost = this.VideoChannel.Account.Pod.host
490 // It means it's our video
491 podHost = CONFIG.WEBSERVER.HOST
498 category: this.category,
499 categoryLabel: this.getCategoryLabel(),
500 licence: this.licence,
501 licenceLabel: this.getLicenceLabel(),
502 language: this.language,
503 languageLabel: this.getLanguageLabel(),
505 description: this.getTruncatedDescription(),
507 isLocal: this.isOwned(),
508 account: this.VideoChannel.Account.name,
509 duration: this.duration,
512 dislikes: this.dislikes,
513 tags: map<TagInstance, string>(this.Tags, 'name'),
514 thumbnailPath: this.getThumbnailPath(),
515 previewPath: this.getPreviewPath(),
516 embedPath: this.getEmbedPath(),
517 createdAt: this.createdAt,
518 updatedAt: this.updatedAt
524 toFormattedDetailsJSON = function (this: VideoInstance) {
525 const formattedJson = this.toFormattedJSON()
527 // Maybe our pod is not up to date and there are new privacy settings since our version
528 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
529 if (!privacyLabel) privacyLabel = 'Unknown'
531 const detailsJson = {
533 privacy: this.privacy,
534 descriptionPath: this.getDescriptionPath(),
535 channel: this.VideoChannel.toFormattedJSON(),
539 // Format and sort video files
540 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
541 detailsJson.files = this.VideoFiles
543 let resolutionLabel = videoFile.resolution + 'p'
545 const videoFileJson = {
546 resolution: videoFile.resolution,
548 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
549 size: videoFile.size,
550 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
551 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
557 if (a.resolution < b.resolution) return 1
558 if (a.resolution === b.resolution) return 0
562 return Object.assign(formattedJson, detailsJson)
565 toActivityPubObject = function (this: VideoInstance) {
566 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
568 const tag = this.Tags.map(t => ({
574 for (const file of this.VideoFiles) {
577 mimeType: 'video/' + file.extname,
578 url: getVideoFileUrl(this, file, baseUrlHttp),
579 width: file.resolution,
585 mimeType: 'application/x-bittorrent',
586 url: getTorrentUrl(this, file, baseUrlHttp),
587 width: file.resolution
592 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
593 url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
594 width: file.resolution
598 const videoObject: VideoTorrentObject = {
600 id: getActivityPubUrl('video', this.uuid),
602 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
603 duration: 'PT' + this.duration + 'S',
608 label: this.getCategoryLabel()
612 name: this.getLicenceLabel()
616 name: this.getLanguageLabel()
620 published: this.createdAt,
621 updated: this.updatedAt,
622 mediaType: 'text/markdown',
623 content: this.getTruncatedDescription(),
626 url: getThumbnailUrl(this, baseUrlHttp),
627 mediaType: 'image/jpeg',
628 width: THUMBNAILS_SIZE.width,
629 height: THUMBNAILS_SIZE.height
637 getTruncatedDescription = function (this: VideoInstance) {
639 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
642 return truncate(this.description, options)
645 optimizeOriginalVideofile = async function (this: VideoInstance) {
646 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
647 const newExtname = '.mp4'
648 const inputVideoFile = this.getOriginalFile()
649 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
650 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
652 const transcodeOptions = {
653 inputPath: videoInputPath,
654 outputPath: videoOutputPath
658 // Could be very long!
659 await transcode(transcodeOptions)
661 await unlinkPromise(videoInputPath)
663 // Important to do this before getVideoFilename() to take in account the new file extension
664 inputVideoFile.set('extname', newExtname)
666 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
667 const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
669 inputVideoFile.set('size', stats.size)
671 await this.createTorrentAndSetInfoHash(inputVideoFile)
672 await inputVideoFile.save()
675 // Auto destruction...
676 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
682 transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) {
683 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
684 const extname = '.mp4'
686 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
687 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
689 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
695 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
697 const transcodeOptions = {
698 inputPath: videoInputPath,
699 outputPath: videoOutputPath,
703 await transcode(transcodeOptions)
705 const stats = await statPromise(videoOutputPath)
707 newVideoFile.set('size', stats.size)
709 await this.createTorrentAndSetInfoHash(newVideoFile)
711 await newVideoFile.save()
713 this.VideoFiles.push(newVideoFile)
716 getOriginalFileHeight = function (this: VideoInstance) {
717 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
719 return getVideoFileHeight(originalFilePath)
722 getDescriptionPath = function (this: VideoInstance) {
723 return `/api/${API_VERSION}/videos/${this.uuid}/description`
726 getCategoryLabel = function (this: VideoInstance) {
727 let categoryLabel = VIDEO_CATEGORIES[this.category]
729 // Maybe our pod is not up to date and there are new categories since our version
730 if (!categoryLabel) categoryLabel = 'Misc'
735 getLicenceLabel = function (this: VideoInstance) {
736 let licenceLabel = VIDEO_LICENCES[this.licence]
738 // Maybe our pod is not up to date and there are new licences since our version
739 if (!licenceLabel) licenceLabel = 'Unknown'
744 getLanguageLabel = function (this: VideoInstance) {
745 // Language is an optional attribute
746 let languageLabel = VIDEO_LANGUAGES[this.language]
747 if (!languageLabel) languageLabel = 'Unknown'
752 removeThumbnail = function (this: VideoInstance) {
753 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
754 return unlinkPromise(thumbnailPath)
757 removePreview = function (this: VideoInstance) {
758 // Same name than video thumbnail
759 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
762 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
763 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
764 return unlinkPromise(filePath)
767 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
768 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
769 return unlinkPromise(torrentPath)
772 // ------------------------------ STATICS ------------------------------
774 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
775 // Creating the thumbnail for a remote video
777 const thumbnailName = video.getThumbnailName()
778 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
779 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
786 include: [ Video['sequelize'].models.VideoFile ]
789 return Video.findAll(query)
792 listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
797 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
800 model: Video['sequelize'].models.VideoChannel,
804 model: Video['sequelize'].models.Account,
812 Video['sequelize'].models.Tag
816 return Video.findAndCountAll(query).then(({ rows, count }) => {
824 listForApi = function (start: number, count: number, sort: string) {
829 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
832 model: Video['sequelize'].models.VideoChannel,
835 model: Video['sequelize'].models.Account,
838 model: Video['sequelize'].models.Pod,
845 Video['sequelize'].models.Tag
847 where: createBaseVideosWhere()
850 return Video.findAndCountAll(query).then(({ rows, count }) => {
858 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
859 const query: Sequelize.FindOptions<VideoAttributes> = {
865 model: Video['sequelize'].models.VideoFile
868 model: Video['sequelize'].models.VideoChannel,
871 model: Video['sequelize'].models.Account,
874 model: Video['sequelize'].models.Pod,
887 if (t !== undefined) query.transaction = t
889 return Video.findOne(query)
892 listOwnedAndPopulateAccountAndTags = function () {
898 Video['sequelize'].models.VideoFile,
900 model: Video['sequelize'].models.VideoChannel,
901 include: [ Video['sequelize'].models.Account ]
903 Video['sequelize'].models.Tag
907 return Video.findAll(query)
910 listOwnedByAccount = function (account: string) {
917 model: Video['sequelize'].models.VideoFile
920 model: Video['sequelize'].models.VideoChannel,
923 model: Video['sequelize'].models.Account,
933 return Video.findAll(query)
936 load = function (id: number) {
937 return Video.findById(id)
940 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
941 const query: Sequelize.FindOptions<VideoAttributes> = {
945 include: [ Video['sequelize'].models.VideoFile ]
948 if (t !== undefined) query.transaction = t
950 return Video.findOne(query)
953 loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) {
954 const query: Sequelize.FindOptions<VideoAttributes> = {
961 include: [ Video['sequelize'].models.VideoFile ]
964 if (t !== undefined) query.transaction = t
966 return Video.findOne(query)
969 loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
970 const query: Sequelize.FindOptions<VideoAttributes> = {
975 include: [ Video['sequelize'].models.VideoFile ]
978 if (t !== undefined) query.transaction = t
980 return Video.findOne(query)
983 loadAndPopulateAccount = function (id: number) {
986 Video['sequelize'].models.VideoFile,
988 model: Video['sequelize'].models.VideoChannel,
989 include: [ Video['sequelize'].models.Account ]
994 return Video.findById(id, options)
997 loadAndPopulateAccountAndPodAndTags = function (id: number) {
1001 model: Video['sequelize'].models.VideoChannel,
1004 model: Video['sequelize'].models.Account,
1005 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
1009 Video['sequelize'].models.Tag,
1010 Video['sequelize'].models.VideoFile
1014 return Video.findById(id, options)
1017 loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) {
1024 model: Video['sequelize'].models.VideoChannel,
1027 model: Video['sequelize'].models.Account,
1028 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
1032 Video['sequelize'].models.Tag,
1033 Video['sequelize'].models.VideoFile
1037 return Video.findOne(options)
1040 searchAndPopulateAccountAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
1041 const podInclude: Sequelize.IncludeOptions = {
1042 model: Video['sequelize'].models.Pod,
1046 const accountInclude: Sequelize.IncludeOptions = {
1047 model: Video['sequelize'].models.Account,
1048 include: [ podInclude ]
1051 const videoChannelInclude: Sequelize.IncludeOptions = {
1052 model: Video['sequelize'].models.VideoChannel,
1053 include: [ accountInclude ],
1057 const tagInclude: Sequelize.IncludeOptions = {
1058 model: Video['sequelize'].models.Tag
1061 const query: Sequelize.FindOptions<VideoAttributes> = {
1063 where: createBaseVideosWhere(),
1066 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
1069 if (field === 'tags') {
1070 const escapedValue = Video['sequelize'].escape('%' + value + '%')
1071 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
1072 `(SELECT "VideoTags"."videoId"
1074 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1075 WHERE name ILIKE ${escapedValue}
1078 } else if (field === 'host') {
1079 // FIXME: Include our pod? (not stored in the database)
1080 podInclude.where = {
1082 [Sequelize.Op.iLike]: '%' + value + '%'
1085 podInclude.required = true
1086 } else if (field === 'account') {
1087 accountInclude.where = {
1089 [Sequelize.Op.iLike]: '%' + value + '%'
1093 query.where[field] = {
1094 [Sequelize.Op.iLike]: '%' + value + '%'
1099 videoChannelInclude, tagInclude
1102 return Video.findAndCountAll(query).then(({ rows, count }) => {
1110 // ---------------------------------------------------------------------------
1112 function createBaseVideosWhere () {
1115 [Sequelize.Op.notIn]: Video['sequelize'].literal(
1116 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1119 privacy: VideoPrivacy.PUBLIC
1123 function getBaseUrls (video: VideoInstance) {
1127 if (video.isOwned()) {
1128 baseUrlHttp = CONFIG.WEBSERVER.URL
1129 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1131 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Pod.host
1132 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Pod.host
1135 return { baseUrlHttp, baseUrlWs }
1138 function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
1139 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
1142 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1143 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1146 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1147 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1150 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1151 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1152 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1153 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1155 const magnetHash = {
1159 infoHash: videoFile.infoHash,
1163 return magnetUtil.encode(magnetHash)