]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Don't cache embed HTML file
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
CommitLineData
39445ead 1import * as Bluebird from 'bluebird'
6b842050 2import { maxBy, minBy, pick } from 'lodash'
098eb377 3import { join } from 'path'
5f3e2425 4import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
3fd3ab2d 5import {
4ba3b8ea
C
6 AllowNull,
7 BeforeDestroy,
8 BelongsTo,
9 BelongsToMany,
10 Column,
11 CreatedAt,
12 DataType,
13 Default,
14 ForeignKey,
15 HasMany,
2baea0c7 16 HasOne,
4ba3b8ea
C
17 Is,
18 IsInt,
19 IsUUID,
20 Min,
21 Model,
22 Scopes,
23 Table,
9a629c6e 24 UpdatedAt
3fd3ab2d 25} from 'sequelize-typescript'
453e83ea 26import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
3fd3ab2d 27import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
d7a25329 28import { Video, VideoDetails } from '../../../shared/models/videos'
066e94c5 29import { VideoFilter } from '../../../shared/models/videos/video-query.type'
30ff39e7 30import { peertubeTruncate } from '../../helpers/core-utils'
da854ddd 31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
d7a25329 32import { isBooleanValid } from '../../helpers/custom-validators/misc'
3fd3ab2d 33import {
4ba3b8ea
C
34 isVideoCategoryValid,
35 isVideoDescriptionValid,
36 isVideoDurationValid,
37 isVideoLanguageValid,
38 isVideoLicenceValid,
418d092a 39 isVideoNameValid,
2baea0c7
C
40 isVideoPrivacyValid,
41 isVideoStateValid,
b64c950a 42 isVideoSupportValid
3fd3ab2d 43} from '../../helpers/custom-validators/videos'
1735c825 44import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
da854ddd 45import { logger } from '../../helpers/logger'
65fcc311 46import {
1297eb5d 47 ACTIVITY_PUB,
4ba3b8ea 48 API_VERSION,
418d092a 49 CONSTRAINTS_FIELDS,
557b13ae 50 LAZY_STATIC_PATHS,
4ba3b8ea 51 REMOTE_SCHEME,
02756fbd 52 STATIC_DOWNLOAD_PATHS,
4ba3b8ea 53 STATIC_PATHS,
4ba3b8ea
C
54 VIDEO_CATEGORIES,
55 VIDEO_LANGUAGES,
56 VIDEO_LICENCES,
2baea0c7 57 VIDEO_PRIVACIES,
6dd9de95
C
58 VIDEO_STATES,
59 WEBSERVER
74dc3bca 60} from '../../initializers/constants'
50d6de9c 61import { sendDeleteVideo } from '../../lib/activitypub/send'
3fd3ab2d
C
62import { AccountModel } from '../account/account'
63import { AccountVideoRateModel } from '../account/account-video-rate'
50d6de9c 64import { ActorModel } from '../activitypub/actor'
b6a4fd6b 65import { AvatarModel } from '../avatar/avatar'
3fd3ab2d 66import { ServerModel } from '../server/server'
5f3e2425 67import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
3fd3ab2d
C
68import { TagModel } from './tag'
69import { VideoAbuseModel } from './video-abuse'
bfbd9128 70import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
da854ddd 71import { VideoCommentModel } from './video-comment'
3fd3ab2d
C
72import { VideoFileModel } from './video-file'
73import { VideoShareModel } from './video-share'
74import { VideoTagModel } from './video-tag'
2baea0c7 75import { ScheduleVideoUpdateModel } from './schedule-video-update'
40e87e9e 76import { VideoCaptionModel } from './video-caption'
26b7305a 77import { VideoBlacklistModel } from './video-blacklist'
d7a25329 78import { remove } from 'fs-extra'
9a629c6e 79import { VideoViewModel } from './video-views'
c48e82b5 80import { VideoRedundancyModel } from '../redundancy/video-redundancy'
098eb377
C
81import {
82 videoFilesModelToFormattedJSON,
83 VideoFormattingJSONOptions,
84 videoModelToActivityPubObject,
85 videoModelToFormattedDetailsJSON,
86 videoModelToFormattedJSON
87} from './video-format-utils'
6e46de09 88import { UserVideoHistoryModel } from '../account/user-video-history'
dc133480 89import { VideoImportModel } from './video-import'
09209296 90import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
418d092a 91import { VideoPlaylistElementModel } from './video-playlist-element'
6dd9de95 92import { CONFIG } from '../../initializers/config'
e8bafea3
C
93import { ThumbnailModel } from './thumbnail'
94import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
e2600d8b 95import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
453e83ea
C
96import {
97 MChannel,
0283eaac 98 MChannelAccountDefault,
453e83ea 99 MChannelId,
d7a25329
C
100 MStreamingPlaylist,
101 MStreamingPlaylistFilesVideo,
453e83ea
C
102 MUserAccountId,
103 MUserId,
453e83ea 104 MVideoAccountLight,
0283eaac 105 MVideoAccountLightBlacklistAllFiles,
b5fecbf4 106 MVideoAP,
453e83ea 107 MVideoDetails,
d7a25329 108 MVideoFileVideo,
b5fecbf4
C
109 MVideoFormattable,
110 MVideoFormattableDetails,
0283eaac 111 MVideoForUser,
453e83ea 112 MVideoFullLight,
5f3e2425
C
113 MVideoIdThumbnail,
114 MVideoImmutable,
453e83ea 115 MVideoThumbnail,
d636ab58 116 MVideoThumbnailBlacklist,
b5fecbf4
C
117 MVideoWithAllFiles,
118 MVideoWithFile,
0374b6b5 119 MVideoWithRights
453e83ea 120} from '../../typings/models'
d7a25329 121import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
453e83ea 122import { MThumbnail } from '../../typings/models/video/thumbnail'
d7a25329 123import { VideoFile } from '@shared/models/videos/video-file.model'
3cf53828 124import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
7eba5e1f 125import { ModelCache } from '@server/models/model-cache'
6b842050 126import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
baab47ca 127import { buildNSFWFilter } from '@server/helpers/express-utils'
8dc8a34e 128import { getServerActor } from '@server/models/application/application'
6e46de09 129
2baea0c7 130export enum ScopeNames {
afd2cba5
C
131 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
132 FOR_API = 'FOR_API',
4cb6d457 133 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
d48ff09d 134 WITH_TAGS = 'WITH_TAGS',
d7a25329 135 WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
191764f3 136 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
6e46de09 137 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
09209296
C
138 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
139 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
e8bafea3 140 WITH_USER_ID = 'WITH_USER_ID',
943e5193 141 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
e8bafea3 142 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
d48ff09d
C
143}
144
bfbd9128
C
145export type ForAPIOptions = {
146 ids?: number[]
418d092a
C
147
148 videoPlaylistId?: number
149
afd2cba5 150 withFiles?: boolean
bfbd9128
C
151
152 withAccountBlockerIds?: number[]
afd2cba5
C
153}
154
bfbd9128 155export type AvailableForListIDsOptions = {
7ad9b984 156 serverAccountId: number
4e74e803 157 followerActorId: number
afd2cba5 158 includeLocalVideos: boolean
418d092a 159
bfbd9128 160 attributesType?: 'none' | 'id' | 'all'
8519cc92 161
afd2cba5
C
162 filter?: VideoFilter
163 categoryOneOf?: number[]
164 nsfw?: boolean
165 licenceOneOf?: number[]
166 languageOneOf?: string[]
167 tagsOneOf?: string[]
168 tagsAllOf?: string[]
418d092a 169
afd2cba5 170 withFiles?: boolean
418d092a 171
afd2cba5 172 accountId?: number
d525fc39 173 videoChannelId?: number
418d092a
C
174
175 videoPlaylistId?: number
176
9a629c6e 177 trendingDays?: number
453e83ea
C
178 user?: MUserAccountId
179 historyOfUser?: MUserId
3caf77d3
C
180
181 baseWhere?: WhereOptions[]
d525fc39
C
182}
183
3acc5084 184@Scopes(() => ({
943e5193
C
185 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
186 attributes: [ 'id', 'url', 'uuid', 'remote' ]
187 },
a1587156 188 [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
1735c825 189 const query: FindOptions = {
418d092a
C
190 include: [
191 {
bfbd9128
C
192 model: VideoChannelModel.scope({
193 method: [
194 VideoChannelScopeNames.SUMMARY, {
195 withAccount: true,
196 withAccountBlockerIds: options.withAccountBlockerIds
197 } as SummaryOptions
198 ]
199 }),
76564702 200 required: true
2fb5b3a5
C
201 },
202 {
203 attributes: [ 'type', 'filename' ],
204 model: ThumbnailModel,
205 required: false
418d092a
C
206 }
207 ]
afd2cba5
C
208 }
209
bfbd9128
C
210 if (options.ids) {
211 query.where = {
212 id: {
a1587156 213 [Op.in]: options.ids
bfbd9128
C
214 }
215 }
216 }
217
afd2cba5
C
218 if (options.withFiles === true) {
219 query.include.push({
8319d6ae 220 model: VideoFileModel,
afd2cba5
C
221 required: true
222 })
223 }
224
418d092a
C
225 if (options.videoPlaylistId) {
226 query.include.push({
227 model: VideoPlaylistElementModel.unscoped(),
15e9d5ca
C
228 required: true,
229 where: {
230 videoPlaylistId: options.videoPlaylistId
231 }
418d092a
C
232 })
233 }
234
afd2cba5
C
235 return query
236 },
a1587156 237 [ScopeNames.WITH_THUMBNAILS]: {
e8bafea3
C
238 include: [
239 {
3acc5084 240 model: ThumbnailModel,
e8bafea3
C
241 required: false
242 }
243 ]
244 },
a1587156 245 [ScopeNames.WITH_USER_ID]: {
09209296
C
246 include: [
247 {
248 attributes: [ 'accountId' ],
3acc5084 249 model: VideoChannelModel.unscoped(),
09209296
C
250 required: true,
251 include: [
252 {
253 attributes: [ 'userId' ],
3acc5084 254 model: AccountModel.unscoped(),
09209296
C
255 required: true
256 }
257 ]
258 }
3acc5084 259 ]
09209296 260 },
a1587156 261 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
d48ff09d
C
262 include: [
263 {
3acc5084 264 model: VideoChannelModel.unscoped(),
d48ff09d
C
265 required: true,
266 include: [
6120941f
C
267 {
268 attributes: {
269 exclude: [ 'privateKey', 'publicKey' ]
270 },
3acc5084 271 model: ActorModel.unscoped(),
3e500247
C
272 required: true,
273 include: [
274 {
275 attributes: [ 'host' ],
3acc5084 276 model: ServerModel.unscoped(),
3e500247 277 required: false
52d9f792
C
278 },
279 {
3acc5084 280 model: AvatarModel.unscoped(),
52d9f792 281 required: false
3e500247
C
282 }
283 ]
6120941f 284 },
d48ff09d 285 {
3acc5084 286 model: AccountModel.unscoped(),
d48ff09d
C
287 required: true,
288 include: [
289 {
3acc5084 290 model: ActorModel.unscoped(),
6120941f
C
291 attributes: {
292 exclude: [ 'privateKey', 'publicKey' ]
293 },
50d6de9c
C
294 required: true,
295 include: [
296 {
3e500247 297 attributes: [ 'host' ],
3acc5084 298 model: ServerModel.unscoped(),
50d6de9c 299 required: false
b6a4fd6b
C
300 },
301 {
3acc5084 302 model: AvatarModel.unscoped(),
b6a4fd6b 303 required: false
50d6de9c
C
304 }
305 ]
d48ff09d
C
306 }
307 ]
308 }
309 ]
310 }
3acc5084 311 ]
d48ff09d 312 },
a1587156 313 [ScopeNames.WITH_TAGS]: {
3acc5084 314 include: [ TagModel ]
d48ff09d 315 },
a1587156 316 [ScopeNames.WITH_BLACKLISTED]: {
191764f3
C
317 include: [
318 {
453e83ea 319 attributes: [ 'id', 'reason', 'unfederated' ],
3acc5084 320 model: VideoBlacklistModel,
191764f3
C
321 required: false
322 }
323 ]
324 },
a1587156 325 [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => {
09209296
C
326 let subInclude: any[] = []
327
328 if (withRedundancies === true) {
329 subInclude = [
330 {
331 attributes: [ 'fileUrl' ],
332 model: VideoRedundancyModel.unscoped(),
333 required: false
334 }
335 ]
336 }
337
338 return {
339 include: [
340 {
8319d6ae 341 model: VideoFileModel,
3acc5084 342 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
09209296
C
343 required: false,
344 include: subInclude
345 }
346 ]
347 }
348 },
a1587156 349 [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
d7a25329
C
350 const subInclude: IncludeOptions[] = [
351 {
8319d6ae 352 model: VideoFileModel,
d7a25329
C
353 required: false
354 }
355 ]
09209296
C
356
357 if (withRedundancies === true) {
d7a25329
C
358 subInclude.push({
359 attributes: [ 'fileUrl' ],
360 model: VideoRedundancyModel.unscoped(),
361 required: false
362 })
09209296
C
363 }
364
365 return {
366 include: [
367 {
368 model: VideoStreamingPlaylistModel.unscoped(),
3acc5084 369 separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
09209296
C
370 required: false,
371 include: subInclude
372 }
373 ]
374 }
bbe0f064 375 },
a1587156 376 [ScopeNames.WITH_SCHEDULED_UPDATE]: {
bbe0f064
C
377 include: [
378 {
3acc5084 379 model: ScheduleVideoUpdateModel.unscoped(),
bbe0f064
C
380 required: false
381 }
382 ]
6e46de09 383 },
a1587156 384 [ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
6e46de09
C
385 return {
386 include: [
387 {
388 attributes: [ 'currentTime' ],
389 model: UserVideoHistoryModel.unscoped(),
390 required: false,
391 where: {
392 userId
393 }
394 }
395 ]
396 }
d48ff09d 397 }
3acc5084 398}))
3fd3ab2d
C
399@Table({
400 tableName: 'video',
0374b6b5
C
401 indexes: [
402 buildTrigramSearchIndex('video_name_trigram', 'name'),
403
404 { fields: [ 'createdAt' ] },
405 {
406 fields: [
407 { name: 'publishedAt', order: 'DESC' },
408 { name: 'id', order: 'ASC' }
409 ]
410 },
411 { fields: [ 'duration' ] },
412 { fields: [ 'views' ] },
413 { fields: [ 'channelId' ] },
414 {
415 fields: [ 'originallyPublishedAt' ],
416 where: {
417 originallyPublishedAt: {
418 [Op.ne]: null
419 }
420 }
421 },
422 {
423 fields: [ 'category' ], // We don't care videos with an unknown category
424 where: {
425 category: {
426 [Op.ne]: null
427 }
428 }
429 },
430 {
431 fields: [ 'licence' ], // We don't care videos with an unknown licence
432 where: {
433 licence: {
434 [Op.ne]: null
435 }
436 }
437 },
438 {
439 fields: [ 'language' ], // We don't care videos with an unknown language
440 where: {
441 language: {
442 [Op.ne]: null
443 }
444 }
445 },
446 {
447 fields: [ 'nsfw' ], // Most of the videos are not NSFW
448 where: {
449 nsfw: true
450 }
451 },
452 {
453 fields: [ 'remote' ], // Only index local videos
454 where: {
455 remote: false
456 }
457 },
458 {
459 fields: [ 'uuid' ],
460 unique: true
461 },
462 {
463 fields: [ 'url' ],
464 unique: true
465 }
466 ]
3fd3ab2d
C
467})
468export class VideoModel extends Model<VideoModel> {
469
470 @AllowNull(false)
471 @Default(DataType.UUIDV4)
472 @IsUUID(4)
473 @Column(DataType.UUID)
474 uuid: string
475
476 @AllowNull(false)
477 @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name'))
478 @Column
479 name: string
480
481 @AllowNull(true)
482 @Default(null)
1735c825 483 @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category', true))
3fd3ab2d
C
484 @Column
485 category: number
486
487 @AllowNull(true)
488 @Default(null)
1735c825 489 @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence', true))
3fd3ab2d
C
490 @Column
491 licence: number
492
493 @AllowNull(true)
494 @Default(null)
1735c825 495 @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language', true))
9d3ef9fe
C
496 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max))
497 language: string
3fd3ab2d
C
498
499 @AllowNull(false)
500 @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
501 @Column
502 privacy: number
503
504 @AllowNull(false)
47564bbe 505 @Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
3fd3ab2d
C
506 @Column
507 nsfw: boolean
508
509 @AllowNull(true)
510 @Default(null)
1735c825 511 @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description', true))
3fd3ab2d
C
512 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
513 description: string
514
2422c46b
C
515 @AllowNull(true)
516 @Default(null)
1735c825 517 @Is('VideoSupport', value => throwIfNotValid(value, isVideoSupportValid, 'support', true))
2422c46b
C
518 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max))
519 support: string
520
3fd3ab2d
C
521 @AllowNull(false)
522 @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration'))
523 @Column
524 duration: number
525
526 @AllowNull(false)
527 @Default(0)
528 @IsInt
529 @Min(0)
530 @Column
531 views: number
532
533 @AllowNull(false)
534 @Default(0)
535 @IsInt
536 @Min(0)
537 @Column
538 likes: number
539
540 @AllowNull(false)
541 @Default(0)
542 @IsInt
543 @Min(0)
544 @Column
545 dislikes: number
546
547 @AllowNull(false)
548 @Column
549 remote: boolean
550
551 @AllowNull(false)
552 @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
553 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
554 url: string
555
47564bbe
C
556 @AllowNull(false)
557 @Column
558 commentsEnabled: boolean
559
156c50af
LD
560 @AllowNull(false)
561 @Column
7f2cfe3a 562 downloadEnabled: boolean
156c50af 563
2186386c
C
564 @AllowNull(false)
565 @Column
566 waitTranscoding: boolean
567
568 @AllowNull(false)
569 @Default(null)
570 @Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
571 @Column
572 state: VideoState
573
3fd3ab2d
C
574 @CreatedAt
575 createdAt: Date
576
577 @UpdatedAt
578 updatedAt: Date
579
2922e048 580 @AllowNull(false)
1735c825 581 @Default(DataType.NOW)
2922e048
JLB
582 @Column
583 publishedAt: Date
584
7519127b
C
585 @AllowNull(true)
586 @Default(null)
c8034165 587 @Column
588 originallyPublishedAt: Date
589
3fd3ab2d
C
590 @ForeignKey(() => VideoChannelModel)
591 @Column
592 channelId: number
593
594 @BelongsTo(() => VideoChannelModel, {
feb4bdfd 595 foreignKey: {
50d6de9c 596 allowNull: true
feb4bdfd 597 },
6b738c7a 598 hooks: true
feb4bdfd 599 })
3fd3ab2d 600 VideoChannel: VideoChannelModel
7920c273 601
3fd3ab2d 602 @BelongsToMany(() => TagModel, {
7920c273 603 foreignKey: 'videoId',
3fd3ab2d
C
604 through: () => VideoTagModel,
605 onDelete: 'CASCADE'
7920c273 606 })
3fd3ab2d 607 Tags: TagModel[]
55fa55a9 608
e8bafea3
C
609 @HasMany(() => ThumbnailModel, {
610 foreignKey: {
611 name: 'videoId',
612 allowNull: true
613 },
614 hooks: true,
615 onDelete: 'cascade'
616 })
617 Thumbnails: ThumbnailModel[]
618
418d092a
C
619 @HasMany(() => VideoPlaylistElementModel, {
620 foreignKey: {
621 name: 'videoId',
bfbd9128 622 allowNull: true
418d092a 623 },
bfbd9128 624 onDelete: 'set null'
418d092a
C
625 })
626 VideoPlaylistElements: VideoPlaylistElementModel[]
627
3fd3ab2d 628 @HasMany(() => VideoAbuseModel, {
55fa55a9
C
629 foreignKey: {
630 name: 'videoId',
68d19a0a 631 allowNull: true
55fa55a9 632 },
68d19a0a 633 onDelete: 'set null'
55fa55a9 634 })
3fd3ab2d 635 VideoAbuses: VideoAbuseModel[]
93e1258c 636
3fd3ab2d 637 @HasMany(() => VideoFileModel, {
93e1258c
C
638 foreignKey: {
639 name: 'videoId',
d7a25329 640 allowNull: true
93e1258c 641 },
c48e82b5 642 hooks: true,
93e1258c
C
643 onDelete: 'cascade'
644 })
3fd3ab2d 645 VideoFiles: VideoFileModel[]
e71bcc0f 646
09209296
C
647 @HasMany(() => VideoStreamingPlaylistModel, {
648 foreignKey: {
649 name: 'videoId',
650 allowNull: false
651 },
652 hooks: true,
653 onDelete: 'cascade'
654 })
655 VideoStreamingPlaylists: VideoStreamingPlaylistModel[]
656
3fd3ab2d 657 @HasMany(() => VideoShareModel, {
e71bcc0f
C
658 foreignKey: {
659 name: 'videoId',
660 allowNull: false
661 },
662 onDelete: 'cascade'
663 })
3fd3ab2d 664 VideoShares: VideoShareModel[]
16b90975 665
3fd3ab2d 666 @HasMany(() => AccountVideoRateModel, {
16b90975
C
667 foreignKey: {
668 name: 'videoId',
669 allowNull: false
670 },
671 onDelete: 'cascade'
672 })
3fd3ab2d 673 AccountVideoRates: AccountVideoRateModel[]
f285faa0 674
da854ddd
C
675 @HasMany(() => VideoCommentModel, {
676 foreignKey: {
677 name: 'videoId',
678 allowNull: false
679 },
f05a1c30
C
680 onDelete: 'cascade',
681 hooks: true
da854ddd
C
682 })
683 VideoComments: VideoCommentModel[]
684
9a629c6e
C
685 @HasMany(() => VideoViewModel, {
686 foreignKey: {
687 name: 'videoId',
688 allowNull: false
689 },
6e46de09 690 onDelete: 'cascade'
9a629c6e
C
691 })
692 VideoViews: VideoViewModel[]
693
6e46de09
C
694 @HasMany(() => UserVideoHistoryModel, {
695 foreignKey: {
696 name: 'videoId',
697 allowNull: false
698 },
699 onDelete: 'cascade'
700 })
701 UserVideoHistories: UserVideoHistoryModel[]
702
2baea0c7
C
703 @HasOne(() => ScheduleVideoUpdateModel, {
704 foreignKey: {
705 name: 'videoId',
706 allowNull: false
707 },
708 onDelete: 'cascade'
709 })
710 ScheduleVideoUpdate: ScheduleVideoUpdateModel
711
26b7305a
C
712 @HasOne(() => VideoBlacklistModel, {
713 foreignKey: {
714 name: 'videoId',
715 allowNull: false
716 },
717 onDelete: 'cascade'
718 })
719 VideoBlacklist: VideoBlacklistModel
720
dc133480
C
721 @HasOne(() => VideoImportModel, {
722 foreignKey: {
723 name: 'videoId',
724 allowNull: true
725 },
726 onDelete: 'set null'
727 })
728 VideoImport: VideoImportModel
729
40e87e9e
C
730 @HasMany(() => VideoCaptionModel, {
731 foreignKey: {
732 name: 'videoId',
733 allowNull: false
734 },
735 onDelete: 'cascade',
736 hooks: true,
a1587156 737 ['separate' as any]: true
40e87e9e
C
738 })
739 VideoCaptions: VideoCaptionModel[]
740
f05a1c30 741 @BeforeDestroy
453e83ea 742 static async sendDelete (instance: MVideoAccountLight, options) {
f05a1c30
C
743 if (instance.isOwned()) {
744 if (!instance.VideoChannel) {
745 instance.VideoChannel = await instance.$get('VideoChannel', {
746 include: [
453e83ea
C
747 ActorModel,
748 AccountModel
f05a1c30
C
749 ],
750 transaction: options.transaction
0283eaac 751 }) as MChannelAccountDefault
f05a1c30
C
752 }
753
f05a1c30
C
754 return sendDeleteVideo(instance, options.transaction)
755 }
756
757 return undefined
758 }
759
6b738c7a 760 @BeforeDestroy
40e87e9e 761 static async removeFiles (instance: VideoModel) {
f05a1c30 762 const tasks: Promise<any>[] = []
f285faa0 763
8e0fd45e 764 logger.info('Removing files of video %s.', instance.url)
6b738c7a 765
3fd3ab2d 766 if (instance.isOwned()) {
f05a1c30 767 if (!Array.isArray(instance.VideoFiles)) {
e6122097 768 instance.VideoFiles = await instance.$get('VideoFiles')
f05a1c30
C
769 }
770
3fd3ab2d
C
771 // Remove physical files and torrents
772 instance.VideoFiles.forEach(file => {
773 tasks.push(instance.removeFile(file))
774 tasks.push(instance.removeTorrent(file))
775 })
09209296
C
776
777 // Remove playlists file
ffc65cbd
C
778 if (!Array.isArray(instance.VideoStreamingPlaylists)) {
779 instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists')
780 }
781
782 for (const p of instance.VideoStreamingPlaylists) {
783 tasks.push(instance.removeStreamingPlaylistFiles(p))
784 }
3fd3ab2d 785 }
40298b02 786
6b738c7a
C
787 // Do not wait video deletion because we could be in a transaction
788 Promise.all(tasks)
8ea6f49a
C
789 .catch(err => {
790 logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err })
791 })
6b738c7a
C
792
793 return undefined
3fd3ab2d 794 }
f285faa0 795
7eba5e1f
C
796 @BeforeDestroy
797 static invalidateCache (instance: VideoModel) {
798 ModelCache.Instance.invalidateCache('video', instance.id)
799 }
800
68d19a0a
RK
801 @BeforeDestroy
802 static async saveEssentialDataToAbuses (instance: VideoModel, options) {
803 const tasks: Promise<any>[] = []
804
805 logger.info('Saving video abuses details of video %s.', instance.url)
806
807 if (!Array.isArray(instance.VideoAbuses)) {
808 instance.VideoAbuses = await instance.$get('VideoAbuses')
809
810 if (instance.VideoAbuses.length === 0) return undefined
811 }
812
86521a67 813 const details = instance.toFormattedDetailsJSON()
68d19a0a
RK
814
815 for (const abuse of instance.VideoAbuses) {
0251197e
RK
816 abuse.deletedVideo = details
817 tasks.push(abuse.save({ transaction: options.transaction }))
68d19a0a
RK
818 }
819
820 Promise.all(tasks)
821 .catch(err => {
822 logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err })
823 })
824
825 return undefined
826 }
827
453e83ea 828 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
9f1ddd24
C
829 const query = {
830 where: {
831 remote: false
832 }
833 }
834
e8bafea3 835 return VideoModel.scope([
d7a25329 836 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
837 ScopeNames.WITH_STREAMING_PLAYLISTS,
838 ScopeNames.WITH_THUMBNAILS
839 ]).findAll(query)
9f1ddd24
C
840 }
841
50d6de9c 842 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
3fd3ab2d
C
843 function getRawQuery (select: string) {
844 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
845 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
50d6de9c
C
846 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
847 'WHERE "Account"."actorId" = ' + actorId
3fd3ab2d
C
848 const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
849 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
50d6de9c 850 'WHERE "VideoShare"."actorId" = ' + actorId
558d7c23 851
3fd3ab2d
C
852 return `(${queryVideo}) UNION (${queryVideoShare})`
853 }
aaf61f38 854
3fd3ab2d
C
855 const rawQuery = getRawQuery('"Video"."id"')
856 const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
857
858 const query = {
859 distinct: true,
860 offset: start,
861 limit: count,
71398458 862 order: getVideoSort('-createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
3fd3ab2d
C
863 where: {
864 id: {
a1587156 865 [Op.in]: Sequelize.literal('(' + rawQuery + ')')
3c75ce12 866 },
a1587156 867 [Op.or]: [
3c75ce12
C
868 { privacy: VideoPrivacy.PUBLIC },
869 { privacy: VideoPrivacy.UNLISTED }
870 ]
3fd3ab2d
C
871 },
872 include: [
40e87e9e 873 {
ca6d3622 874 attributes: [ 'language', 'fileUrl' ],
40e87e9e
C
875 model: VideoCaptionModel.unscoped(),
876 required: false
877 },
3fd3ab2d 878 {
1d230c44 879 attributes: [ 'id', 'url' ],
2c897999 880 model: VideoShareModel.unscoped(),
3fd3ab2d 881 required: false,
e3d5ea4f
C
882 // We only want videos shared by this actor
883 where: {
a1587156 884 [Op.and]: [
e3d5ea4f
C
885 {
886 id: {
a1587156 887 [Op.not]: null
e3d5ea4f
C
888 }
889 },
890 {
891 actorId
892 }
893 ]
894 },
50d6de9c
C
895 include: [
896 {
2c897999
C
897 attributes: [ 'id', 'url' ],
898 model: ActorModel.unscoped()
50d6de9c
C
899 }
900 ]
3fd3ab2d
C
901 },
902 {
2c897999 903 model: VideoChannelModel.unscoped(),
3fd3ab2d
C
904 required: true,
905 include: [
906 {
2c897999
C
907 attributes: [ 'name' ],
908 model: AccountModel.unscoped(),
909 required: true,
910 include: [
911 {
e3d5ea4f 912 attributes: [ 'id', 'url', 'followersUrl' ],
2c897999
C
913 model: ActorModel.unscoped(),
914 required: true
915 }
916 ]
917 },
918 {
e3d5ea4f 919 attributes: [ 'id', 'url', 'followersUrl' ],
2c897999 920 model: ActorModel.unscoped(),
3fd3ab2d
C
921 required: true
922 }
923 ]
924 },
3fd3ab2d 925 VideoFileModel,
2c897999 926 TagModel
3fd3ab2d
C
927 ]
928 }
164174a6 929
3fd3ab2d 930 return Bluebird.all([
3acc5084
C
931 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
932 VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
3fd3ab2d
C
933 ]).then(([ rows, totals ]) => {
934 // totals: totalVideos + totalVideoShares
935 let totalVideos = 0
936 let totalVideoShares = 0
a1587156
C
937 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
938 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
3fd3ab2d
C
939
940 const total = totalVideos + totalVideoShares
941 return {
942 data: rows,
943 total: total
944 }
945 })
946 }
93e1258c 947
bf64ed41
RK
948 static listUserVideosForApi (
949 accountId: number,
950 start: number,
951 count: number,
952 sort: string,
953 search?: string
954 ) {
3acc5084 955 function buildBaseQuery (): FindOptions {
bf64ed41 956 let baseQuery = {
3acc5084
C
957 offset: start,
958 limit: count,
959 order: getVideoSort(sort),
960 include: [
961 {
962 model: VideoChannelModel,
963 required: true,
964 include: [
965 {
966 model: AccountModel,
967 where: {
968 id: accountId
969 },
970 required: true
971 }
972 ]
973 }
974 ]
975 }
bf64ed41
RK
976
977 if (search) {
978 baseQuery = Object.assign(baseQuery, {
979 where: {
980 name: {
a1587156 981 [Op.iLike]: '%' + search + '%'
bf64ed41
RK
982 }
983 }
984 })
985 }
986
987 return baseQuery
3fd3ab2d 988 }
d8755eed 989
3acc5084
C
990 const countQuery = buildBaseQuery()
991 const findQuery = buildBaseQuery()
992
bf64ed41 993 const findScopes: (string | ScopeOptions)[] = [
a18f275d
C
994 ScopeNames.WITH_SCHEDULED_UPDATE,
995 ScopeNames.WITH_BLACKLISTED,
996 ScopeNames.WITH_THUMBNAILS
997 ]
3acc5084 998
3acc5084
C
999 return Promise.all([
1000 VideoModel.count(countQuery),
0283eaac 1001 VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
3acc5084
C
1002 ]).then(([ count, rows ]) => {
1003 return {
0283eaac 1004 data: rows,
3acc5084
C
1005 total: count
1006 }
1007 })
3fd3ab2d 1008 }
93e1258c 1009
48dce1c9 1010 static async listForApi (options: {
a1587156
C
1011 start: number
1012 count: number
1013 sort: string
1014 nsfw: boolean
1015 includeLocalVideos: boolean
1016 withFiles: boolean
1017 categoryOneOf?: number[]
1018 licenceOneOf?: number[]
1019 languageOneOf?: string[]
1020 tagsOneOf?: string[]
1021 tagsAllOf?: string[]
1022 filter?: VideoFilter
1023 accountId?: number
1024 videoChannelId?: number
4e74e803 1025 followerActorId?: number
a1587156
C
1026 videoPlaylistId?: number
1027 trendingDays?: number
1028 user?: MUserAccountId
1029 historyOfUser?: MUserId
fe987656
C
1030 countVideos?: boolean
1031 }) {
7ad9b984
C
1032 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1033 throw new Error('Try to filter all-local but no user has not the see all videos right')
1cd3facc
C
1034 }
1035
5f3e2425
C
1036 const trendingDays = options.sort.endsWith('trending')
1037 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
1038 : undefined
93e1258c 1039
7ad9b984
C
1040 const serverActor = await getServerActor()
1041
4e74e803 1042 // followerActorId === null has a meaning, so just check undefined
5f3e2425
C
1043 const followerActorId = options.followerActorId !== undefined
1044 ? options.followerActorId
1045 : serverActor.id
06a05d5f 1046
afd2cba5 1047 const queryOptions = {
5f3e2425
C
1048 start: options.start,
1049 count: options.count,
1050 sort: options.sort,
4e74e803 1051 followerActorId,
7ad9b984 1052 serverAccountId: serverActor.Account.id,
afd2cba5
C
1053 nsfw: options.nsfw,
1054 categoryOneOf: options.categoryOneOf,
1055 licenceOneOf: options.licenceOneOf,
1056 languageOneOf: options.languageOneOf,
1057 tagsOneOf: options.tagsOneOf,
1058 tagsAllOf: options.tagsAllOf,
1059 filter: options.filter,
1060 withFiles: options.withFiles,
1061 accountId: options.accountId,
1062 videoChannelId: options.videoChannelId,
418d092a 1063 videoPlaylistId: options.videoPlaylistId,
9a629c6e 1064 includeLocalVideos: options.includeLocalVideos,
7ad9b984 1065 user: options.user,
8b9a525a 1066 historyOfUser: options.historyOfUser,
9a629c6e 1067 trendingDays
48dce1c9
C
1068 }
1069
5f3e2425 1070 return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
93e1258c
C
1071 }
1072
0b18f4aa 1073 static async searchAndPopulateAccountAndServer (options: {
06a05d5f 1074 includeLocalVideos: boolean
d4112450 1075 search?: string
0b18f4aa
C
1076 start?: number
1077 count?: number
1078 sort?: string
1079 startDate?: string // ISO 8601
1080 endDate?: string // ISO 8601
31d065cc
AM
1081 originallyPublishedStartDate?: string
1082 originallyPublishedEndDate?: string
0b18f4aa
C
1083 nsfw?: boolean
1084 categoryOneOf?: number[]
1085 licenceOneOf?: number[]
1086 languageOneOf?: string[]
1087 tagsOneOf?: string[]
1088 tagsAllOf?: string[]
1089 durationMin?: number // seconds
1090 durationMax?: number // seconds
a1587156 1091 user?: MUserAccountId
1cd3facc 1092 filter?: VideoFilter
0b18f4aa 1093 }) {
f05a1c30 1094 const serverActor = await getServerActor()
afd2cba5 1095 const queryOptions = {
4e74e803 1096 followerActorId: serverActor.id,
7ad9b984 1097 serverAccountId: serverActor.Account.id,
afd2cba5
C
1098 includeLocalVideos: options.includeLocalVideos,
1099 nsfw: options.nsfw,
1100 categoryOneOf: options.categoryOneOf,
1101 licenceOneOf: options.licenceOneOf,
1102 languageOneOf: options.languageOneOf,
1103 tagsOneOf: options.tagsOneOf,
6e46de09 1104 tagsAllOf: options.tagsAllOf,
7ad9b984 1105 user: options.user,
3caf77d3 1106 filter: options.filter,
5f3e2425
C
1107 start: options.start,
1108 count: options.count,
1109 sort: options.sort,
1110 startDate: options.startDate,
1111 endDate: options.endDate,
1112 originallyPublishedStartDate: options.originallyPublishedStartDate,
1113 originallyPublishedEndDate: options.originallyPublishedEndDate,
1114
1115 durationMin: options.durationMin,
1116 durationMax: options.durationMax,
1117
1118 search: options.search
48dce1c9 1119 }
f05a1c30 1120
5f3e2425 1121 return VideoModel.getAvailableForApi(queryOptions)
f05a1c30
C
1122 }
1123
453e83ea 1124 static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
418d092a 1125 const where = buildWhereIdOrUUID(id)
627621c1
C
1126 const options = {
1127 where,
1128 transaction: t
3fd3ab2d 1129 }
d8755eed 1130
e8bafea3 1131 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
3fd3ab2d 1132 }
d8755eed 1133
d636ab58
C
1134 static loadWithBlacklist (id: number | string, t?: Transaction): Bluebird<MVideoThumbnailBlacklist> {
1135 const where = buildWhereIdOrUUID(id)
1136 const options = {
1137 where,
1138 transaction: t
1139 }
1140
1141 return VideoModel.scope([
1142 ScopeNames.WITH_THUMBNAILS,
1143 ScopeNames.WITH_BLACKLISTED
1144 ]).findOne(options)
1145 }
1146
7eba5e1f
C
1147 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1148 const fun = () => {
943e5193
C
1149 const query = {
1150 where: buildWhereIdOrUUID(id),
7eba5e1f
C
1151 transaction: t
1152 }
1153
943e5193 1154 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
7eba5e1f
C
1155 }
1156
1157 return ModelCache.Instance.doCache({
943e5193 1158 cacheType: 'load-video-immutable-id',
7eba5e1f
C
1159 key: '' + id,
1160 deleteKey: 'video',
1161 fun
1162 })
1163 }
1164
453e83ea 1165 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
418d092a 1166 const where = buildWhereIdOrUUID(id)
09209296
C
1167 const options = {
1168 where,
1169 transaction: t
1170 }
1171
e8bafea3
C
1172 return VideoModel.scope([
1173 ScopeNames.WITH_BLACKLISTED,
1174 ScopeNames.WITH_USER_ID,
1175 ScopeNames.WITH_THUMBNAILS
1176 ]).findOne(options)
09209296
C
1177 }
1178
453e83ea 1179 static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
418d092a 1180 const where = buildWhereIdOrUUID(id)
627621c1 1181
3fd3ab2d 1182 const options = {
627621c1
C
1183 attributes: [ 'id' ],
1184 where,
1185 transaction: t
3fd3ab2d 1186 }
72c7248b 1187
e8bafea3 1188 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
627621c1
C
1189 }
1190
453e83ea 1191 static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
e2600d8b
C
1192 const where = buildWhereIdOrUUID(id)
1193
1194 const query = {
1195 where,
1196 transaction: t,
1197 logging
1198 }
1199
e8bafea3 1200 return VideoModel.scope([
d7a25329 1201 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
1202 ScopeNames.WITH_STREAMING_PLAYLISTS,
1203 ScopeNames.WITH_THUMBNAILS
e2600d8b 1204 ]).findOne(query)
3fd3ab2d 1205 }
72c7248b 1206
453e83ea 1207 static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
8fa5653a
C
1208 const options = {
1209 where: {
1210 uuid
1211 }
1212 }
1213
e8bafea3 1214 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
8fa5653a
C
1215 }
1216
453e83ea 1217 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
1735c825 1218 const query: FindOptions = {
627621c1
C
1219 where: {
1220 url
4157cdb1
C
1221 },
1222 transaction
627621c1
C
1223 }
1224
e8bafea3 1225 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
4157cdb1
C
1226 }
1227
943e5193
C
1228 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1229 const fun = () => {
1230 const query: FindOptions = {
1231 where: {
1232 url
1233 },
1234 transaction
1235 }
1236
1237 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1238 }
1239
1240 return ModelCache.Instance.doCache({
1241 cacheType: 'load-video-immutable-url',
1242 key: url,
1243 deleteKey: 'video',
1244 fun
1245 })
1246 }
1247
0283eaac 1248 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1735c825 1249 const query: FindOptions = {
4157cdb1
C
1250 where: {
1251 url
1252 },
1253 transaction
1254 }
627621c1 1255
09209296
C
1256 return VideoModel.scope([
1257 ScopeNames.WITH_ACCOUNT_DETAILS,
d7a25329 1258 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3 1259 ScopeNames.WITH_STREAMING_PLAYLISTS,
453e83ea
C
1260 ScopeNames.WITH_THUMBNAILS,
1261 ScopeNames.WITH_BLACKLISTED
09209296 1262 ]).findOne(query)
627621c1
C
1263 }
1264
453e83ea 1265 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
418d092a 1266 const where = buildWhereIdOrUUID(id)
627621c1 1267
3fd3ab2d 1268 const options = {
3acc5084 1269 order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
627621c1 1270 where,
2186386c 1271 transaction: t
3fd3ab2d 1272 }
fd45e8f4 1273
3acc5084 1274 const scopes: (string | ScopeOptions)[] = [
6e46de09
C
1275 ScopeNames.WITH_TAGS,
1276 ScopeNames.WITH_BLACKLISTED,
09209296
C
1277 ScopeNames.WITH_ACCOUNT_DETAILS,
1278 ScopeNames.WITH_SCHEDULED_UPDATE,
d7a25329 1279 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
1280 ScopeNames.WITH_STREAMING_PLAYLISTS,
1281 ScopeNames.WITH_THUMBNAILS
09209296
C
1282 ]
1283
1284 if (userId) {
3acc5084 1285 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
09209296
C
1286 }
1287
1288 return VideoModel
1289 .scope(scopes)
1290 .findOne(options)
1291 }
1292
89cd1275 1293 static loadForGetAPI (parameters: {
a1587156
C
1294 id: number | string
1295 t?: Transaction
89cd1275 1296 userId?: number
453e83ea 1297 }): Bluebird<MVideoDetails> {
89cd1275 1298 const { id, t, userId } = parameters
418d092a 1299 const where = buildWhereIdOrUUID(id)
09209296
C
1300
1301 const options = {
1735c825 1302 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
09209296
C
1303 where,
1304 transaction: t
1305 }
1306
3acc5084 1307 const scopes: (string | ScopeOptions)[] = [
09209296
C
1308 ScopeNames.WITH_TAGS,
1309 ScopeNames.WITH_BLACKLISTED,
6e46de09 1310 ScopeNames.WITH_ACCOUNT_DETAILS,
09209296 1311 ScopeNames.WITH_SCHEDULED_UPDATE,
e8bafea3 1312 ScopeNames.WITH_THUMBNAILS,
d7a25329 1313 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
3acc5084 1314 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
6e46de09
C
1315 ]
1316
1317 if (userId) {
3acc5084 1318 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
6e46de09
C
1319 }
1320
d48ff09d 1321 return VideoModel
6e46de09 1322 .scope(scopes)
da854ddd
C
1323 .findOne(options)
1324 }
1325
09cababd
C
1326 static async getStats () {
1327 const totalLocalVideos = await VideoModel.count({
1328 where: {
1329 remote: false
1330 }
1331 })
09cababd
C
1332
1333 let totalLocalVideoViews = await VideoModel.sum('views', {
1334 where: {
1335 remote: false
1336 }
1337 })
baab47ca 1338
09cababd
C
1339 // Sequelize could return null...
1340 if (!totalLocalVideoViews) totalLocalVideoViews = 0
1341
baab47ca
C
1342 const { total: totalVideos } = await VideoModel.listForApi({
1343 start: 0,
1344 count: 0,
1345 sort: '-publishedAt',
1346 nsfw: buildNSFWFilter(),
1347 includeLocalVideos: true,
1348 withFiles: false
1349 })
1350
09cababd
C
1351 return {
1352 totalLocalVideos,
1353 totalLocalVideoViews,
1354 totalVideos
1355 }
1356 }
1357
6b616860
C
1358 static incrementViews (id: number, views: number) {
1359 return VideoModel.increment('views', {
1360 by: views,
1361 where: {
1362 id
1363 }
1364 })
1365 }
1366
8d427346
C
1367 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1368 // Instances only share videos
1369 const query = 'SELECT 1 FROM "videoShare" ' +
a1587156
C
1370 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1371 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1372 'LIMIT 1'
8d427346
C
1373
1374 const options = {
d5d9b6d7 1375 type: QueryTypes.SELECT as QueryTypes.SELECT,
8d427346
C
1376 bind: { followerActorId, videoId },
1377 raw: true
1378 }
1379
1380 return VideoModel.sequelize.query(query, options)
1381 .then(results => results.length === 1)
1382 }
1383
453e83ea 1384 static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
7d14d4d2
C
1385 const options = {
1386 where: {
1387 channelId: videoChannel.id
1388 },
1389 transaction: t
1390 }
1391
1392 return VideoModel.update({ support: videoChannel.support }, options)
1393 }
1394
453e83ea 1395 static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
7d14d4d2
C
1396 const query = {
1397 attributes: [ 'id' ],
1398 where: {
1399 channelId: videoChannel.id
1400 }
1401 }
1402
1403 return VideoModel.findAll(query)
a1587156 1404 .then(videos => videos.map(v => v.id))
7d14d4d2
C
1405 }
1406
2d3741d6 1407 // threshold corresponds to how many video the field should have to be returned
7348b1fd 1408 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
65b21c96 1409 const serverActor = await getServerActor()
4e74e803 1410 const followerActorId = serverActor.id
7348b1fd 1411
5f3e2425
C
1412 const queryOptions: BuildVideosQueryOptions = {
1413 attributes: [ `"${field}"` ],
1414 group: `GROUP BY "${field}"`,
1415 having: `HAVING COUNT("${field}") >= ${threshold}`,
1416 start: 0,
1417 sort: 'random',
1418 count,
65b21c96 1419 serverAccountId: serverActor.Account.id,
4e74e803 1420 followerActorId,
5f3e2425 1421 includeLocalVideos: true
7348b1fd
C
1422 }
1423
5f3e2425 1424 const { query, replacements } = buildListQuery(VideoModel, queryOptions)
2d3741d6 1425
5f3e2425
C
1426 return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
1427 .then(rows => rows.map(r => r[field]))
2d3741d6
C
1428 }
1429
b36f41ca
C
1430 static buildTrendingQuery (trendingDays: number) {
1431 return {
1432 attributes: [],
1433 subQuery: false,
1434 model: VideoViewModel,
1435 required: false,
1436 where: {
1437 startDate: {
a1587156 1438 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
b36f41ca
C
1439 }
1440 }
1441 }
1442 }
1443
6e46de09 1444 private static async getAvailableForApi (
5f3e2425 1445 options: BuildVideosQueryOptions,
6e46de09
C
1446 countVideos = true
1447 ) {
6b842050
C
1448 function getCount () {
1449 if (countVideos !== true) return Promise.resolve(undefined)
8ea6f49a 1450
6b842050
C
1451 const countOptions = Object.assign({}, options, { isCount: true })
1452 const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
3caf77d3 1453
6b842050
C
1454 return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
1455 .then(rows => rows.length !== 0 ? rows[0].total : 0)
1456 }
1457
1458 function getModels () {
baab47ca
C
1459 if (options.count === 0) return Promise.resolve([])
1460
6b842050
C
1461 const { query, replacements, order } = buildListQuery(VideoModel, options)
1462 const queryModels = wrapForAPIResults(query, replacements, options, order)
1463
1464 return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
1465 .then(rows => VideoModel.buildAPIResult(rows))
1466 }
1467
1468 const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
afd2cba5 1469
ddc07312
C
1470 return {
1471 data: rows,
1472 total: count
1473 }
1474 }
1475
6b842050
C
1476 private static buildAPIResult (rows: any[]) {
1477 const memo: { [ id: number ]: VideoModel } = {}
1478
1479 const thumbnailsDone = new Set<number>()
1480 const historyDone = new Set<number>()
1481 const videoFilesDone = new Set<number>()
1482
1483 const videos: VideoModel[] = []
1484
1485 const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ]
1486 const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
1487 const serverKeys = [ 'id', 'host' ]
1488 const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ]
1489 const videoKeys = [
1490 'id',
1491 'uuid',
1492 'name',
1493 'category',
1494 'licence',
1495 'language',
1496 'privacy',
1497 'nsfw',
1498 'description',
1499 'support',
1500 'duration',
1501 'views',
1502 'likes',
1503 'dislikes',
1504 'remote',
1505 'url',
1506 'commentsEnabled',
1507 'downloadEnabled',
1508 'waitTranscoding',
1509 'state',
1510 'publishedAt',
1511 'originallyPublishedAt',
1512 'channelId',
1513 'createdAt',
1514 'updatedAt'
1515 ]
afd2cba5 1516
6b842050
C
1517 function buildActor (rowActor: any) {
1518 const avatarModel = rowActor.Avatar.id !== null
1519 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
1520 : null
df0b219d 1521
6b842050
C
1522 const serverModel = rowActor.Server.id !== null
1523 ? new ServerModel(pick(rowActor.Server, serverKeys))
1524 : null
df0b219d 1525
6b842050
C
1526 const actorModel = new ActorModel(pick(rowActor, actorKeys))
1527 actorModel.Avatar = avatarModel
1528 actorModel.Server = serverModel
1529
1530 return actorModel
df0b219d
C
1531 }
1532
6b842050
C
1533 for (const row of rows) {
1534 if (!memo[row.id]) {
1535 // Build Channel
1536 const channel = row.VideoChannel
1537 const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
1538 channelModel.Actor = buildActor(channel.Actor)
1539
1540 const account = row.VideoChannel.Account
1541 const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
1542 accountModel.Actor = buildActor(account.Actor)
1543
1544 channelModel.Account = accountModel
1545
1546 const videoModel = new VideoModel(pick(row, videoKeys))
1547 videoModel.VideoChannel = channelModel
1548
1549 videoModel.UserVideoHistories = []
1550 videoModel.Thumbnails = []
1551 videoModel.VideoFiles = []
1552
1553 memo[row.id] = videoModel
1554 // Don't take object value to have a sorted array
1555 videos.push(videoModel)
1556 }
1557
1558 const videoModel = memo[row.id]
1559
1560 if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
1561 const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
1562 videoModel.UserVideoHistories.push(historyModel)
1563
1564 historyDone.add(row.userVideoHistory.id)
1565 }
1566
1567 if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
1568 const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]))
1569 videoModel.Thumbnails.push(thumbnailModel)
1570
1571 thumbnailsDone.add(row.Thumbnails.id)
1572 }
1573
1574 if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
1575 const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
1576 videoModel.VideoFiles.push(videoFileModel)
1577
1578 videoFilesDone.add(row.VideoFiles.id)
1579 }
1580 }
df0b219d 1581
6b842050 1582 return videos
afd2cba5 1583 }
066e94c5 1584
22a73cb8 1585 private static isPrivacyForFederation (privacy: VideoPrivacy) {
dce659fa
C
1586 const castedPrivacy = parseInt(privacy + '', 10)
1587
1588 return castedPrivacy === VideoPrivacy.PUBLIC || castedPrivacy === VideoPrivacy.UNLISTED
22a73cb8
C
1589 }
1590
098eb377 1591 static getCategoryLabel (id: number) {
a1587156 1592 return VIDEO_CATEGORIES[id] || 'Misc'
ae5a3dd6
C
1593 }
1594
098eb377 1595 static getLicenceLabel (id: number) {
a1587156 1596 return VIDEO_LICENCES[id] || 'Unknown'
ae5a3dd6
C
1597 }
1598
098eb377 1599 static getLanguageLabel (id: string) {
a1587156 1600 return VIDEO_LANGUAGES[id] || 'Unknown'
ae5a3dd6
C
1601 }
1602
098eb377 1603 static getPrivacyLabel (id: number) {
a1587156 1604 return VIDEO_PRIVACIES[id] || 'Unknown'
2186386c 1605 }
2243730c 1606
098eb377 1607 static getStateLabel (id: number) {
a1587156 1608 return VIDEO_STATES[id] || 'Unknown'
2243730c
C
1609 }
1610
5b77537c
C
1611 isBlacklisted () {
1612 return !!this.VideoBlacklist
1613 }
1614
bfbd9128
C
1615 isBlocked () {
1616 return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
1617 this.VideoChannel.Account.isBlocked()
1618 }
1619
a1587156 1620 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
d7a25329 1621 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
92e0f42e 1622 const file = fun(this.VideoFiles, file => file.resolution)
d7a25329
C
1623
1624 return Object.assign(file, { Video: this })
1625 }
1626
1627 // No webtorrent files, try with streaming playlist files
1628 if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
1629 const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
1630
92e0f42e 1631 const file = fun(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
d7a25329
C
1632 return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
1633 }
aaf61f38 1634
d7a25329 1635 return undefined
e4f97bab 1636 }
aaf61f38 1637
a1587156 1638 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1639 return this.getQualityFileBy(maxBy)
1640 }
1641
a1587156 1642 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1643 return this.getQualityFileBy(minBy)
1644 }
1645
a1587156 1646 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
29d4e137
C
1647 if (Array.isArray(this.VideoFiles) === false) return undefined
1648
d7a25329
C
1649 const file = this.VideoFiles.find(f => f.resolution === resolution)
1650 if (!file) return undefined
1651
1652 return Object.assign(file, { Video: this })
29d4e137
C
1653 }
1654
453e83ea 1655 async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
3acc5084
C
1656 thumbnail.videoId = this.id
1657
1658 const savedThumbnail = await thumbnail.save({ transaction })
1659
e8bafea3
C
1660 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1661
1662 // Already have this thumbnail, skip
3acc5084 1663 if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
e8bafea3 1664
3acc5084 1665 this.Thumbnails.push(savedThumbnail)
e8bafea3
C
1666 }
1667
e8bafea3
C
1668 generateThumbnailName () {
1669 return this.uuid + '.jpg'
7b1f49de
C
1670 }
1671
3acc5084 1672 getMiniature () {
e8bafea3
C
1673 if (Array.isArray(this.Thumbnails) === false) return undefined
1674
3acc5084 1675 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
e8bafea3
C
1676 }
1677
1678 generatePreviewName () {
1679 return this.uuid + '.jpg'
1680 }
1681
6872996d
C
1682 hasPreview () {
1683 return !!this.getPreview()
1684 }
1685
e8bafea3
C
1686 getPreview () {
1687 if (Array.isArray(this.Thumbnails) === false) return undefined
1688
1689 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
3fd3ab2d 1690 }
7b1f49de 1691
3fd3ab2d
C
1692 isOwned () {
1693 return this.remote === false
9567011b
C
1694 }
1695
cef534ed
C
1696 getWatchStaticPath () {
1697 return '/videos/watch/' + this.uuid
1698 }
1699
40e87e9e 1700 getEmbedStaticPath () {
3fd3ab2d
C
1701 return '/videos/embed/' + this.uuid
1702 }
e4f97bab 1703
3acc5084
C
1704 getMiniatureStaticPath () {
1705 const thumbnail = this.getMiniature()
e8bafea3
C
1706 if (!thumbnail) return null
1707
1708 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
e4f97bab 1709 }
227d02fe 1710
40e87e9e 1711 getPreviewStaticPath () {
e8bafea3
C
1712 const preview = this.getPreview()
1713 if (!preview) return null
1714
1715 // We use a local cache, so specify our cache endpoint instead of potential remote URL
557b13ae 1716 return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
3fd3ab2d 1717 }
40298b02 1718
b5fecbf4 1719 toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
098eb377 1720 return videoModelToFormattedJSON(this, options)
14d3270f 1721 }
14d3270f 1722
b5fecbf4 1723 toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
098eb377 1724 return videoModelToFormattedDetailsJSON(this)
244e76a5
RK
1725 }
1726
1727 getFormattedVideoFilesJSON (): VideoFile[] {
d7a25329
C
1728 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
1729 return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
3fd3ab2d 1730 }
e4f97bab 1731
b5fecbf4 1732 toActivityPubObject (this: MVideoAP): VideoTorrentObject {
098eb377 1733 return videoModelToActivityPubObject(this)
3fd3ab2d
C
1734 }
1735
1736 getTruncatedDescription () {
1737 if (!this.description) return null
93e1258c 1738
bffbebbe 1739 const maxLength = CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
687c6180 1740 return peertubeTruncate(this.description, { length: maxLength })
93e1258c
C
1741 }
1742
d7a25329
C
1743 getMaxQualityResolution () {
1744 const file = this.getMaxQualityFile()
1745 const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
1746 const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
0d0e8dd0 1747
056aa7f2 1748 return getVideoFileResolution(originalFilePath)
3fd3ab2d 1749 }
0d0e8dd0 1750
96f29c0f 1751 getDescriptionAPIPath () {
3fd3ab2d 1752 return `/api/${API_VERSION}/videos/${this.uuid}/description`
feb4bdfd
C
1753 }
1754
d7a25329 1755 getHLSPlaylist (): MStreamingPlaylistFilesVideo {
e2600d8b
C
1756 if (!this.VideoStreamingPlaylists) return undefined
1757
d7a25329
C
1758 const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
1759 playlist.Video = this
1760
1761 return playlist
e2600d8b
C
1762 }
1763
d7a25329
C
1764 setHLSPlaylist (playlist: MStreamingPlaylist) {
1765 const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
b9fffa29 1766
d7a25329
C
1767 if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
1768 this.VideoStreamingPlaylists = toAdd
1769 return
1770 }
1771
1772 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
a1587156
C
1773 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1774 .concat(toAdd)
d7a25329
C
1775 }
1776
1777 removeFile (videoFile: MVideoFile, isRedundancy = false) {
1778 const filePath = getVideoFilePath(this, videoFile, isRedundancy)
62689b94 1779 return remove(filePath)
ed31c059 1780 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
feb4bdfd
C
1781 }
1782
453e83ea 1783 removeTorrent (videoFile: MVideoFile) {
d7a25329 1784 const torrentPath = getTorrentFilePath(this, videoFile)
62689b94 1785 return remove(torrentPath)
ed31c059 1786 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
aaf61f38
C
1787 }
1788
ffc65cbd 1789 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
66fb2aa3 1790 const directoryPath = getHLSDirectory(this, isRedundancy)
09209296 1791
ffc65cbd
C
1792 await remove(directoryPath)
1793
1794 if (isRedundancy !== true) {
a1587156 1795 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
ffc65cbd
C
1796 streamingPlaylistWithFiles.Video = this
1797
1798 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
1799 streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
1800 }
1801
1802 // Remove physical files and torrents
1803 await Promise.all(
1804 streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file))
1805 )
1806 }
09209296
C
1807 }
1808
1297eb5d
C
1809 isOutdated () {
1810 if (this.isOwned()) return false
1811
9f79ade6 1812 return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
1297eb5d
C
1813 }
1814
22a73cb8
C
1815 hasPrivacyForFederation () {
1816 return VideoModel.isPrivacyForFederation(this.privacy)
1817 }
1818
1819 isNewVideo (newPrivacy: VideoPrivacy) {
1820 return this.hasPrivacyForFederation() === false && VideoModel.isPrivacyForFederation(newPrivacy) === true
1821 }
1822
04b8c3fb
C
1823 setAsRefreshed () {
1824 this.changed('updatedAt', true)
1825
1826 return this.save()
1827 }
1828
22a73cb8
C
1829 requiresAuth () {
1830 return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
1831 }
1832
1833 setPrivacy (newPrivacy: VideoPrivacy) {
1834 if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) {
1835 this.publishedAt = new Date()
1836 }
1837
1838 this.privacy = newPrivacy
1839 }
1840
1841 isConfidential () {
1842 return this.privacy === VideoPrivacy.PRIVATE ||
1843 this.privacy === VideoPrivacy.UNLISTED ||
1844 this.privacy === VideoPrivacy.INTERNAL
1845 }
1846
d7a25329
C
1847 async publishIfNeededAndSave (t: Transaction) {
1848 if (this.state !== VideoState.PUBLISHED) {
1849 this.state = VideoState.PUBLISHED
1850 this.publishedAt = new Date()
1851 await this.save({ transaction: t })
7920c273 1852
d7a25329 1853 return true
6fcd19ba 1854 }
aaf61f38 1855
d7a25329 1856 return false
15d4ee04 1857 }
a96aed15 1858
d7a25329
C
1859 getBaseUrls () {
1860 if (this.isOwned()) {
1861 return {
1862 baseUrlHttp: WEBSERVER.URL,
1863 baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
1864 }
c48e82b5
C
1865 }
1866
d7a25329
C
1867 return {
1868 baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
1869 baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
1870 }
c48e82b5
C
1871 }
1872
09209296
C
1873 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1874 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1875 }
1876
453e83ea 1877 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1878 return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
3fd3ab2d 1879 }
e4f97bab 1880
453e83ea 1881 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1882 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
02756fbd
C
1883 }
1884
453e83ea 1885 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1886 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
3fd3ab2d 1887 }
a96aed15 1888
8319d6ae
RK
1889 getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1890 const path = '/api/v1/videos/'
7b81edc8
C
1891
1892 return this.isOwned()
8319d6ae
RK
1893 ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
1894 : videoFile.metadataUrl
1895 }
1896
453e83ea 1897 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1898 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
b9fffa29
C
1899 }
1900
453e83ea 1901 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1902 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
02756fbd 1903 }
09209296 1904
453e83ea 1905 getBandwidthBits (videoFile: MVideoFile) {
09209296
C
1906 return Math.ceil((videoFile.size * 8) / this.duration)
1907 }
a96aed15 1908}