]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Do not display deleted comments in RSS feed
[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'
f05a1c30 46import { getServerActor } from '../../helpers/utils'
65fcc311 47import {
1297eb5d 48 ACTIVITY_PUB,
4ba3b8ea 49 API_VERSION,
418d092a 50 CONSTRAINTS_FIELDS,
557b13ae 51 LAZY_STATIC_PATHS,
4ba3b8ea 52 REMOTE_SCHEME,
02756fbd 53 STATIC_DOWNLOAD_PATHS,
4ba3b8ea 54 STATIC_PATHS,
4ba3b8ea
C
55 VIDEO_CATEGORIES,
56 VIDEO_LANGUAGES,
57 VIDEO_LICENCES,
2baea0c7 58 VIDEO_PRIVACIES,
6dd9de95
C
59 VIDEO_STATES,
60 WEBSERVER
74dc3bca 61} from '../../initializers/constants'
50d6de9c 62import { sendDeleteVideo } from '../../lib/activitypub/send'
3fd3ab2d
C
63import { AccountModel } from '../account/account'
64import { AccountVideoRateModel } from '../account/account-video-rate'
50d6de9c 65import { ActorModel } from '../activitypub/actor'
b6a4fd6b 66import { AvatarModel } from '../avatar/avatar'
3fd3ab2d 67import { ServerModel } from '../server/server'
5f3e2425 68import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
3fd3ab2d
C
69import { TagModel } from './tag'
70import { VideoAbuseModel } from './video-abuse'
bfbd9128 71import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
da854ddd 72import { VideoCommentModel } from './video-comment'
3fd3ab2d
C
73import { VideoFileModel } from './video-file'
74import { VideoShareModel } from './video-share'
75import { VideoTagModel } from './video-tag'
2baea0c7 76import { ScheduleVideoUpdateModel } from './schedule-video-update'
40e87e9e 77import { VideoCaptionModel } from './video-caption'
26b7305a 78import { VideoBlacklistModel } from './video-blacklist'
d7a25329 79import { remove } from 'fs-extra'
9a629c6e 80import { VideoViewModel } from './video-views'
c48e82b5 81import { VideoRedundancyModel } from '../redundancy/video-redundancy'
098eb377
C
82import {
83 videoFilesModelToFormattedJSON,
84 VideoFormattingJSONOptions,
85 videoModelToActivityPubObject,
86 videoModelToFormattedDetailsJSON,
87 videoModelToFormattedJSON
88} from './video-format-utils'
6e46de09 89import { UserVideoHistoryModel } from '../account/user-video-history'
dc133480 90import { VideoImportModel } from './video-import'
09209296 91import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
418d092a 92import { VideoPlaylistElementModel } from './video-playlist-element'
6dd9de95 93import { CONFIG } from '../../initializers/config'
e8bafea3
C
94import { ThumbnailModel } from './thumbnail'
95import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
e2600d8b 96import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
453e83ea
C
97import {
98 MChannel,
0283eaac 99 MChannelAccountDefault,
453e83ea 100 MChannelId,
d7a25329
C
101 MStreamingPlaylist,
102 MStreamingPlaylistFilesVideo,
453e83ea
C
103 MUserAccountId,
104 MUserId,
453e83ea 105 MVideoAccountLight,
0283eaac 106 MVideoAccountLightBlacklistAllFiles,
b5fecbf4 107 MVideoAP,
453e83ea 108 MVideoDetails,
d7a25329 109 MVideoFileVideo,
b5fecbf4
C
110 MVideoFormattable,
111 MVideoFormattableDetails,
0283eaac 112 MVideoForUser,
453e83ea 113 MVideoFullLight,
5f3e2425
C
114 MVideoIdThumbnail,
115 MVideoImmutable,
453e83ea 116 MVideoThumbnail,
d636ab58 117 MVideoThumbnailBlacklist,
b5fecbf4
C
118 MVideoWithAllFiles,
119 MVideoWithFile,
0374b6b5 120 MVideoWithRights
453e83ea 121} from '../../typings/models'
d7a25329 122import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
453e83ea 123import { MThumbnail } from '../../typings/models/video/thumbnail'
d7a25329 124import { VideoFile } from '@shared/models/videos/video-file.model'
3cf53828 125import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
7eba5e1f 126import { ModelCache } from '@server/models/model-cache'
6b842050 127import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
baab47ca 128import { buildNSFWFilter } from '@server/helpers/express-utils'
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',
631 allowNull: false
632 },
633 onDelete: 'cascade'
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
453e83ea 801 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
9f1ddd24
C
802 const query = {
803 where: {
804 remote: false
805 }
806 }
807
e8bafea3 808 return VideoModel.scope([
d7a25329 809 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
810 ScopeNames.WITH_STREAMING_PLAYLISTS,
811 ScopeNames.WITH_THUMBNAILS
812 ]).findAll(query)
9f1ddd24
C
813 }
814
50d6de9c 815 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
3fd3ab2d
C
816 function getRawQuery (select: string) {
817 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
818 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
50d6de9c
C
819 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
820 'WHERE "Account"."actorId" = ' + actorId
3fd3ab2d
C
821 const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
822 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
50d6de9c 823 'WHERE "VideoShare"."actorId" = ' + actorId
558d7c23 824
3fd3ab2d
C
825 return `(${queryVideo}) UNION (${queryVideoShare})`
826 }
aaf61f38 827
3fd3ab2d
C
828 const rawQuery = getRawQuery('"Video"."id"')
829 const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
830
831 const query = {
832 distinct: true,
833 offset: start,
834 limit: count,
1735c825 835 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
3fd3ab2d
C
836 where: {
837 id: {
a1587156 838 [Op.in]: Sequelize.literal('(' + rawQuery + ')')
3c75ce12 839 },
a1587156 840 [Op.or]: [
3c75ce12
C
841 { privacy: VideoPrivacy.PUBLIC },
842 { privacy: VideoPrivacy.UNLISTED }
843 ]
3fd3ab2d
C
844 },
845 include: [
40e87e9e 846 {
ca6d3622 847 attributes: [ 'language', 'fileUrl' ],
40e87e9e
C
848 model: VideoCaptionModel.unscoped(),
849 required: false
850 },
3fd3ab2d 851 {
1d230c44 852 attributes: [ 'id', 'url' ],
2c897999 853 model: VideoShareModel.unscoped(),
3fd3ab2d 854 required: false,
e3d5ea4f
C
855 // We only want videos shared by this actor
856 where: {
a1587156 857 [Op.and]: [
e3d5ea4f
C
858 {
859 id: {
a1587156 860 [Op.not]: null
e3d5ea4f
C
861 }
862 },
863 {
864 actorId
865 }
866 ]
867 },
50d6de9c
C
868 include: [
869 {
2c897999
C
870 attributes: [ 'id', 'url' ],
871 model: ActorModel.unscoped()
50d6de9c
C
872 }
873 ]
3fd3ab2d
C
874 },
875 {
2c897999 876 model: VideoChannelModel.unscoped(),
3fd3ab2d
C
877 required: true,
878 include: [
879 {
2c897999
C
880 attributes: [ 'name' ],
881 model: AccountModel.unscoped(),
882 required: true,
883 include: [
884 {
e3d5ea4f 885 attributes: [ 'id', 'url', 'followersUrl' ],
2c897999
C
886 model: ActorModel.unscoped(),
887 required: true
888 }
889 ]
890 },
891 {
e3d5ea4f 892 attributes: [ 'id', 'url', 'followersUrl' ],
2c897999 893 model: ActorModel.unscoped(),
3fd3ab2d
C
894 required: true
895 }
896 ]
897 },
3fd3ab2d 898 VideoFileModel,
2c897999 899 TagModel
3fd3ab2d
C
900 ]
901 }
164174a6 902
3fd3ab2d 903 return Bluebird.all([
3acc5084
C
904 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
905 VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
3fd3ab2d
C
906 ]).then(([ rows, totals ]) => {
907 // totals: totalVideos + totalVideoShares
908 let totalVideos = 0
909 let totalVideoShares = 0
a1587156
C
910 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
911 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
3fd3ab2d
C
912
913 const total = totalVideos + totalVideoShares
914 return {
915 data: rows,
916 total: total
917 }
918 })
919 }
93e1258c 920
bf64ed41
RK
921 static listUserVideosForApi (
922 accountId: number,
923 start: number,
924 count: number,
925 sort: string,
926 search?: string
927 ) {
3acc5084 928 function buildBaseQuery (): FindOptions {
bf64ed41 929 let baseQuery = {
3acc5084
C
930 offset: start,
931 limit: count,
932 order: getVideoSort(sort),
933 include: [
934 {
935 model: VideoChannelModel,
936 required: true,
937 include: [
938 {
939 model: AccountModel,
940 where: {
941 id: accountId
942 },
943 required: true
944 }
945 ]
946 }
947 ]
948 }
bf64ed41
RK
949
950 if (search) {
951 baseQuery = Object.assign(baseQuery, {
952 where: {
953 name: {
a1587156 954 [Op.iLike]: '%' + search + '%'
bf64ed41
RK
955 }
956 }
957 })
958 }
959
960 return baseQuery
3fd3ab2d 961 }
d8755eed 962
3acc5084
C
963 const countQuery = buildBaseQuery()
964 const findQuery = buildBaseQuery()
965
bf64ed41 966 const findScopes: (string | ScopeOptions)[] = [
a18f275d
C
967 ScopeNames.WITH_SCHEDULED_UPDATE,
968 ScopeNames.WITH_BLACKLISTED,
969 ScopeNames.WITH_THUMBNAILS
970 ]
3acc5084 971
3acc5084
C
972 return Promise.all([
973 VideoModel.count(countQuery),
0283eaac 974 VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
3acc5084
C
975 ]).then(([ count, rows ]) => {
976 return {
0283eaac 977 data: rows,
3acc5084
C
978 total: count
979 }
980 })
3fd3ab2d 981 }
93e1258c 982
48dce1c9 983 static async listForApi (options: {
a1587156
C
984 start: number
985 count: number
986 sort: string
987 nsfw: boolean
988 includeLocalVideos: boolean
989 withFiles: boolean
990 categoryOneOf?: number[]
991 licenceOneOf?: number[]
992 languageOneOf?: string[]
993 tagsOneOf?: string[]
994 tagsAllOf?: string[]
995 filter?: VideoFilter
996 accountId?: number
997 videoChannelId?: number
4e74e803 998 followerActorId?: number
a1587156
C
999 videoPlaylistId?: number
1000 trendingDays?: number
1001 user?: MUserAccountId
1002 historyOfUser?: MUserId
fe987656
C
1003 countVideos?: boolean
1004 }) {
7ad9b984
C
1005 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1006 throw new Error('Try to filter all-local but no user has not the see all videos right')
1cd3facc
C
1007 }
1008
5f3e2425
C
1009 const trendingDays = options.sort.endsWith('trending')
1010 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
1011 : undefined
93e1258c 1012
7ad9b984
C
1013 const serverActor = await getServerActor()
1014
4e74e803 1015 // followerActorId === null has a meaning, so just check undefined
5f3e2425
C
1016 const followerActorId = options.followerActorId !== undefined
1017 ? options.followerActorId
1018 : serverActor.id
06a05d5f 1019
afd2cba5 1020 const queryOptions = {
5f3e2425
C
1021 start: options.start,
1022 count: options.count,
1023 sort: options.sort,
4e74e803 1024 followerActorId,
7ad9b984 1025 serverAccountId: serverActor.Account.id,
afd2cba5
C
1026 nsfw: options.nsfw,
1027 categoryOneOf: options.categoryOneOf,
1028 licenceOneOf: options.licenceOneOf,
1029 languageOneOf: options.languageOneOf,
1030 tagsOneOf: options.tagsOneOf,
1031 tagsAllOf: options.tagsAllOf,
1032 filter: options.filter,
1033 withFiles: options.withFiles,
1034 accountId: options.accountId,
1035 videoChannelId: options.videoChannelId,
418d092a 1036 videoPlaylistId: options.videoPlaylistId,
9a629c6e 1037 includeLocalVideos: options.includeLocalVideos,
7ad9b984 1038 user: options.user,
8b9a525a 1039 historyOfUser: options.historyOfUser,
9a629c6e 1040 trendingDays
48dce1c9
C
1041 }
1042
5f3e2425 1043 return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
93e1258c
C
1044 }
1045
0b18f4aa 1046 static async searchAndPopulateAccountAndServer (options: {
06a05d5f 1047 includeLocalVideos: boolean
d4112450 1048 search?: string
0b18f4aa
C
1049 start?: number
1050 count?: number
1051 sort?: string
1052 startDate?: string // ISO 8601
1053 endDate?: string // ISO 8601
31d065cc
AM
1054 originallyPublishedStartDate?: string
1055 originallyPublishedEndDate?: string
0b18f4aa
C
1056 nsfw?: boolean
1057 categoryOneOf?: number[]
1058 licenceOneOf?: number[]
1059 languageOneOf?: string[]
1060 tagsOneOf?: string[]
1061 tagsAllOf?: string[]
1062 durationMin?: number // seconds
1063 durationMax?: number // seconds
a1587156 1064 user?: MUserAccountId
1cd3facc 1065 filter?: VideoFilter
0b18f4aa 1066 }) {
f05a1c30 1067 const serverActor = await getServerActor()
afd2cba5 1068 const queryOptions = {
4e74e803 1069 followerActorId: serverActor.id,
7ad9b984 1070 serverAccountId: serverActor.Account.id,
afd2cba5
C
1071 includeLocalVideos: options.includeLocalVideos,
1072 nsfw: options.nsfw,
1073 categoryOneOf: options.categoryOneOf,
1074 licenceOneOf: options.licenceOneOf,
1075 languageOneOf: options.languageOneOf,
1076 tagsOneOf: options.tagsOneOf,
6e46de09 1077 tagsAllOf: options.tagsAllOf,
7ad9b984 1078 user: options.user,
3caf77d3 1079 filter: options.filter,
5f3e2425
C
1080 start: options.start,
1081 count: options.count,
1082 sort: options.sort,
1083 startDate: options.startDate,
1084 endDate: options.endDate,
1085 originallyPublishedStartDate: options.originallyPublishedStartDate,
1086 originallyPublishedEndDate: options.originallyPublishedEndDate,
1087
1088 durationMin: options.durationMin,
1089 durationMax: options.durationMax,
1090
1091 search: options.search
48dce1c9 1092 }
f05a1c30 1093
5f3e2425 1094 return VideoModel.getAvailableForApi(queryOptions)
f05a1c30
C
1095 }
1096
453e83ea 1097 static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
418d092a 1098 const where = buildWhereIdOrUUID(id)
627621c1
C
1099 const options = {
1100 where,
1101 transaction: t
3fd3ab2d 1102 }
d8755eed 1103
e8bafea3 1104 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
3fd3ab2d 1105 }
d8755eed 1106
d636ab58
C
1107 static loadWithBlacklist (id: number | string, t?: Transaction): Bluebird<MVideoThumbnailBlacklist> {
1108 const where = buildWhereIdOrUUID(id)
1109 const options = {
1110 where,
1111 transaction: t
1112 }
1113
1114 return VideoModel.scope([
1115 ScopeNames.WITH_THUMBNAILS,
1116 ScopeNames.WITH_BLACKLISTED
1117 ]).findOne(options)
1118 }
1119
7eba5e1f
C
1120 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1121 const fun = () => {
943e5193
C
1122 const query = {
1123 where: buildWhereIdOrUUID(id),
7eba5e1f
C
1124 transaction: t
1125 }
1126
943e5193 1127 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
7eba5e1f
C
1128 }
1129
1130 return ModelCache.Instance.doCache({
943e5193 1131 cacheType: 'load-video-immutable-id',
7eba5e1f
C
1132 key: '' + id,
1133 deleteKey: 'video',
1134 fun
1135 })
1136 }
1137
453e83ea 1138 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
418d092a 1139 const where = buildWhereIdOrUUID(id)
09209296
C
1140 const options = {
1141 where,
1142 transaction: t
1143 }
1144
e8bafea3
C
1145 return VideoModel.scope([
1146 ScopeNames.WITH_BLACKLISTED,
1147 ScopeNames.WITH_USER_ID,
1148 ScopeNames.WITH_THUMBNAILS
1149 ]).findOne(options)
09209296
C
1150 }
1151
453e83ea 1152 static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
418d092a 1153 const where = buildWhereIdOrUUID(id)
627621c1 1154
3fd3ab2d 1155 const options = {
627621c1
C
1156 attributes: [ 'id' ],
1157 where,
1158 transaction: t
3fd3ab2d 1159 }
72c7248b 1160
e8bafea3 1161 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
627621c1
C
1162 }
1163
453e83ea 1164 static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
e2600d8b
C
1165 const where = buildWhereIdOrUUID(id)
1166
1167 const query = {
1168 where,
1169 transaction: t,
1170 logging
1171 }
1172
e8bafea3 1173 return VideoModel.scope([
d7a25329 1174 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
1175 ScopeNames.WITH_STREAMING_PLAYLISTS,
1176 ScopeNames.WITH_THUMBNAILS
e2600d8b 1177 ]).findOne(query)
3fd3ab2d 1178 }
72c7248b 1179
453e83ea 1180 static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
8fa5653a
C
1181 const options = {
1182 where: {
1183 uuid
1184 }
1185 }
1186
e8bafea3 1187 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
8fa5653a
C
1188 }
1189
453e83ea 1190 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
1735c825 1191 const query: FindOptions = {
627621c1
C
1192 where: {
1193 url
4157cdb1
C
1194 },
1195 transaction
627621c1
C
1196 }
1197
e8bafea3 1198 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
4157cdb1
C
1199 }
1200
943e5193
C
1201 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1202 const fun = () => {
1203 const query: FindOptions = {
1204 where: {
1205 url
1206 },
1207 transaction
1208 }
1209
1210 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1211 }
1212
1213 return ModelCache.Instance.doCache({
1214 cacheType: 'load-video-immutable-url',
1215 key: url,
1216 deleteKey: 'video',
1217 fun
1218 })
1219 }
1220
0283eaac 1221 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1735c825 1222 const query: FindOptions = {
4157cdb1
C
1223 where: {
1224 url
1225 },
1226 transaction
1227 }
627621c1 1228
09209296
C
1229 return VideoModel.scope([
1230 ScopeNames.WITH_ACCOUNT_DETAILS,
d7a25329 1231 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3 1232 ScopeNames.WITH_STREAMING_PLAYLISTS,
453e83ea
C
1233 ScopeNames.WITH_THUMBNAILS,
1234 ScopeNames.WITH_BLACKLISTED
09209296 1235 ]).findOne(query)
627621c1
C
1236 }
1237
453e83ea 1238 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
418d092a 1239 const where = buildWhereIdOrUUID(id)
627621c1 1240
3fd3ab2d 1241 const options = {
3acc5084 1242 order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
627621c1 1243 where,
2186386c 1244 transaction: t
3fd3ab2d 1245 }
fd45e8f4 1246
3acc5084 1247 const scopes: (string | ScopeOptions)[] = [
6e46de09
C
1248 ScopeNames.WITH_TAGS,
1249 ScopeNames.WITH_BLACKLISTED,
09209296
C
1250 ScopeNames.WITH_ACCOUNT_DETAILS,
1251 ScopeNames.WITH_SCHEDULED_UPDATE,
d7a25329 1252 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
1253 ScopeNames.WITH_STREAMING_PLAYLISTS,
1254 ScopeNames.WITH_THUMBNAILS
09209296
C
1255 ]
1256
1257 if (userId) {
3acc5084 1258 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
09209296
C
1259 }
1260
1261 return VideoModel
1262 .scope(scopes)
1263 .findOne(options)
1264 }
1265
89cd1275 1266 static loadForGetAPI (parameters: {
a1587156
C
1267 id: number | string
1268 t?: Transaction
89cd1275 1269 userId?: number
453e83ea 1270 }): Bluebird<MVideoDetails> {
89cd1275 1271 const { id, t, userId } = parameters
418d092a 1272 const where = buildWhereIdOrUUID(id)
09209296
C
1273
1274 const options = {
1735c825 1275 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
09209296
C
1276 where,
1277 transaction: t
1278 }
1279
3acc5084 1280 const scopes: (string | ScopeOptions)[] = [
09209296
C
1281 ScopeNames.WITH_TAGS,
1282 ScopeNames.WITH_BLACKLISTED,
6e46de09 1283 ScopeNames.WITH_ACCOUNT_DETAILS,
09209296 1284 ScopeNames.WITH_SCHEDULED_UPDATE,
e8bafea3 1285 ScopeNames.WITH_THUMBNAILS,
d7a25329 1286 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
3acc5084 1287 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
6e46de09
C
1288 ]
1289
1290 if (userId) {
3acc5084 1291 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
6e46de09
C
1292 }
1293
d48ff09d 1294 return VideoModel
6e46de09 1295 .scope(scopes)
da854ddd
C
1296 .findOne(options)
1297 }
1298
09cababd
C
1299 static async getStats () {
1300 const totalLocalVideos = await VideoModel.count({
1301 where: {
1302 remote: false
1303 }
1304 })
09cababd
C
1305
1306 let totalLocalVideoViews = await VideoModel.sum('views', {
1307 where: {
1308 remote: false
1309 }
1310 })
baab47ca 1311
09cababd
C
1312 // Sequelize could return null...
1313 if (!totalLocalVideoViews) totalLocalVideoViews = 0
1314
baab47ca
C
1315 const { total: totalVideos } = await VideoModel.listForApi({
1316 start: 0,
1317 count: 0,
1318 sort: '-publishedAt',
1319 nsfw: buildNSFWFilter(),
1320 includeLocalVideos: true,
1321 withFiles: false
1322 })
1323
09cababd
C
1324 return {
1325 totalLocalVideos,
1326 totalLocalVideoViews,
1327 totalVideos
1328 }
1329 }
1330
6b616860
C
1331 static incrementViews (id: number, views: number) {
1332 return VideoModel.increment('views', {
1333 by: views,
1334 where: {
1335 id
1336 }
1337 })
1338 }
1339
8d427346
C
1340 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1341 // Instances only share videos
1342 const query = 'SELECT 1 FROM "videoShare" ' +
a1587156
C
1343 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1344 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1345 'LIMIT 1'
8d427346
C
1346
1347 const options = {
d5d9b6d7 1348 type: QueryTypes.SELECT as QueryTypes.SELECT,
8d427346
C
1349 bind: { followerActorId, videoId },
1350 raw: true
1351 }
1352
1353 return VideoModel.sequelize.query(query, options)
1354 .then(results => results.length === 1)
1355 }
1356
453e83ea 1357 static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
7d14d4d2
C
1358 const options = {
1359 where: {
1360 channelId: videoChannel.id
1361 },
1362 transaction: t
1363 }
1364
1365 return VideoModel.update({ support: videoChannel.support }, options)
1366 }
1367
453e83ea 1368 static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
7d14d4d2
C
1369 const query = {
1370 attributes: [ 'id' ],
1371 where: {
1372 channelId: videoChannel.id
1373 }
1374 }
1375
1376 return VideoModel.findAll(query)
a1587156 1377 .then(videos => videos.map(v => v.id))
7d14d4d2
C
1378 }
1379
2d3741d6 1380 // threshold corresponds to how many video the field should have to be returned
7348b1fd 1381 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
65b21c96 1382 const serverActor = await getServerActor()
4e74e803 1383 const followerActorId = serverActor.id
7348b1fd 1384
5f3e2425
C
1385 const queryOptions: BuildVideosQueryOptions = {
1386 attributes: [ `"${field}"` ],
1387 group: `GROUP BY "${field}"`,
1388 having: `HAVING COUNT("${field}") >= ${threshold}`,
1389 start: 0,
1390 sort: 'random',
1391 count,
65b21c96 1392 serverAccountId: serverActor.Account.id,
4e74e803 1393 followerActorId,
5f3e2425 1394 includeLocalVideos: true
7348b1fd
C
1395 }
1396
5f3e2425 1397 const { query, replacements } = buildListQuery(VideoModel, queryOptions)
2d3741d6 1398
5f3e2425
C
1399 return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
1400 .then(rows => rows.map(r => r[field]))
2d3741d6
C
1401 }
1402
b36f41ca
C
1403 static buildTrendingQuery (trendingDays: number) {
1404 return {
1405 attributes: [],
1406 subQuery: false,
1407 model: VideoViewModel,
1408 required: false,
1409 where: {
1410 startDate: {
a1587156 1411 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
b36f41ca
C
1412 }
1413 }
1414 }
1415 }
1416
6e46de09 1417 private static async getAvailableForApi (
5f3e2425 1418 options: BuildVideosQueryOptions,
6e46de09
C
1419 countVideos = true
1420 ) {
6b842050
C
1421 function getCount () {
1422 if (countVideos !== true) return Promise.resolve(undefined)
8ea6f49a 1423
6b842050
C
1424 const countOptions = Object.assign({}, options, { isCount: true })
1425 const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
3caf77d3 1426
6b842050
C
1427 return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
1428 .then(rows => rows.length !== 0 ? rows[0].total : 0)
1429 }
1430
1431 function getModels () {
baab47ca
C
1432 if (options.count === 0) return Promise.resolve([])
1433
6b842050
C
1434 const { query, replacements, order } = buildListQuery(VideoModel, options)
1435 const queryModels = wrapForAPIResults(query, replacements, options, order)
1436
1437 return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
1438 .then(rows => VideoModel.buildAPIResult(rows))
1439 }
1440
1441 const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
afd2cba5 1442
ddc07312
C
1443 return {
1444 data: rows,
1445 total: count
1446 }
1447 }
1448
6b842050
C
1449 private static buildAPIResult (rows: any[]) {
1450 const memo: { [ id: number ]: VideoModel } = {}
1451
1452 const thumbnailsDone = new Set<number>()
1453 const historyDone = new Set<number>()
1454 const videoFilesDone = new Set<number>()
1455
1456 const videos: VideoModel[] = []
1457
1458 const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ]
1459 const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
1460 const serverKeys = [ 'id', 'host' ]
1461 const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ]
1462 const videoKeys = [
1463 'id',
1464 'uuid',
1465 'name',
1466 'category',
1467 'licence',
1468 'language',
1469 'privacy',
1470 'nsfw',
1471 'description',
1472 'support',
1473 'duration',
1474 'views',
1475 'likes',
1476 'dislikes',
1477 'remote',
1478 'url',
1479 'commentsEnabled',
1480 'downloadEnabled',
1481 'waitTranscoding',
1482 'state',
1483 'publishedAt',
1484 'originallyPublishedAt',
1485 'channelId',
1486 'createdAt',
1487 'updatedAt'
1488 ]
afd2cba5 1489
6b842050
C
1490 function buildActor (rowActor: any) {
1491 const avatarModel = rowActor.Avatar.id !== null
1492 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
1493 : null
df0b219d 1494
6b842050
C
1495 const serverModel = rowActor.Server.id !== null
1496 ? new ServerModel(pick(rowActor.Server, serverKeys))
1497 : null
df0b219d 1498
6b842050
C
1499 const actorModel = new ActorModel(pick(rowActor, actorKeys))
1500 actorModel.Avatar = avatarModel
1501 actorModel.Server = serverModel
1502
1503 return actorModel
df0b219d
C
1504 }
1505
6b842050
C
1506 for (const row of rows) {
1507 if (!memo[row.id]) {
1508 // Build Channel
1509 const channel = row.VideoChannel
1510 const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
1511 channelModel.Actor = buildActor(channel.Actor)
1512
1513 const account = row.VideoChannel.Account
1514 const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
1515 accountModel.Actor = buildActor(account.Actor)
1516
1517 channelModel.Account = accountModel
1518
1519 const videoModel = new VideoModel(pick(row, videoKeys))
1520 videoModel.VideoChannel = channelModel
1521
1522 videoModel.UserVideoHistories = []
1523 videoModel.Thumbnails = []
1524 videoModel.VideoFiles = []
1525
1526 memo[row.id] = videoModel
1527 // Don't take object value to have a sorted array
1528 videos.push(videoModel)
1529 }
1530
1531 const videoModel = memo[row.id]
1532
1533 if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
1534 const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
1535 videoModel.UserVideoHistories.push(historyModel)
1536
1537 historyDone.add(row.userVideoHistory.id)
1538 }
1539
1540 if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
1541 const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]))
1542 videoModel.Thumbnails.push(thumbnailModel)
1543
1544 thumbnailsDone.add(row.Thumbnails.id)
1545 }
1546
1547 if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
1548 const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
1549 videoModel.VideoFiles.push(videoFileModel)
1550
1551 videoFilesDone.add(row.VideoFiles.id)
1552 }
1553 }
df0b219d 1554
6b842050 1555 return videos
afd2cba5 1556 }
066e94c5 1557
22a73cb8 1558 private static isPrivacyForFederation (privacy: VideoPrivacy) {
dce659fa
C
1559 const castedPrivacy = parseInt(privacy + '', 10)
1560
1561 return castedPrivacy === VideoPrivacy.PUBLIC || castedPrivacy === VideoPrivacy.UNLISTED
22a73cb8
C
1562 }
1563
098eb377 1564 static getCategoryLabel (id: number) {
a1587156 1565 return VIDEO_CATEGORIES[id] || 'Misc'
ae5a3dd6
C
1566 }
1567
098eb377 1568 static getLicenceLabel (id: number) {
a1587156 1569 return VIDEO_LICENCES[id] || 'Unknown'
ae5a3dd6
C
1570 }
1571
098eb377 1572 static getLanguageLabel (id: string) {
a1587156 1573 return VIDEO_LANGUAGES[id] || 'Unknown'
ae5a3dd6
C
1574 }
1575
098eb377 1576 static getPrivacyLabel (id: number) {
a1587156 1577 return VIDEO_PRIVACIES[id] || 'Unknown'
2186386c 1578 }
2243730c 1579
098eb377 1580 static getStateLabel (id: number) {
a1587156 1581 return VIDEO_STATES[id] || 'Unknown'
2243730c
C
1582 }
1583
5b77537c
C
1584 isBlacklisted () {
1585 return !!this.VideoBlacklist
1586 }
1587
bfbd9128
C
1588 isBlocked () {
1589 return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
1590 this.VideoChannel.Account.isBlocked()
1591 }
1592
a1587156 1593 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
d7a25329 1594 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
92e0f42e 1595 const file = fun(this.VideoFiles, file => file.resolution)
d7a25329
C
1596
1597 return Object.assign(file, { Video: this })
1598 }
1599
1600 // No webtorrent files, try with streaming playlist files
1601 if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
1602 const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
1603
92e0f42e 1604 const file = fun(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
d7a25329
C
1605 return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
1606 }
aaf61f38 1607
d7a25329 1608 return undefined
e4f97bab 1609 }
aaf61f38 1610
a1587156 1611 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1612 return this.getQualityFileBy(maxBy)
1613 }
1614
a1587156 1615 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1616 return this.getQualityFileBy(minBy)
1617 }
1618
a1587156 1619 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
29d4e137
C
1620 if (Array.isArray(this.VideoFiles) === false) return undefined
1621
d7a25329
C
1622 const file = this.VideoFiles.find(f => f.resolution === resolution)
1623 if (!file) return undefined
1624
1625 return Object.assign(file, { Video: this })
29d4e137
C
1626 }
1627
453e83ea 1628 async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
3acc5084
C
1629 thumbnail.videoId = this.id
1630
1631 const savedThumbnail = await thumbnail.save({ transaction })
1632
e8bafea3
C
1633 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1634
1635 // Already have this thumbnail, skip
3acc5084 1636 if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
e8bafea3 1637
3acc5084 1638 this.Thumbnails.push(savedThumbnail)
e8bafea3
C
1639 }
1640
e8bafea3
C
1641 generateThumbnailName () {
1642 return this.uuid + '.jpg'
7b1f49de
C
1643 }
1644
3acc5084 1645 getMiniature () {
e8bafea3
C
1646 if (Array.isArray(this.Thumbnails) === false) return undefined
1647
3acc5084 1648 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
e8bafea3
C
1649 }
1650
1651 generatePreviewName () {
1652 return this.uuid + '.jpg'
1653 }
1654
6872996d
C
1655 hasPreview () {
1656 return !!this.getPreview()
1657 }
1658
e8bafea3
C
1659 getPreview () {
1660 if (Array.isArray(this.Thumbnails) === false) return undefined
1661
1662 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
3fd3ab2d 1663 }
7b1f49de 1664
3fd3ab2d
C
1665 isOwned () {
1666 return this.remote === false
9567011b
C
1667 }
1668
cef534ed
C
1669 getWatchStaticPath () {
1670 return '/videos/watch/' + this.uuid
1671 }
1672
40e87e9e 1673 getEmbedStaticPath () {
3fd3ab2d
C
1674 return '/videos/embed/' + this.uuid
1675 }
e4f97bab 1676
3acc5084
C
1677 getMiniatureStaticPath () {
1678 const thumbnail = this.getMiniature()
e8bafea3
C
1679 if (!thumbnail) return null
1680
1681 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
e4f97bab 1682 }
227d02fe 1683
40e87e9e 1684 getPreviewStaticPath () {
e8bafea3
C
1685 const preview = this.getPreview()
1686 if (!preview) return null
1687
1688 // We use a local cache, so specify our cache endpoint instead of potential remote URL
557b13ae 1689 return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
3fd3ab2d 1690 }
40298b02 1691
b5fecbf4 1692 toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
098eb377 1693 return videoModelToFormattedJSON(this, options)
14d3270f 1694 }
14d3270f 1695
b5fecbf4 1696 toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
098eb377 1697 return videoModelToFormattedDetailsJSON(this)
244e76a5
RK
1698 }
1699
1700 getFormattedVideoFilesJSON (): VideoFile[] {
d7a25329
C
1701 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
1702 return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
3fd3ab2d 1703 }
e4f97bab 1704
b5fecbf4 1705 toActivityPubObject (this: MVideoAP): VideoTorrentObject {
098eb377 1706 return videoModelToActivityPubObject(this)
3fd3ab2d
C
1707 }
1708
1709 getTruncatedDescription () {
1710 if (!this.description) return null
93e1258c 1711
bffbebbe 1712 const maxLength = CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
687c6180 1713 return peertubeTruncate(this.description, { length: maxLength })
93e1258c
C
1714 }
1715
d7a25329
C
1716 getMaxQualityResolution () {
1717 const file = this.getMaxQualityFile()
1718 const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
1719 const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
0d0e8dd0 1720
056aa7f2 1721 return getVideoFileResolution(originalFilePath)
3fd3ab2d 1722 }
0d0e8dd0 1723
96f29c0f 1724 getDescriptionAPIPath () {
3fd3ab2d 1725 return `/api/${API_VERSION}/videos/${this.uuid}/description`
feb4bdfd
C
1726 }
1727
d7a25329 1728 getHLSPlaylist (): MStreamingPlaylistFilesVideo {
e2600d8b
C
1729 if (!this.VideoStreamingPlaylists) return undefined
1730
d7a25329
C
1731 const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
1732 playlist.Video = this
1733
1734 return playlist
e2600d8b
C
1735 }
1736
d7a25329
C
1737 setHLSPlaylist (playlist: MStreamingPlaylist) {
1738 const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
b9fffa29 1739
d7a25329
C
1740 if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
1741 this.VideoStreamingPlaylists = toAdd
1742 return
1743 }
1744
1745 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
a1587156
C
1746 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1747 .concat(toAdd)
d7a25329
C
1748 }
1749
1750 removeFile (videoFile: MVideoFile, isRedundancy = false) {
1751 const filePath = getVideoFilePath(this, videoFile, isRedundancy)
62689b94 1752 return remove(filePath)
ed31c059 1753 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
feb4bdfd
C
1754 }
1755
453e83ea 1756 removeTorrent (videoFile: MVideoFile) {
d7a25329 1757 const torrentPath = getTorrentFilePath(this, videoFile)
62689b94 1758 return remove(torrentPath)
ed31c059 1759 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
aaf61f38
C
1760 }
1761
ffc65cbd 1762 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
66fb2aa3 1763 const directoryPath = getHLSDirectory(this, isRedundancy)
09209296 1764
ffc65cbd
C
1765 await remove(directoryPath)
1766
1767 if (isRedundancy !== true) {
a1587156 1768 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
ffc65cbd
C
1769 streamingPlaylistWithFiles.Video = this
1770
1771 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
1772 streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
1773 }
1774
1775 // Remove physical files and torrents
1776 await Promise.all(
1777 streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file))
1778 )
1779 }
09209296
C
1780 }
1781
1297eb5d
C
1782 isOutdated () {
1783 if (this.isOwned()) return false
1784
9f79ade6 1785 return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
1297eb5d
C
1786 }
1787
22a73cb8
C
1788 hasPrivacyForFederation () {
1789 return VideoModel.isPrivacyForFederation(this.privacy)
1790 }
1791
1792 isNewVideo (newPrivacy: VideoPrivacy) {
1793 return this.hasPrivacyForFederation() === false && VideoModel.isPrivacyForFederation(newPrivacy) === true
1794 }
1795
04b8c3fb
C
1796 setAsRefreshed () {
1797 this.changed('updatedAt', true)
1798
1799 return this.save()
1800 }
1801
22a73cb8
C
1802 requiresAuth () {
1803 return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
1804 }
1805
1806 setPrivacy (newPrivacy: VideoPrivacy) {
1807 if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) {
1808 this.publishedAt = new Date()
1809 }
1810
1811 this.privacy = newPrivacy
1812 }
1813
1814 isConfidential () {
1815 return this.privacy === VideoPrivacy.PRIVATE ||
1816 this.privacy === VideoPrivacy.UNLISTED ||
1817 this.privacy === VideoPrivacy.INTERNAL
1818 }
1819
d7a25329
C
1820 async publishIfNeededAndSave (t: Transaction) {
1821 if (this.state !== VideoState.PUBLISHED) {
1822 this.state = VideoState.PUBLISHED
1823 this.publishedAt = new Date()
1824 await this.save({ transaction: t })
7920c273 1825
d7a25329 1826 return true
6fcd19ba 1827 }
aaf61f38 1828
d7a25329 1829 return false
15d4ee04 1830 }
a96aed15 1831
d7a25329
C
1832 getBaseUrls () {
1833 if (this.isOwned()) {
1834 return {
1835 baseUrlHttp: WEBSERVER.URL,
1836 baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
1837 }
c48e82b5
C
1838 }
1839
d7a25329
C
1840 return {
1841 baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
1842 baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
1843 }
c48e82b5
C
1844 }
1845
09209296
C
1846 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1847 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1848 }
1849
453e83ea 1850 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1851 return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
3fd3ab2d 1852 }
e4f97bab 1853
453e83ea 1854 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1855 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
02756fbd
C
1856 }
1857
453e83ea 1858 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1859 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
3fd3ab2d 1860 }
a96aed15 1861
8319d6ae
RK
1862 getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1863 const path = '/api/v1/videos/'
7b81edc8
C
1864
1865 return this.isOwned()
8319d6ae
RK
1866 ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
1867 : videoFile.metadataUrl
1868 }
1869
453e83ea 1870 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1871 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
b9fffa29
C
1872 }
1873
453e83ea 1874 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1875 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
02756fbd 1876 }
09209296 1877
453e83ea 1878 getBandwidthBits (videoFile: MVideoFile) {
09209296
C
1879 return Math.ceil((videoFile.size * 8) / this.duration)
1880 }
a96aed15 1881}