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