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