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