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