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