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