]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Use preview instead of thumbnail for oembed
[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 generateMagnetUri: VideoMethods.GenerateMagnetUri
56let getVideoFilename: VideoMethods.GetVideoFilename
57let getThumbnailName: VideoMethods.GetThumbnailName
d8755eed 58let getThumbnailPath: VideoMethods.GetThumbnailPath
e02643f3 59let getPreviewName: VideoMethods.GetPreviewName
d8755eed 60let getPreviewPath: VideoMethods.GetPreviewPath
93e1258c 61let getTorrentFileName: VideoMethods.GetTorrentFileName
e02643f3 62let isOwned: VideoMethods.IsOwned
0aef76c4 63let toFormattedJSON: VideoMethods.ToFormattedJSON
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
C
208 indexes: [
209 {
210 fields: [ 'authorId' ]
211 },
319d072e
C
212 {
213 fields: [ 'name' ]
214 },
215 {
216 fields: [ 'createdAt' ]
217 },
218 {
219 fields: [ 'duration' ]
220 },
9e167724
C
221 {
222 fields: [ 'views' ]
d38b8281
C
223 },
224 {
225 fields: [ 'likes' ]
0a6658fd
C
226 },
227 {
228 fields: [ 'uuid' ]
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 generateMagnetUri,
e02643f3 258 getPreviewName,
d8755eed 259 getPreviewPath,
93e1258c 260 getThumbnailName,
d8755eed 261 getThumbnailPath,
93e1258c
C
262 getTorrentFileName,
263 getVideoFilename,
264 getVideoFilePath,
40298b02 265 getOriginalFile,
e02643f3 266 isOwned,
93e1258c
C
267 removeFile,
268 removePreview,
269 removeThumbnail,
270 removeTorrent,
e02643f3 271 toAddRemoteJSON,
0aef76c4 272 toFormattedJSON,
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) {
e02643f3 287 Video.belongsTo(models.Author, {
feb4bdfd
C
288 foreignKey: {
289 name: 'authorId',
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
C
336 video.VideoFiles.forEach(file => {
337 video.removeFile(file),
338 video.removeTorrent(file)
339 })
f285faa0
C
340 }
341
93e1258c 342 return Promise.all(tasks)
558d7c23
C
343}
344
40298b02
C
345getOriginalFile = function (this: VideoInstance) {
346 if (Array.isArray(this.VideoFiles) === false) return undefined
347
14d3270f
C
348 // The original file is the file that have the higher resolution
349 return maxBy(this.VideoFiles, file => file.resolution)
40298b02
C
350}
351
93e1258c 352getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
14d3270f 353 return this.uuid + '-' + videoFile.resolution + videoFile.extname
f285faa0
C
354}
355
70c065d6 356getThumbnailName = function (this: VideoInstance) {
f285faa0 357 // We always have a copy of the thumbnail
0a6658fd
C
358 const extension = '.jpg'
359 return this.uuid + extension
558d7c23
C
360}
361
70c065d6 362getPreviewName = function (this: VideoInstance) {
f285faa0 363 const extension = '.jpg'
0a6658fd 364 return this.uuid + extension
f285faa0
C
365}
366
93e1258c 367getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
f285faa0 368 const extension = '.torrent'
14d3270f 369 return this.uuid + '-' + videoFile.resolution + extension
558d7c23
C
370}
371
70c065d6 372isOwned = function (this: VideoInstance) {
0a6658fd 373 return this.remote === false
aaf61f38
C
374}
375
93e1258c 376createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
164174a6
C
377 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
378
14d3270f
C
379 return generateImageFromVideoFile(
380 this.getVideoFilePath(videoFile),
381 CONFIG.STORAGE.PREVIEWS_DIR,
164174a6
C
382 this.getPreviewName(),
383 imageSize
14d3270f 384 )
93e1258c
C
385}
386
387createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
d8755eed
C
388 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
389
14d3270f
C
390 return generateImageFromVideoFile(
391 this.getVideoFilePath(videoFile),
392 CONFIG.STORAGE.THUMBNAILS_DIR,
393 this.getThumbnailName(),
d8755eed 394 imageSize
14d3270f 395 )
93e1258c
C
396}
397
398getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
399 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
400}
401
402createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
403 const options = {
404 announceList: [
405 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
406 ],
407 urlList: [
408 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
409 ]
410 }
411
412 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
413 .then(torrent => {
414 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
fdbda9e3
C
415 logger.info('Creating torrent %s.', filePath)
416
93e1258c
C
417 return writeFilePromise(filePath, torrent).then(() => torrent)
418 })
419 .then(torrent => {
420 const parsedTorrent = parseTorrent(torrent)
421
422 videoFile.infoHash = parsedTorrent.infoHash
423 })
424}
425
426generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) {
427 let baseUrlHttp
428 let baseUrlWs
429
430 if (this.isOwned()) {
431 baseUrlHttp = CONFIG.WEBSERVER.URL
432 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
433 } else {
434 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
435 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
436 }
437
438 const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
439 const announce = [ baseUrlWs + '/tracker/socket' ]
440 const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
441
442 const magnetHash = {
443 xs,
444 announce,
445 urlList,
446 infoHash: videoFile.infoHash,
447 name: this.name
448 }
449
450 return magnetUtil.encode(magnetHash)
451}
452
d8755eed
C
453getEmbedPath = function (this: VideoInstance) {
454 return '/videos/embed/' + this.uuid
455}
456
457getThumbnailPath = function (this: VideoInstance) {
458 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
459}
460
461getPreviewPath = function (this: VideoInstance) {
462 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
463}
464
0aef76c4 465toFormattedJSON = function (this: VideoInstance) {
feb4bdfd
C
466 let podHost
467
468 if (this.Author.Pod) {
469 podHost = this.Author.Pod.host
470 } else {
471 // It means it's our video
65fcc311 472 podHost = CONFIG.WEBSERVER.HOST
feb4bdfd
C
473 }
474
6e07c3de 475 // Maybe our pod is not up to date and there are new categories since our version
65fcc311 476 let categoryLabel = VIDEO_CATEGORIES[this.category]
6e07c3de
C
477 if (!categoryLabel) categoryLabel = 'Misc'
478
6f0c39e2 479 // Maybe our pod is not up to date and there are new licences since our version
65fcc311 480 let licenceLabel = VIDEO_LICENCES[this.licence]
6f0c39e2
C
481 if (!licenceLabel) licenceLabel = 'Unknown'
482
3092476e 483 // Language is an optional attribute
65fcc311 484 let languageLabel = VIDEO_LANGUAGES[this.language]
3092476e
C
485 if (!languageLabel) languageLabel = 'Unknown'
486
aaf61f38 487 const json = {
feb4bdfd 488 id: this.id,
0a6658fd 489 uuid: this.uuid,
aaf61f38 490 name: this.name,
6e07c3de
C
491 category: this.category,
492 categoryLabel,
6f0c39e2
C
493 licence: this.licence,
494 licenceLabel,
3092476e
C
495 language: this.language,
496 languageLabel,
31b59b47 497 nsfw: this.nsfw,
aaf61f38 498 description: this.description,
feb4bdfd 499 podHost,
aaf61f38 500 isLocal: this.isOwned(),
feb4bdfd 501 author: this.Author.name,
aaf61f38 502 duration: this.duration,
9e167724 503 views: this.views,
d38b8281
C
504 likes: this.likes,
505 dislikes: this.dislikes,
6fcd19ba 506 tags: map<TagInstance, string>(this.Tags, 'name'),
d8755eed
C
507 thumbnailPath: this.getThumbnailPath(),
508 previewPath: this.getPreviewPath(),
509 embedPath: this.getEmbedPath(),
79066fdf 510 createdAt: this.createdAt,
93e1258c
C
511 updatedAt: this.updatedAt,
512 files: []
aaf61f38
C
513 }
514
aa8b6df4
C
515 // Format and sort video files
516 json.files = this.VideoFiles
517 .map(videoFile => {
14d3270f 518 let resolutionLabel = videoFile.resolution + 'p'
aa8b6df4
C
519
520 const videoFileJson = {
521 resolution: videoFile.resolution,
522 resolutionLabel,
523 magnetUri: this.generateMagnetUri(videoFile),
524 size: videoFile.size
525 }
526
527 return videoFileJson
528 })
529 .sort((a, b) => {
530 if (a.resolution < b.resolution) return 1
531 if (a.resolution === b.resolution) return 0
532 return -1
533 })
93e1258c 534
aaf61f38
C
535 return json
536}
537
6fcd19ba 538toAddRemoteJSON = function (this: VideoInstance) {
4d324488 539 // Get thumbnail data to send to the other pod
65fcc311 540 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
aaf61f38 541
6fcd19ba 542 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
aaf61f38 543 const remoteVideo = {
0a6658fd 544 uuid: this.uuid,
e02643f3
C
545 name: this.name,
546 category: this.category,
547 licence: this.licence,
548 language: this.language,
549 nsfw: this.nsfw,
550 description: this.description,
e02643f3
C
551 author: this.Author.name,
552 duration: this.duration,
4d324488 553 thumbnailData: thumbnailData.toString('binary'),
6fcd19ba 554 tags: map<TagInstance, string>(this.Tags, 'name'),
e02643f3
C
555 createdAt: this.createdAt,
556 updatedAt: this.updatedAt,
e02643f3
C
557 views: this.views,
558 likes: this.likes,
93e1258c
C
559 dislikes: this.dislikes,
560 files: []
aaf61f38
C
561 }
562
93e1258c
C
563 this.VideoFiles.forEach(videoFile => {
564 remoteVideo.files.push({
565 infoHash: videoFile.infoHash,
566 resolution: videoFile.resolution,
567 extname: videoFile.extname,
568 size: videoFile.size
569 })
570 })
571
6fcd19ba 572 return remoteVideo
aaf61f38
C
573 })
574}
575
70c065d6 576toUpdateRemoteJSON = function (this: VideoInstance) {
7b1f49de 577 const json = {
0a6658fd 578 uuid: this.uuid,
7b1f49de 579 name: this.name,
6e07c3de 580 category: this.category,
6f0c39e2 581 licence: this.licence,
3092476e 582 language: this.language,
31b59b47 583 nsfw: this.nsfw,
7b1f49de 584 description: this.description,
7b1f49de
C
585 author: this.Author.name,
586 duration: this.duration,
6fcd19ba 587 tags: map<TagInstance, string>(this.Tags, 'name'),
7b1f49de 588 createdAt: this.createdAt,
79066fdf 589 updatedAt: this.updatedAt,
d38b8281
C
590 views: this.views,
591 likes: this.likes,
93e1258c
C
592 dislikes: this.dislikes,
593 files: []
7b1f49de
C
594 }
595
93e1258c
C
596 this.VideoFiles.forEach(videoFile => {
597 json.files.push({
598 infoHash: videoFile.infoHash,
599 resolution: videoFile.resolution,
600 extname: videoFile.extname,
601 size: videoFile.size
602 })
603 })
604
7b1f49de
C
605 return json
606}
607
40298b02 608optimizeOriginalVideofile = function (this: VideoInstance) {
65fcc311 609 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
227d02fe 610 const newExtname = '.mp4'
40298b02 611 const inputVideoFile = this.getOriginalFile()
93e1258c
C
612 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
613 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
227d02fe 614
14d3270f
C
615 const transcodeOptions = {
616 inputPath: videoInputPath,
617 outputPath: videoOutputPath
618 }
619
620 return transcode(transcodeOptions)
621 .then(() => {
622 return unlinkPromise(videoInputPath)
623 })
624 .then(() => {
625 // Important to do this before getVideoFilename() to take in account the new file extension
626 inputVideoFile.set('extname', newExtname)
627
628 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
629 })
630 .then(() => {
631 return statPromise(this.getVideoFilePath(inputVideoFile))
632 })
633 .then(stats => {
634 return inputVideoFile.set('size', stats.size)
635 })
636 .then(() => {
637 return this.createTorrentAndSetInfoHash(inputVideoFile)
638 })
639 .then(() => {
640 return inputVideoFile.save()
641 })
642 .then(() => {
643 return undefined
644 })
645 .catch(err => {
646 // Auto destruction...
647 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
648
649 throw err
650 })
227d02fe
C
651}
652
40298b02
C
653transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
654 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
655 const extname = '.mp4'
656
657 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
658 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
659
660 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
661 resolution,
662 extname,
663 size: 0,
664 videoId: this.id
665 })
666 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
14d3270f
C
667
668 const transcodeOptions = {
669 inputPath: videoInputPath,
670 outputPath: videoOutputPath,
671 resolution
672 }
673 return transcode(transcodeOptions)
674 .then(() => {
675 return statPromise(videoOutputPath)
676 })
677 .then(stats => {
678 newVideoFile.set('size', stats.size)
679
680 return undefined
681 })
682 .then(() => {
683 return this.createTorrentAndSetInfoHash(newVideoFile)
684 })
685 .then(() => {
686 return newVideoFile.save()
687 })
688 .then(() => {
689 return this.VideoFiles.push(newVideoFile)
690 })
691 .then(() => undefined)
40298b02
C
692}
693
694getOriginalFileHeight = function (this: VideoInstance) {
695 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
696
14d3270f 697 return getVideoFileHeight(originalFilePath)
40298b02
C
698}
699
93e1258c
C
700removeThumbnail = function (this: VideoInstance) {
701 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
702 return unlinkPromise(thumbnailPath)
703}
704
705removePreview = function (this: VideoInstance) {
706 // Same name than video thumbnail
707 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
708}
709
710removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
711 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
712 return unlinkPromise(filePath)
713}
714
715removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
b0f9f39e
C
716 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
717 return unlinkPromise(torrentPath)
93e1258c
C
718}
719
aaf61f38
C
720// ------------------------------ STATICS ------------------------------
721
6fcd19ba 722generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
c77fa067
C
723 // Creating the thumbnail for a remote video
724
725 const thumbnailName = video.getThumbnailName()
65fcc311 726 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
6fcd19ba
C
727 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
728 return thumbnailName
c77fa067
C
729 })
730}
731
6fcd19ba 732list = function () {
93e1258c
C
733 const query = {
734 include: [ Video['sequelize'].models.VideoFile ]
735 }
736
737 return Video.findAll(query)
b769007f
C
738}
739
6fcd19ba 740listForApi = function (start: number, count: number, sort: string) {
556ddc31 741 // Exclude blacklisted videos from the list
feb4bdfd 742 const query = {
e02643f3 743 distinct: true,
feb4bdfd
C
744 offset: start,
745 limit: count,
e02643f3 746 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
feb4bdfd
C
747 include: [
748 {
e02643f3
C
749 model: Video['sequelize'].models.Author,
750 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
7920c273 751 },
93e1258c
C
752 Video['sequelize'].models.Tag,
753 Video['sequelize'].models.VideoFile
198b205c 754 ],
e02643f3 755 where: createBaseVideosWhere()
feb4bdfd
C
756 }
757
6fcd19ba
C
758 return Video.findAndCountAll(query).then(({ rows, count }) => {
759 return {
760 data: rows,
761 total: count
762 }
feb4bdfd 763 })
aaf61f38
C
764}
765
0a6658fd 766loadByHostAndUUID = function (fromHost: string, uuid: string) {
feb4bdfd
C
767 const query = {
768 where: {
0a6658fd 769 uuid
feb4bdfd
C
770 },
771 include: [
93e1258c
C
772 {
773 model: Video['sequelize'].models.VideoFile
774 },
feb4bdfd 775 {
e02643f3 776 model: Video['sequelize'].models.Author,
feb4bdfd
C
777 include: [
778 {
e02643f3 779 model: Video['sequelize'].models.Pod,
7920c273 780 required: true,
feb4bdfd
C
781 where: {
782 host: fromHost
783 }
784 }
785 ]
786 }
787 ]
788 }
aaf61f38 789
6fcd19ba 790 return Video.findOne(query)
aaf61f38
C
791}
792
6fcd19ba 793listOwnedAndPopulateAuthorAndTags = function () {
feb4bdfd
C
794 const query = {
795 where: {
0a6658fd 796 remote: false
feb4bdfd 797 },
93e1258c
C
798 include: [
799 Video['sequelize'].models.VideoFile,
800 Video['sequelize'].models.Author,
801 Video['sequelize'].models.Tag
802 ]
feb4bdfd
C
803 }
804
6fcd19ba 805 return Video.findAll(query)
aaf61f38
C
806}
807
6fcd19ba 808listOwnedByAuthor = function (author: string) {
feb4bdfd
C
809 const query = {
810 where: {
0a6658fd 811 remote: false
feb4bdfd
C
812 },
813 include: [
93e1258c
C
814 {
815 model: Video['sequelize'].models.VideoFile
816 },
feb4bdfd 817 {
e02643f3 818 model: Video['sequelize'].models.Author,
feb4bdfd
C
819 where: {
820 name: author
821 }
822 }
823 ]
824 }
9bd26629 825
6fcd19ba 826 return Video.findAll(query)
aaf61f38
C
827}
828
0a6658fd 829load = function (id: number) {
6fcd19ba 830 return Video.findById(id)
feb4bdfd
C
831}
832
0a6658fd
C
833loadByUUID = function (uuid: string) {
834 const query = {
835 where: {
836 uuid
93e1258c
C
837 },
838 include: [ Video['sequelize'].models.VideoFile ]
0a6658fd
C
839 }
840 return Video.findOne(query)
841}
842
843loadAndPopulateAuthor = function (id: number) {
feb4bdfd 844 const options = {
93e1258c 845 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
feb4bdfd
C
846 }
847
6fcd19ba 848 return Video.findById(id, options)
feb4bdfd
C
849}
850
0a6658fd 851loadAndPopulateAuthorAndPodAndTags = function (id: number) {
feb4bdfd
C
852 const options = {
853 include: [
854 {
e02643f3
C
855 model: Video['sequelize'].models.Author,
856 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
7920c273 857 },
93e1258c
C
858 Video['sequelize'].models.Tag,
859 Video['sequelize'].models.VideoFile
feb4bdfd
C
860 ]
861 }
862
6fcd19ba 863 return Video.findById(id, options)
aaf61f38
C
864}
865
0a6658fd
C
866loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
867 const options = {
868 where: {
869 uuid
870 },
871 include: [
872 {
873 model: Video['sequelize'].models.Author,
874 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
875 },
93e1258c
C
876 Video['sequelize'].models.Tag,
877 Video['sequelize'].models.VideoFile
0a6658fd
C
878 ]
879 }
880
881 return Video.findOne(options)
882}
883
6fcd19ba 884searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
e6d4b0ff 885 const podInclude: Sequelize.IncludeOptions = {
e02643f3 886 model: Video['sequelize'].models.Pod,
7920c273 887 required: false
feb4bdfd 888 }
7920c273 889
e6d4b0ff 890 const authorInclude: Sequelize.IncludeOptions = {
e02643f3 891 model: Video['sequelize'].models.Author,
feb4bdfd
C
892 include: [
893 podInclude
894 ]
895 }
896
e6d4b0ff 897 const tagInclude: Sequelize.IncludeOptions = {
e02643f3 898 model: Video['sequelize'].models.Tag
7920c273
C
899 }
900
93e1258c
C
901 const videoFileInclude: Sequelize.IncludeOptions = {
902 model: Video['sequelize'].models.VideoFile
903 }
904
556ddc31 905 const query: Sequelize.FindOptions<VideoAttributes> = {
e02643f3
C
906 distinct: true,
907 where: createBaseVideosWhere(),
feb4bdfd
C
908 offset: start,
909 limit: count,
e02643f3 910 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
feb4bdfd
C
911 }
912
aaf61f38 913 // Make an exact search with the magnet
55723d16 914 if (field === 'magnetUri') {
93e1258c
C
915 videoFileInclude.where = {
916 infoHash: magnetUtil.decode(value).infoHash
917 }
55723d16 918 } else if (field === 'tags') {
e02643f3 919 const escapedValue = Video['sequelize'].escape('%' + value + '%')
e6d4b0ff 920 query.where['id'].$in = Video['sequelize'].literal(
6fcd19ba
C
921 `(SELECT "VideoTags"."videoId"
922 FROM "Tags"
923 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
18c8e945 924 WHERE name ILIKE ${escapedValue}
6fcd19ba 925 )`
198b205c 926 )
7920c273
C
927 } else if (field === 'host') {
928 // FIXME: Include our pod? (not stored in the database)
929 podInclude.where = {
930 host: {
18c8e945 931 $iLike: '%' + value + '%'
feb4bdfd 932 }
feb4bdfd 933 }
7920c273 934 podInclude.required = true
feb4bdfd 935 } else if (field === 'author') {
7920c273
C
936 authorInclude.where = {
937 name: {
18c8e945 938 $iLike: '%' + value + '%'
feb4bdfd
C
939 }
940 }
7920c273
C
941
942 // authorInclude.or = true
aaf61f38 943 } else {
feb4bdfd 944 query.where[field] = {
18c8e945 945 $iLike: '%' + value + '%'
feb4bdfd 946 }
aaf61f38
C
947 }
948
7920c273 949 query.include = [
93e1258c 950 authorInclude, tagInclude, videoFileInclude
7920c273
C
951 ]
952
6fcd19ba
C
953 return Video.findAndCountAll(query).then(({ rows, count }) => {
954 return {
955 data: rows,
956 total: count
957 }
feb4bdfd 958 })
aaf61f38
C
959}
960
aaf61f38
C
961// ---------------------------------------------------------------------------
962
15d4ee04
C
963function createBaseVideosWhere () {
964 return {
965 id: {
e02643f3 966 $notIn: Video['sequelize'].literal(
15d4ee04
C
967 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
968 )
969 }
970 }
971}