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