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