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