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