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