]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Optimize signature verification
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
CommitLineData
53abc4c2 1import { map, maxBy, truncate } from 'lodash'
571389d4 2import * as magnetUtil from 'magnet-uri'
4d4e5cd4 3import * as parseTorrent from 'parse-torrent'
65fcc311 4import { join } from 'path'
571389d4 5import * as safeBuffer from 'safe-buffer'
e02643f3 6import * as Sequelize from 'sequelize'
571389d4
C
7import { VideoPrivacy, VideoResolution } from '../../../shared'
8import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
65fcc311 9import {
571389d4
C
10 createTorrentPromise,
11 generateImageFromVideoFile,
12 getActivityPubUrl,
13 getVideoFileHeight,
65fcc311 14 isVideoCategoryValid,
65fcc311 15 isVideoDescriptionValid,
6fcd19ba 16 isVideoDurationValid,
571389d4
C
17 isVideoLanguageValid,
18 isVideoLicenceValid,
19 isVideoNameValid,
20 isVideoNSFWValid,
fd45e8f4 21 isVideoPrivacyValid,
571389d4 22 logger,
6fcd19ba 23 renamePromise,
14d3270f 24 statPromise,
14d3270f 25 transcode,
571389d4
C
26 unlinkPromise,
27 writeFilePromise
74889a71 28} from '../../helpers'
21e0727a 29import { isVideoUrlValid } from '../../helpers/custom-validators/videos'
65fcc311 30import {
571389d4 31 API_VERSION,
65fcc311 32 CONFIG,
571389d4
C
33 CONSTRAINTS_FIELDS,
34 PREVIEWS_SIZE,
65fcc311
C
35 REMOTE_SCHEME,
36 STATIC_PATHS,
571389d4 37 THUMBNAILS_SIZE,
65fcc311 38 VIDEO_CATEGORIES,
65fcc311 39 VIDEO_LANGUAGES,
571389d4 40 VIDEO_LICENCES,
fd45e8f4 41 VIDEO_PRIVACIES
74889a71 42} from '../../initializers'
21e0727a 43import { sendDeleteVideo } from '../../lib/activitypub/send-request'
aaf61f38 44
74889a71 45import { addMethodsToModel, getSort } from '../utils'
e02643f3 46
571389d4
C
47import { TagInstance } from './tag-interface'
48import { VideoFileInstance, VideoFileModel } from './video-file-interface'
49import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface'
50
51const Buffer = safeBuffer.Buffer
e02643f3
C
52
53let Video: Sequelize.Model<VideoInstance, VideoAttributes>
40298b02 54let getOriginalFile: VideoMethods.GetOriginalFile
e02643f3
C
55let getVideoFilename: VideoMethods.GetVideoFilename
56let getThumbnailName: VideoMethods.GetThumbnailName
d8755eed 57let getThumbnailPath: VideoMethods.GetThumbnailPath
e02643f3 58let getPreviewName: VideoMethods.GetPreviewName
d8755eed 59let getPreviewPath: VideoMethods.GetPreviewPath
93e1258c 60let getTorrentFileName: VideoMethods.GetTorrentFileName
e02643f3 61let isOwned: VideoMethods.IsOwned
0aef76c4 62let toFormattedJSON: VideoMethods.ToFormattedJSON
72c7248b 63let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
e4f97bab 64let toActivityPubObject: VideoMethods.ToActivityPubObject
40298b02
C
65let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
66let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
93e1258c
C
67let createPreview: VideoMethods.CreatePreview
68let createThumbnail: VideoMethods.CreateThumbnail
69let getVideoFilePath: VideoMethods.GetVideoFilePath
70let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
40298b02 71let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
d8755eed 72let getEmbedPath: VideoMethods.GetEmbedPath
9567011b
C
73let getDescriptionPath: VideoMethods.GetDescriptionPath
74let getTruncatedDescription: VideoMethods.GetTruncatedDescription
e4f97bab
C
75let getCategoryLabel: VideoMethods.GetCategoryLabel
76let getLicenceLabel: VideoMethods.GetLicenceLabel
77let getLanguageLabel: VideoMethods.GetLanguageLabel
e02643f3
C
78
79let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
e02643f3
C
80let list: VideoMethods.List
81let listForApi: VideoMethods.ListForApi
fd45e8f4 82let listUserVideosForApi: VideoMethods.ListUserVideosForApi
0a6658fd 83let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
e4f97bab
C
84let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
85let listOwnedByAccount: VideoMethods.ListOwnedByAccount
e02643f3 86let load: VideoMethods.Load
d7d5611c 87let loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount
0a6658fd 88let loadByUUID: VideoMethods.LoadByUUID
0d0e8dd0 89let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
a041b171 90let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
e4f97bab 91let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
60862425
C
92let loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags
93let loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags
94let searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags
93e1258c
C
95let removeThumbnail: VideoMethods.RemoveThumbnail
96let removePreview: VideoMethods.RemovePreview
97let removeFile: VideoMethods.RemoveFile
98let removeTorrent: VideoMethods.RemoveTorrent
e02643f3 99
127944aa
C
100export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
101 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
feb4bdfd 102 {
0a6658fd 103 uuid: {
feb4bdfd
C
104 type: DataTypes.UUID,
105 defaultValue: DataTypes.UUIDV4,
0a6658fd 106 allowNull: false,
67bf9b96
C
107 validate: {
108 isUUID: 4
109 }
aaf61f38 110 },
feb4bdfd 111 name: {
67bf9b96
C
112 type: DataTypes.STRING,
113 allowNull: false,
114 validate: {
075f16ca 115 nameValid: value => {
65fcc311 116 const res = isVideoNameValid(value)
67bf9b96
C
117 if (res === false) throw new Error('Video name is not valid.')
118 }
119 }
6a94a109 120 },
6e07c3de
C
121 category: {
122 type: DataTypes.INTEGER,
123 allowNull: false,
124 validate: {
075f16ca 125 categoryValid: value => {
65fcc311 126 const res = isVideoCategoryValid(value)
6e07c3de
C
127 if (res === false) throw new Error('Video category is not valid.')
128 }
129 }
130 },
6f0c39e2
C
131 licence: {
132 type: DataTypes.INTEGER,
133 allowNull: false,
3092476e 134 defaultValue: null,
6f0c39e2 135 validate: {
075f16ca 136 licenceValid: value => {
65fcc311 137 const res = isVideoLicenceValid(value)
6f0c39e2
C
138 if (res === false) throw new Error('Video licence is not valid.')
139 }
140 }
141 },
3092476e
C
142 language: {
143 type: DataTypes.INTEGER,
144 allowNull: true,
145 validate: {
075f16ca 146 languageValid: value => {
65fcc311 147 const res = isVideoLanguageValid(value)
3092476e
C
148 if (res === false) throw new Error('Video language is not valid.')
149 }
150 }
151 },
fd45e8f4
C
152 privacy: {
153 type: DataTypes.INTEGER,
154 allowNull: false,
155 validate: {
156 privacyValid: value => {
157 const res = isVideoPrivacyValid(value)
158 if (res === false) throw new Error('Video privacy is not valid.')
159 }
160 }
161 },
31b59b47
C
162 nsfw: {
163 type: DataTypes.BOOLEAN,
164 allowNull: false,
165 validate: {
075f16ca 166 nsfwValid: value => {
65fcc311 167 const res = isVideoNSFWValid(value)
31b59b47
C
168 if (res === false) throw new Error('Video nsfw attribute is not valid.')
169 }
170 }
171 },
feb4bdfd 172 description: {
9567011b 173 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
67bf9b96
C
174 allowNull: false,
175 validate: {
075f16ca 176 descriptionValid: value => {
65fcc311 177 const res = isVideoDescriptionValid(value)
67bf9b96
C
178 if (res === false) throw new Error('Video description is not valid.')
179 }
180 }
feb4bdfd 181 },
feb4bdfd 182 duration: {
67bf9b96
C
183 type: DataTypes.INTEGER,
184 allowNull: false,
185 validate: {
075f16ca 186 durationValid: value => {
65fcc311 187 const res = isVideoDurationValid(value)
67bf9b96
C
188 if (res === false) throw new Error('Video duration is not valid.')
189 }
190 }
9e167724
C
191 },
192 views: {
193 type: DataTypes.INTEGER,
194 allowNull: false,
195 defaultValue: 0,
196 validate: {
197 min: 0,
198 isInt: true
199 }
d38b8281
C
200 },
201 likes: {
202 type: DataTypes.INTEGER,
203 allowNull: false,
204 defaultValue: 0,
205 validate: {
206 min: 0,
207 isInt: true
208 }
209 },
210 dislikes: {
211 type: DataTypes.INTEGER,
212 allowNull: false,
213 defaultValue: 0,
214 validate: {
215 min: 0,
216 isInt: true
217 }
0a6658fd
C
218 },
219 remote: {
220 type: DataTypes.BOOLEAN,
221 allowNull: false,
222 defaultValue: false
e4f97bab
C
223 },
224 url: {
e34c85e5 225 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max),
e4f97bab
C
226 allowNull: false,
227 validate: {
e34c85e5
C
228 urlValid: value => {
229 const res = isVideoUrlValid(value)
230 if (res === false) throw new Error('Video URL is not valid.')
231 }
e4f97bab 232 }
aaf61f38 233 }
feb4bdfd
C
234 },
235 {
319d072e 236 indexes: [
319d072e
C
237 {
238 fields: [ 'name' ]
239 },
240 {
241 fields: [ 'createdAt' ]
242 },
243 {
244 fields: [ 'duration' ]
245 },
9e167724
C
246 {
247 fields: [ 'views' ]
d38b8281
C
248 },
249 {
250 fields: [ 'likes' ]
0a6658fd
C
251 },
252 {
253 fields: [ 'uuid' ]
72c7248b
C
254 },
255 {
256 fields: [ 'channelId' ]
319d072e
C
257 }
258 ],
feb4bdfd 259 hooks: {
feb4bdfd
C
260 afterDestroy
261 }
262 }
263 )
aaf61f38 264
e02643f3
C
265 const classMethods = [
266 associate,
267
268 generateThumbnailFromData,
e02643f3
C
269 list,
270 listForApi,
fd45e8f4 271 listUserVideosForApi,
e4f97bab
C
272 listOwnedAndPopulateAccountAndTags,
273 listOwnedByAccount,
e02643f3 274 load,
d7d5611c 275 loadByUrlAndPopulateAccount,
e4f97bab 276 loadAndPopulateAccount,
60862425 277 loadAndPopulateAccountAndServerAndTags,
93e1258c 278 loadByHostAndUUID,
0d0e8dd0 279 loadByUUIDOrURL,
93e1258c 280 loadByUUID,
a041b171 281 loadLocalVideoByUUID,
60862425
C
282 loadByUUIDAndPopulateAccountAndServerAndTags,
283 searchAndPopulateAccountAndServerAndTags
e02643f3
C
284 ]
285 const instanceMethods = [
93e1258c
C
286 createPreview,
287 createThumbnail,
288 createTorrentAndSetInfoHash,
e02643f3 289 getPreviewName,
d8755eed 290 getPreviewPath,
93e1258c 291 getThumbnailName,
d8755eed 292 getThumbnailPath,
93e1258c
C
293 getTorrentFileName,
294 getVideoFilename,
295 getVideoFilePath,
40298b02 296 getOriginalFile,
e02643f3 297 isOwned,
93e1258c
C
298 removeFile,
299 removePreview,
300 removeThumbnail,
301 removeTorrent,
e4f97bab 302 toActivityPubObject,
0aef76c4 303 toFormattedJSON,
72c7248b 304 toFormattedDetailsJSON,
40298b02
C
305 optimizeOriginalVideofile,
306 transcodeOriginalVideofile,
d8755eed 307 getOriginalFileHeight,
9567011b
C
308 getEmbedPath,
309 getTruncatedDescription,
e4f97bab
C
310 getDescriptionPath,
311 getCategoryLabel,
312 getLicenceLabel,
313 getLanguageLabel
e02643f3
C
314 ]
315 addMethodsToModel(Video, classMethods, instanceMethods)
316
feb4bdfd
C
317 return Video
318}
aaf61f38 319
aaf61f38
C
320// ------------------------------ METHODS ------------------------------
321
feb4bdfd 322function associate (models) {
72c7248b 323 Video.belongsTo(models.VideoChannel, {
feb4bdfd 324 foreignKey: {
72c7248b 325 name: 'channelId',
feb4bdfd
C
326 allowNull: false
327 },
328 onDelete: 'cascade'
329 })
7920c273 330
e02643f3 331 Video.belongsToMany(models.Tag, {
7920c273
C
332 foreignKey: 'videoId',
333 through: models.VideoTag,
334 onDelete: 'cascade'
335 })
55fa55a9 336
e02643f3 337 Video.hasMany(models.VideoAbuse, {
55fa55a9
C
338 foreignKey: {
339 name: 'videoId',
340 allowNull: false
341 },
342 onDelete: 'cascade'
343 })
93e1258c
C
344
345 Video.hasMany(models.VideoFile, {
346 foreignKey: {
347 name: 'videoId',
348 allowNull: false
349 },
350 onDelete: 'cascade'
351 })
feb4bdfd
C
352}
353
911238e3 354function afterDestroy (video: VideoInstance) {
93e1258c 355 const tasks = []
f285faa0 356
93e1258c
C
357 tasks.push(
358 video.removeThumbnail()
359 )
f285faa0 360
93e1258c 361 if (video.isOwned()) {
93e1258c 362 tasks.push(
7a7724e6
C
363 video.removePreview(),
364 sendDeleteVideo(video, undefined)
93e1258c
C
365 )
366
91f6f169 367 // Remove physical files and torrents
93e1258c 368 video.VideoFiles.forEach(file => {
9fd54056
C
369 tasks.push(video.removeFile(file))
370 tasks.push(video.removeTorrent(file))
93e1258c 371 })
f285faa0
C
372 }
373
93e1258c 374 return Promise.all(tasks)
9fd54056 375 .catch(err => {
6cd44728 376 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
9fd54056 377 })
558d7c23
C
378}
379
40298b02
C
380getOriginalFile = function (this: VideoInstance) {
381 if (Array.isArray(this.VideoFiles) === false) return undefined
382
14d3270f
C
383 // The original file is the file that have the higher resolution
384 return maxBy(this.VideoFiles, file => file.resolution)
40298b02
C
385}
386
93e1258c 387getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
14d3270f 388 return this.uuid + '-' + videoFile.resolution + videoFile.extname
f285faa0
C
389}
390
70c065d6 391getThumbnailName = function (this: VideoInstance) {
f285faa0 392 // We always have a copy of the thumbnail
0a6658fd
C
393 const extension = '.jpg'
394 return this.uuid + extension
558d7c23
C
395}
396
70c065d6 397getPreviewName = function (this: VideoInstance) {
f285faa0 398 const extension = '.jpg'
0a6658fd 399 return this.uuid + extension
f285faa0
C
400}
401
93e1258c 402getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
f285faa0 403 const extension = '.torrent'
14d3270f 404 return this.uuid + '-' + videoFile.resolution + extension
558d7c23
C
405}
406
70c065d6 407isOwned = function (this: VideoInstance) {
0a6658fd 408 return this.remote === false
aaf61f38
C
409}
410
93e1258c 411createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
164174a6
C
412 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
413
14d3270f
C
414 return generateImageFromVideoFile(
415 this.getVideoFilePath(videoFile),
416 CONFIG.STORAGE.PREVIEWS_DIR,
164174a6
C
417 this.getPreviewName(),
418 imageSize
14d3270f 419 )
93e1258c
C
420}
421
422createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
d8755eed
C
423 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
424
14d3270f
C
425 return generateImageFromVideoFile(
426 this.getVideoFilePath(videoFile),
427 CONFIG.STORAGE.THUMBNAILS_DIR,
428 this.getThumbnailName(),
d8755eed 429 imageSize
14d3270f 430 )
93e1258c
C
431}
432
433getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
434 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
435}
436
e4f97bab 437createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
93e1258c
C
438 const options = {
439 announceList: [
440 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
441 ],
442 urlList: [
443 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
444 ]
445 }
446
e4f97bab 447 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
fdbda9e3 448
e4f97bab
C
449 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
450 logger.info('Creating torrent %s.', filePath)
93e1258c 451
e4f97bab
C
452 await writeFilePromise(filePath, torrent)
453
454 const parsedTorrent = parseTorrent(torrent)
455 videoFile.infoHash = parsedTorrent.infoHash
93e1258c
C
456}
457
d8755eed
C
458getEmbedPath = function (this: VideoInstance) {
459 return '/videos/embed/' + this.uuid
460}
461
462getThumbnailPath = function (this: VideoInstance) {
463 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
464}
465
466getPreviewPath = function (this: VideoInstance) {
467 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
468}
469
0aef76c4 470toFormattedJSON = function (this: VideoInstance) {
60862425 471 let serverHost
feb4bdfd 472
60862425
C
473 if (this.VideoChannel.Account.Server) {
474 serverHost = this.VideoChannel.Account.Server.host
feb4bdfd
C
475 } else {
476 // It means it's our video
60862425 477 serverHost = CONFIG.WEBSERVER.HOST
feb4bdfd
C
478 }
479
aaf61f38 480 const json = {
feb4bdfd 481 id: this.id,
0a6658fd 482 uuid: this.uuid,
aaf61f38 483 name: this.name,
6e07c3de 484 category: this.category,
e4f97bab 485 categoryLabel: this.getCategoryLabel(),
6f0c39e2 486 licence: this.licence,
e4f97bab 487 licenceLabel: this.getLicenceLabel(),
3092476e 488 language: this.language,
e4f97bab 489 languageLabel: this.getLanguageLabel(),
31b59b47 490 nsfw: this.nsfw,
9567011b 491 description: this.getTruncatedDescription(),
60862425 492 serverHost,
aaf61f38 493 isLocal: this.isOwned(),
e4f97bab 494 account: this.VideoChannel.Account.name,
72c7248b
C
495 duration: this.duration,
496 views: this.views,
497 likes: this.likes,
498 dislikes: this.dislikes,
499 tags: map<TagInstance, string>(this.Tags, 'name'),
500 thumbnailPath: this.getThumbnailPath(),
501 previewPath: this.getPreviewPath(),
502 embedPath: this.getEmbedPath(),
503 createdAt: this.createdAt,
504 updatedAt: this.updatedAt
505 }
506
507 return json
508}
509
510toFormattedDetailsJSON = function (this: VideoInstance) {
9567011b 511 const formattedJson = this.toFormattedJSON()
72c7248b 512
60862425 513 // Maybe our server is not up to date and there are new privacy settings since our version
fd45e8f4
C
514 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
515 if (!privacyLabel) privacyLabel = 'Unknown'
516
9567011b 517 const detailsJson = {
fd45e8f4
C
518 privacyLabel,
519 privacy: this.privacy,
9567011b 520 descriptionPath: this.getDescriptionPath(),
72c7248b 521 channel: this.VideoChannel.toFormattedJSON(),
93e1258c 522 files: []
aaf61f38
C
523 }
524
aa8b6df4 525 // Format and sort video files
a96aed15 526 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
9567011b 527 detailsJson.files = this.VideoFiles
aa8b6df4 528 .map(videoFile => {
14d3270f 529 let resolutionLabel = videoFile.resolution + 'p'
aa8b6df4
C
530
531 const videoFileJson = {
532 resolution: videoFile.resolution,
533 resolutionLabel,
a96aed15
C
534 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
535 size: videoFile.size,
536 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
537 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
aa8b6df4
C
538 }
539
540 return videoFileJson
541 })
542 .sort((a, b) => {
543 if (a.resolution < b.resolution) return 1
544 if (a.resolution === b.resolution) return 0
545 return -1
546 })
93e1258c 547
9567011b 548 return Object.assign(formattedJson, detailsJson)
aaf61f38
C
549}
550
e4f97bab
C
551toActivityPubObject = function (this: VideoInstance) {
552 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
9a27cdc2 553 if (!this.Tags) this.Tags = []
aaf61f38 554
e4f97bab 555 const tag = this.Tags.map(t => ({
571389d4 556 type: 'Hashtag' as 'Hashtag',
e4f97bab
C
557 name: t.name
558 }))
559
560 const url = []
561 for (const file of this.VideoFiles) {
562 url.push({
563 type: 'Link',
efc32059 564 mimeType: 'video/' + file.extname.replace('.', ''),
e4f97bab
C
565 url: getVideoFileUrl(this, file, baseUrlHttp),
566 width: file.resolution,
567 size: file.size
568 })
aaf61f38 569
e4f97bab
C
570 url.push({
571 type: 'Link',
572 mimeType: 'application/x-bittorrent',
573 url: getTorrentUrl(this, file, baseUrlHttp),
574 width: file.resolution
93e1258c
C
575 })
576
e4f97bab
C
577 url.push({
578 type: 'Link',
579 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
580 url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
581 width: file.resolution
582 })
583 }
aaf61f38 584
e4f97bab 585 const videoObject: VideoTorrentObject = {
571389d4 586 type: 'Video' as 'Video',
0d0e8dd0 587 id: getActivityPubUrl('video', this.uuid),
7b1f49de 588 name: this.name,
e4f97bab
C
589 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
590 duration: 'PT' + this.duration + 'S',
591 uuid: this.uuid,
592 tag,
593 category: {
571389d4
C
594 identifier: this.category + '',
595 name: this.getCategoryLabel()
e4f97bab
C
596 },
597 licence: {
571389d4 598 identifier: this.licence + '',
e4f97bab
C
599 name: this.getLicenceLabel()
600 },
601 language: {
571389d4 602 identifier: this.language + '',
e4f97bab
C
603 name: this.getLanguageLabel()
604 },
d38b8281 605 views: this.views,
e4f97bab 606 nsfw: this.nsfw,
efc32059
C
607 published: this.createdAt.toISOString(),
608 updated: this.updatedAt.toISOString(),
e4f97bab
C
609 mediaType: 'text/markdown',
610 content: this.getTruncatedDescription(),
611 icon: {
612 type: 'Image',
613 url: getThumbnailUrl(this, baseUrlHttp),
614 mediaType: 'image/jpeg',
615 width: THUMBNAILS_SIZE.width,
616 height: THUMBNAILS_SIZE.height
617 },
618 url
7b1f49de
C
619 }
620
e4f97bab 621 return videoObject
7b1f49de
C
622}
623
9567011b
C
624getTruncatedDescription = function (this: VideoInstance) {
625 const options = {
626 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
627 }
628
629 return truncate(this.description, options)
630}
631
e4f97bab 632optimizeOriginalVideofile = async function (this: VideoInstance) {
65fcc311 633 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
227d02fe 634 const newExtname = '.mp4'
40298b02 635 const inputVideoFile = this.getOriginalFile()
93e1258c
C
636 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
637 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
227d02fe 638
14d3270f
C
639 const transcodeOptions = {
640 inputPath: videoInputPath,
641 outputPath: videoOutputPath
642 }
643
e4f97bab
C
644 try {
645 // Could be very long!
646 await transcode(transcodeOptions)
14d3270f 647
e4f97bab 648 await unlinkPromise(videoInputPath)
14d3270f 649
e4f97bab
C
650 // Important to do this before getVideoFilename() to take in account the new file extension
651 inputVideoFile.set('extname', newExtname)
652
653 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
654 const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
655
656 inputVideoFile.set('size', stats.size)
657
658 await this.createTorrentAndSetInfoHash(inputVideoFile)
659 await inputVideoFile.save()
660
661 } catch (err) {
662 // Auto destruction...
663 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
664
665 throw err
666 }
227d02fe
C
667}
668
e4f97bab 669transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) {
40298b02
C
670 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
671 const extname = '.mp4'
672
673 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
674 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
675
676 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
677 resolution,
678 extname,
679 size: 0,
680 videoId: this.id
681 })
682 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
14d3270f
C
683
684 const transcodeOptions = {
685 inputPath: videoInputPath,
686 outputPath: videoOutputPath,
687 resolution
688 }
14d3270f 689
e4f97bab
C
690 await transcode(transcodeOptions)
691
692 const stats = await statPromise(videoOutputPath)
693
694 newVideoFile.set('size', stats.size)
695
696 await this.createTorrentAndSetInfoHash(newVideoFile)
697
698 await newVideoFile.save()
699
700 this.VideoFiles.push(newVideoFile)
40298b02
C
701}
702
703getOriginalFileHeight = function (this: VideoInstance) {
704 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
705
14d3270f 706 return getVideoFileHeight(originalFilePath)
40298b02
C
707}
708
9567011b
C
709getDescriptionPath = function (this: VideoInstance) {
710 return `/api/${API_VERSION}/videos/${this.uuid}/description`
711}
712
e4f97bab
C
713getCategoryLabel = function (this: VideoInstance) {
714 let categoryLabel = VIDEO_CATEGORIES[this.category]
715
60862425 716 // Maybe our server is not up to date and there are new categories since our version
e4f97bab
C
717 if (!categoryLabel) categoryLabel = 'Misc'
718
719 return categoryLabel
720}
721
722getLicenceLabel = function (this: VideoInstance) {
723 let licenceLabel = VIDEO_LICENCES[this.licence]
0d0e8dd0 724
60862425 725 // Maybe our server is not up to date and there are new licences since our version
e4f97bab
C
726 if (!licenceLabel) licenceLabel = 'Unknown'
727
728 return licenceLabel
729}
730
731getLanguageLabel = function (this: VideoInstance) {
732 // Language is an optional attribute
733 let languageLabel = VIDEO_LANGUAGES[this.language]
734 if (!languageLabel) languageLabel = 'Unknown'
735
736 return languageLabel
737}
738
93e1258c
C
739removeThumbnail = function (this: VideoInstance) {
740 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
741 return unlinkPromise(thumbnailPath)
742}
743
744removePreview = function (this: VideoInstance) {
745 // Same name than video thumbnail
746 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
747}
748
749removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
750 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
751 return unlinkPromise(filePath)
752}
753
754removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
b0f9f39e
C
755 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
756 return unlinkPromise(torrentPath)
93e1258c
C
757}
758
aaf61f38
C
759// ------------------------------ STATICS ------------------------------
760
6fcd19ba 761generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
c77fa067
C
762 // Creating the thumbnail for a remote video
763
764 const thumbnailName = video.getThumbnailName()
65fcc311 765 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
6fcd19ba
C
766 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
767 return thumbnailName
c77fa067
C
768 })
769}
770
6fcd19ba 771list = function () {
93e1258c
C
772 const query = {
773 include: [ Video['sequelize'].models.VideoFile ]
774 }
775
776 return Video.findAll(query)
b769007f
C
777}
778
fd45e8f4
C
779listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
780 const query = {
781 distinct: true,
782 offset: start,
783 limit: count,
784 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
785 include: [
786 {
787 model: Video['sequelize'].models.VideoChannel,
788 required: true,
789 include: [
790 {
e4f97bab 791 model: Video['sequelize'].models.Account,
fd45e8f4
C
792 where: {
793 userId
794 },
795 required: true
796 }
797 ]
798 },
799 Video['sequelize'].models.Tag
800 ]
801 }
802
803 return Video.findAndCountAll(query).then(({ rows, count }) => {
804 return {
805 data: rows,
806 total: count
807 }
808 })
809}
810
6fcd19ba 811listForApi = function (start: number, count: number, sort: string) {
feb4bdfd 812 const query = {
e02643f3 813 distinct: true,
feb4bdfd
C
814 offset: start,
815 limit: count,
e02643f3 816 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
feb4bdfd
C
817 include: [
818 {
72c7248b 819 model: Video['sequelize'].models.VideoChannel,
8e10cf1a 820 required: true,
72c7248b
C
821 include: [
822 {
e4f97bab 823 model: Video['sequelize'].models.Account,
8e10cf1a 824 required: true,
72c7248b
C
825 include: [
826 {
60862425 827 model: Video['sequelize'].models.Server,
72c7248b
C
828 required: false
829 }
830 ]
831 }
832 ]
7920c273 833 },
fd45e8f4 834 Video['sequelize'].models.Tag
198b205c 835 ],
e02643f3 836 where: createBaseVideosWhere()
feb4bdfd
C
837 }
838
6fcd19ba
C
839 return Video.findAndCountAll(query).then(({ rows, count }) => {
840 return {
841 data: rows,
842 total: count
843 }
feb4bdfd 844 })
aaf61f38
C
845}
846
72c7248b
C
847loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
848 const query: Sequelize.FindOptions<VideoAttributes> = {
feb4bdfd 849 where: {
0a6658fd 850 uuid
feb4bdfd
C
851 },
852 include: [
93e1258c
C
853 {
854 model: Video['sequelize'].models.VideoFile
855 },
feb4bdfd 856 {
72c7248b 857 model: Video['sequelize'].models.VideoChannel,
feb4bdfd
C
858 include: [
859 {
e4f97bab 860 model: Video['sequelize'].models.Account,
72c7248b
C
861 include: [
862 {
60862425 863 model: Video['sequelize'].models.Server,
72c7248b
C
864 required: true,
865 where: {
866 host: fromHost
867 }
868 }
869 ]
feb4bdfd
C
870 }
871 ]
872 }
873 ]
874 }
aaf61f38 875
72c7248b
C
876 if (t !== undefined) query.transaction = t
877
6fcd19ba 878 return Video.findOne(query)
aaf61f38
C
879}
880
e4f97bab 881listOwnedAndPopulateAccountAndTags = function () {
feb4bdfd
C
882 const query = {
883 where: {
0a6658fd 884 remote: false
feb4bdfd 885 },
93e1258c
C
886 include: [
887 Video['sequelize'].models.VideoFile,
72c7248b
C
888 {
889 model: Video['sequelize'].models.VideoChannel,
e4f97bab 890 include: [ Video['sequelize'].models.Account ]
72c7248b 891 },
93e1258c
C
892 Video['sequelize'].models.Tag
893 ]
feb4bdfd
C
894 }
895
6fcd19ba 896 return Video.findAll(query)
aaf61f38
C
897}
898
e4f97bab 899listOwnedByAccount = function (account: string) {
feb4bdfd
C
900 const query = {
901 where: {
0a6658fd 902 remote: false
feb4bdfd
C
903 },
904 include: [
93e1258c
C
905 {
906 model: Video['sequelize'].models.VideoFile
907 },
feb4bdfd 908 {
72c7248b
C
909 model: Video['sequelize'].models.VideoChannel,
910 include: [
911 {
e4f97bab 912 model: Video['sequelize'].models.Account,
72c7248b 913 where: {
e4f97bab 914 name: account
72c7248b
C
915 }
916 }
917 ]
feb4bdfd
C
918 }
919 ]
920 }
9bd26629 921
6fcd19ba 922 return Video.findAll(query)
aaf61f38
C
923}
924
0a6658fd 925load = function (id: number) {
6fcd19ba 926 return Video.findById(id)
feb4bdfd
C
927}
928
72c7248b
C
929loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
930 const query: Sequelize.FindOptions<VideoAttributes> = {
0a6658fd
C
931 where: {
932 uuid
93e1258c
C
933 },
934 include: [ Video['sequelize'].models.VideoFile ]
a041b171
C
935 }
936
937 if (t !== undefined) query.transaction = t
938
939 return Video.findOne(query)
940}
941
d7d5611c
C
942loadByUrlAndPopulateAccount = function (url: string, t?: Sequelize.Transaction) {
943 const query: Sequelize.FindOptions<VideoAttributes> = {
944 where: {
945 url
946 },
947 include: [
948 Video['sequelize'].models.VideoFile,
949 {
950 model: Video['sequelize'].models.VideoChannel,
951 include: [ Video['sequelize'].models.Account ]
952 }
953 ]
954 }
955
956 if (t !== undefined) query.transaction = t
957
958 return Video.findOne(query)
959}
960
0d0e8dd0
C
961loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) {
962 const query: Sequelize.FindOptions<VideoAttributes> = {
963 where: {
964 [Sequelize.Op.or]: [
965 { uuid },
966 { url }
967 ]
968 },
969 include: [ Video['sequelize'].models.VideoFile ]
970 }
971
972 if (t !== undefined) query.transaction = t
973
974 return Video.findOne(query)
975}
976
a041b171
C
977loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
978 const query: Sequelize.FindOptions<VideoAttributes> = {
979 where: {
980 uuid,
981 remote: false
982 },
983 include: [ Video['sequelize'].models.VideoFile ]
0a6658fd 984 }
72c7248b
C
985
986 if (t !== undefined) query.transaction = t
987
0a6658fd
C
988 return Video.findOne(query)
989}
990
e4f97bab 991loadAndPopulateAccount = function (id: number) {
feb4bdfd 992 const options = {
72c7248b
C
993 include: [
994 Video['sequelize'].models.VideoFile,
995 {
996 model: Video['sequelize'].models.VideoChannel,
e4f97bab 997 include: [ Video['sequelize'].models.Account ]
72c7248b
C
998 }
999 ]
feb4bdfd
C
1000 }
1001
6fcd19ba 1002 return Video.findById(id, options)
feb4bdfd
C
1003}
1004
60862425 1005loadAndPopulateAccountAndServerAndTags = function (id: number) {
feb4bdfd
C
1006 const options = {
1007 include: [
1008 {
72c7248b
C
1009 model: Video['sequelize'].models.VideoChannel,
1010 include: [
1011 {
e4f97bab 1012 model: Video['sequelize'].models.Account,
60862425 1013 include: [ { model: Video['sequelize'].models.Server, required: false } ]
72c7248b
C
1014 }
1015 ]
7920c273 1016 },
93e1258c
C
1017 Video['sequelize'].models.Tag,
1018 Video['sequelize'].models.VideoFile
feb4bdfd
C
1019 ]
1020 }
1021
6fcd19ba 1022 return Video.findById(id, options)
aaf61f38
C
1023}
1024
60862425 1025loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) {
0a6658fd
C
1026 const options = {
1027 where: {
1028 uuid
1029 },
1030 include: [
1031 {
72c7248b
C
1032 model: Video['sequelize'].models.VideoChannel,
1033 include: [
1034 {
e4f97bab 1035 model: Video['sequelize'].models.Account,
60862425 1036 include: [ { model: Video['sequelize'].models.Server, required: false } ]
72c7248b
C
1037 }
1038 ]
0a6658fd 1039 },
93e1258c
C
1040 Video['sequelize'].models.Tag,
1041 Video['sequelize'].models.VideoFile
0a6658fd
C
1042 ]
1043 }
1044
1045 return Video.findOne(options)
1046}
1047
60862425
C
1048searchAndPopulateAccountAndServerAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
1049 const serverInclude: Sequelize.IncludeOptions = {
1050 model: Video['sequelize'].models.Server,
7920c273 1051 required: false
feb4bdfd 1052 }
7920c273 1053
e4f97bab
C
1054 const accountInclude: Sequelize.IncludeOptions = {
1055 model: Video['sequelize'].models.Account,
60862425 1056 include: [ serverInclude ]
72c7248b
C
1057 }
1058
1059 const videoChannelInclude: Sequelize.IncludeOptions = {
1060 model: Video['sequelize'].models.VideoChannel,
e4f97bab 1061 include: [ accountInclude ],
72c7248b 1062 required: true
feb4bdfd
C
1063 }
1064
e6d4b0ff 1065 const tagInclude: Sequelize.IncludeOptions = {
e02643f3 1066 model: Video['sequelize'].models.Tag
7920c273
C
1067 }
1068
556ddc31 1069 const query: Sequelize.FindOptions<VideoAttributes> = {
e02643f3
C
1070 distinct: true,
1071 where: createBaseVideosWhere(),
feb4bdfd
C
1072 offset: start,
1073 limit: count,
e02643f3 1074 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
feb4bdfd
C
1075 }
1076
fd45e8f4 1077 if (field === 'tags') {
e02643f3 1078 const escapedValue = Video['sequelize'].escape('%' + value + '%')
c2962505 1079 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
6fcd19ba
C
1080 `(SELECT "VideoTags"."videoId"
1081 FROM "Tags"
1082 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
18c8e945 1083 WHERE name ILIKE ${escapedValue}
6fcd19ba 1084 )`
198b205c 1085 )
7920c273 1086 } else if (field === 'host') {
60862425
C
1087 // FIXME: Include our server? (not stored in the database)
1088 serverInclude.where = {
7920c273 1089 host: {
c2962505 1090 [Sequelize.Op.iLike]: '%' + value + '%'
feb4bdfd 1091 }
feb4bdfd 1092 }
60862425 1093 serverInclude.required = true
e4f97bab
C
1094 } else if (field === 'account') {
1095 accountInclude.where = {
7920c273 1096 name: {
c2962505 1097 [Sequelize.Op.iLike]: '%' + value + '%'
feb4bdfd
C
1098 }
1099 }
aaf61f38 1100 } else {
feb4bdfd 1101 query.where[field] = {
c2962505 1102 [Sequelize.Op.iLike]: '%' + value + '%'
feb4bdfd 1103 }
aaf61f38
C
1104 }
1105
7920c273 1106 query.include = [
fd45e8f4 1107 videoChannelInclude, tagInclude
7920c273
C
1108 ]
1109
6fcd19ba
C
1110 return Video.findAndCountAll(query).then(({ rows, count }) => {
1111 return {
1112 data: rows,
1113 total: count
1114 }
feb4bdfd 1115 })
aaf61f38
C
1116}
1117
aaf61f38
C
1118// ---------------------------------------------------------------------------
1119
15d4ee04
C
1120function createBaseVideosWhere () {
1121 return {
1122 id: {
c2962505 1123 [Sequelize.Op.notIn]: Video['sequelize'].literal(
15d4ee04
C
1124 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1125 )
fd45e8f4
C
1126 },
1127 privacy: VideoPrivacy.PUBLIC
15d4ee04
C
1128 }
1129}
a96aed15
C
1130
1131function getBaseUrls (video: VideoInstance) {
1132 let baseUrlHttp
1133 let baseUrlWs
1134
1135 if (video.isOwned()) {
1136 baseUrlHttp = CONFIG.WEBSERVER.URL
1137 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1138 } else {
60862425
C
1139 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Server.host
1140 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Server.host
a96aed15
C
1141 }
1142
1143 return { baseUrlHttp, baseUrlWs }
1144}
1145
e4f97bab
C
1146function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
1147 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
1148}
1149
a96aed15
C
1150function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1151 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1152}
1153
1154function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1155 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1156}
1157
1158function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1159 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1160 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1161 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1162
1163 const magnetHash = {
1164 xs,
1165 announce,
1166 urlList,
1167 infoHash: videoFile.infoHash,
1168 name: video.name
1169 }
1170
1171 return magnetUtil.encode(magnetHash)
1172}