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