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