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