]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Fix little typo in variable.
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
CommitLineData
39445ead 1import * as Bluebird from 'bluebird'
c73e83da 2import { map, maxBy } from 'lodash'
571389d4 3import * as magnetUtil from 'magnet-uri'
4d4e5cd4 4import * as parseTorrent from 'parse-torrent'
65fcc311 5import { join } from 'path'
e02643f3 6import * as Sequelize from 'sequelize'
3fd3ab2d 7import {
4ba3b8ea
C
8 AfterDestroy,
9 AllowNull,
10 BeforeDestroy,
11 BelongsTo,
12 BelongsToMany,
13 Column,
14 CreatedAt,
15 DataType,
16 Default,
17 ForeignKey,
18 HasMany,
19 IFindOptions,
20 Is,
21 IsInt,
22 IsUUID,
23 Min,
24 Model,
25 Scopes,
26 Table,
27 UpdatedAt
3fd3ab2d 28} from 'sequelize-typescript'
571389d4 29import { VideoPrivacy, VideoResolution } from '../../../shared'
3fd3ab2d 30import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
d48ff09d 31import { Video, VideoDetails } from '../../../shared/models/videos'
066e94c5 32import { VideoFilter } from '../../../shared/models/videos/video-query.type'
da854ddd 33import { activityPubCollection } from '../../helpers/activitypub'
c73e83da
C
34import {
35 createTorrentPromise, peertubeTruncate, renamePromise, statPromise, unlinkPromise,
36 writeFilePromise
37} from '../../helpers/core-utils'
da854ddd 38import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
47564bbe 39import { isBooleanValid } from '../../helpers/custom-validators/misc'
3fd3ab2d 40import {
4ba3b8ea
C
41 isVideoCategoryValid,
42 isVideoDescriptionValid,
43 isVideoDurationValid,
44 isVideoLanguageValid,
45 isVideoLicenceValid,
46 isVideoNameValid,
b64c950a
C
47 isVideoPrivacyValid,
48 isVideoSupportValid
3fd3ab2d 49} from '../../helpers/custom-validators/videos'
056aa7f2 50import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
da854ddd 51import { logger } from '../../helpers/logger'
f05a1c30 52import { getServerActor } from '../../helpers/utils'
65fcc311 53import {
4ba3b8ea
C
54 API_VERSION,
55 CONFIG,
56 CONSTRAINTS_FIELDS,
57 PREVIEWS_SIZE,
58 REMOTE_SCHEME,
59 STATIC_PATHS,
60 THUMBNAILS_SIZE,
61 VIDEO_CATEGORIES,
62 VIDEO_LANGUAGES,
63 VIDEO_LICENCES,
64 VIDEO_PRIVACIES
3fd3ab2d 65} from '../../initializers'
46531a0a
C
66import {
67 getVideoCommentsActivityPubUrl,
68 getVideoDislikesActivityPubUrl,
69 getVideoLikesActivityPubUrl,
70 getVideoSharesActivityPubUrl
71} from '../../lib/activitypub'
50d6de9c 72import { sendDeleteVideo } from '../../lib/activitypub/send'
3fd3ab2d
C
73import { AccountModel } from '../account/account'
74import { AccountVideoRateModel } from '../account/account-video-rate'
50d6de9c 75import { ActorModel } from '../activitypub/actor'
b6a4fd6b 76import { AvatarModel } from '../avatar/avatar'
3fd3ab2d
C
77import { ServerModel } from '../server/server'
78import { getSort, throwIfNotValid } from '../utils'
79import { TagModel } from './tag'
80import { VideoAbuseModel } from './video-abuse'
81import { VideoChannelModel } from './video-channel'
da854ddd 82import { VideoCommentModel } from './video-comment'
3fd3ab2d
C
83import { VideoFileModel } from './video-file'
84import { VideoShareModel } from './video-share'
85import { VideoTagModel } from './video-tag'
86
d48ff09d 87enum ScopeNames {
50d6de9c 88 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
4cb6d457 89 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
d48ff09d
C
90 WITH_TAGS = 'WITH_TAGS',
91 WITH_FILES = 'WITH_FILES',
92 WITH_SHARES = 'WITH_SHARES',
da854ddd
C
93 WITH_RATES = 'WITH_RATES',
94 WITH_COMMENTS = 'WITH_COMMENTS'
d48ff09d
C
95}
96
97@Scopes({
066e94c5 98 [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter) => ({
d48ff09d
C
99 where: {
100 id: {
101 [Sequelize.Op.notIn]: Sequelize.literal(
102 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
2d9ab590
C
103 ),
104 [ Sequelize.Op.in ]: Sequelize.literal(
105 '(' +
106 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
107 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
108 'WHERE "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) +
109 ' UNION ' +
110 'SELECT "video"."id" AS "id" FROM "video" ' +
111 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
112 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
113 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
114 'LEFT JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
115 'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) +
116 ')'
d48ff09d 117 )
50d6de9c 118 },
2d9ab590 119 privacy: VideoPrivacy.PUBLIC
f05a1c30 120 },
4cb6d457
C
121 include: [
122 {
f05a1c30
C
123 attributes: [ 'name', 'description' ],
124 model: VideoChannelModel.unscoped(),
4cb6d457
C
125 required: true,
126 include: [
127 {
128 attributes: [ 'name' ],
f05a1c30 129 model: AccountModel.unscoped(),
4cb6d457
C
130 required: true,
131 include: [
132 {
b64c950a 133 attributes: [ 'preferredUsername', 'url', 'serverId' ],
f05a1c30 134 model: ActorModel.unscoped(),
4cb6d457 135 required: true,
066e94c5 136 where: VideoModel.buildActorWhereWithFilter(filter),
4cb6d457
C
137 include: [
138 {
f05a1c30 139 attributes: [ 'host' ],
b64c950a
C
140 model: ServerModel.unscoped(),
141 required: false
142 },
143 {
144 model: AvatarModel.unscoped(),
145 required: false
4cb6d457
C
146 }
147 ]
148 }
149 ]
150 }
151 ]
152 }
153 ]
f05a1c30 154 }),
4cb6d457 155 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
d48ff09d
C
156 include: [
157 {
6120941f 158 model: () => VideoChannelModel.unscoped(),
d48ff09d
C
159 required: true,
160 include: [
6120941f
C
161 {
162 attributes: {
163 exclude: [ 'privateKey', 'publicKey' ]
164 },
3e500247
C
165 model: () => ActorModel.unscoped(),
166 required: true,
167 include: [
168 {
169 attributes: [ 'host' ],
170 model: () => ServerModel.unscoped(),
171 required: false
172 }
173 ]
6120941f 174 },
d48ff09d 175 {
3e500247 176 model: () => AccountModel.unscoped(),
d48ff09d
C
177 required: true,
178 include: [
179 {
3e500247 180 model: () => ActorModel.unscoped(),
6120941f
C
181 attributes: {
182 exclude: [ 'privateKey', 'publicKey' ]
183 },
50d6de9c
C
184 required: true,
185 include: [
186 {
3e500247
C
187 attributes: [ 'host' ],
188 model: () => ServerModel.unscoped(),
50d6de9c 189 required: false
b6a4fd6b
C
190 },
191 {
192 model: () => AvatarModel.unscoped(),
193 required: false
50d6de9c
C
194 }
195 ]
d48ff09d
C
196 }
197 ]
198 }
199 ]
200 }
201 ]
202 },
203 [ScopeNames.WITH_TAGS]: {
204 include: [ () => TagModel ]
205 },
206 [ScopeNames.WITH_FILES]: {
207 include: [
208 {
e53f952e 209 model: () => VideoFileModel.unscoped(),
d48ff09d
C
210 required: true
211 }
212 ]
213 },
214 [ScopeNames.WITH_SHARES]: {
215 include: [
216 {
e53f952e 217 model: () => VideoShareModel.unscoped()
d48ff09d
C
218 }
219 ]
220 },
221 [ScopeNames.WITH_RATES]: {
222 include: [
223 {
224 model: () => AccountVideoRateModel,
e53f952e
C
225 include: [
226 {
227 model: () => AccountModel.unscoped(),
228 required: true,
229 include: [
230 {
231 attributes: [ 'url' ],
232 model: () => ActorModel.unscoped()
233 }
234 ]
235 }
236 ]
d48ff09d
C
237 }
238 ]
da854ddd
C
239 },
240 [ScopeNames.WITH_COMMENTS]: {
241 include: [
242 {
e53f952e 243 model: () => VideoCommentModel.unscoped()
da854ddd
C
244 }
245 ]
d48ff09d
C
246 }
247})
3fd3ab2d
C
248@Table({
249 tableName: 'video',
250 indexes: [
feb4bdfd 251 {
3fd3ab2d 252 fields: [ 'name' ]
feb4bdfd
C
253 },
254 {
3fd3ab2d
C
255 fields: [ 'createdAt' ]
256 },
257 {
258 fields: [ 'duration' ]
259 },
260 {
261 fields: [ 'views' ]
262 },
263 {
264 fields: [ 'likes' ]
265 },
266 {
267 fields: [ 'uuid' ]
268 },
269 {
270 fields: [ 'channelId' ]
4cb6d457
C
271 },
272 {
273 fields: [ 'id', 'privacy' ]
2ccaeeb3
C
274 },
275 {
276 fields: [ 'url'],
277 unique: true
feb4bdfd 278 }
e02643f3 279 ]
3fd3ab2d
C
280})
281export class VideoModel extends Model<VideoModel> {
282
283 @AllowNull(false)
284 @Default(DataType.UUIDV4)
285 @IsUUID(4)
286 @Column(DataType.UUID)
287 uuid: string
288
289 @AllowNull(false)
290 @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name'))
291 @Column
292 name: string
293
294 @AllowNull(true)
295 @Default(null)
296 @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category'))
297 @Column
298 category: number
299
300 @AllowNull(true)
301 @Default(null)
302 @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence'))
303 @Column
304 licence: number
305
306 @AllowNull(true)
307 @Default(null)
308 @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language'))
309 @Column
310 language: number
311
312 @AllowNull(false)
313 @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
314 @Column
315 privacy: number
316
317 @AllowNull(false)
47564bbe 318 @Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
3fd3ab2d
C
319 @Column
320 nsfw: boolean
321
322 @AllowNull(true)
323 @Default(null)
324 @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description'))
325 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
326 description: string
327
2422c46b
C
328 @AllowNull(true)
329 @Default(null)
330 @Is('VideoSupport', value => throwIfNotValid(value, isVideoSupportValid, 'support'))
331 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max))
332 support: string
333
3fd3ab2d
C
334 @AllowNull(false)
335 @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration'))
336 @Column
337 duration: number
338
339 @AllowNull(false)
340 @Default(0)
341 @IsInt
342 @Min(0)
343 @Column
344 views: number
345
346 @AllowNull(false)
347 @Default(0)
348 @IsInt
349 @Min(0)
350 @Column
351 likes: number
352
353 @AllowNull(false)
354 @Default(0)
355 @IsInt
356 @Min(0)
357 @Column
358 dislikes: number
359
360 @AllowNull(false)
361 @Column
362 remote: boolean
363
364 @AllowNull(false)
365 @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
366 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
367 url: string
368
47564bbe
C
369 @AllowNull(false)
370 @Column
371 commentsEnabled: boolean
372
3fd3ab2d
C
373 @CreatedAt
374 createdAt: Date
375
376 @UpdatedAt
377 updatedAt: Date
378
379 @ForeignKey(() => VideoChannelModel)
380 @Column
381 channelId: number
382
383 @BelongsTo(() => VideoChannelModel, {
feb4bdfd 384 foreignKey: {
50d6de9c 385 allowNull: true
feb4bdfd
C
386 },
387 onDelete: 'cascade'
388 })
3fd3ab2d 389 VideoChannel: VideoChannelModel
7920c273 390
3fd3ab2d 391 @BelongsToMany(() => TagModel, {
7920c273 392 foreignKey: 'videoId',
3fd3ab2d
C
393 through: () => VideoTagModel,
394 onDelete: 'CASCADE'
7920c273 395 })
3fd3ab2d 396 Tags: TagModel[]
55fa55a9 397
3fd3ab2d 398 @HasMany(() => VideoAbuseModel, {
55fa55a9
C
399 foreignKey: {
400 name: 'videoId',
401 allowNull: false
402 },
403 onDelete: 'cascade'
404 })
3fd3ab2d 405 VideoAbuses: VideoAbuseModel[]
93e1258c 406
3fd3ab2d 407 @HasMany(() => VideoFileModel, {
93e1258c
C
408 foreignKey: {
409 name: 'videoId',
410 allowNull: false
411 },
412 onDelete: 'cascade'
413 })
3fd3ab2d 414 VideoFiles: VideoFileModel[]
e71bcc0f 415
3fd3ab2d 416 @HasMany(() => VideoShareModel, {
e71bcc0f
C
417 foreignKey: {
418 name: 'videoId',
419 allowNull: false
420 },
421 onDelete: 'cascade'
422 })
3fd3ab2d 423 VideoShares: VideoShareModel[]
16b90975 424
3fd3ab2d 425 @HasMany(() => AccountVideoRateModel, {
16b90975
C
426 foreignKey: {
427 name: 'videoId',
428 allowNull: false
429 },
430 onDelete: 'cascade'
431 })
3fd3ab2d 432 AccountVideoRates: AccountVideoRateModel[]
f285faa0 433
da854ddd
C
434 @HasMany(() => VideoCommentModel, {
435 foreignKey: {
436 name: 'videoId',
437 allowNull: false
438 },
f05a1c30
C
439 onDelete: 'cascade',
440 hooks: true
da854ddd
C
441 })
442 VideoComments: VideoCommentModel[]
443
f05a1c30
C
444 @BeforeDestroy
445 static async sendDelete (instance: VideoModel, options) {
446 if (instance.isOwned()) {
447 if (!instance.VideoChannel) {
448 instance.VideoChannel = await instance.$get('VideoChannel', {
449 include: [
450 {
451 model: AccountModel,
452 include: [ ActorModel ]
453 }
454 ],
455 transaction: options.transaction
456 }) as VideoChannelModel
457 }
458
459 logger.debug('Sending delete of video %s.', instance.url)
460
461 return sendDeleteVideo(instance, options.transaction)
462 }
463
464 return undefined
465 }
466
3fd3ab2d 467 @AfterDestroy
f05a1c30
C
468 static async removeFilesAndSendDelete (instance: VideoModel) {
469 const tasks: Promise<any>[] = []
f285faa0 470
f05a1c30 471 tasks.push(instance.removeThumbnail())
93e1258c 472
3fd3ab2d 473 if (instance.isOwned()) {
f05a1c30
C
474 if (!Array.isArray(instance.VideoFiles)) {
475 instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
476 }
477
478 tasks.push(instance.removePreview())
40298b02 479
3fd3ab2d
C
480 // Remove physical files and torrents
481 instance.VideoFiles.forEach(file => {
482 tasks.push(instance.removeFile(file))
483 tasks.push(instance.removeTorrent(file))
484 })
485 }
40298b02 486
3fd3ab2d
C
487 return Promise.all(tasks)
488 .catch(err => {
d5b7d911 489 logger.error('Some errors when removing files of video %s in after destroy hook.', instance.uuid, { err })
3fd3ab2d
C
490 })
491 }
f285faa0 492
3fd3ab2d 493 static list () {
d48ff09d 494 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
3fd3ab2d 495 }
f285faa0 496
50d6de9c 497 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
3fd3ab2d
C
498 function getRawQuery (select: string) {
499 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
500 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
50d6de9c
C
501 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
502 'WHERE "Account"."actorId" = ' + actorId
3fd3ab2d
C
503 const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
504 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
50d6de9c 505 'WHERE "VideoShare"."actorId" = ' + actorId
558d7c23 506
3fd3ab2d
C
507 return `(${queryVideo}) UNION (${queryVideoShare})`
508 }
aaf61f38 509
3fd3ab2d
C
510 const rawQuery = getRawQuery('"Video"."id"')
511 const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
512
513 const query = {
514 distinct: true,
515 offset: start,
516 limit: count,
3bb6c526 517 order: getSort('createdAt', [ 'Tags', 'name', 'ASC' ]),
3fd3ab2d
C
518 where: {
519 id: {
520 [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')')
3c75ce12
C
521 },
522 [Sequelize.Op.or]: [
523 { privacy: VideoPrivacy.PUBLIC },
524 { privacy: VideoPrivacy.UNLISTED }
525 ]
3fd3ab2d
C
526 },
527 include: [
528 {
1d230c44 529 attributes: [ 'id', 'url' ],
2c897999 530 model: VideoShareModel.unscoped(),
3fd3ab2d
C
531 required: false,
532 where: {
533 [Sequelize.Op.and]: [
534 {
535 id: {
536 [Sequelize.Op.not]: null
537 }
538 },
539 {
50d6de9c 540 actorId
3fd3ab2d
C
541 }
542 ]
543 },
50d6de9c
C
544 include: [
545 {
2c897999
C
546 attributes: [ 'id', 'url' ],
547 model: ActorModel.unscoped()
50d6de9c
C
548 }
549 ]
3fd3ab2d
C
550 },
551 {
2c897999 552 model: VideoChannelModel.unscoped(),
3fd3ab2d
C
553 required: true,
554 include: [
555 {
2c897999
C
556 attributes: [ 'name' ],
557 model: AccountModel.unscoped(),
558 required: true,
559 include: [
560 {
561 attributes: [ 'id', 'url' ],
562 model: ActorModel.unscoped(),
563 required: true
564 }
565 ]
566 },
567 {
568 attributes: [ 'id', 'url' ],
569 model: ActorModel.unscoped(),
3fd3ab2d
C
570 required: true
571 }
572 ]
573 },
574 {
2c897999 575 attributes: [ 'type' ],
3fd3ab2d 576 model: AccountVideoRateModel,
2c897999
C
577 required: false,
578 include: [
579 {
580 attributes: [ 'id' ],
581 model: AccountModel.unscoped(),
582 include: [
583 {
584 attributes: [ 'url' ],
585 model: ActorModel.unscoped(),
586 include: [
587 {
588 attributes: [ 'host' ],
589 model: ServerModel,
590 required: false
591 }
592 ]
593 }
594 ]
595 }
596 ]
597 },
598 {
599 attributes: [ 'url' ],
600 model: VideoCommentModel,
601 required: false
3fd3ab2d
C
602 },
603 VideoFileModel,
2c897999 604 TagModel
3fd3ab2d
C
605 ]
606 }
164174a6 607
3fd3ab2d
C
608 return Bluebird.all([
609 // FIXME: typing issue
610 VideoModel.findAll(query as any),
611 VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT })
612 ]).then(([ rows, totals ]) => {
613 // totals: totalVideos + totalVideoShares
614 let totalVideos = 0
615 let totalVideoShares = 0
616 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
617 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
618
619 const total = totalVideos + totalVideoShares
620 return {
621 data: rows,
622 total: total
623 }
624 })
625 }
93e1258c 626
3fd3ab2d
C
627 static listUserVideosForApi (userId: number, start: number, count: number, sort: string) {
628 const query = {
3fd3ab2d
C
629 offset: start,
630 limit: count,
3bb6c526 631 order: getSort(sort),
3fd3ab2d
C
632 include: [
633 {
634 model: VideoChannelModel,
635 required: true,
636 include: [
637 {
638 model: AccountModel,
639 where: {
640 userId
641 },
642 required: true
643 }
644 ]
d48ff09d 645 }
3fd3ab2d
C
646 ]
647 }
d8755eed 648
3fd3ab2d
C
649 return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
650 return {
651 data: rows,
652 total: count
653 }
654 })
655 }
93e1258c 656
066e94c5 657 static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter) {
3fd3ab2d 658 const query = {
3fd3ab2d
C
659 offset: start,
660 limit: count,
6ff9c676 661 order: getSort(sort)
3fd3ab2d 662 }
93e1258c 663
f05a1c30
C
664 const serverActor = await getServerActor()
665
066e94c5 666 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter ] })
d48ff09d
C
667 .findAndCountAll(query)
668 .then(({ rows, count }) => {
669 return {
670 data: rows,
671 total: count
672 }
673 })
93e1258c
C
674 }
675
f05a1c30
C
676 static async searchAndPopulateAccountAndServerAndTags (value: string, start: number, count: number, sort: string) {
677 const query: IFindOptions<VideoModel> = {
678 offset: start,
679 limit: count,
3bb6c526 680 order: getSort(sort),
f05a1c30 681 where: {
3e0c9ff5
C
682 [Sequelize.Op.or]: [
683 {
684 name: {
685 [ Sequelize.Op.iLike ]: '%' + value + '%'
686 }
687 },
688 {
689 preferredUsername: Sequelize.where(Sequelize.col('preferredUsername'), {
690 [ Sequelize.Op.iLike ]: '%' + value + '%'
691 })
692 },
693 {
694 host: Sequelize.where(Sequelize.col('host'), {
695 [ Sequelize.Op.iLike ]: '%' + value + '%'
696 })
697 }
698 ]
f05a1c30
C
699 }
700 }
701
702 const serverActor = await getServerActor()
703
704 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] })
705 .findAndCountAll(query).then(({ rows, count }) => {
706 return {
707 data: rows,
708 total: count
709 }
710 })
711 }
712
3fd3ab2d
C
713 static load (id: number) {
714 return VideoModel.findById(id)
715 }
fdbda9e3 716
3fd3ab2d
C
717 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
718 const query: IFindOptions<VideoModel> = {
719 where: {
720 url
d48ff09d 721 }
3fd3ab2d 722 }
d8755eed 723
3fd3ab2d 724 if (t !== undefined) query.transaction = t
d8755eed 725
4cb6d457 726 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
3fd3ab2d 727 }
d8755eed 728
2ccaeeb3 729 static loadByUUIDOrURLAndPopulateAccount (uuid: string, url: string, t?: Sequelize.Transaction) {
3fd3ab2d
C
730 const query: IFindOptions<VideoModel> = {
731 where: {
732 [Sequelize.Op.or]: [
733 { uuid },
734 { url }
735 ]
d48ff09d 736 }
3fd3ab2d 737 }
feb4bdfd 738
3fd3ab2d 739 if (t !== undefined) query.transaction = t
feb4bdfd 740
2ccaeeb3 741 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
72c7248b
C
742 }
743
3fd3ab2d
C
744 static loadAndPopulateAccountAndServerAndTags (id: number) {
745 const options = {
d48ff09d 746 order: [ [ 'Tags', 'name', 'ASC' ] ]
3fd3ab2d 747 }
72c7248b 748
d48ff09d 749 return VideoModel
4cb6d457 750 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ])
d48ff09d 751 .findById(id, options)
3fd3ab2d 752 }
72c7248b 753
8fa5653a
C
754 static loadByUUID (uuid: string) {
755 const options = {
756 where: {
757 uuid
758 }
759 }
760
761 return VideoModel
762 .scope([ ScopeNames.WITH_FILES ])
763 .findOne(options)
764 }
765
3fd3ab2d
C
766 static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) {
767 const options = {
768 order: [ [ 'Tags', 'name', 'ASC' ] ],
769 where: {
770 uuid
d48ff09d 771 }
3fd3ab2d 772 }
fd45e8f4 773
d48ff09d 774 return VideoModel
4cb6d457 775 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ])
da854ddd
C
776 .findOne(options)
777 }
778
779 static loadAndPopulateAll (id: number) {
780 const options = {
781 order: [ [ 'Tags', 'name', 'ASC' ] ],
782 where: {
783 id
784 }
785 }
786
787 return VideoModel
788 .scope([
789 ScopeNames.WITH_RATES,
790 ScopeNames.WITH_SHARES,
791 ScopeNames.WITH_TAGS,
792 ScopeNames.WITH_FILES,
4cb6d457 793 ScopeNames.WITH_ACCOUNT_DETAILS,
da854ddd
C
794 ScopeNames.WITH_COMMENTS
795 ])
d48ff09d 796 .findOne(options)
aaf61f38
C
797 }
798
09cababd
C
799 static async getStats () {
800 const totalLocalVideos = await VideoModel.count({
801 where: {
802 remote: false
803 }
804 })
805 const totalVideos = await VideoModel.count()
806
807 let totalLocalVideoViews = await VideoModel.sum('views', {
808 where: {
809 remote: false
810 }
811 })
812 // Sequelize could return null...
813 if (!totalLocalVideoViews) totalLocalVideoViews = 0
814
815 return {
816 totalLocalVideos,
817 totalLocalVideoViews,
818 totalVideos
819 }
820 }
821
066e94c5
C
822 private static buildActorWhereWithFilter (filter?: VideoFilter) {
823 if (filter && filter === 'local') {
824 return {
825 serverId: null
826 }
827 }
828
829 return {}
830 }
831
ae5a3dd6
C
832 private static getCategoryLabel (id: number) {
833 let categoryLabel = VIDEO_CATEGORIES[id]
834 if (!categoryLabel) categoryLabel = 'Misc'
835
836 return categoryLabel
837 }
838
839 private static getLicenceLabel (id: number) {
840 let licenceLabel = VIDEO_LICENCES[id]
841 if (!licenceLabel) licenceLabel = 'Unknown'
842
843 return licenceLabel
844 }
845
846 private static getLanguageLabel (id: number) {
847 let languageLabel = VIDEO_LANGUAGES[id]
848 if (!languageLabel) languageLabel = 'Unknown'
849
850 return languageLabel
851 }
852
3fd3ab2d
C
853 getOriginalFile () {
854 if (Array.isArray(this.VideoFiles) === false) return undefined
aaf61f38 855
3fd3ab2d
C
856 // The original file is the file that have the higher resolution
857 return maxBy(this.VideoFiles, file => file.resolution)
e4f97bab 858 }
aaf61f38 859
3fd3ab2d
C
860 getVideoFilename (videoFile: VideoFileModel) {
861 return this.uuid + '-' + videoFile.resolution + videoFile.extname
862 }
165cdc75 863
3fd3ab2d
C
864 getThumbnailName () {
865 // We always have a copy of the thumbnail
866 const extension = '.jpg'
867 return this.uuid + extension
7b1f49de
C
868 }
869
3fd3ab2d
C
870 getPreviewName () {
871 const extension = '.jpg'
872 return this.uuid + extension
873 }
7b1f49de 874
3fd3ab2d
C
875 getTorrentFileName (videoFile: VideoFileModel) {
876 const extension = '.torrent'
877 return this.uuid + '-' + videoFile.resolution + extension
878 }
8e7f08b5 879
3fd3ab2d
C
880 isOwned () {
881 return this.remote === false
9567011b
C
882 }
883
3fd3ab2d 884 createPreview (videoFile: VideoFileModel) {
3fd3ab2d
C
885 return generateImageFromVideoFile(
886 this.getVideoFilePath(videoFile),
887 CONFIG.STORAGE.PREVIEWS_DIR,
888 this.getPreviewName(),
26670720 889 PREVIEWS_SIZE
3fd3ab2d
C
890 )
891 }
9567011b 892
3fd3ab2d 893 createThumbnail (videoFile: VideoFileModel) {
3fd3ab2d
C
894 return generateImageFromVideoFile(
895 this.getVideoFilePath(videoFile),
896 CONFIG.STORAGE.THUMBNAILS_DIR,
897 this.getThumbnailName(),
26670720 898 THUMBNAILS_SIZE
3fd3ab2d 899 )
14d3270f
C
900 }
901
3fd3ab2d
C
902 getVideoFilePath (videoFile: VideoFileModel) {
903 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
904 }
14d3270f 905
3fd3ab2d
C
906 createTorrentAndSetInfoHash = async function (videoFile: VideoFileModel) {
907 const options = {
908 announceList: [
0edf0581
C
909 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ],
910 [ CONFIG.WEBSERVER.URL + '/tracker/announce' ]
3fd3ab2d
C
911 ],
912 urlList: [
913 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
914 ]
915 }
14d3270f 916
3fd3ab2d 917 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
e4f97bab 918
3fd3ab2d
C
919 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
920 logger.info('Creating torrent %s.', filePath)
e4f97bab 921
3fd3ab2d 922 await writeFilePromise(filePath, torrent)
e4f97bab 923
3fd3ab2d
C
924 const parsedTorrent = parseTorrent(torrent)
925 videoFile.infoHash = parsedTorrent.infoHash
926 }
e4f97bab 927
3fd3ab2d
C
928 getEmbedPath () {
929 return '/videos/embed/' + this.uuid
930 }
e4f97bab 931
3fd3ab2d
C
932 getThumbnailPath () {
933 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
e4f97bab 934 }
227d02fe 935
3fd3ab2d
C
936 getPreviewPath () {
937 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
938 }
40298b02 939
2422c46b 940 toFormattedJSON (): Video {
b64c950a 941 const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
14d3270f 942
3fd3ab2d
C
943 return {
944 id: this.id,
945 uuid: this.uuid,
946 name: this.name,
ae5a3dd6
C
947 category: {
948 id: this.category,
949 label: VideoModel.getCategoryLabel(this.category)
950 },
951 licence: {
952 id: this.licence,
953 label: VideoModel.getLicenceLabel(this.licence)
954 },
955 language: {
956 id: this.language,
957 label: VideoModel.getLanguageLabel(this.language)
958 },
3fd3ab2d
C
959 nsfw: this.nsfw,
960 description: this.getTruncatedDescription(),
3fd3ab2d 961 isLocal: this.isOwned(),
3fd3ab2d
C
962 duration: this.duration,
963 views: this.views,
964 likes: this.likes,
965 dislikes: this.dislikes,
3fd3ab2d
C
966 thumbnailPath: this.getThumbnailPath(),
967 previewPath: this.getPreviewPath(),
968 embedPath: this.getEmbedPath(),
969 createdAt: this.createdAt,
b64c950a
C
970 updatedAt: this.updatedAt,
971 account: {
972 name: formattedAccount.name,
973 displayName: formattedAccount.displayName,
974 url: formattedAccount.url,
975 host: formattedAccount.host,
976 avatar: formattedAccount.avatar
977 }
2422c46b 978 }
14d3270f 979 }
14d3270f 980
2422c46b 981 toFormattedDetailsJSON (): VideoDetails {
3fd3ab2d 982 const formattedJson = this.toFormattedJSON()
e4f97bab 983
3fd3ab2d
C
984 // Maybe our server is not up to date and there are new privacy settings since our version
985 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
986 if (!privacyLabel) privacyLabel = 'Unknown'
e4f97bab 987
3fd3ab2d 988 const detailsJson = {
ae5a3dd6
C
989 privacy: {
990 id: this.privacy,
991 label: privacyLabel
992 },
2422c46b 993 support: this.support,
3fd3ab2d
C
994 descriptionPath: this.getDescriptionPath(),
995 channel: this.VideoChannel.toFormattedJSON(),
996 account: this.VideoChannel.Account.toFormattedJSON(),
d48ff09d 997 tags: map<TagModel, string>(this.Tags, 'name'),
47564bbe 998 commentsEnabled: this.commentsEnabled,
3fd3ab2d
C
999 files: []
1000 }
e4f97bab 1001
3fd3ab2d
C
1002 // Format and sort video files
1003 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
1004 detailsJson.files = this.VideoFiles
1005 .map(videoFile => {
1006 let resolutionLabel = videoFile.resolution + 'p'
1007
1008 return {
ae5a3dd6
C
1009 resolution: {
1010 id: videoFile.resolution,
1011 label: resolutionLabel
1012 },
3fd3ab2d
C
1013 magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
1014 size: videoFile.size,
1015 torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
1016 fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp)
1017 }
1018 })
1019 .sort((a, b) => {
552586ed
C
1020 if (a.resolution.id < b.resolution.id) return 1
1021 if (a.resolution.id === b.resolution.id) return 0
3fd3ab2d
C
1022 return -1
1023 })
1024
2422c46b 1025 return Object.assign(formattedJson, detailsJson)
3fd3ab2d 1026 }
e4f97bab 1027
3fd3ab2d
C
1028 toActivityPubObject (): VideoTorrentObject {
1029 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
1030 if (!this.Tags) this.Tags = []
e4f97bab 1031
3fd3ab2d
C
1032 const tag = this.Tags.map(t => ({
1033 type: 'Hashtag' as 'Hashtag',
1034 name: t.name
1035 }))
40298b02 1036
3fd3ab2d
C
1037 let language
1038 if (this.language) {
1039 language = {
5d00a3d7 1040 identifier: this.language + '',
ae5a3dd6 1041 name: VideoModel.getLanguageLabel(this.language)
3fd3ab2d
C
1042 }
1043 }
40298b02 1044
3fd3ab2d
C
1045 let category
1046 if (this.category) {
1047 category = {
1048 identifier: this.category + '',
ae5a3dd6 1049 name: VideoModel.getCategoryLabel(this.category)
3fd3ab2d
C
1050 }
1051 }
40298b02 1052
3fd3ab2d
C
1053 let licence
1054 if (this.licence) {
1055 licence = {
1056 identifier: this.licence + '',
ae5a3dd6 1057 name: VideoModel.getLicenceLabel(this.licence)
3fd3ab2d
C
1058 }
1059 }
9567011b 1060
3fd3ab2d
C
1061 let likesObject
1062 let dislikesObject
e4f97bab 1063
3fd3ab2d 1064 if (Array.isArray(this.AccountVideoRates)) {
46531a0a
C
1065 const res = this.toRatesActivityPubObjects()
1066 likesObject = res.likesObject
1067 dislikesObject = res.dislikesObject
3fd3ab2d 1068 }
e4f97bab 1069
3fd3ab2d
C
1070 let sharesObject
1071 if (Array.isArray(this.VideoShares)) {
46531a0a 1072 sharesObject = this.toAnnouncesActivityPubObject()
3fd3ab2d 1073 }
93e1258c 1074
da854ddd
C
1075 let commentsObject
1076 if (Array.isArray(this.VideoComments)) {
46531a0a 1077 commentsObject = this.toCommentsActivityPubObject()
da854ddd
C
1078 }
1079
3fd3ab2d
C
1080 const url = []
1081 for (const file of this.VideoFiles) {
1082 url.push({
1083 type: 'Link',
1084 mimeType: 'video/' + file.extname.replace('.', ''),
9fb3abfd 1085 href: this.getVideoFileUrl(file, baseUrlHttp),
3fd3ab2d
C
1086 width: file.resolution,
1087 size: file.size
1088 })
1089
1090 url.push({
1091 type: 'Link',
1092 mimeType: 'application/x-bittorrent',
9fb3abfd 1093 href: this.getTorrentUrl(file, baseUrlHttp),
3fd3ab2d
C
1094 width: file.resolution
1095 })
1096
1097 url.push({
1098 type: 'Link',
1099 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
9fb3abfd 1100 href: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
3fd3ab2d
C
1101 width: file.resolution
1102 })
1103 }
93e1258c 1104
3fd3ab2d
C
1105 // Add video url too
1106 url.push({
1107 type: 'Link',
1108 mimeType: 'text/html',
9fb3abfd 1109 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid
3fd3ab2d 1110 })
93e1258c 1111
3fd3ab2d
C
1112 return {
1113 type: 'Video' as 'Video',
1114 id: this.url,
1115 name: this.name,
093237cf 1116 duration: this.getActivityStreamDuration(),
3fd3ab2d
C
1117 uuid: this.uuid,
1118 tag,
1119 category,
1120 licence,
1121 language,
1122 views: this.views,
0a67e28b 1123 sensitive: this.nsfw,
47564bbe 1124 commentsEnabled: this.commentsEnabled,
3fd3ab2d
C
1125 published: this.createdAt.toISOString(),
1126 updated: this.updatedAt.toISOString(),
1127 mediaType: 'text/markdown',
1128 content: this.getTruncatedDescription(),
2422c46b 1129 support: this.support,
3fd3ab2d
C
1130 icon: {
1131 type: 'Image',
1132 url: this.getThumbnailUrl(baseUrlHttp),
1133 mediaType: 'image/jpeg',
1134 width: THUMBNAILS_SIZE.width,
1135 height: THUMBNAILS_SIZE.height
1136 },
1137 url,
1138 likes: likesObject,
1139 dislikes: dislikesObject,
50d6de9c 1140 shares: sharesObject,
da854ddd 1141 comments: commentsObject,
50d6de9c 1142 attributedTo: [
2ccaeeb3
C
1143 {
1144 type: 'Person',
1145 id: this.VideoChannel.Account.Actor.url
fc27b17c
C
1146 },
1147 {
1148 type: 'Group',
1149 id: this.VideoChannel.Actor.url
50d6de9c
C
1150 }
1151 ]
3fd3ab2d
C
1152 }
1153 }
1154
46531a0a
C
1155 toAnnouncesActivityPubObject () {
1156 const shares: string[] = []
1157
1158 for (const videoShare of this.VideoShares) {
1159 shares.push(videoShare.url)
1160 }
1161
1162 return activityPubCollection(getVideoSharesActivityPubUrl(this), shares)
1163 }
1164
1165 toCommentsActivityPubObject () {
1166 const comments: string[] = []
1167
1168 for (const videoComment of this.VideoComments) {
1169 comments.push(videoComment.url)
1170 }
1171
1172 return activityPubCollection(getVideoCommentsActivityPubUrl(this), comments)
1173 }
1174
1175 toRatesActivityPubObjects () {
1176 const likes: string[] = []
1177 const dislikes: string[] = []
1178
1179 for (const rate of this.AccountVideoRates) {
1180 if (rate.type === 'like') {
1181 likes.push(rate.Account.Actor.url)
1182 } else if (rate.type === 'dislike') {
1183 dislikes.push(rate.Account.Actor.url)
1184 }
1185 }
1186
1187 const likesObject = activityPubCollection(getVideoLikesActivityPubUrl(this), likes)
1188 const dislikesObject = activityPubCollection(getVideoDislikesActivityPubUrl(this), dislikes)
1189
1190 return { likesObject, dislikesObject }
1191 }
1192
3fd3ab2d
C
1193 getTruncatedDescription () {
1194 if (!this.description) return null
93e1258c 1195
bffbebbe 1196 const maxLength = CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
c73e83da 1197 return peertubeTruncate(this.description, maxLength)
93e1258c
C
1198 }
1199
3fd3ab2d
C
1200 optimizeOriginalVideofile = async function () {
1201 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
1202 const newExtname = '.mp4'
1203 const inputVideoFile = this.getOriginalFile()
1204 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
1205 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
b769007f 1206
3fd3ab2d
C
1207 const transcodeOptions = {
1208 inputPath: videoInputPath,
1209 outputPath: videoOutputPath
1210 }
c46edbc2 1211
b0ef1782
C
1212 // Could be very long!
1213 await transcode(transcodeOptions)
c46edbc2 1214
b0ef1782 1215 try {
3fd3ab2d 1216 await unlinkPromise(videoInputPath)
c46edbc2 1217
3fd3ab2d
C
1218 // Important to do this before getVideoFilename() to take in account the new file extension
1219 inputVideoFile.set('extname', newExtname)
e71bcc0f 1220
3fd3ab2d
C
1221 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
1222 const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
e71bcc0f 1223
3fd3ab2d 1224 inputVideoFile.set('size', stats.size)
e71bcc0f 1225
3fd3ab2d
C
1226 await this.createTorrentAndSetInfoHash(inputVideoFile)
1227 await inputVideoFile.save()
fd45e8f4 1228
3fd3ab2d
C
1229 } catch (err) {
1230 // Auto destruction...
d5b7d911 1231 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
fd45e8f4 1232
3fd3ab2d
C
1233 throw err
1234 }
feb4bdfd
C
1235 }
1236
056aa7f2 1237 transcodeOriginalVideofile = async function (resolution: VideoResolution, isPortraitMode: boolean) {
3fd3ab2d
C
1238 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
1239 const extname = '.mp4'
aaf61f38 1240
3fd3ab2d
C
1241 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
1242 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
feb4bdfd 1243
3fd3ab2d
C
1244 const newVideoFile = new VideoFileModel({
1245 resolution,
1246 extname,
1247 size: 0,
1248 videoId: this.id
1249 })
1250 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
a041b171 1251
3fd3ab2d
C
1252 const transcodeOptions = {
1253 inputPath: videoInputPath,
1254 outputPath: videoOutputPath,
056aa7f2
C
1255 resolution,
1256 isPortraitMode
3fd3ab2d 1257 }
a041b171 1258
3fd3ab2d 1259 await transcode(transcodeOptions)
a041b171 1260
3fd3ab2d 1261 const stats = await statPromise(videoOutputPath)
d7d5611c 1262
3fd3ab2d 1263 newVideoFile.set('size', stats.size)
d7d5611c 1264
3fd3ab2d 1265 await this.createTorrentAndSetInfoHash(newVideoFile)
d7d5611c 1266
3fd3ab2d
C
1267 await newVideoFile.save()
1268
1269 this.VideoFiles.push(newVideoFile)
0d0e8dd0
C
1270 }
1271
056aa7f2 1272 getOriginalFileResolution () {
3fd3ab2d 1273 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
0d0e8dd0 1274
056aa7f2 1275 return getVideoFileResolution(originalFilePath)
3fd3ab2d 1276 }
0d0e8dd0 1277
3fd3ab2d
C
1278 getDescriptionPath () {
1279 return `/api/${API_VERSION}/videos/${this.uuid}/description`
feb4bdfd
C
1280 }
1281
3fd3ab2d
C
1282 removeThumbnail () {
1283 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
1284 return unlinkPromise(thumbnailPath)
feb4bdfd
C
1285 }
1286
3fd3ab2d
C
1287 removePreview () {
1288 // Same name than video thumbnail
1289 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
7920c273
C
1290 }
1291
3fd3ab2d
C
1292 removeFile (videoFile: VideoFileModel) {
1293 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
1294 return unlinkPromise(filePath)
feb4bdfd
C
1295 }
1296
3fd3ab2d
C
1297 removeTorrent (videoFile: VideoFileModel) {
1298 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1299 return unlinkPromise(torrentPath)
aaf61f38
C
1300 }
1301
093237cf
C
1302 getActivityStreamDuration () {
1303 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
1304 return 'PT' + this.duration + 'S'
1305 }
1306
3fd3ab2d
C
1307 private getBaseUrls () {
1308 let baseUrlHttp
1309 let baseUrlWs
7920c273 1310
3fd3ab2d
C
1311 if (this.isOwned()) {
1312 baseUrlHttp = CONFIG.WEBSERVER.URL
1313 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1314 } else {
50d6de9c
C
1315 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
1316 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
6fcd19ba 1317 }
aaf61f38 1318
3fd3ab2d 1319 return { baseUrlHttp, baseUrlWs }
15d4ee04 1320 }
a96aed15 1321
3fd3ab2d
C
1322 private getThumbnailUrl (baseUrlHttp: string) {
1323 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
a96aed15
C
1324 }
1325
3fd3ab2d
C
1326 private getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1327 return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
1328 }
e4f97bab 1329
3fd3ab2d
C
1330 private getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1331 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
1332 }
a96aed15 1333
3fd3ab2d
C
1334 private generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
1335 const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
1336 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1337 const urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
1338
1339 const magnetHash = {
1340 xs,
1341 announce,
1342 urlList,
1343 infoHash: videoFile.infoHash,
1344 name: this.name
1345 }
a96aed15 1346
3fd3ab2d 1347 return magnetUtil.encode(magnetHash)
a96aed15 1348 }
a96aed15 1349}